vocalize-cli 0.8.0__tar.gz → 0.9.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.
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/.gitignore +1 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/CHANGELOG.md +69 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/PKG-INFO +203 -5
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/README.md +200 -4
- vocalize_cli-0.9.0/docs/provider-credentials.md +143 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/pyproject.toml +1 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/conftest.py +11 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_audio.py +101 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_auth.py +81 -0
- vocalize_cli-0.9.0/tests/test_cache.py +105 -0
- vocalize_cli-0.9.0/tests/test_chain.py +682 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_claude_stop_hook.py +7 -3
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_cli.py +672 -47
- vocalize_cli-0.9.0/tests/test_config.py +559 -0
- vocalize_cli-0.9.0/tests/test_elevenlabs_provider.py +228 -0
- vocalize_cli-0.9.0/tests/test_exceptions.py +77 -0
- vocalize_cli-0.9.0/tests/test_google_provider.py +337 -0
- vocalize_cli-0.9.0/tests/test_http.py +217 -0
- vocalize_cli-0.9.0/tests/test_kokoro_manifest.py +62 -0
- vocalize_cli-0.9.0/tests/test_kokoro_provider.py +485 -0
- vocalize_cli-0.9.0/tests/test_kokoro_worker.py +284 -0
- vocalize_cli-0.9.0/tests/test_ledger.py +193 -0
- vocalize_cli-0.9.0/tests/test_local_install.py +548 -0
- vocalize_cli-0.9.0/tests/test_openai_provider.py +259 -0
- vocalize_cli-0.9.0/tests/test_polly_provider.py +340 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_preprocess.py +8 -0
- vocalize_cli-0.9.0/tests/test_providers_registry.py +78 -0
- vocalize_cli-0.9.0/tests/test_say_provider.py +142 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_speak_options.py +9 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_tts.py +11 -1
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_wizard.py +147 -1
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/vocalize/__init__.py +1 -1
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/vocalize/audio.py +76 -7
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/vocalize/auth.py +113 -29
- vocalize_cli-0.9.0/vocalize/cache.py +70 -0
- vocalize_cli-0.9.0/vocalize/chain.py +274 -0
- vocalize_cli-0.9.0/vocalize/cli.py +859 -0
- vocalize_cli-0.9.0/vocalize/config.py +458 -0
- vocalize_cli-0.9.0/vocalize/exceptions.py +105 -0
- vocalize_cli-0.9.0/vocalize/ledger.py +185 -0
- vocalize_cli-0.9.0/vocalize/local/__init__.py +7 -0
- vocalize_cli-0.9.0/vocalize/local/install.py +217 -0
- vocalize_cli-0.9.0/vocalize/local/kokoro_manifest.py +90 -0
- vocalize_cli-0.9.0/vocalize/local/kokoro_worker.py +159 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/vocalize/preprocess.py +3 -2
- vocalize_cli-0.9.0/vocalize/providers/__init__.py +87 -0
- vocalize_cli-0.9.0/vocalize/providers/_http.py +111 -0
- vocalize_cli-0.9.0/vocalize/providers/elevenlabs.py +126 -0
- vocalize_cli-0.9.0/vocalize/providers/google.py +183 -0
- vocalize_cli-0.9.0/vocalize/providers/kokoro.py +299 -0
- vocalize_cli-0.9.0/vocalize/providers/openai.py +144 -0
- vocalize_cli-0.9.0/vocalize/providers/polly.py +221 -0
- vocalize_cli-0.9.0/vocalize/providers/say.py +123 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/vocalize/tts.py +20 -30
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/vocalize/wizard.py +117 -13
- vocalize_cli-0.8.0/tests/test_config.py +0 -249
- vocalize_cli-0.8.0/vocalize/cli.py +0 -456
- vocalize_cli-0.8.0/vocalize/config.py +0 -237
- vocalize_cli-0.8.0/vocalize/exceptions.py +0 -42
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/.env.example +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/.github/workflows/ci.yml +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/LICENSE +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/claude_stop_hook.py +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/install_hook.py +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/install_quick_action.py +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/quick_actions/Speak Latest Plan.workflow/Contents/Info.plist +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/quick_actions/Speak Latest Plan.workflow/Contents/Resources/document.wflow +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/quick_actions/Speak with Vocalize.workflow/Contents/Info.plist +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/quick_actions/Speak with Vocalize.workflow/Contents/Resources/document.wflow +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/quick_actions/Stop Vocalize.workflow/Contents/Info.plist +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/quick_actions/Stop Vocalize.workflow/Contents/Resources/document.wflow +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/speak_options.py +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/hooks/speak_url_gate.py +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_clipboard.py +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_install_hook.py +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_install_quick_action.py +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/tests/test_speak_url_gate.py +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/vocalize/__main__.py +0 -0
- {vocalize_cli-0.8.0 → vocalize_cli-0.9.0}/vocalize/clipboard.py +0 -0
|
@@ -3,6 +3,75 @@
|
|
|
3
3
|
All notable changes to this project are documented here. Format follows
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
5
5
|
|
|
6
|
+
## 0.9.0 - 2026-09-01
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- Multi-provider text-to-speech with a fallback chain. Alongside ElevenLabs,
|
|
11
|
+
vocalize can now speak through OpenAI, Google Cloud Text-to-Speech, Amazon
|
|
12
|
+
Polly, macOS `say`, and a new local Kokoro provider — tried in order until
|
|
13
|
+
one succeeds. Default chain when nothing is configured: `elevenlabs, say`.
|
|
14
|
+
- `--provider` on `speak`/`speak-file`/`clip` forces a single provider and
|
|
15
|
+
turns fallback off. `vocalize chain` shows the resolved order and its
|
|
16
|
+
source (flag/env/config/default), or writes a new one to `config.toml`
|
|
17
|
+
(`vocalize chain google polly say`) with every other key and table
|
|
18
|
+
preserved.
|
|
19
|
+
- A local monthly character budget per cloud provider (`monthly_chars` under
|
|
20
|
+
`[providers.<name>]`), tracked in `~/.cache/vocalize/usage.json`. A
|
|
21
|
+
provider that returns a real quota error from the vendor is remembered as
|
|
22
|
+
exhausted for the rest of the calendar month. `vocalize usage` now reports
|
|
23
|
+
every provider's tally against its budget alongside the existing
|
|
24
|
+
ElevenLabs remote quota.
|
|
25
|
+
- Per-provider `vocalize auth login|status|logout --provider <name>` and
|
|
26
|
+
`vocalize voices --provider <name>`.
|
|
27
|
+
- `vocalize local install` and `vocalize local status` — opt-in setup for
|
|
28
|
+
Kokoro, an offline local voice. Nothing is downloaded until you run
|
|
29
|
+
`install`: it prints exactly what it will fetch (sizes, source URLs,
|
|
30
|
+
destination), verifies every file against a pinned sha256, and runs the
|
|
31
|
+
model under its own `uv`-managed Python 3.12 so vocalize's own environment
|
|
32
|
+
never changes. `pip install vocalize-cli` pulls in none of it.
|
|
33
|
+
- Streaming playback for Kokoro: long text renders in ~400-character pieces
|
|
34
|
+
and starts playing after the first one instead of waiting for the whole
|
|
35
|
+
read to finish. `vocalize stop` works mid-read same as any other provider.
|
|
36
|
+
- New optional extra `pip install "vocalize-cli[polly]"` for Amazon Polly
|
|
37
|
+
(boto3, lazy-imported — nothing else pays for it).
|
|
38
|
+
- New `docs/provider-credentials.md`: click-by-click setup for OpenAI,
|
|
39
|
+
Google, Polly, and Kokoro.
|
|
40
|
+
|
|
41
|
+
### Changed
|
|
42
|
+
|
|
43
|
+
- The default output file is now `~/.cache/vocalize/last.<ext>` (`.mp3`,
|
|
44
|
+
`.m4a`, or `.wav` depending on which provider spoke), not always `.mp3`.
|
|
45
|
+
- `vocalize usage` no longer fails outright when no ElevenLabs key is
|
|
46
|
+
configured — it prints "no key configured, skipped" for that section and
|
|
47
|
+
still shows every provider's local budget line and the cache stats.
|
|
48
|
+
- Request progress on stderr now names the provider that's speaking
|
|
49
|
+
(`Requesting 340 characters from google...`) instead of always saying
|
|
50
|
+
ElevenLabs, and a fallback that succeeds says so (`Spoke via say
|
|
51
|
+
(fallback).`).
|
|
52
|
+
- `vocalize config`'s wizard step labels are now suffixed `(ElevenLabs)` —
|
|
53
|
+
the wizard still only sets up ElevenLabs; use `vocalize chain` or hand-edit
|
|
54
|
+
`config.toml` for the rest of the chain.
|
|
55
|
+
- `vocalize settings` gains one additive line: `chain=elevenlabs,say`.
|
|
56
|
+
|
|
57
|
+
### Security
|
|
58
|
+
|
|
59
|
+
- Every provider's API key stays out of URLs, argv, logs, and error
|
|
60
|
+
messages — headers, the OS keychain, or environment variables only.
|
|
61
|
+
- Kokoro's model downloads are pinned by URL, size, and sha256; a mismatch
|
|
62
|
+
deletes the file and refuses rather than installing anything unverified.
|
|
63
|
+
- Text reaches every local worker (`say`, Kokoro) through a file or stdin,
|
|
64
|
+
never as a command-line argument or environment variable.
|
|
65
|
+
|
|
66
|
+
## 0.8.1 - 2026-09-01
|
|
67
|
+
|
|
68
|
+
### Fixed
|
|
69
|
+
|
|
70
|
+
- Truncated speech no longer reads the words "dot dot dot truncated" aloud.
|
|
71
|
+
The `... (truncated)` marker was being appended to the text sent to
|
|
72
|
+
ElevenLabs and spoken; it's gone from the audio now. The CLI still prints
|
|
73
|
+
a "Note: input truncated to N characters." line to stderr.
|
|
74
|
+
|
|
6
75
|
## 0.8.0 - 2026-08-31
|
|
7
76
|
|
|
8
77
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vocalize-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: A CLI that turns text, markdown, or piped stdin into speech via the ElevenLabs API, with markdown-table-aware preprocessing.
|
|
5
5
|
Project-URL: Homepage, https://github.com/matthager12-collab/vocalize
|
|
6
6
|
Project-URL: Repository, https://github.com/matthager12-collab/vocalize
|
|
@@ -30,6 +30,8 @@ Requires-Dist: ruff; extra == 'dev'
|
|
|
30
30
|
Requires-Dist: twine; extra == 'dev'
|
|
31
31
|
Provides-Extra: dotenv
|
|
32
32
|
Requires-Dist: python-dotenv>=1.0; extra == 'dotenv'
|
|
33
|
+
Provides-Extra: polly
|
|
34
|
+
Requires-Dist: boto3>=1.34; extra == 'polly'
|
|
33
35
|
Description-Content-Type: text/markdown
|
|
34
36
|
|
|
35
37
|
# vocalize
|
|
@@ -157,6 +159,15 @@ vocalize speak-file report.md --speed 0.9
|
|
|
157
159
|
|
|
158
160
|
# Skip the markdown flattening entirely
|
|
159
161
|
vocalize speak "raw **markdown** stays raw" --raw
|
|
162
|
+
|
|
163
|
+
# Speak through one specific provider, no fallback
|
|
164
|
+
vocalize speak "Test" --provider google
|
|
165
|
+
|
|
166
|
+
# See (or set) the order providers are tried in
|
|
167
|
+
vocalize chain
|
|
168
|
+
|
|
169
|
+
# Set up the offline, opt-in local voice (downloads ~354 MB once)
|
|
170
|
+
vocalize local install
|
|
160
171
|
```
|
|
161
172
|
|
|
162
173
|
Every synthesis result is cached on disk under `~/.cache/vocalize/`, keyed
|
|
@@ -213,19 +224,45 @@ writing anything.
|
|
|
213
224
|
| Speed | `--speed` | `VOCALIZE_SPEED` | `speed` | unset — the API's own 1.0 |
|
|
214
225
|
| Max characters | `--max-chars` | `VOCALIZE_MAX_CHARS` | `max_chars` | unset on the CLI; the hook supplies a 500 fallback |
|
|
215
226
|
| Overflow mode | `--overflow` | `VOCALIZE_OVERFLOW` | `overflow` | `truncate` |
|
|
227
|
+
| Provider chain | `--provider` (forces one, no fallback) | `VOCALIZE_CHAIN` (comma-separated) | `chain` (array) | `["elevenlabs", "say"]` |
|
|
216
228
|
| Hook binary | — | `VOCALIZE_BIN` | not read from the config file | `vocalize` as found on `PATH` |
|
|
217
229
|
|
|
230
|
+
`--voice`/`--model`/`--speed` and their `VOCALIZE_*` env vars only ever apply
|
|
231
|
+
to the **primary** provider — the first in the chain, or the one `--provider`
|
|
232
|
+
forces. Every other link reads only its own `[providers.<name>]` table and
|
|
233
|
+
its own built-in defaults; see
|
|
234
|
+
[Providers and fallback](#providers-and-fallback) below.
|
|
235
|
+
|
|
218
236
|
The config file is TOML at `$XDG_CONFIG_HOME/vocalize/config.toml`, falling
|
|
219
237
|
back to `~/.config/vocalize/config.toml`. Flat keys, no sections:
|
|
220
238
|
|
|
221
239
|
```toml
|
|
240
|
+
chain = ["elevenlabs", "google", "say"]
|
|
241
|
+
|
|
242
|
+
# Flat keys = ElevenLabs, unchanged since before there was a chain.
|
|
222
243
|
voice = "21m00Tcm4TlvDq8ikWAM"
|
|
223
244
|
model = "eleven_flash_v2_5"
|
|
224
245
|
speed = 0.95
|
|
225
246
|
max_chars = 1000
|
|
226
247
|
overflow = "ask"
|
|
248
|
+
|
|
249
|
+
[providers.google]
|
|
250
|
+
voice = "en-US-Neural2-F"
|
|
251
|
+
language = "en-US"
|
|
252
|
+
monthly_chars = 1000000
|
|
253
|
+
|
|
254
|
+
[providers.say]
|
|
255
|
+
voice = "Samantha"
|
|
256
|
+
|
|
257
|
+
[providers.kokoro]
|
|
258
|
+
voice = "af_heart"
|
|
227
259
|
```
|
|
228
260
|
|
|
261
|
+
Every other provider gets its own `[providers.<name>]` table. The keys it
|
|
262
|
+
can hold: `voice`, `model`, `engine` (an alias for `model` — Polly's field is
|
|
263
|
+
called that), `speed`, `language`, `region`, `profile`, and `monthly_chars`.
|
|
264
|
+
A key outside that set warns on stderr rather than failing the run.
|
|
265
|
+
|
|
229
266
|
`overflow` decides what happens when input is longer than the resolved
|
|
230
267
|
`max_chars` cap: `truncate` (the default) cuts it at the cap, `ask` prompts
|
|
231
268
|
on the controlling terminal first — and degrades to `truncate` with a note
|
|
@@ -252,6 +289,134 @@ in the current directory, then the OS keychain. `vocalize auth login` sets
|
|
|
252
289
|
up the keychain entry; `vocalize auth status` shows which of those sources
|
|
253
290
|
is currently supplying the key.
|
|
254
291
|
|
|
292
|
+
## Providers and fallback
|
|
293
|
+
|
|
294
|
+
vocalize tries providers in order — a **chain** — until one speaks. The
|
|
295
|
+
default is `elevenlabs, say`: ElevenLabs behaves exactly as before, and a
|
|
296
|
+
failure now degrades to the always-free `say` instead of erroring out.
|
|
297
|
+
|
|
298
|
+
| Provider | Credentials | Config table | Per-request cap | Free tier | What `check` needs |
|
|
299
|
+
|---|---|---|---|---|---|
|
|
300
|
+
| `elevenlabs` | keychain / env / `.env` / `--api-key` | `[providers.elevenlabs]` or the flat legacy keys | 9,500 chars | 10,000 chars/month | an API key |
|
|
301
|
+
| `openai` | keychain / env / `.env` | `[providers.openai]` | 4,000 chars | none — prepaid credit only, ~$15/million chars | an API key |
|
|
302
|
+
| `google` | keychain / env / `.env` | `[providers.google]` | 4,500 chars (also a 4,900-byte hard cap) | ~4M Standard or ~1M Neural2/WaveNet chars/month, then bills | an API key |
|
|
303
|
+
| `polly` | your normal AWS credentials (env, `~/.aws/credentials`, a profile, or a role) — vocalize stores none of it | `[providers.polly]` | 2,900 chars | Standard 5M/month ongoing; Neural 1M/month for 12 months, then $4–$16/million | `boto3` installed + AWS credentials discoverable |
|
|
304
|
+
| `say` | none | `[providers.say]` | none (one call, any length) | free, offline | macOS with the `say` binary |
|
|
305
|
+
| `kokoro` | none | `[providers.kokoro]` | 400 chars per streamed piece | free, offline, one-time ~354 MB download | `uv` + `vocalize local install` done |
|
|
306
|
+
|
|
307
|
+
**Fallback rules**, decided by typed errors, not string-matching:
|
|
308
|
+
|
|
309
|
+
- **Unavailable / auth / transient** errors (missing key, bad credentials, a
|
|
310
|
+
5xx or rate limit) skip straight to the next provider in the chain.
|
|
311
|
+
- **Quota** errors do the same, but also mark that provider exhausted in the
|
|
312
|
+
local ledger for the rest of the calendar month — see
|
|
313
|
+
[Budgets and the usage ledger](#budgets-and-the-usage-ledger).
|
|
314
|
+
- **Content** errors — a bad voice name, text longer than the API actually
|
|
315
|
+
accepts — **stop the chain immediately**, loudly, naming the bad config
|
|
316
|
+
key. A silent misconfiguration would be the worse bug to ship.
|
|
317
|
+
- Anything else (a bug in vocalize itself) is never treated as "try the next
|
|
318
|
+
one" — it propagates as a real error.
|
|
319
|
+
- Once Kokoro's streaming playback has started, a later failure can't fall
|
|
320
|
+
through to another provider either — you can't un-hear the first half of
|
|
321
|
+
a read.
|
|
322
|
+
|
|
323
|
+
You'll see the handoff on stderr as it happens:
|
|
324
|
+
|
|
325
|
+
```
|
|
326
|
+
openai: out of credit — trying google
|
|
327
|
+
google: local budget reached (1,004,233/1,000,000 chars this month) — trying polly
|
|
328
|
+
Spoke via say (fallback).
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
If every provider fails, the error lists each one's reason, plus a hint to
|
|
332
|
+
add `say` to the chain if it's missing.
|
|
333
|
+
|
|
334
|
+
`--provider` is the opt-out: it forces exactly one provider with no
|
|
335
|
+
fallback at all, same as vocalize behaved before it had a chain.
|
|
336
|
+
|
|
337
|
+
**A chain is multi-vendor egress.** `elevenlabs, google, say` can, on a bad
|
|
338
|
+
day, send the same text to both ElevenLabs and Google before `say` finally
|
|
339
|
+
speaks it — every attempt that reaches a provider's `synthesize` call is a
|
|
340
|
+
real request to that vendor. `say` and `kokoro` are the exception: they
|
|
341
|
+
never send text off the machine.
|
|
342
|
+
|
|
343
|
+
Amazon Polly needs the optional extra:
|
|
344
|
+
|
|
345
|
+
```bash
|
|
346
|
+
pip install "vocalize-cli[polly]"
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
For the click-by-click setup of each provider — where to go, what to click,
|
|
350
|
+
the one command that stores the credential, the one command that proves it
|
|
351
|
+
works — see [docs/provider-credentials.md](docs/provider-credentials.md).
|
|
352
|
+
|
|
353
|
+
## Budgets and the usage ledger
|
|
354
|
+
|
|
355
|
+
Cloud providers don't stop at their free tier — they bill past it. vocalize
|
|
356
|
+
can't see your vendor invoice, so it keeps its own local estimate instead
|
|
357
|
+
and stops using a provider once you say where the line is.
|
|
358
|
+
|
|
359
|
+
Set `monthly_chars` under that provider's `[providers.<name>]` table:
|
|
360
|
+
|
|
361
|
+
```toml
|
|
362
|
+
[providers.google]
|
|
363
|
+
monthly_chars = 1000000
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
Usage is tracked in `~/.cache/vocalize/usage.json`, one entry per provider
|
|
367
|
+
per calendar month, decided by your machine's local time. A provider that
|
|
368
|
+
comes back with a real quota error from the vendor is remembered as
|
|
369
|
+
exhausted for the rest of that month — no further requests to it, even if
|
|
370
|
+
you raise `monthly_chars` in between; only the new month clears it.
|
|
371
|
+
|
|
372
|
+
`vocalize usage` prints every provider's tally against its budget (or
|
|
373
|
+
"unlimited" with no `monthly_chars` set), flags any that are exhausted, then
|
|
374
|
+
ElevenLabs's own remote quota (skipped gracefully, not a failure, when no
|
|
375
|
+
key is configured), then local disk-cache stats.
|
|
376
|
+
|
|
377
|
+
The ledger is per-machine and an estimate, not a bill: a cached (repeat)
|
|
378
|
+
request costs nothing and isn't counted, and usage from a different machine
|
|
379
|
+
never shows up here. Google's own limits and billing are byte-based, not
|
|
380
|
+
character-based, so vocalize counts Google's usage in UTF-8 bytes too — the
|
|
381
|
+
same text can cost a different amount against Google's cap than everyone
|
|
382
|
+
else's.
|
|
383
|
+
|
|
384
|
+
## Local providers
|
|
385
|
+
|
|
386
|
+
Two providers never leave the machine.
|
|
387
|
+
|
|
388
|
+
**`say`** is built in — macOS only, no setup, no network, no quota. Output
|
|
389
|
+
is `.m4a`, not `.mp3`. It uses whichever voices `say -v ?` lists on your
|
|
390
|
+
Mac; set one with `[providers.say] voice = "Samantha"`.
|
|
391
|
+
|
|
392
|
+
**Kokoro** is opt-in. `pip install vocalize-cli` brings none of it — no
|
|
393
|
+
model weights, no extra runtime — until you ask for it:
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
vocalize local install
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
This prints exactly what it's about to download before asking to confirm:
|
|
400
|
+
`kokoro-v1.0.onnx` (326 MB) and `voices-v1.0.bin` (28 MB) from a pinned
|
|
401
|
+
GitHub release, into `~/.cache/vocalize/models/kokoro/`, plus about 230 MB
|
|
402
|
+
more that `uv` fetches into its own cache (Python 3.12 and the `kokoro-onnx`
|
|
403
|
+
runtime). Every file is checked against a pinned sha256 before it's kept —
|
|
404
|
+
a mismatch deletes it and refuses rather than installing anything
|
|
405
|
+
unverified. The runtime runs under `uv run --python 3.12`, entirely apart
|
|
406
|
+
from vocalize's own environment, so installing Kokoro never touches or
|
|
407
|
+
upgrades the Python vocalize itself runs in. `vocalize local status`
|
|
408
|
+
reports what's present, missing, or unverified.
|
|
409
|
+
|
|
410
|
+
Use it for one read with `--provider kokoro`, or add it to your chain in
|
|
411
|
+
`config.toml`.
|
|
412
|
+
|
|
413
|
+
Long text streams: it's broken into ~400-character pieces, and playback
|
|
414
|
+
starts after the first one is ready — roughly 20–25 seconds of speech —
|
|
415
|
+
instead of waiting for the whole thing to render. Measured on this Mac
|
|
416
|
+
(M3): about 5x faster than real time, peaking around 870 MB of RAM while
|
|
417
|
+
rendering. `vocalize stop`, run from any terminal, halts a Kokoro read
|
|
418
|
+
mid-sentence the same as any other provider.
|
|
419
|
+
|
|
255
420
|
## macOS Quick Actions (highlight → speak)
|
|
256
421
|
|
|
257
422
|
Two Services let you use vocalize from any app without a terminal:
|
|
@@ -305,6 +470,10 @@ CLI — so it works identically whether Claude Code is running in a bare
|
|
|
305
470
|
terminal or inside an IDE's integrated terminal (VS Code, Cursor, etc.),
|
|
306
471
|
since both use the same `~/.claude/settings.json` hook config.
|
|
307
472
|
|
|
473
|
+
Whatever speaks a response is whichever provider your chain resolves to —
|
|
474
|
+
`vocalize settings` now prints a `chain=` line alongside `overflow=` and
|
|
475
|
+
`max_chars=`, which is how a wrapper script like `/speak` can check it.
|
|
476
|
+
|
|
308
477
|
**On-demand mode.** If you'd rather trigger speech yourself than have every
|
|
309
478
|
response spoken, skip the install and run the script with `--latest`. It
|
|
310
479
|
finds your most recent Claude Code response — in any session — and speaks
|
|
@@ -372,8 +541,9 @@ the main session the same way any other summary does: written to a file
|
|
|
372
541
|
and passed to `speak-file`.
|
|
373
542
|
|
|
374
543
|
If you wire this into a slash command of your own, treat it as a security
|
|
375
|
-
surface, because **every character you speak is sent to
|
|
376
|
-
|
|
544
|
+
surface, because **every character you speak is sent to whichever provider
|
|
545
|
+
in your chain ends up speaking it** — unless that provider is `say` or
|
|
546
|
+
`kokoro`. The guard principles that matter, in order:
|
|
377
547
|
|
|
378
548
|
1. Resolve paths (`realpath`, expand `~`, casefold) and check an
|
|
379
549
|
**allow-list** of speakable directories — symlinks and `../` defeat
|
|
@@ -428,9 +598,14 @@ vocalize/
|
|
|
428
598
|
__main__.py # python -m vocalize entry point
|
|
429
599
|
preprocess.py # markdown -> speakable text (pure function, fully unit tested)
|
|
430
600
|
config.py # API key resolution + settings: flag > env > config.toml > default
|
|
431
|
-
exceptions.py # VocalizeError / TTSRequestError
|
|
432
|
-
tts.py # ElevenLabs API wrapper
|
|
601
|
+
exceptions.py # VocalizeError / TTSRequestError / typed ProviderError family
|
|
602
|
+
tts.py # ElevenLabs API wrapper (client is injected, so
|
|
433
603
|
# it's mockable in tests without hitting the network)
|
|
604
|
+
cache.py # disk cache: cache_key/get/put, shared by every provider
|
|
605
|
+
chain.py # tries each provider in the chain in turn until one speaks
|
|
606
|
+
ledger.py # ~/.cache/vocalize/usage.json — local monthly budget tracking
|
|
607
|
+
providers/ # elevenlabs.py, openai.py, google.py, polly.py, say.py, kokoro.py
|
|
608
|
+
local/ # Kokoro's opt-in download/verify + the uv-run worker script
|
|
434
609
|
audio.py # save to disk + play via the OS's native player
|
|
435
610
|
# (afplay / mpg123 / ffplay / PowerShell, whichever exists)
|
|
436
611
|
cli.py # click-based CLI wiring the above together
|
|
@@ -478,6 +653,29 @@ All tests run offline: the ElevenLabs client is dependency-injected into
|
|
|
478
653
|
`ELEVENLABS_API_KEY` environment variable, or a `.env` file instead.
|
|
479
654
|
- `vocalize voices` lists only the first page of results from the
|
|
480
655
|
ElevenLabs API.
|
|
656
|
+
- **A chain is multi-vendor egress.** A fallback chain can send the same
|
|
657
|
+
text to more than one cloud vendor before one of them succeeds — see
|
|
658
|
+
[Providers and fallback](#providers-and-fallback).
|
|
659
|
+
- **The usage ledger is local and an estimate, not a bill.** It doesn't see
|
|
660
|
+
what your vendor actually charges, doesn't know about usage from another
|
|
661
|
+
machine, and a cached (repeat) request isn't counted at all.
|
|
662
|
+
- **Polly ignores `--speed`.** Its rate control needs SSML, which this
|
|
663
|
+
release doesn't wrap plain text into; `[providers.polly]` has no speed
|
|
664
|
+
knob yet.
|
|
665
|
+
- **Joined MP3 chunks are a byte concatenation, not a re-encode.** Most
|
|
666
|
+
players handle it fine, but the frame boundary between chunks can
|
|
667
|
+
occasionally produce an audible click.
|
|
668
|
+
- **`vocalize config`'s wizard only sets up ElevenLabs.** Configure the rest
|
|
669
|
+
of the chain with `vocalize chain` or by hand-editing `config.toml`.
|
|
670
|
+
- **Kokoro needs `uv`**, and its shipped pack carries 54 voices across nine
|
|
671
|
+
languages but is phonemized for `en-us` unless you set `language` under
|
|
672
|
+
`[providers.kokoro]` to match a non-English voice; it's opt-in for a
|
|
673
|
+
reason — see [Local providers](#local-providers).
|
|
674
|
+
- **A locked or first-use macOS keychain blocks silently.** Reading a stored
|
|
675
|
+
key can raise a macOS permission dialog ("python wants to use your
|
|
676
|
+
confidential information"); until you click Always Allow, every command
|
|
677
|
+
that needs that key waits. Click it once per Python binary, or supply the
|
|
678
|
+
key through its environment variable instead.
|
|
481
679
|
|
|
482
680
|
## License
|
|
483
681
|
|
|
@@ -123,6 +123,15 @@ vocalize speak-file report.md --speed 0.9
|
|
|
123
123
|
|
|
124
124
|
# Skip the markdown flattening entirely
|
|
125
125
|
vocalize speak "raw **markdown** stays raw" --raw
|
|
126
|
+
|
|
127
|
+
# Speak through one specific provider, no fallback
|
|
128
|
+
vocalize speak "Test" --provider google
|
|
129
|
+
|
|
130
|
+
# See (or set) the order providers are tried in
|
|
131
|
+
vocalize chain
|
|
132
|
+
|
|
133
|
+
# Set up the offline, opt-in local voice (downloads ~354 MB once)
|
|
134
|
+
vocalize local install
|
|
126
135
|
```
|
|
127
136
|
|
|
128
137
|
Every synthesis result is cached on disk under `~/.cache/vocalize/`, keyed
|
|
@@ -179,19 +188,45 @@ writing anything.
|
|
|
179
188
|
| Speed | `--speed` | `VOCALIZE_SPEED` | `speed` | unset — the API's own 1.0 |
|
|
180
189
|
| Max characters | `--max-chars` | `VOCALIZE_MAX_CHARS` | `max_chars` | unset on the CLI; the hook supplies a 500 fallback |
|
|
181
190
|
| Overflow mode | `--overflow` | `VOCALIZE_OVERFLOW` | `overflow` | `truncate` |
|
|
191
|
+
| Provider chain | `--provider` (forces one, no fallback) | `VOCALIZE_CHAIN` (comma-separated) | `chain` (array) | `["elevenlabs", "say"]` |
|
|
182
192
|
| Hook binary | — | `VOCALIZE_BIN` | not read from the config file | `vocalize` as found on `PATH` |
|
|
183
193
|
|
|
194
|
+
`--voice`/`--model`/`--speed` and their `VOCALIZE_*` env vars only ever apply
|
|
195
|
+
to the **primary** provider — the first in the chain, or the one `--provider`
|
|
196
|
+
forces. Every other link reads only its own `[providers.<name>]` table and
|
|
197
|
+
its own built-in defaults; see
|
|
198
|
+
[Providers and fallback](#providers-and-fallback) below.
|
|
199
|
+
|
|
184
200
|
The config file is TOML at `$XDG_CONFIG_HOME/vocalize/config.toml`, falling
|
|
185
201
|
back to `~/.config/vocalize/config.toml`. Flat keys, no sections:
|
|
186
202
|
|
|
187
203
|
```toml
|
|
204
|
+
chain = ["elevenlabs", "google", "say"]
|
|
205
|
+
|
|
206
|
+
# Flat keys = ElevenLabs, unchanged since before there was a chain.
|
|
188
207
|
voice = "21m00Tcm4TlvDq8ikWAM"
|
|
189
208
|
model = "eleven_flash_v2_5"
|
|
190
209
|
speed = 0.95
|
|
191
210
|
max_chars = 1000
|
|
192
211
|
overflow = "ask"
|
|
212
|
+
|
|
213
|
+
[providers.google]
|
|
214
|
+
voice = "en-US-Neural2-F"
|
|
215
|
+
language = "en-US"
|
|
216
|
+
monthly_chars = 1000000
|
|
217
|
+
|
|
218
|
+
[providers.say]
|
|
219
|
+
voice = "Samantha"
|
|
220
|
+
|
|
221
|
+
[providers.kokoro]
|
|
222
|
+
voice = "af_heart"
|
|
193
223
|
```
|
|
194
224
|
|
|
225
|
+
Every other provider gets its own `[providers.<name>]` table. The keys it
|
|
226
|
+
can hold: `voice`, `model`, `engine` (an alias for `model` — Polly's field is
|
|
227
|
+
called that), `speed`, `language`, `region`, `profile`, and `monthly_chars`.
|
|
228
|
+
A key outside that set warns on stderr rather than failing the run.
|
|
229
|
+
|
|
195
230
|
`overflow` decides what happens when input is longer than the resolved
|
|
196
231
|
`max_chars` cap: `truncate` (the default) cuts it at the cap, `ask` prompts
|
|
197
232
|
on the controlling terminal first — and degrades to `truncate` with a note
|
|
@@ -218,6 +253,134 @@ in the current directory, then the OS keychain. `vocalize auth login` sets
|
|
|
218
253
|
up the keychain entry; `vocalize auth status` shows which of those sources
|
|
219
254
|
is currently supplying the key.
|
|
220
255
|
|
|
256
|
+
## Providers and fallback
|
|
257
|
+
|
|
258
|
+
vocalize tries providers in order — a **chain** — until one speaks. The
|
|
259
|
+
default is `elevenlabs, say`: ElevenLabs behaves exactly as before, and a
|
|
260
|
+
failure now degrades to the always-free `say` instead of erroring out.
|
|
261
|
+
|
|
262
|
+
| Provider | Credentials | Config table | Per-request cap | Free tier | What `check` needs |
|
|
263
|
+
|---|---|---|---|---|---|
|
|
264
|
+
| `elevenlabs` | keychain / env / `.env` / `--api-key` | `[providers.elevenlabs]` or the flat legacy keys | 9,500 chars | 10,000 chars/month | an API key |
|
|
265
|
+
| `openai` | keychain / env / `.env` | `[providers.openai]` | 4,000 chars | none — prepaid credit only, ~$15/million chars | an API key |
|
|
266
|
+
| `google` | keychain / env / `.env` | `[providers.google]` | 4,500 chars (also a 4,900-byte hard cap) | ~4M Standard or ~1M Neural2/WaveNet chars/month, then bills | an API key |
|
|
267
|
+
| `polly` | your normal AWS credentials (env, `~/.aws/credentials`, a profile, or a role) — vocalize stores none of it | `[providers.polly]` | 2,900 chars | Standard 5M/month ongoing; Neural 1M/month for 12 months, then $4–$16/million | `boto3` installed + AWS credentials discoverable |
|
|
268
|
+
| `say` | none | `[providers.say]` | none (one call, any length) | free, offline | macOS with the `say` binary |
|
|
269
|
+
| `kokoro` | none | `[providers.kokoro]` | 400 chars per streamed piece | free, offline, one-time ~354 MB download | `uv` + `vocalize local install` done |
|
|
270
|
+
|
|
271
|
+
**Fallback rules**, decided by typed errors, not string-matching:
|
|
272
|
+
|
|
273
|
+
- **Unavailable / auth / transient** errors (missing key, bad credentials, a
|
|
274
|
+
5xx or rate limit) skip straight to the next provider in the chain.
|
|
275
|
+
- **Quota** errors do the same, but also mark that provider exhausted in the
|
|
276
|
+
local ledger for the rest of the calendar month — see
|
|
277
|
+
[Budgets and the usage ledger](#budgets-and-the-usage-ledger).
|
|
278
|
+
- **Content** errors — a bad voice name, text longer than the API actually
|
|
279
|
+
accepts — **stop the chain immediately**, loudly, naming the bad config
|
|
280
|
+
key. A silent misconfiguration would be the worse bug to ship.
|
|
281
|
+
- Anything else (a bug in vocalize itself) is never treated as "try the next
|
|
282
|
+
one" — it propagates as a real error.
|
|
283
|
+
- Once Kokoro's streaming playback has started, a later failure can't fall
|
|
284
|
+
through to another provider either — you can't un-hear the first half of
|
|
285
|
+
a read.
|
|
286
|
+
|
|
287
|
+
You'll see the handoff on stderr as it happens:
|
|
288
|
+
|
|
289
|
+
```
|
|
290
|
+
openai: out of credit — trying google
|
|
291
|
+
google: local budget reached (1,004,233/1,000,000 chars this month) — trying polly
|
|
292
|
+
Spoke via say (fallback).
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
If every provider fails, the error lists each one's reason, plus a hint to
|
|
296
|
+
add `say` to the chain if it's missing.
|
|
297
|
+
|
|
298
|
+
`--provider` is the opt-out: it forces exactly one provider with no
|
|
299
|
+
fallback at all, same as vocalize behaved before it had a chain.
|
|
300
|
+
|
|
301
|
+
**A chain is multi-vendor egress.** `elevenlabs, google, say` can, on a bad
|
|
302
|
+
day, send the same text to both ElevenLabs and Google before `say` finally
|
|
303
|
+
speaks it — every attempt that reaches a provider's `synthesize` call is a
|
|
304
|
+
real request to that vendor. `say` and `kokoro` are the exception: they
|
|
305
|
+
never send text off the machine.
|
|
306
|
+
|
|
307
|
+
Amazon Polly needs the optional extra:
|
|
308
|
+
|
|
309
|
+
```bash
|
|
310
|
+
pip install "vocalize-cli[polly]"
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
For the click-by-click setup of each provider — where to go, what to click,
|
|
314
|
+
the one command that stores the credential, the one command that proves it
|
|
315
|
+
works — see [docs/provider-credentials.md](docs/provider-credentials.md).
|
|
316
|
+
|
|
317
|
+
## Budgets and the usage ledger
|
|
318
|
+
|
|
319
|
+
Cloud providers don't stop at their free tier — they bill past it. vocalize
|
|
320
|
+
can't see your vendor invoice, so it keeps its own local estimate instead
|
|
321
|
+
and stops using a provider once you say where the line is.
|
|
322
|
+
|
|
323
|
+
Set `monthly_chars` under that provider's `[providers.<name>]` table:
|
|
324
|
+
|
|
325
|
+
```toml
|
|
326
|
+
[providers.google]
|
|
327
|
+
monthly_chars = 1000000
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
Usage is tracked in `~/.cache/vocalize/usage.json`, one entry per provider
|
|
331
|
+
per calendar month, decided by your machine's local time. A provider that
|
|
332
|
+
comes back with a real quota error from the vendor is remembered as
|
|
333
|
+
exhausted for the rest of that month — no further requests to it, even if
|
|
334
|
+
you raise `monthly_chars` in between; only the new month clears it.
|
|
335
|
+
|
|
336
|
+
`vocalize usage` prints every provider's tally against its budget (or
|
|
337
|
+
"unlimited" with no `monthly_chars` set), flags any that are exhausted, then
|
|
338
|
+
ElevenLabs's own remote quota (skipped gracefully, not a failure, when no
|
|
339
|
+
key is configured), then local disk-cache stats.
|
|
340
|
+
|
|
341
|
+
The ledger is per-machine and an estimate, not a bill: a cached (repeat)
|
|
342
|
+
request costs nothing and isn't counted, and usage from a different machine
|
|
343
|
+
never shows up here. Google's own limits and billing are byte-based, not
|
|
344
|
+
character-based, so vocalize counts Google's usage in UTF-8 bytes too — the
|
|
345
|
+
same text can cost a different amount against Google's cap than everyone
|
|
346
|
+
else's.
|
|
347
|
+
|
|
348
|
+
## Local providers
|
|
349
|
+
|
|
350
|
+
Two providers never leave the machine.
|
|
351
|
+
|
|
352
|
+
**`say`** is built in — macOS only, no setup, no network, no quota. Output
|
|
353
|
+
is `.m4a`, not `.mp3`. It uses whichever voices `say -v ?` lists on your
|
|
354
|
+
Mac; set one with `[providers.say] voice = "Samantha"`.
|
|
355
|
+
|
|
356
|
+
**Kokoro** is opt-in. `pip install vocalize-cli` brings none of it — no
|
|
357
|
+
model weights, no extra runtime — until you ask for it:
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
vocalize local install
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
This prints exactly what it's about to download before asking to confirm:
|
|
364
|
+
`kokoro-v1.0.onnx` (326 MB) and `voices-v1.0.bin` (28 MB) from a pinned
|
|
365
|
+
GitHub release, into `~/.cache/vocalize/models/kokoro/`, plus about 230 MB
|
|
366
|
+
more that `uv` fetches into its own cache (Python 3.12 and the `kokoro-onnx`
|
|
367
|
+
runtime). Every file is checked against a pinned sha256 before it's kept —
|
|
368
|
+
a mismatch deletes it and refuses rather than installing anything
|
|
369
|
+
unverified. The runtime runs under `uv run --python 3.12`, entirely apart
|
|
370
|
+
from vocalize's own environment, so installing Kokoro never touches or
|
|
371
|
+
upgrades the Python vocalize itself runs in. `vocalize local status`
|
|
372
|
+
reports what's present, missing, or unverified.
|
|
373
|
+
|
|
374
|
+
Use it for one read with `--provider kokoro`, or add it to your chain in
|
|
375
|
+
`config.toml`.
|
|
376
|
+
|
|
377
|
+
Long text streams: it's broken into ~400-character pieces, and playback
|
|
378
|
+
starts after the first one is ready — roughly 20–25 seconds of speech —
|
|
379
|
+
instead of waiting for the whole thing to render. Measured on this Mac
|
|
380
|
+
(M3): about 5x faster than real time, peaking around 870 MB of RAM while
|
|
381
|
+
rendering. `vocalize stop`, run from any terminal, halts a Kokoro read
|
|
382
|
+
mid-sentence the same as any other provider.
|
|
383
|
+
|
|
221
384
|
## macOS Quick Actions (highlight → speak)
|
|
222
385
|
|
|
223
386
|
Two Services let you use vocalize from any app without a terminal:
|
|
@@ -271,6 +434,10 @@ CLI — so it works identically whether Claude Code is running in a bare
|
|
|
271
434
|
terminal or inside an IDE's integrated terminal (VS Code, Cursor, etc.),
|
|
272
435
|
since both use the same `~/.claude/settings.json` hook config.
|
|
273
436
|
|
|
437
|
+
Whatever speaks a response is whichever provider your chain resolves to —
|
|
438
|
+
`vocalize settings` now prints a `chain=` line alongside `overflow=` and
|
|
439
|
+
`max_chars=`, which is how a wrapper script like `/speak` can check it.
|
|
440
|
+
|
|
274
441
|
**On-demand mode.** If you'd rather trigger speech yourself than have every
|
|
275
442
|
response spoken, skip the install and run the script with `--latest`. It
|
|
276
443
|
finds your most recent Claude Code response — in any session — and speaks
|
|
@@ -338,8 +505,9 @@ the main session the same way any other summary does: written to a file
|
|
|
338
505
|
and passed to `speak-file`.
|
|
339
506
|
|
|
340
507
|
If you wire this into a slash command of your own, treat it as a security
|
|
341
|
-
surface, because **every character you speak is sent to
|
|
342
|
-
|
|
508
|
+
surface, because **every character you speak is sent to whichever provider
|
|
509
|
+
in your chain ends up speaking it** — unless that provider is `say` or
|
|
510
|
+
`kokoro`. The guard principles that matter, in order:
|
|
343
511
|
|
|
344
512
|
1. Resolve paths (`realpath`, expand `~`, casefold) and check an
|
|
345
513
|
**allow-list** of speakable directories — symlinks and `../` defeat
|
|
@@ -394,9 +562,14 @@ vocalize/
|
|
|
394
562
|
__main__.py # python -m vocalize entry point
|
|
395
563
|
preprocess.py # markdown -> speakable text (pure function, fully unit tested)
|
|
396
564
|
config.py # API key resolution + settings: flag > env > config.toml > default
|
|
397
|
-
exceptions.py # VocalizeError / TTSRequestError
|
|
398
|
-
tts.py # ElevenLabs API wrapper
|
|
565
|
+
exceptions.py # VocalizeError / TTSRequestError / typed ProviderError family
|
|
566
|
+
tts.py # ElevenLabs API wrapper (client is injected, so
|
|
399
567
|
# it's mockable in tests without hitting the network)
|
|
568
|
+
cache.py # disk cache: cache_key/get/put, shared by every provider
|
|
569
|
+
chain.py # tries each provider in the chain in turn until one speaks
|
|
570
|
+
ledger.py # ~/.cache/vocalize/usage.json — local monthly budget tracking
|
|
571
|
+
providers/ # elevenlabs.py, openai.py, google.py, polly.py, say.py, kokoro.py
|
|
572
|
+
local/ # Kokoro's opt-in download/verify + the uv-run worker script
|
|
400
573
|
audio.py # save to disk + play via the OS's native player
|
|
401
574
|
# (afplay / mpg123 / ffplay / PowerShell, whichever exists)
|
|
402
575
|
cli.py # click-based CLI wiring the above together
|
|
@@ -444,6 +617,29 @@ All tests run offline: the ElevenLabs client is dependency-injected into
|
|
|
444
617
|
`ELEVENLABS_API_KEY` environment variable, or a `.env` file instead.
|
|
445
618
|
- `vocalize voices` lists only the first page of results from the
|
|
446
619
|
ElevenLabs API.
|
|
620
|
+
- **A chain is multi-vendor egress.** A fallback chain can send the same
|
|
621
|
+
text to more than one cloud vendor before one of them succeeds — see
|
|
622
|
+
[Providers and fallback](#providers-and-fallback).
|
|
623
|
+
- **The usage ledger is local and an estimate, not a bill.** It doesn't see
|
|
624
|
+
what your vendor actually charges, doesn't know about usage from another
|
|
625
|
+
machine, and a cached (repeat) request isn't counted at all.
|
|
626
|
+
- **Polly ignores `--speed`.** Its rate control needs SSML, which this
|
|
627
|
+
release doesn't wrap plain text into; `[providers.polly]` has no speed
|
|
628
|
+
knob yet.
|
|
629
|
+
- **Joined MP3 chunks are a byte concatenation, not a re-encode.** Most
|
|
630
|
+
players handle it fine, but the frame boundary between chunks can
|
|
631
|
+
occasionally produce an audible click.
|
|
632
|
+
- **`vocalize config`'s wizard only sets up ElevenLabs.** Configure the rest
|
|
633
|
+
of the chain with `vocalize chain` or by hand-editing `config.toml`.
|
|
634
|
+
- **Kokoro needs `uv`**, and its shipped pack carries 54 voices across nine
|
|
635
|
+
languages but is phonemized for `en-us` unless you set `language` under
|
|
636
|
+
`[providers.kokoro]` to match a non-English voice; it's opt-in for a
|
|
637
|
+
reason — see [Local providers](#local-providers).
|
|
638
|
+
- **A locked or first-use macOS keychain blocks silently.** Reading a stored
|
|
639
|
+
key can raise a macOS permission dialog ("python wants to use your
|
|
640
|
+
confidential information"); until you click Always Allow, every command
|
|
641
|
+
that needs that key waits. Click it once per Python binary, or supply the
|
|
642
|
+
key through its environment variable instead.
|
|
447
643
|
|
|
448
644
|
## License
|
|
449
645
|
|