stackvox 0.13.0__tar.gz → 0.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {stackvox-0.13.0 → stackvox-0.14.0}/NOTICE +25 -0
- {stackvox-0.13.0/stackvox.egg-info → stackvox-0.14.0}/PKG-INFO +142 -5
- {stackvox-0.13.0 → stackvox-0.14.0}/README.md +138 -4
- {stackvox-0.13.0 → stackvox-0.14.0}/pyproject.toml +8 -2
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox/__init__.py +2 -2
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox/cli.py +127 -15
- stackvox-0.14.0/stackvox/config.py +150 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox/daemon.py +186 -11
- stackvox-0.14.0/stackvox/engine.py +692 -0
- stackvox-0.14.0/stackvox/mlx_backend.py +261 -0
- stackvox-0.14.0/stackvox/voices.py +89 -0
- {stackvox-0.13.0 → stackvox-0.14.0/stackvox.egg-info}/PKG-INFO +142 -5
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox.egg-info/SOURCES.txt +3 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox.egg-info/requires.txt +6 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/tests/test_cli.py +260 -8
- {stackvox-0.13.0 → stackvox-0.14.0}/tests/test_config.py +48 -2
- {stackvox-0.13.0 → stackvox-0.14.0}/tests/test_daemon.py +199 -4
- {stackvox-0.13.0 → stackvox-0.14.0}/tests/test_daemon_protocol.py +42 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/tests/test_engine.py +324 -1
- stackvox-0.14.0/tests/test_mlx_backend.py +335 -0
- stackvox-0.13.0/stackvox/config.py +0 -80
- stackvox-0.13.0/stackvox/engine.py +0 -375
- {stackvox-0.13.0 → stackvox-0.14.0}/LICENSE +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/setup.cfg +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox/__main__.py +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox/data/stackvox-say +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox/paths.py +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox/py.typed +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox/text.py +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox/updates.py +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox.egg-info/dependency_links.txt +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox.egg-info/entry_points.txt +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/stackvox.egg-info/top_level.txt +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/tests/test_paths.py +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/tests/test_text.py +0 -0
- {stackvox-0.13.0 → stackvox-0.14.0}/tests/test_updates.py +0 -0
|
@@ -60,3 +60,28 @@ phonemizer-fork
|
|
|
60
60
|
runtime. Downstream users who distribute a combined work that includes
|
|
61
61
|
phonemizer-fork should review GPL-3.0 obligations for their own
|
|
62
62
|
distribution.
|
|
63
|
+
|
|
64
|
+
--------------------------------------------------------------------------------
|
|
65
|
+
Optional MLX backend (`stackvox[mlx]`)
|
|
66
|
+
--------------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
The components below are used only when the optional MLX backend is installed
|
|
69
|
+
and selected. stackvox does not download, bundle, modify, or redistribute any
|
|
70
|
+
MLX model; the user supplies a local model directory, and that model's own
|
|
71
|
+
license governs its use.
|
|
72
|
+
|
|
73
|
+
mlx-audio
|
|
74
|
+
Copyright (c) 2024 Prince Canuma
|
|
75
|
+
Licensed under the MIT License
|
|
76
|
+
https://github.com/Blaizzy/mlx-audio
|
|
77
|
+
|
|
78
|
+
mlx and mlx-lm
|
|
79
|
+
Copyright © 2023 Apple Inc.
|
|
80
|
+
Licensed under the MIT License
|
|
81
|
+
https://github.com/ml-explore/mlx
|
|
82
|
+
https://github.com/ml-explore/mlx-lm
|
|
83
|
+
|
|
84
|
+
transformers
|
|
85
|
+
Copyright 2018- The Hugging Face team. All rights reserved.
|
|
86
|
+
Licensed under the Apache License, Version 2.0
|
|
87
|
+
https://github.com/huggingface/transformers
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stackvox
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.0
|
|
4
4
|
Summary: Offline TTS library using Kokoro-82M
|
|
5
5
|
Author-email: "StackOne Technologies Ltd." <support@stackone.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -18,10 +18,13 @@ Description-Content-Type: text/markdown
|
|
|
18
18
|
License-File: LICENSE
|
|
19
19
|
License-File: NOTICE
|
|
20
20
|
Requires-Dist: kokoro-onnx>=0.4.0
|
|
21
|
+
Requires-Dist: onnxruntime>=1.20.1
|
|
21
22
|
Requires-Dist: soundfile>=0.12.1
|
|
22
23
|
Requires-Dist: sounddevice>=0.4.6
|
|
23
24
|
Requires-Dist: numpy>=1.24
|
|
24
25
|
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
26
|
+
Provides-Extra: mlx
|
|
27
|
+
Requires-Dist: mlx-audio<0.5,>=0.4.5; (platform_system == "Darwin" and platform_machine == "arm64") and extra == "mlx"
|
|
25
28
|
Provides-Extra: dev
|
|
26
29
|
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
27
30
|
Requires-Dist: pytest-mock>=3.12; extra == "dev"
|
|
@@ -131,7 +134,7 @@ speed = 1.1
|
|
|
131
134
|
lang = "en-gb"
|
|
132
135
|
```
|
|
133
136
|
|
|
134
|
-
CLI flags always win over config values, and config values always win over the built-in defaults. A missing file is fine — built-ins apply. A malformed file logs a warning and is ignored.
|
|
137
|
+
CLI flags always win over config values, and config values always win over the built-in defaults. The one thing between a flag and the config is a [voice mix](#voice-mixes)'s own `lang` and `speed`, so `--voice chip` stays transatlantic even when `[defaults]` sets `lang = "en-us"`. A missing file is fine — built-ins apply. A malformed file logs a warning and is ignored.
|
|
135
138
|
|
|
136
139
|
## Daemon mode
|
|
137
140
|
|
|
@@ -141,7 +144,7 @@ Keeps the model resident so each subsequent call is instant:
|
|
|
141
144
|
stackvox serve # foreground; run with `nohup stackvox serve &` to background
|
|
142
145
|
stackvox status # is the daemon up? also shows version + any pending PyPI update
|
|
143
146
|
stackvox say "Hello" # send text to the daemon (fails if not running)
|
|
144
|
-
stackvox stop # graceful shutdown
|
|
147
|
+
stackvox stop # graceful shutdown, force-killed if it acknowledges but is still up after 5s
|
|
145
148
|
```
|
|
146
149
|
|
|
147
150
|
stackvox checks PyPI for newer versions but only at two moments — when you run `stackvox status` and at daemon startup. The script-heavy paths (`say`, `speak`, `stackvox-say`, hooks, CI) never make a network call. To see notices on every invocation set `STACKVOX_UPDATE_NOTICE=1`. To disable the check entirely set `STACKVOX_NO_UPDATE_CHECK=1`. The check is auto-skipped when common CI env vars (`CI`, `GITHUB_ACTIONS`, etc.) are set so build logs stay clean.
|
|
@@ -161,7 +164,7 @@ Exit codes: `0` ok, `2` daemon unreachable (unless `--fallback-say` was given).
|
|
|
161
164
|
## Python library
|
|
162
165
|
|
|
163
166
|
```python
|
|
164
|
-
from stackvox import Stackvox, speak, synthesize
|
|
167
|
+
from stackvox import Stackvox, speak, synthesize, synthesize_stream
|
|
165
168
|
|
|
166
169
|
# One-shot — model loads on first call, reused for subsequent calls.
|
|
167
170
|
speak("Hello world")
|
|
@@ -171,6 +174,10 @@ tts = Stackvox(voice="af_bella")
|
|
|
171
174
|
tts.speak("First line")
|
|
172
175
|
tts.speak("Faster", speed=1.2)
|
|
173
176
|
|
|
177
|
+
# Cap ONNX Runtime's thread pools — it otherwise sizes them from the host's core
|
|
178
|
+
# count, which oversubscribes inside a container with a smaller CPU quota.
|
|
179
|
+
tts = Stackvox(threads=4)
|
|
180
|
+
|
|
174
181
|
# Non-blocking playback.
|
|
175
182
|
tts.speak("async", blocking=False)
|
|
176
183
|
tts.stop()
|
|
@@ -178,6 +185,11 @@ tts.stop()
|
|
|
178
185
|
# Raw samples for custom processing.
|
|
179
186
|
samples, sr = tts.synthesize("give me the array")
|
|
180
187
|
|
|
188
|
+
# Streaming: one chunk per sentence, yielded as soon as it is synthesized, so
|
|
189
|
+
# a caller encoding audio itself waits one sentence rather than the whole text.
|
|
190
|
+
for chunk, sr in tts.synthesize_stream("First sentence. Second sentence."):
|
|
191
|
+
encode_and_send(chunk, sr)
|
|
192
|
+
|
|
181
193
|
# Gapless multi-line playback with concurrent synthesis.
|
|
182
194
|
tts.speak_sequence([
|
|
183
195
|
{"text": "Hello", "voice": "af_heart", "lang": "en-us"},
|
|
@@ -195,6 +207,100 @@ if daemon.is_running():
|
|
|
195
207
|
daemon.stop()
|
|
196
208
|
```
|
|
197
209
|
|
|
210
|
+
## Local MLX models (Apple Silicon)
|
|
211
|
+
|
|
212
|
+
The optional `mlx` backend runs any text-to-speech model that
|
|
213
|
+
[MLX Audio](https://github.com/Blaizzy/mlx-audio) can load from a local
|
|
214
|
+
directory, including ones that support voice cloning and style instructions.
|
|
215
|
+
Kokoro (ONNX) remains the default backend and dependency set.
|
|
216
|
+
|
|
217
|
+
```bash
|
|
218
|
+
pip install 'stackvox[mlx]'
|
|
219
|
+
|
|
220
|
+
stackvox speak \
|
|
221
|
+
--backend mlx \
|
|
222
|
+
--model-dir /path/to/model \
|
|
223
|
+
--reference-audio /path/to/reference.wav \
|
|
224
|
+
--reference-text "The exact transcript of the reference audio." \
|
|
225
|
+
--out cloned.wav \
|
|
226
|
+
"Hello from a local MLX model."
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
> **Model licenses:** stackvox never downloads, bundles, or recommends MLX
|
|
230
|
+
> models. You supply the model directory, and its license (which may restrict
|
|
231
|
+
> commercial use) applies to your use of it. Some models also need their own
|
|
232
|
+
> extra Python packages; MLX Audio names them if they are missing.
|
|
233
|
+
|
|
234
|
+
Keep a large model resident for repeated requests by selecting it when the
|
|
235
|
+
daemon starts. The backend and model cannot be changed without restarting the
|
|
236
|
+
daemon. Generation options given to `serve` become the daemon's defaults, and
|
|
237
|
+
any `say` request can override them:
|
|
238
|
+
|
|
239
|
+
```bash
|
|
240
|
+
stackvox serve --backend mlx --model-dir /path/to/model
|
|
241
|
+
|
|
242
|
+
stackvox say \
|
|
243
|
+
--reference-audio /path/to/reference.wav \
|
|
244
|
+
--reference-text "The exact transcript of the reference audio." \
|
|
245
|
+
"This request uses the resident model."
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Generation options (`--reference-audio`, `--reference-text`, `--instruct`,
|
|
249
|
+
`--temperature`, `--top-p`, `--top-k`) are forwarded only when set, so each
|
|
250
|
+
model's own defaults apply otherwise; which ones a model honours depends on
|
|
251
|
+
the model. They are rejected with an error unless the `mlx` backend is in use,
|
|
252
|
+
both by `speak` and by a daemon running Kokoro. `--voice` and `--lang` apply to
|
|
253
|
+
Kokoro only. Playback streams in chunks of about two sentences (up to ~170
|
|
254
|
+
characters), so the first audio arrives once the first chunk is generated;
|
|
255
|
+
short chunks also stop models drifting off-script on long text. Models slower
|
|
256
|
+
than real time will pause between chunks.
|
|
257
|
+
|
|
258
|
+
### Model adapters
|
|
259
|
+
|
|
260
|
+
Some models need custom loading, such as renamed weights, a codec path, or a
|
|
261
|
+
tokenizer quirk. Rather than carrying model-specific code, stackvox accepts an
|
|
262
|
+
adapter script that you write and keep locally, for example under
|
|
263
|
+
`~/.config/stackvox/adapters/`:
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
# my_adapter.py
|
|
267
|
+
from pathlib import Path
|
|
268
|
+
from stackvox.mlx_backend import swapped
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def load(model_dir: Path, load_model):
|
|
272
|
+
# Patch whatever the model needs for the duration of the load, then
|
|
273
|
+
# delegate to MLX Audio's loader (or replace it entirely).
|
|
274
|
+
from some_library import SomeClass
|
|
275
|
+
|
|
276
|
+
with swapped(SomeClass, "some_method", lambda *args, **kwargs: ...):
|
|
277
|
+
return load_model(model_dir)
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
```bash
|
|
281
|
+
stackvox speak --backend mlx --model-dir /path/to/model \
|
|
282
|
+
--model-adapter ~/.config/stackvox/adapters/my_adapter.py "Hello"
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
`load(model_dir, load_model)` must return the loaded model. `swapped` patches
|
|
286
|
+
an attribute and restores it afterwards, even on error. Adapters run as normal
|
|
287
|
+
Python with your privileges and are only used when passed explicitly; nothing
|
|
288
|
+
is picked up from a model directory.
|
|
289
|
+
|
|
290
|
+
The Python API uses the same waveform contract as Kokoro:
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
from pathlib import Path
|
|
294
|
+
from stackvox import Stackvox
|
|
295
|
+
|
|
296
|
+
tts = Stackvox(backend="mlx", model_dir=Path("/path/to/model"))
|
|
297
|
+
samples, sample_rate = tts.synthesize(
|
|
298
|
+
"A calm and natural delivery.",
|
|
299
|
+
reference_audio=Path("reference.wav"),
|
|
300
|
+
reference_text="The reference transcript.",
|
|
301
|
+
)
|
|
302
|
+
```
|
|
303
|
+
|
|
198
304
|
## Voices
|
|
199
305
|
|
|
200
306
|
Kokoro ships voices across several languages. Voice prefix encodes gender + language:
|
|
@@ -211,7 +317,34 @@ Kokoro ships voices across several languages. Voice prefix encodes gender + lang
|
|
|
211
317
|
| `jf_*`, `jm_*` | Japanese | `jf_alpha` |
|
|
212
318
|
| `zf_*`, `zm_*` | Mandarin Chinese | `zf_xiaoxiao` |
|
|
213
319
|
|
|
214
|
-
Run `stackvox voices` for the authoritative list.
|
|
320
|
+
Run `stackvox voices` for the authoritative list, which includes the voice mixes below.
|
|
321
|
+
|
|
322
|
+
### Voice mixes
|
|
323
|
+
|
|
324
|
+
Each Kokoro voice is a style vector, so a weighted blend of several makes a new voice between them. A mix is named, and the name works anywhere a voice id does: `--voice`, `Stackvox(voice=...)`, and the daemon's socket protocol. It also carries the `lang` and `speed` it was tuned with, which apply unless you pass your own.
|
|
325
|
+
|
|
326
|
+
Two ship built in, a transatlantic newsreader pair (American delivery, British vowels):
|
|
327
|
+
|
|
328
|
+
| Name | Mix | lang | speed |
|
|
329
|
+
| -------- | ----------------------------------------------------- | ------- | ----- |
|
|
330
|
+
| `chip` | `am_liam` 0.6, `bm_daniel` 0.4 | `en-gb` | 1.0 |
|
|
331
|
+
| `ramona` | `af_aoede` 0.5, `af_bella` 0.2, `bf_emma` 0.3 | `en-gb` | 1.0 |
|
|
332
|
+
|
|
333
|
+
```bash
|
|
334
|
+
stackvox speak --voice chip "Good evening, and welcome to the news."
|
|
335
|
+
stackvox say --voice ramona "Thanks Chip."
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Define your own in the config file. Weights needn't sum to 1, and a table named after a built-in replaces it:
|
|
339
|
+
|
|
340
|
+
```toml
|
|
341
|
+
[voices.narrator]
|
|
342
|
+
mix = { am_michael = 0.6, bm_george = 0.4 }
|
|
343
|
+
lang = "en-gb" # optional
|
|
344
|
+
speed = 1.0 # optional
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
The daemon reads mixes when it starts, so restart it (`stackvox stop`, then `stackvox serve`) after editing them.
|
|
215
348
|
|
|
216
349
|
## Architecture
|
|
217
350
|
|
|
@@ -234,6 +367,8 @@ Socket lives at `~/.cache/stackvox/daemon.sock` (override with `STACKVOX_SOCKET`
|
|
|
234
367
|
|
|
235
368
|
Queue depth is 2 — rapid-fire requests beyond that get `busy` rather than piling up.
|
|
236
369
|
|
|
370
|
+
If playback hangs the daemon exits rather than answering `busy` forever. Each utterance gets 60s plus half a second per character, slower than any voice reads it even at speed 0.5. Past that the daemon removes its socket and exits with status 70, so `say` reports it isn't running and anything that starts the daemon on demand gets a fresh one. On macOS this has been PortAudio deadlocked stopping a stream, or stuck writing to an output device that stopped taking audio, and `cancel` can't break either.
|
|
371
|
+
|
|
237
372
|
Before each utterance the daemon resets PortAudio so it picks up the current system default output device. Swap from speakers to Bluetooth headphones mid-session and the next `say` follows you — no daemon restart needed. The refresh costs ~10–50ms per play, which is invisible next to synthesis time.
|
|
238
373
|
|
|
239
374
|
## Requirements
|
|
@@ -277,4 +412,6 @@ stackvox itself is licensed under the **Apache License, Version 2.0** — see [`
|
|
|
277
412
|
|
|
278
413
|
**Runtime dependencies.** [`kokoro-onnx`](https://github.com/thewh1teagle/kokoro-onnx) (MIT, © thewh1teagle), [`onnxruntime`](https://github.com/microsoft/onnxruntime) (MIT, © Microsoft), [`sounddevice`](https://python-sounddevice.readthedocs.io/) (MIT, © Matthias Geier), [`soundfile`](https://github.com/bastibe/python-soundfile) (BSD-3, © Bastian Bechtold), [`numpy`](https://numpy.org/) (BSD-3).
|
|
279
414
|
|
|
415
|
+
**Optional MLX backend.** `stackvox[mlx]` runs on [`mlx-audio`](https://github.com/Blaizzy/mlx-audio) (MIT, © Prince Canuma), [`mlx`](https://github.com/ml-explore/mlx) and [`mlx-lm`](https://github.com/ml-explore/mlx-lm) (MIT, © Apple), and [`transformers`](https://github.com/huggingface/transformers) (Apache 2.0, © Hugging Face). stackvox does not download, bundle, or redistribute any MLX model; the license of whichever model you supply governs your use of it.
|
|
416
|
+
|
|
280
417
|
**GPL note.** `kokoro-onnx` pulls in [`phonemizer-fork`](https://github.com/bootphon/phonemizer) as a transitive runtime dependency; it is licensed under **GPL-3.0**. stackvox does not bundle, modify, or statically link it — pip installs it alongside stackvox and the two communicate through phonemizer's published Python API at runtime. If you redistribute a combined work (e.g. a frozen binary, container image, or vendored wheel set) that includes phonemizer-fork, review GPL-3.0 obligations for that distribution.
|
|
@@ -97,7 +97,7 @@ speed = 1.1
|
|
|
97
97
|
lang = "en-gb"
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
-
CLI flags always win over config values, and config values always win over the built-in defaults. A missing file is fine — built-ins apply. A malformed file logs a warning and is ignored.
|
|
100
|
+
CLI flags always win over config values, and config values always win over the built-in defaults. The one thing between a flag and the config is a [voice mix](#voice-mixes)'s own `lang` and `speed`, so `--voice chip` stays transatlantic even when `[defaults]` sets `lang = "en-us"`. A missing file is fine — built-ins apply. A malformed file logs a warning and is ignored.
|
|
101
101
|
|
|
102
102
|
## Daemon mode
|
|
103
103
|
|
|
@@ -107,7 +107,7 @@ Keeps the model resident so each subsequent call is instant:
|
|
|
107
107
|
stackvox serve # foreground; run with `nohup stackvox serve &` to background
|
|
108
108
|
stackvox status # is the daemon up? also shows version + any pending PyPI update
|
|
109
109
|
stackvox say "Hello" # send text to the daemon (fails if not running)
|
|
110
|
-
stackvox stop # graceful shutdown
|
|
110
|
+
stackvox stop # graceful shutdown, force-killed if it acknowledges but is still up after 5s
|
|
111
111
|
```
|
|
112
112
|
|
|
113
113
|
stackvox checks PyPI for newer versions but only at two moments — when you run `stackvox status` and at daemon startup. The script-heavy paths (`say`, `speak`, `stackvox-say`, hooks, CI) never make a network call. To see notices on every invocation set `STACKVOX_UPDATE_NOTICE=1`. To disable the check entirely set `STACKVOX_NO_UPDATE_CHECK=1`. The check is auto-skipped when common CI env vars (`CI`, `GITHUB_ACTIONS`, etc.) are set so build logs stay clean.
|
|
@@ -127,7 +127,7 @@ Exit codes: `0` ok, `2` daemon unreachable (unless `--fallback-say` was given).
|
|
|
127
127
|
## Python library
|
|
128
128
|
|
|
129
129
|
```python
|
|
130
|
-
from stackvox import Stackvox, speak, synthesize
|
|
130
|
+
from stackvox import Stackvox, speak, synthesize, synthesize_stream
|
|
131
131
|
|
|
132
132
|
# One-shot — model loads on first call, reused for subsequent calls.
|
|
133
133
|
speak("Hello world")
|
|
@@ -137,6 +137,10 @@ tts = Stackvox(voice="af_bella")
|
|
|
137
137
|
tts.speak("First line")
|
|
138
138
|
tts.speak("Faster", speed=1.2)
|
|
139
139
|
|
|
140
|
+
# Cap ONNX Runtime's thread pools — it otherwise sizes them from the host's core
|
|
141
|
+
# count, which oversubscribes inside a container with a smaller CPU quota.
|
|
142
|
+
tts = Stackvox(threads=4)
|
|
143
|
+
|
|
140
144
|
# Non-blocking playback.
|
|
141
145
|
tts.speak("async", blocking=False)
|
|
142
146
|
tts.stop()
|
|
@@ -144,6 +148,11 @@ tts.stop()
|
|
|
144
148
|
# Raw samples for custom processing.
|
|
145
149
|
samples, sr = tts.synthesize("give me the array")
|
|
146
150
|
|
|
151
|
+
# Streaming: one chunk per sentence, yielded as soon as it is synthesized, so
|
|
152
|
+
# a caller encoding audio itself waits one sentence rather than the whole text.
|
|
153
|
+
for chunk, sr in tts.synthesize_stream("First sentence. Second sentence."):
|
|
154
|
+
encode_and_send(chunk, sr)
|
|
155
|
+
|
|
147
156
|
# Gapless multi-line playback with concurrent synthesis.
|
|
148
157
|
tts.speak_sequence([
|
|
149
158
|
{"text": "Hello", "voice": "af_heart", "lang": "en-us"},
|
|
@@ -161,6 +170,100 @@ if daemon.is_running():
|
|
|
161
170
|
daemon.stop()
|
|
162
171
|
```
|
|
163
172
|
|
|
173
|
+
## Local MLX models (Apple Silicon)
|
|
174
|
+
|
|
175
|
+
The optional `mlx` backend runs any text-to-speech model that
|
|
176
|
+
[MLX Audio](https://github.com/Blaizzy/mlx-audio) can load from a local
|
|
177
|
+
directory, including ones that support voice cloning and style instructions.
|
|
178
|
+
Kokoro (ONNX) remains the default backend and dependency set.
|
|
179
|
+
|
|
180
|
+
```bash
|
|
181
|
+
pip install 'stackvox[mlx]'
|
|
182
|
+
|
|
183
|
+
stackvox speak \
|
|
184
|
+
--backend mlx \
|
|
185
|
+
--model-dir /path/to/model \
|
|
186
|
+
--reference-audio /path/to/reference.wav \
|
|
187
|
+
--reference-text "The exact transcript of the reference audio." \
|
|
188
|
+
--out cloned.wav \
|
|
189
|
+
"Hello from a local MLX model."
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
> **Model licenses:** stackvox never downloads, bundles, or recommends MLX
|
|
193
|
+
> models. You supply the model directory, and its license (which may restrict
|
|
194
|
+
> commercial use) applies to your use of it. Some models also need their own
|
|
195
|
+
> extra Python packages; MLX Audio names them if they are missing.
|
|
196
|
+
|
|
197
|
+
Keep a large model resident for repeated requests by selecting it when the
|
|
198
|
+
daemon starts. The backend and model cannot be changed without restarting the
|
|
199
|
+
daemon. Generation options given to `serve` become the daemon's defaults, and
|
|
200
|
+
any `say` request can override them:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
stackvox serve --backend mlx --model-dir /path/to/model
|
|
204
|
+
|
|
205
|
+
stackvox say \
|
|
206
|
+
--reference-audio /path/to/reference.wav \
|
|
207
|
+
--reference-text "The exact transcript of the reference audio." \
|
|
208
|
+
"This request uses the resident model."
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Generation options (`--reference-audio`, `--reference-text`, `--instruct`,
|
|
212
|
+
`--temperature`, `--top-p`, `--top-k`) are forwarded only when set, so each
|
|
213
|
+
model's own defaults apply otherwise; which ones a model honours depends on
|
|
214
|
+
the model. They are rejected with an error unless the `mlx` backend is in use,
|
|
215
|
+
both by `speak` and by a daemon running Kokoro. `--voice` and `--lang` apply to
|
|
216
|
+
Kokoro only. Playback streams in chunks of about two sentences (up to ~170
|
|
217
|
+
characters), so the first audio arrives once the first chunk is generated;
|
|
218
|
+
short chunks also stop models drifting off-script on long text. Models slower
|
|
219
|
+
than real time will pause between chunks.
|
|
220
|
+
|
|
221
|
+
### Model adapters
|
|
222
|
+
|
|
223
|
+
Some models need custom loading, such as renamed weights, a codec path, or a
|
|
224
|
+
tokenizer quirk. Rather than carrying model-specific code, stackvox accepts an
|
|
225
|
+
adapter script that you write and keep locally, for example under
|
|
226
|
+
`~/.config/stackvox/adapters/`:
|
|
227
|
+
|
|
228
|
+
```python
|
|
229
|
+
# my_adapter.py
|
|
230
|
+
from pathlib import Path
|
|
231
|
+
from stackvox.mlx_backend import swapped
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def load(model_dir: Path, load_model):
|
|
235
|
+
# Patch whatever the model needs for the duration of the load, then
|
|
236
|
+
# delegate to MLX Audio's loader (or replace it entirely).
|
|
237
|
+
from some_library import SomeClass
|
|
238
|
+
|
|
239
|
+
with swapped(SomeClass, "some_method", lambda *args, **kwargs: ...):
|
|
240
|
+
return load_model(model_dir)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
stackvox speak --backend mlx --model-dir /path/to/model \
|
|
245
|
+
--model-adapter ~/.config/stackvox/adapters/my_adapter.py "Hello"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
`load(model_dir, load_model)` must return the loaded model. `swapped` patches
|
|
249
|
+
an attribute and restores it afterwards, even on error. Adapters run as normal
|
|
250
|
+
Python with your privileges and are only used when passed explicitly; nothing
|
|
251
|
+
is picked up from a model directory.
|
|
252
|
+
|
|
253
|
+
The Python API uses the same waveform contract as Kokoro:
|
|
254
|
+
|
|
255
|
+
```python
|
|
256
|
+
from pathlib import Path
|
|
257
|
+
from stackvox import Stackvox
|
|
258
|
+
|
|
259
|
+
tts = Stackvox(backend="mlx", model_dir=Path("/path/to/model"))
|
|
260
|
+
samples, sample_rate = tts.synthesize(
|
|
261
|
+
"A calm and natural delivery.",
|
|
262
|
+
reference_audio=Path("reference.wav"),
|
|
263
|
+
reference_text="The reference transcript.",
|
|
264
|
+
)
|
|
265
|
+
```
|
|
266
|
+
|
|
164
267
|
## Voices
|
|
165
268
|
|
|
166
269
|
Kokoro ships voices across several languages. Voice prefix encodes gender + language:
|
|
@@ -177,7 +280,34 @@ Kokoro ships voices across several languages. Voice prefix encodes gender + lang
|
|
|
177
280
|
| `jf_*`, `jm_*` | Japanese | `jf_alpha` |
|
|
178
281
|
| `zf_*`, `zm_*` | Mandarin Chinese | `zf_xiaoxiao` |
|
|
179
282
|
|
|
180
|
-
Run `stackvox voices` for the authoritative list.
|
|
283
|
+
Run `stackvox voices` for the authoritative list, which includes the voice mixes below.
|
|
284
|
+
|
|
285
|
+
### Voice mixes
|
|
286
|
+
|
|
287
|
+
Each Kokoro voice is a style vector, so a weighted blend of several makes a new voice between them. A mix is named, and the name works anywhere a voice id does: `--voice`, `Stackvox(voice=...)`, and the daemon's socket protocol. It also carries the `lang` and `speed` it was tuned with, which apply unless you pass your own.
|
|
288
|
+
|
|
289
|
+
Two ship built in, a transatlantic newsreader pair (American delivery, British vowels):
|
|
290
|
+
|
|
291
|
+
| Name | Mix | lang | speed |
|
|
292
|
+
| -------- | ----------------------------------------------------- | ------- | ----- |
|
|
293
|
+
| `chip` | `am_liam` 0.6, `bm_daniel` 0.4 | `en-gb` | 1.0 |
|
|
294
|
+
| `ramona` | `af_aoede` 0.5, `af_bella` 0.2, `bf_emma` 0.3 | `en-gb` | 1.0 |
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
stackvox speak --voice chip "Good evening, and welcome to the news."
|
|
298
|
+
stackvox say --voice ramona "Thanks Chip."
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Define your own in the config file. Weights needn't sum to 1, and a table named after a built-in replaces it:
|
|
302
|
+
|
|
303
|
+
```toml
|
|
304
|
+
[voices.narrator]
|
|
305
|
+
mix = { am_michael = 0.6, bm_george = 0.4 }
|
|
306
|
+
lang = "en-gb" # optional
|
|
307
|
+
speed = 1.0 # optional
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
The daemon reads mixes when it starts, so restart it (`stackvox stop`, then `stackvox serve`) after editing them.
|
|
181
311
|
|
|
182
312
|
## Architecture
|
|
183
313
|
|
|
@@ -200,6 +330,8 @@ Socket lives at `~/.cache/stackvox/daemon.sock` (override with `STACKVOX_SOCKET`
|
|
|
200
330
|
|
|
201
331
|
Queue depth is 2 — rapid-fire requests beyond that get `busy` rather than piling up.
|
|
202
332
|
|
|
333
|
+
If playback hangs the daemon exits rather than answering `busy` forever. Each utterance gets 60s plus half a second per character, slower than any voice reads it even at speed 0.5. Past that the daemon removes its socket and exits with status 70, so `say` reports it isn't running and anything that starts the daemon on demand gets a fresh one. On macOS this has been PortAudio deadlocked stopping a stream, or stuck writing to an output device that stopped taking audio, and `cancel` can't break either.
|
|
334
|
+
|
|
203
335
|
Before each utterance the daemon resets PortAudio so it picks up the current system default output device. Swap from speakers to Bluetooth headphones mid-session and the next `say` follows you — no daemon restart needed. The refresh costs ~10–50ms per play, which is invisible next to synthesis time.
|
|
204
336
|
|
|
205
337
|
## Requirements
|
|
@@ -243,4 +375,6 @@ stackvox itself is licensed under the **Apache License, Version 2.0** — see [`
|
|
|
243
375
|
|
|
244
376
|
**Runtime dependencies.** [`kokoro-onnx`](https://github.com/thewh1teagle/kokoro-onnx) (MIT, © thewh1teagle), [`onnxruntime`](https://github.com/microsoft/onnxruntime) (MIT, © Microsoft), [`sounddevice`](https://python-sounddevice.readthedocs.io/) (MIT, © Matthias Geier), [`soundfile`](https://github.com/bastibe/python-soundfile) (BSD-3, © Bastian Bechtold), [`numpy`](https://numpy.org/) (BSD-3).
|
|
245
377
|
|
|
378
|
+
**Optional MLX backend.** `stackvox[mlx]` runs on [`mlx-audio`](https://github.com/Blaizzy/mlx-audio) (MIT, © Prince Canuma), [`mlx`](https://github.com/ml-explore/mlx) and [`mlx-lm`](https://github.com/ml-explore/mlx-lm) (MIT, © Apple), and [`transformers`](https://github.com/huggingface/transformers) (Apache 2.0, © Hugging Face). stackvox does not download, bundle, or redistribute any MLX model; the license of whichever model you supply governs your use of it.
|
|
379
|
+
|
|
246
380
|
**GPL note.** `kokoro-onnx` pulls in [`phonemizer-fork`](https://github.com/bootphon/phonemizer) as a transitive runtime dependency; it is licensed under **GPL-3.0**. stackvox does not bundle, modify, or statically link it — pip installs it alongside stackvox and the two communicate through phonemizer's published Python API at runtime. If you redistribute a combined work (e.g. a frozen binary, container image, or vendored wheel set) that includes phonemizer-fork, review GPL-3.0 obligations for that distribution.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "stackvox"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.14.0"
|
|
8
8
|
description = "Offline TTS library using Kokoro-82M"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -21,6 +21,7 @@ classifiers = [
|
|
|
21
21
|
]
|
|
22
22
|
dependencies = [
|
|
23
23
|
"kokoro-onnx>=0.4.0",
|
|
24
|
+
"onnxruntime>=1.20.1",
|
|
24
25
|
"soundfile>=0.12.1",
|
|
25
26
|
"sounddevice>=0.4.6",
|
|
26
27
|
"numpy>=1.24",
|
|
@@ -28,6 +29,11 @@ dependencies = [
|
|
|
28
29
|
]
|
|
29
30
|
|
|
30
31
|
[project.optional-dependencies]
|
|
32
|
+
# MLX Audio is pre-1.0, so cap at the next minor; the floor is the version
|
|
33
|
+
# verified end to end with stackvox's loader and generate() usage.
|
|
34
|
+
mlx = [
|
|
35
|
+
"mlx-audio>=0.4.5,<0.5; platform_system=='Darwin' and platform_machine=='arm64'",
|
|
36
|
+
]
|
|
31
37
|
dev = [
|
|
32
38
|
"pytest>=8.0",
|
|
33
39
|
"pytest-mock>=3.12",
|
|
@@ -124,7 +130,7 @@ disallow_untyped_defs = true
|
|
|
124
130
|
[[tool.mypy.overrides]]
|
|
125
131
|
# Third-party deps without inline type stubs (or, for `tomli`, a 3.10-only
|
|
126
132
|
# fallback that's not installed on the typechecker's Python).
|
|
127
|
-
module = ["kokoro_onnx", "sounddevice", "soundfile", "tomli"]
|
|
133
|
+
module = ["kokoro_onnx", "mlx_audio.*", "onnxruntime", "sounddevice", "soundfile", "tomli"]
|
|
128
134
|
ignore_missing_imports = true
|
|
129
135
|
|
|
130
136
|
[[tool.mypy.overrides]]
|
|
@@ -4,7 +4,7 @@ from importlib.metadata import PackageNotFoundError
|
|
|
4
4
|
from importlib.metadata import version as _pkg_version
|
|
5
5
|
|
|
6
6
|
from stackvox import daemon
|
|
7
|
-
from stackvox.engine import Stackvox, speak, synthesize
|
|
7
|
+
from stackvox.engine import Stackvox, speak, synthesize, synthesize_stream
|
|
8
8
|
from stackvox.text import normalize_for_speech
|
|
9
9
|
|
|
10
10
|
try:
|
|
@@ -14,4 +14,4 @@ except PackageNotFoundError:
|
|
|
14
14
|
# bootstrap before `pip install -e .` has completed).
|
|
15
15
|
__version__ = "0.0.0+unknown"
|
|
16
16
|
|
|
17
|
-
__all__ = ["Stackvox", "daemon", "normalize_for_speech", "speak", "synthesize"]
|
|
17
|
+
__all__ = ["Stackvox", "daemon", "normalize_for_speech", "speak", "synthesize", "synthesize_stream"]
|