vocalize-cli 0.2.1__tar.gz → 0.4.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.
Files changed (31) hide show
  1. vocalize_cli-0.4.0/CHANGELOG.md +89 -0
  2. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/PKG-INFO +79 -6
  3. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/README.md +77 -5
  4. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/pyproject.toml +1 -0
  5. vocalize_cli-0.4.0/tests/conftest.py +56 -0
  6. vocalize_cli-0.4.0/tests/test_auth.py +333 -0
  7. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_cli.py +45 -0
  8. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_config.py +13 -0
  9. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_tts.py +46 -1
  10. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_wizard.py +94 -4
  11. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/__init__.py +1 -1
  12. vocalize_cli-0.4.0/vocalize/auth.py +215 -0
  13. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/cli.py +98 -1
  14. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/config.py +10 -1
  15. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/exceptions.py +9 -4
  16. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/tts.py +19 -0
  17. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/wizard.py +46 -4
  18. vocalize_cli-0.2.1/tests/conftest.py +0 -12
  19. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/.env.example +0 -0
  20. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/.github/workflows/ci.yml +0 -0
  21. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/.gitignore +0 -0
  22. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/LICENSE +0 -0
  23. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/hooks/claude_stop_hook.py +0 -0
  24. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/hooks/install_hook.py +0 -0
  25. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_audio.py +0 -0
  26. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_claude_stop_hook.py +0 -0
  27. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_install_hook.py +0 -0
  28. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_preprocess.py +0 -0
  29. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/__main__.py +0 -0
  30. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/audio.py +0 -0
  31. {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/preprocess.py +0 -0
@@ -0,0 +1,89 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
5
+
6
+ ## 0.4.0
7
+
8
+ ### Added
9
+
10
+ - `vocalize usage` — ElevenLabs quota and local cache at a glance.
11
+
12
+ ## 0.3.0
13
+
14
+ ### Added
15
+
16
+ - `vocalize auth` command group for storing your ElevenLabs API key in the OS
17
+ keychain (macOS Keychain, Windows Credential Locker, or Linux Secret
18
+ Service) instead of an environment variable or `.env` file.
19
+ - `vocalize auth login` prompts for the key (hidden input), validates it
20
+ against the ElevenLabs API, and stores it. `--stdin` reads the key from a
21
+ pipe instead, for secret managers — e.g.
22
+ `op read op://vault/elevenlabs/key | vocalize auth login --stdin`.
23
+ - `vocalize auth status` shows where the active key is coming from (flag,
24
+ environment, `.env` file, keychain, or not found), with a masked preview.
25
+ - `vocalize auth logout` removes the stored key.
26
+ - `vocalize config` now offers to set up your API key when none is found,
27
+ before walking through voice, model, and speed — so setup is install, then
28
+ `vocalize config`, done.
29
+
30
+ ### Changed
31
+
32
+ - API key resolution order is now: `--api-key` flag, then
33
+ `ELEVENLABS_API_KEY`, then a `.env` file in the current directory, then the
34
+ OS keychain.
35
+
36
+ ## 0.2.1
37
+
38
+ ### Fixed
39
+
40
+ - The config wizard now paints on the controlling terminal (`/dev/tty`)
41
+ instead of stdout, so it still works under output-capturing wrappers like
42
+ `op run` instead of corrupting their captured output.
43
+
44
+ ## 0.2.0
45
+
46
+ ### Added
47
+
48
+ - A TOML config file (`~/.config/vocalize/config.toml` or
49
+ `$XDG_CONFIG_HOME/vocalize/config.toml`) and matching environment
50
+ variables for voice, model, and speed, resolved as flag, then env var,
51
+ then config file, then default.
52
+ - `--speed` flag and `speed` config/env setting (0.7-1.2).
53
+ - `vocalize config`, an interactive wizard that walks through voice (with a
54
+ live preview of the highlighted choice), model, and speed, then writes the
55
+ config file — no need to hand-write TOML.
56
+
57
+ ## 0.1.1
58
+
59
+ ### Added
60
+
61
+ - `--latest` flag on the Claude Code Stop hook, for speaking your most
62
+ recent response on demand instead of installing an automatic hook.
63
+
64
+ ### Changed
65
+
66
+ - Fenced code blocks are now spoken as a single short placeholder instead of
67
+ being read out character by character.
68
+
69
+ ### Fixed
70
+
71
+ - The CLI's reported version now comes from one place instead of two.
72
+
73
+ ## 0.1.0
74
+
75
+ Initial release.
76
+
77
+ ### Added
78
+
79
+ - `vocalize speak`, `vocalize speak-file`, and `vocalize voices` commands,
80
+ backed by the ElevenLabs TTS API.
81
+ - A markdown-to-speech preprocessing pass: tables, bullet lists, links, and
82
+ code blocks are rewritten into short declarative sentences before
83
+ synthesis.
84
+ - A disk cache keyed by a hash of (text, voice, model, format, speed), so
85
+ repeat runs don't re-spend API quota.
86
+ - A Claude Code Stop hook (`hooks/claude_stop_hook.py`,
87
+ `hooks/install_hook.py`) that speaks Claude's response after every turn.
88
+ - Published to PyPI as `vocalize-cli`; CI running lint and tests with
89
+ coverage on every push.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vocalize-cli
3
- Version: 0.2.1
3
+ Version: 0.4.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
@@ -20,6 +20,7 @@ Classifier: Topic :: Multimedia :: Sound/Audio :: Speech
20
20
  Requires-Python: >=3.10
21
21
  Requires-Dist: click>=8.1
22
22
  Requires-Dist: elevenlabs>=2.0
23
+ Requires-Dist: keyring>=25
23
24
  Requires-Dist: tomli>=2.0; python_version < '3.11'
24
25
  Provides-Extra: dev
25
26
  Requires-Dist: build; extra == 'dev'
@@ -40,6 +41,25 @@ natural-sounding speech using the [ElevenLabs](https://elevenlabs.io) API —
40
41
  plus a hook that wires it directly into [Claude Code](https://claude.com/claude-code),
41
42
  so Claude's responses get read aloud automatically in your terminal or IDE.
42
43
 
44
+ ## Quickstart
45
+
46
+ ```bash
47
+ pipx install vocalize-cli
48
+ ```
49
+
50
+ ```bash
51
+ vocalize config
52
+ ```
53
+
54
+ Walks you through your API key, a voice, and a speed, and saves it all.
55
+
56
+ ```bash
57
+ vocalize speak "hello"
58
+ ```
59
+
60
+ To set up just the key, skip the wizard and run `vocalize auth login` — it
61
+ stores the key in your OS keychain.
62
+
43
63
  ## Why this exists
44
64
 
45
65
  Text-to-speech readers are good at *voices* and bad at *structure*. Point one
@@ -79,7 +99,23 @@ pip install -e .
79
99
  Get a free ElevenLabs API key at
80
100
  [elevenlabs.io/app/settings/api-keys](https://elevenlabs.io/app/settings/api-keys)
81
101
  (free tier: 10,000 characters/month, API access included, no commercial
82
- license). Then either:
102
+ license). Then, recommended, store it in your OS keychain:
103
+
104
+ ```bash
105
+ vocalize auth login
106
+ ```
107
+
108
+ This prompts for the key (input hidden), validates it against the
109
+ ElevenLabs API, and stores it via your OS's own keychain (macOS Keychain,
110
+ Windows Credential Locker, Linux Secret Service) — no plaintext file to
111
+ manage. Piping it in from a secret manager works too:
112
+
113
+ ```bash
114
+ op read op://vault/elevenlabs/key | vocalize auth login --stdin
115
+ ```
116
+
117
+ An environment variable or `.env` file work as well, and take priority over
118
+ the keychain if both are set:
83
119
 
84
120
  ```bash
85
121
  export ELEVENLABS_API_KEY=your-key-here
@@ -103,6 +139,9 @@ cat notes.md | vocalize speak-file -
103
139
  # List available voices and grab an ID
104
140
  vocalize voices
105
141
 
142
+ # Check your quota and cache
143
+ vocalize usage
144
+
106
145
  # Use a specific voice/model, save without playing
107
146
  vocalize speak-file report.md --voice <voice-id> --model eleven_flash_v2_5 \
108
147
  --output out.mp3 --no-play
@@ -146,6 +185,7 @@ writing anything.
146
185
 
147
186
  | Setting | Flag | Env var | Config file key | Default |
148
187
  |---|---|---|---|---|
188
+ | API key | `--api-key` | `ELEVENLABS_API_KEY` | not read from the config file | stored via `vocalize auth` |
149
189
  | Voice ID | `--voice` | `VOCALIZE_VOICE` | `voice` | `21m00Tcm4TlvDq8ikWAM` ("Rachel") |
150
190
  | Model ID | `--model` | `VOCALIZE_MODEL` | `model` | `eleven_multilingual_v2` |
151
191
  | Speed | `--speed` | `VOCALIZE_SPEED` | `speed` | unset — the API's own 1.0 |
@@ -167,8 +207,11 @@ stderr, so a typo doesn't pass unnoticed but doesn't stop the run either.
167
207
  `speed` must be a number between 0.7 and 1.2 — anything else is a one-line
168
208
  error naming the source it came from.
169
209
 
170
- The API key is separate and never read from this file: use `--api-key`,
171
- `ELEVENLABS_API_KEY`, or a `.env` file.
210
+ The API key is separate and never read from this file. It resolves in its
211
+ own order: `--api-key` flag, then `ELEVENLABS_API_KEY`, then a `.env` file
212
+ in the current directory, then the OS keychain. `vocalize auth login` sets
213
+ up the keychain entry; `vocalize auth status` shows which of those sources
214
+ is currently supplying the key.
172
215
 
173
216
  ## Claude Code integration
174
217
 
@@ -218,6 +261,36 @@ run in Claude Code's own environment, not your interactive shell — if
218
261
  `VOCALIZE_BIN` to the full path (e.g. `/path/to/.venv/bin/vocalize`) to
219
262
  point the hook at it directly.
220
263
 
264
+ ### Speaking files, artifacts, and more
265
+
266
+ Two primitives cover almost everything: `vocalize speak-file <path>` speaks
267
+ any local file (markdown flattened first), and the hook's `--latest` mode
268
+ speaks the most recent Claude Code response. Anything Claude itself has to
269
+ fetch — a claude.ai artifact, for instance — has to be fetched *by Claude*
270
+ (the CLI has no session), summarized, and piped in:
271
+
272
+ ```bash
273
+ printf '%s' "the summary text" | vocalize speak-file -
274
+ ```
275
+
276
+ If you wire this into a slash command of your own, treat it as a security
277
+ surface, because **every character you speak is sent to ElevenLabs**. The
278
+ guard principles that matter, in order:
279
+
280
+ 1. Resolve paths (`realpath`, expand `~`, casefold) and check an
281
+ **allow-list** of speakable directories — symlinks and `../` defeat
282
+ string matching on the raw argument.
283
+ 2. Hard-refuse secret-shaped files (`.env*`, keys, credentials) and your
284
+ sensitive directories; confirm before speaking anything else unusual.
285
+ 3. Summarize long or fetched content in an **isolated subagent** that
286
+ returns only the summary — content you fetched can carry instructions
287
+ aimed at your session.
288
+ 4. Pipe summaries over stdin (as above) — no temp files, nothing in argv.
289
+ 5. Confirm before any read that will spend real quota; a free tier is
290
+ 10,000 characters a month.
291
+ 6. Remember the disk cache: everything spoken leaves an mp3 under
292
+ `~/.cache/vocalize/`.
293
+
221
294
  ## How it's built
222
295
 
223
296
  Four decisions shaped the design:
@@ -293,8 +366,8 @@ All tests run offline: the ElevenLabs client is dependency-injected into
293
366
  so it's always safe to delete some or all of it — nothing will break,
294
367
  you'll just re-pay for a re-synthesized clip.
295
368
  - **`--api-key` on the command line is visible to other local processes**
296
- (anything that can run `ps`). Prefer the `ELEVENLABS_API_KEY` environment
297
- variable or a `.env` file instead.
369
+ (anything that can run `ps`). Prefer `vocalize auth login`, the
370
+ `ELEVENLABS_API_KEY` environment variable, or a `.env` file instead.
298
371
  - `vocalize voices` lists only the first page of results from the
299
372
  ElevenLabs API.
300
373
 
@@ -7,6 +7,25 @@ natural-sounding speech using the [ElevenLabs](https://elevenlabs.io) API —
7
7
  plus a hook that wires it directly into [Claude Code](https://claude.com/claude-code),
8
8
  so Claude's responses get read aloud automatically in your terminal or IDE.
9
9
 
10
+ ## Quickstart
11
+
12
+ ```bash
13
+ pipx install vocalize-cli
14
+ ```
15
+
16
+ ```bash
17
+ vocalize config
18
+ ```
19
+
20
+ Walks you through your API key, a voice, and a speed, and saves it all.
21
+
22
+ ```bash
23
+ vocalize speak "hello"
24
+ ```
25
+
26
+ To set up just the key, skip the wizard and run `vocalize auth login` — it
27
+ stores the key in your OS keychain.
28
+
10
29
  ## Why this exists
11
30
 
12
31
  Text-to-speech readers are good at *voices* and bad at *structure*. Point one
@@ -46,7 +65,23 @@ pip install -e .
46
65
  Get a free ElevenLabs API key at
47
66
  [elevenlabs.io/app/settings/api-keys](https://elevenlabs.io/app/settings/api-keys)
48
67
  (free tier: 10,000 characters/month, API access included, no commercial
49
- license). Then either:
68
+ license). Then, recommended, store it in your OS keychain:
69
+
70
+ ```bash
71
+ vocalize auth login
72
+ ```
73
+
74
+ This prompts for the key (input hidden), validates it against the
75
+ ElevenLabs API, and stores it via your OS's own keychain (macOS Keychain,
76
+ Windows Credential Locker, Linux Secret Service) — no plaintext file to
77
+ manage. Piping it in from a secret manager works too:
78
+
79
+ ```bash
80
+ op read op://vault/elevenlabs/key | vocalize auth login --stdin
81
+ ```
82
+
83
+ An environment variable or `.env` file work as well, and take priority over
84
+ the keychain if both are set:
50
85
 
51
86
  ```bash
52
87
  export ELEVENLABS_API_KEY=your-key-here
@@ -70,6 +105,9 @@ cat notes.md | vocalize speak-file -
70
105
  # List available voices and grab an ID
71
106
  vocalize voices
72
107
 
108
+ # Check your quota and cache
109
+ vocalize usage
110
+
73
111
  # Use a specific voice/model, save without playing
74
112
  vocalize speak-file report.md --voice <voice-id> --model eleven_flash_v2_5 \
75
113
  --output out.mp3 --no-play
@@ -113,6 +151,7 @@ writing anything.
113
151
 
114
152
  | Setting | Flag | Env var | Config file key | Default |
115
153
  |---|---|---|---|---|
154
+ | API key | `--api-key` | `ELEVENLABS_API_KEY` | not read from the config file | stored via `vocalize auth` |
116
155
  | Voice ID | `--voice` | `VOCALIZE_VOICE` | `voice` | `21m00Tcm4TlvDq8ikWAM` ("Rachel") |
117
156
  | Model ID | `--model` | `VOCALIZE_MODEL` | `model` | `eleven_multilingual_v2` |
118
157
  | Speed | `--speed` | `VOCALIZE_SPEED` | `speed` | unset — the API's own 1.0 |
@@ -134,8 +173,11 @@ stderr, so a typo doesn't pass unnoticed but doesn't stop the run either.
134
173
  `speed` must be a number between 0.7 and 1.2 — anything else is a one-line
135
174
  error naming the source it came from.
136
175
 
137
- The API key is separate and never read from this file: use `--api-key`,
138
- `ELEVENLABS_API_KEY`, or a `.env` file.
176
+ The API key is separate and never read from this file. It resolves in its
177
+ own order: `--api-key` flag, then `ELEVENLABS_API_KEY`, then a `.env` file
178
+ in the current directory, then the OS keychain. `vocalize auth login` sets
179
+ up the keychain entry; `vocalize auth status` shows which of those sources
180
+ is currently supplying the key.
139
181
 
140
182
  ## Claude Code integration
141
183
 
@@ -185,6 +227,36 @@ run in Claude Code's own environment, not your interactive shell — if
185
227
  `VOCALIZE_BIN` to the full path (e.g. `/path/to/.venv/bin/vocalize`) to
186
228
  point the hook at it directly.
187
229
 
230
+ ### Speaking files, artifacts, and more
231
+
232
+ Two primitives cover almost everything: `vocalize speak-file <path>` speaks
233
+ any local file (markdown flattened first), and the hook's `--latest` mode
234
+ speaks the most recent Claude Code response. Anything Claude itself has to
235
+ fetch — a claude.ai artifact, for instance — has to be fetched *by Claude*
236
+ (the CLI has no session), summarized, and piped in:
237
+
238
+ ```bash
239
+ printf '%s' "the summary text" | vocalize speak-file -
240
+ ```
241
+
242
+ If you wire this into a slash command of your own, treat it as a security
243
+ surface, because **every character you speak is sent to ElevenLabs**. The
244
+ guard principles that matter, in order:
245
+
246
+ 1. Resolve paths (`realpath`, expand `~`, casefold) and check an
247
+ **allow-list** of speakable directories — symlinks and `../` defeat
248
+ string matching on the raw argument.
249
+ 2. Hard-refuse secret-shaped files (`.env*`, keys, credentials) and your
250
+ sensitive directories; confirm before speaking anything else unusual.
251
+ 3. Summarize long or fetched content in an **isolated subagent** that
252
+ returns only the summary — content you fetched can carry instructions
253
+ aimed at your session.
254
+ 4. Pipe summaries over stdin (as above) — no temp files, nothing in argv.
255
+ 5. Confirm before any read that will spend real quota; a free tier is
256
+ 10,000 characters a month.
257
+ 6. Remember the disk cache: everything spoken leaves an mp3 under
258
+ `~/.cache/vocalize/`.
259
+
188
260
  ## How it's built
189
261
 
190
262
  Four decisions shaped the design:
@@ -260,8 +332,8 @@ All tests run offline: the ElevenLabs client is dependency-injected into
260
332
  so it's always safe to delete some or all of it — nothing will break,
261
333
  you'll just re-pay for a re-synthesized clip.
262
334
  - **`--api-key` on the command line is visible to other local processes**
263
- (anything that can run `ps`). Prefer the `ELEVENLABS_API_KEY` environment
264
- variable or a `.env` file instead.
335
+ (anything that can run `ps`). Prefer `vocalize auth login`, the
336
+ `ELEVENLABS_API_KEY` environment variable, or a `.env` file instead.
265
337
  - `vocalize voices` lists only the first page of results from the
266
338
  ElevenLabs API.
267
339
 
@@ -25,6 +25,7 @@ classifiers = [
25
25
  dependencies = [
26
26
  "elevenlabs>=2.0",
27
27
  "click>=8.1",
28
+ "keyring>=25",
28
29
  "tomli>=2.0; python_version < '3.11'",
29
30
  ]
30
31
 
@@ -0,0 +1,56 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "hooks"))
5
+
6
+ import pytest
7
+
8
+
9
+ @pytest.fixture(autouse=True)
10
+ def _no_dotenv(monkeypatch):
11
+ """Prevent tests from loading the developer's real .env file."""
12
+ monkeypatch.setattr("vocalize.config._load_dotenv_if_present", lambda: None)
13
+
14
+
15
+ class _FakeKeychain(dict):
16
+ """The stored entries, plus the switches a test needs to break them."""
17
+
18
+ deny_delete = False
19
+
20
+
21
+ class _FakeKeyring:
22
+ """An in-memory stand-in for the keyring module's three calls."""
23
+
24
+ def __init__(self, store):
25
+ self._store = store
26
+
27
+ def get_password(self, service, username):
28
+ return self._store.get((service, username))
29
+
30
+ def set_password(self, service, username, password):
31
+ self._store[(service, username)] = password
32
+
33
+ def delete_password(self, service, username):
34
+ from keyring.errors import PasswordDeleteError
35
+
36
+ if self._store.deny_delete:
37
+ # The macOS backend's worst habit: a denied or locked keychain
38
+ # raises the very same error a missing entry does, while the
39
+ # entry is still sitting there.
40
+ raise PasswordDeleteError("failed to delete password")
41
+ if self._store.pop((service, username), None) is None:
42
+ raise PasswordDeleteError("no such password")
43
+
44
+
45
+ @pytest.fixture(autouse=True)
46
+ def fake_keychain(monkeypatch):
47
+ """Keep every test off the real OS keychain.
48
+
49
+ Autouse because the damage of missing one is silent: a stored key on
50
+ the developer's machine would otherwise satisfy resolve_api_key and
51
+ quietly turn the "no key found" tests green for the wrong reason.
52
+ Request it by name to seed, inspect, or break the store.
53
+ """
54
+ store = _FakeKeychain()
55
+ monkeypatch.setattr("vocalize.auth._backend", lambda: _FakeKeyring(store))
56
+ return store