This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

tools.stt: remove, with pkgs.vosk

-391
-53
pkgs/vosk/package.nix
··· 1 - { 2 - lib, 3 - python3Packages, 4 - fetchurl, 5 - stdenv, 6 - autoPatchelfHook, 7 - }: 8 - let 9 - # Per-platform wheels; versions can differ because upstream stopped shipping 10 - # macOS wheels after 0.3.44. Maintained by ./update.sh. 11 - wheels = lib.importJSON ./wheels.json; 12 - 13 - wheel = 14 - wheels.${stdenv.hostPlatform.system} 15 - or (throw "Unsupported platform: ${stdenv.hostPlatform.system}"); 16 - in 17 - python3Packages.buildPythonPackage { 18 - pname = "vosk"; 19 - inherit (wheel) version; 20 - format = "wheel"; 21 - 22 - src = fetchurl { 23 - url = "https://files.pythonhosted.org/packages/py3/v/vosk/vosk-${wheel.version}-py3-none-${wheel.tag}.whl"; 24 - inherit (wheel) hash; 25 - }; 26 - 27 - # Only the Linux wheels ship ELF libraries; the macOS one is Mach-O. 28 - nativeBuildInputs = lib.optionals stdenv.hostPlatform.isLinux [ autoPatchelfHook ]; 29 - 30 - buildInputs = lib.optionals stdenv.hostPlatform.isLinux [ stdenv.cc.cc.lib ]; 31 - 32 - dependencies = with python3Packages; [ 33 - cffi 34 - requests 35 - tqdm 36 - srt 37 - websockets 38 - ]; 39 - 40 - # Skip tests as they require audio files and models 41 - doCheck = false; 42 - 43 - pythonImportsCheck = [ "vosk" ]; 44 - 45 - passthru.updateScript = ./update.sh; 46 - 47 - meta = { 48 - description = "Offline speech recognition API based on Kaldi and Vosk"; 49 - homepage = "https://alphacephei.com/vosk/"; 50 - license = lib.licenses.asl20; 51 - platforms = lib.attrNames wheels; 52 - }; 53 - }
-43
pkgs/vosk/update.sh
··· 1 - #!/usr/bin/env bash 2 - set -euo pipefail 3 - 4 - # Upstream publishes a different set of platform wheels per release (no macOS 5 - # wheel since 0.3.45), so each platform in wheels.json tracks the newest PyPI 6 - # release that still ships its wheel tag. Hashes come from PyPI's sha256 7 - # digests; a final build verifies the result. 8 - # Run by update-pkgs from the flake root ($0 is a read-only store copy). 9 - attr=vosk 10 - json=pkgs/vosk/wheels.json 11 - system=$(nix eval --raw --impure --expr 'builtins.currentSystem') 12 - 13 - pypi=$(curl -fsSL https://pypi.org/pypi/vosk/json) 14 - 15 - new=$(jq -c . "$json") 16 - for platform in $(jq -r 'keys[]' "$json"); do 17 - tag=$(jq -r --arg p "$platform" '.[$p].tag' "$json") 18 - # Newest non-yanked release providing vosk-<v>-py3-none-<tag>.whl. 19 - match=$(jq -r --arg tag "$tag" ' 20 - .releases | to_entries[] 21 - | .key as $v 22 - | .value[] 23 - | select(.yanked | not) 24 - | select(.filename == "vosk-\($v)-py3-none-\($tag).whl") 25 - | "\($v) \(.digests.sha256)"' <<<"$pypi" | sort -V -k1,1 | tail -n1) 26 - if [ -z "$match" ]; then 27 - echo "$attr: no wheel for $platform ($tag) on PyPI" >&2 28 - exit 1 29 - fi 30 - version=${match%% *} 31 - hash=$(nix hash convert --hash-algo sha256 --to sri "${match##* }") 32 - new=$(jq -c --arg p "$platform" --arg v "$version" --arg h "$hash" \ 33 - '.[$p].version = $v | .[$p].hash = $h' <<<"$new") 34 - done 35 - 36 - if [ "$(jq -S . <<<"$new")" = "$(jq -S . "$json")" ]; then 37 - echo "$attr: already up to date ($(jq -r '[.[].version] | unique | join(", ")' "$json"))" 38 - exit 0 39 - fi 40 - 41 - jq . <<<"$new" >"$json" 42 - echo "$attr: updated to $(jq -r 'to_entries | map("\(.key)=\(.value.version)") | join(", ")' "$json")" 43 - nix build --no-link ".#packages.$system.$attr"
-22
pkgs/vosk/wheels.json
··· 1 - { 2 - "x86_64-linux": { 3 - "version": "0.3.45", 4 - "tag": "manylinux_2_12_x86_64.manylinux2010_x86_64", 5 - "hash": "sha256-JeAlCTxDmdcnj1Q1aO2MxUYKw6S/SMI2c6zh4l0mYZ8=" 6 - }, 7 - "aarch64-linux": { 8 - "version": "0.3.45", 9 - "tag": "manylinux2014_aarch64", 10 - "hash": "sha256-VO+0fdiQ5UTp4g8DFkE6zsf4aA0E7AlcYUCrTnAmJwQ=" 11 - }, 12 - "x86_64-darwin": { 13 - "version": "0.3.44", 14 - "tag": "macosx_10_6_universal2", 15 - "hash": "sha256-Ap0LPWpc/4dMV1uKWBSkzM+zdgjP9S6EbAKoxnqIKAE=" 16 - }, 17 - "aarch64-darwin": { 18 - "version": "0.3.44", 19 - "tag": "macosx_10_6_universal2", 20 - "hash": "sha256-Ap0LPWpc/4dMV1uKWBSkzM+zdgjP9S6EbAKoxnqIKAE=" 21 - } 22 - }
-30
tools/stt/flake-module.nix
··· 1 - { ... }: 2 - { 3 - perSystem = 4 - { pkgs, self', ... }: 5 - let 6 - python = pkgs.python3.withPackages (_ps: [ 7 - self'.packages.vosk 8 - ]); 9 - in 10 - { 11 - packages.stt = pkgs.stdenv.mkDerivation { 12 - pname = "stt"; 13 - version = "0.4.0"; 14 - src = ./.; 15 - nativeBuildInputs = [ pkgs.makeWrapper ]; 16 - installPhase = '' 17 - runHook preInstall 18 - mkdir -p $out/bin 19 - cp stt.py $out/bin/stt 20 - chmod +x $out/bin/stt 21 - wrapProgram $out/bin/stt \ 22 - --prefix PATH : ${python}/bin \ 23 - --set PYTHONPATH ${python}/${python.sitePackages} 24 - runHook postInstall 25 - ''; 26 - passthru.usage = builtins.readFile ./usage.kdl; 27 - meta.mainProgram = "stt"; 28 - }; 29 - }; 30 - }
-237
tools/stt/stt.py
··· 1 - #!/usr/bin/env python3 2 - """ 3 - Real-time streaming speech-to-text using Vosk. 4 - Reads audio from stdin and outputs words as they're recognized. 5 - """ 6 - 7 - import argparse 8 - import json 9 - import os 10 - import struct 11 - import sys 12 - import zipfile 13 - from pathlib import Path 14 - from urllib.request import urlretrieve 15 - 16 - from vosk import KaldiRecognizer, Model, SetLogLevel 17 - 18 - MODEL_DIR = Path(os.environ.get("XDG_DATA_HOME", Path.home() / ".local/share")) / "vosk" 19 - 20 - # Models organized by size: small (~40MB), medium (~128MB), large (~1-2GB) 21 - MODELS = { 22 - "en": { 23 - "small": "vosk-model-small-en-us-0.15", 24 - "medium": "vosk-model-en-us-0.22-lgraph", 25 - "large": "vosk-model-en-us-0.22", 26 - }, 27 - "de": { 28 - "small": "vosk-model-small-de-0.15", 29 - "medium": "vosk-model-de-0.21", 30 - "large": "vosk-model-de-0.21", 31 - }, 32 - "fr": { 33 - "small": "vosk-model-small-fr-0.22", 34 - "medium": "vosk-model-fr-0.22", 35 - "large": "vosk-model-fr-0.22", 36 - }, 37 - "es": { 38 - "small": "vosk-model-small-es-0.42", 39 - "medium": "vosk-model-es-0.42", 40 - "large": "vosk-model-es-0.42", 41 - }, 42 - "ru": { 43 - "small": "vosk-model-small-ru-0.22", 44 - "medium": "vosk-model-ru-0.42", 45 - "large": "vosk-model-ru-0.42", 46 - }, 47 - "cn": { 48 - "small": "vosk-model-small-cn-0.22", 49 - "medium": "vosk-model-cn-0.22", 50 - "large": "vosk-model-cn-0.22", 51 - }, 52 - "ja": { 53 - "small": "vosk-model-small-ja-0.22", 54 - "medium": "vosk-model-ja-0.22", 55 - "large": "vosk-model-ja-0.22", 56 - }, 57 - } 58 - 59 - 60 - def download_model(lang: str, size: str = "small") -> Path: 61 - """Download vosk model if not present.""" 62 - lang_models = MODELS.get(lang) 63 - if not lang_models: 64 - # Fall back to English if language not available 65 - print(f"No model for '{lang}', using English", file=sys.stderr) 66 - lang_models = MODELS["en"] 67 - lang = "en" 68 - 69 - model_name = lang_models.get(size, lang_models["small"]) 70 - 71 - model_path = MODEL_DIR / model_name 72 - if model_path.exists(): 73 - return model_path 74 - 75 - print(f"Downloading model {model_name}...", file=sys.stderr) 76 - MODEL_DIR.mkdir(parents=True, exist_ok=True) 77 - 78 - url = f"https://alphacephei.com/vosk/models/{model_name}.zip" 79 - zip_path = MODEL_DIR / f"{model_name}.zip" 80 - 81 - urlretrieve(url, zip_path) 82 - 83 - print("Extracting...", file=sys.stderr) 84 - with zipfile.ZipFile(zip_path, "r") as z: 85 - z.extractall(MODEL_DIR) 86 - zip_path.unlink() 87 - 88 - print("Model ready", file=sys.stderr) 89 - return model_path 90 - 91 - 92 - def parse_wav_header(data: bytes) -> tuple[int, int, int, int]: 93 - """Parse WAV header, return (sample_rate, channels, bits_per_sample, data_offset).""" 94 - if len(data) < 44 or data[:4] != b"RIFF" or data[8:12] != b"WAVE": 95 - raise ValueError("Invalid WAV header") 96 - 97 - pos = 12 98 - sample_rate = channels = bits_per_sample = 0 99 - while pos < len(data) - 8: 100 - chunk_id = data[pos : pos + 4] 101 - chunk_size = struct.unpack("<I", data[pos + 4 : pos + 8])[0] 102 - if chunk_id == b"fmt ": 103 - channels = struct.unpack("<H", data[pos + 10 : pos + 12])[0] 104 - sample_rate = struct.unpack("<I", data[pos + 12 : pos + 16])[0] 105 - bits_per_sample = struct.unpack("<H", data[pos + 22 : pos + 24])[0] 106 - elif chunk_id == b"data": 107 - return sample_rate, channels, bits_per_sample, pos + 8 108 - pos += 8 + chunk_size 109 - 110 - raise ValueError("No data chunk found") 111 - 112 - 113 - def convert_audio(data: bytes, in_rate: int, in_channels: int, in_bits: int) -> bytes: 114 - """Convert audio to 16kHz mono 16-bit for Vosk.""" 115 - import array 116 - 117 - # Align data to sample boundary 118 - bytes_per_sample = in_bits // 8 119 - frame_size = in_channels * bytes_per_sample 120 - aligned_len = (len(data) // frame_size) * frame_size 121 - if aligned_len == 0: 122 - return b"" 123 - data = data[:aligned_len] 124 - 125 - # Convert to samples 126 - if in_bits == 16: 127 - samples = array.array("h", data) 128 - elif in_bits == 8: 129 - samples = array.array("h", [(s - 128) * 256 for s in data]) 130 - else: 131 - raise ValueError(f"Unsupported bit depth: {in_bits}") 132 - 133 - # Convert to mono 134 - if in_channels == 2: 135 - mono = array.array("h") 136 - for i in range(0, len(samples) - 1, 2): 137 - mono.append((samples[i] + samples[i + 1]) // 2) 138 - samples = mono 139 - 140 - # Resample to 16kHz if needed 141 - if in_rate != 16000: 142 - ratio = 16000 / in_rate 143 - new_len = int(len(samples) * ratio) 144 - resampled = array.array("h") 145 - for i in range(new_len): 146 - src_idx = min(int(i / ratio), len(samples) - 1) 147 - resampled.append(samples[src_idx]) 148 - samples = resampled 149 - 150 - return samples.tobytes() 151 - 152 - 153 - def main(): 154 - parser = argparse.ArgumentParser(description="Real-time STT using Vosk") 155 - parser.add_argument( 156 - "-l", 157 - "--language", 158 - default=os.environ.get("VOSK_LANG", "en"), 159 - help="Language: en, de, fr, es, ru, cn, ja (default: en)", 160 - ) 161 - parser.add_argument( 162 - "-s", 163 - "--size", 164 - default=os.environ.get("VOSK_SIZE", "small"), 165 - choices=["small", "medium", "large"], 166 - help="Model size: small (~40MB), medium (~128MB), large (~1-2GB) (default: small)", 167 - ) 168 - parser.add_argument( 169 - "-w", 170 - "--words", 171 - action="store_true", 172 - help="Output individual words as they're recognized", 173 - ) 174 - args = parser.parse_args() 175 - 176 - # Suppress Vosk logging 177 - SetLogLevel(-1) 178 - 179 - model_path = download_model(args.language, args.size) 180 - model = Model(str(model_path)) 181 - 182 - # Read WAV header 183 - stdin = sys.stdin.buffer 184 - header_buf = stdin.read(4096) 185 - sample_rate, channels, bits, data_offset = parse_wav_header(header_buf) 186 - 187 - print(f"Audio: {sample_rate}Hz, {channels}ch, {bits}bit", file=sys.stderr) 188 - print("Listening...", file=sys.stderr) 189 - 190 - # Create recognizer for 16kHz audio 191 - rec = KaldiRecognizer(model, 16000) 192 - rec.SetWords(True) 193 - 194 - # Process initial audio from header buffer 195 - initial_audio = header_buf[data_offset:] 196 - if initial_audio: 197 - converted = convert_audio(initial_audio, sample_rate, channels, bits) 198 - rec.AcceptWaveform(converted) 199 - 200 - # Process audio stream 201 - chunk_size = int(sample_rate * channels * (bits // 8) * 0.1) # 100ms chunks 202 - 203 - while True: 204 - data = stdin.read(chunk_size) 205 - if not data: 206 - break 207 - 208 - converted = convert_audio(data, sample_rate, channels, bits) 209 - 210 - if rec.AcceptWaveform(converted): 211 - result = json.loads(rec.Result()) 212 - text = result.get("text", "") 213 - if text: 214 - if args.words: 215 - # Show final sentence on stderr with period and newline 216 - print(f"\r\033[K{text}.", file=sys.stderr, flush=True) 217 - print(text, flush=True) 218 - elif args.words: 219 - partial = json.loads(rec.PartialResult()) 220 - text = partial.get("partial", "") 221 - if text: 222 - # Show partial with corrections on stderr 223 - print(f"\r\033[K{text}", end="", file=sys.stderr, flush=True) 224 - 225 - # Final result 226 - result = json.loads(rec.FinalResult()) 227 - text = result.get("text", "") 228 - if text: 229 - if args.words: 230 - print(f"\r\033[K{text}.", file=sys.stderr, flush=True) 231 - print(text, flush=True) 232 - 233 - print("Stream ended", file=sys.stderr) 234 - 235 - 236 - if __name__ == "__main__": 237 - main()
-6
tools/stt/usage.kdl
··· 1 - bin "stt" 2 - about "Real-time streaming speech-to-text using Vosk" 3 - 4 - flag "-l --language <lang>" help="Language: en, de, fr, es, ru, cn, ja (default: en)" env="VOSK_LANG" 5 - flag "-s --size <size>" help="Model size: small, medium, large (default: small)" env="VOSK_SIZE" 6 - flag "-w --words" help="Output individual words as they are recognized"