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.
- vocalize_cli-0.4.0/CHANGELOG.md +89 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/PKG-INFO +79 -6
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/README.md +77 -5
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/pyproject.toml +1 -0
- vocalize_cli-0.4.0/tests/conftest.py +56 -0
- vocalize_cli-0.4.0/tests/test_auth.py +333 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_cli.py +45 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_config.py +13 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_tts.py +46 -1
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_wizard.py +94 -4
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/__init__.py +1 -1
- vocalize_cli-0.4.0/vocalize/auth.py +215 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/cli.py +98 -1
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/config.py +10 -1
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/exceptions.py +9 -4
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/tts.py +19 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/wizard.py +46 -4
- vocalize_cli-0.2.1/tests/conftest.py +0 -12
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/.env.example +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/.github/workflows/ci.yml +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/.gitignore +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/LICENSE +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/hooks/claude_stop_hook.py +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/hooks/install_hook.py +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_audio.py +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_claude_stop_hook.py +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_install_hook.py +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/tests/test_preprocess.py +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/__main__.py +0 -0
- {vocalize_cli-0.2.1 → vocalize_cli-0.4.0}/vocalize/audio.py +0 -0
- {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.
|
|
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
|
|
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
|
|
171
|
-
`ELEVENLABS_API_KEY`,
|
|
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
|
|
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
|
|
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
|
|
138
|
-
`ELEVENLABS_API_KEY`,
|
|
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
|
|
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
|
|
|
@@ -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
|