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