vocalize-cli 0.10.1__tar.gz → 0.11.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 (153) hide show
  1. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/.gitignore +1 -0
  2. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/CHANGELOG.md +122 -0
  3. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/PKG-INFO +43 -1
  4. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/README.md +42 -0
  5. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/dictation.md +14 -1
  6. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/installation.md +13 -0
  7. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/decisions.md +471 -1
  8. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/design.md +127 -1
  9. vocalize_cli-0.11.0/docs/plans/2026-09-next-features/review-0.11.0.md +134 -0
  10. vocalize_cli-0.11.0/docs/plans/2026-09-next-features/run-10-release-0-11-0/report.md +153 -0
  11. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-10-release-0-11-0/validate-exit.sh +31 -2
  12. vocalize_cli-0.11.0/docs/plans/2026-09-next-features/run-7-portal-read/report.md +297 -0
  13. vocalize_cli-0.11.0/docs/plans/2026-09-next-features/run-7-portal-read/review-findings.md +130 -0
  14. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-7-portal-read/validate-exit.sh +8 -1
  15. vocalize_cli-0.11.0/docs/plans/2026-09-next-features/run-8-portal-write/report.md +346 -0
  16. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-8-portal-write/validate-exit.sh +29 -2
  17. vocalize_cli-0.11.0/docs/plans/2026-09-next-features/run-9-portal-page/report.md +231 -0
  18. vocalize_cli-0.11.0/docs/plans/2026-09-next-features/run-9-portal-page/review-findings.md +0 -0
  19. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-9-portal-page/validate-exit.sh +25 -1
  20. vocalize_cli-0.11.0/docs/research/2026-09-04-app-roadmap-analysis.md +290 -0
  21. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/roadmap.md +5 -1
  22. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/conftest.py +33 -0
  23. vocalize_cli-0.11.0/tests/portal_page_harness.js +713 -0
  24. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_auth.py +10 -0
  25. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_cli.py +227 -2
  26. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_config.py +45 -0
  27. vocalize_cli-0.11.0/tests/test_cue_assets.py +28 -0
  28. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_dictate.py +102 -0
  29. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_elevenlabs_provider.py +1 -1
  30. vocalize_cli-0.11.0/tests/test_portal.py +3835 -0
  31. vocalize_cli-0.11.0/tests/test_portal_assets.py +393 -0
  32. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_readiness.py +44 -15
  33. vocalize_cli-0.11.0/tests/test_tts.py +411 -0
  34. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_wizard.py +375 -1
  35. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/__init__.py +1 -1
  36. vocalize_cli-0.11.0/vocalize/assets/cues/README.md +11 -0
  37. vocalize_cli-0.11.0/vocalize/assets/cues/ready.wav +0 -0
  38. vocalize_cli-0.11.0/vocalize/assets/cues/start.wav +0 -0
  39. vocalize_cli-0.11.0/vocalize/assets/cues/stopped.wav +0 -0
  40. vocalize_cli-0.11.0/vocalize/assets/portal.html +295 -0
  41. vocalize_cli-0.11.0/vocalize/assets/portal.js +1486 -0
  42. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/auth.py +8 -1
  43. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/cli.py +59 -9
  44. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/config.py +34 -8
  45. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/dictate.py +39 -4
  46. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/exceptions.py +15 -0
  47. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/local/install.py +10 -0
  48. vocalize_cli-0.11.0/vocalize/portal.py +1693 -0
  49. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/readiness.py +31 -6
  50. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/tts.py +44 -5
  51. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/wizard.py +183 -17
  52. vocalize_cli-0.10.1/tests/test_tts.py +0 -201
  53. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/.env.example +0 -0
  54. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/.github/workflows/ci.yml +0 -0
  55. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/LICENSE +0 -0
  56. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/next-features-analysis.md +0 -0
  57. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/choreography.md +0 -0
  58. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/plan.md +0 -0
  59. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/review-0.10.0.md +0 -0
  60. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-1-status/project-plan.md +0 -0
  61. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-1-status/report.md +0 -0
  62. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-1-status/validate-exit.sh +0 -0
  63. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-10-release-0-11-0/project-plan.md +0 -0
  64. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-2-stt-runtime/project-plan.md +0 -0
  65. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-2-stt-runtime/report.md +0 -0
  66. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-2-stt-runtime/validate-exit.sh +0 -0
  67. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-3-recorder/project-plan.md +0 -0
  68. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-3-recorder/report.md +0 -0
  69. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-3-recorder/validate-exit.sh +0 -0
  70. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-4-dictation/project-plan.md +0 -0
  71. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-4-dictation/report.md +0 -0
  72. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-4-dictation/validate-exit.sh +0 -0
  73. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-5-resume/project-plan.md +0 -0
  74. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-5-resume/report.md +0 -0
  75. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-5-resume/validate-exit.sh +0 -0
  76. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-6-release-0-10-0/project-plan.md +0 -0
  77. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-6-release-0-10-0/report.md +0 -0
  78. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-6-release-0-10-0/validate-exit.sh +0 -0
  79. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-7-portal-read/project-plan.md +0 -0
  80. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-8-portal-write/project-plan.md +0 -0
  81. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/run-9-portal-page/project-plan.md +0 -0
  82. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/spike-2026-09-01.md +0 -0
  83. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/split-assessment.md +0 -0
  84. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/plans/2026-09-next-features/verification.md +0 -0
  85. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/provider-credentials.md +0 -0
  86. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/research/2026-09-01-config-portal-design.md +0 -0
  87. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/research/2026-09-01-dictation-design.md +0 -0
  88. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/docs/research/2026-09-01-voicebox-findings.md +0 -0
  89. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/claude_stop_hook.py +0 -0
  90. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/install_hook.py +0 -0
  91. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/install_quick_action.py +0 -0
  92. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/quick_actions/Dictate with Vocalize.workflow/Contents/Info.plist +0 -0
  93. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/quick_actions/Dictate with Vocalize.workflow/Contents/Resources/document.wflow +0 -0
  94. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/quick_actions/Speak Latest Plan.workflow/Contents/Info.plist +0 -0
  95. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/quick_actions/Speak Latest Plan.workflow/Contents/Resources/document.wflow +0 -0
  96. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/quick_actions/Speak with Vocalize.workflow/Contents/Info.plist +0 -0
  97. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/quick_actions/Speak with Vocalize.workflow/Contents/Resources/document.wflow +0 -0
  98. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/quick_actions/Stop Vocalize.workflow/Contents/Info.plist +0 -0
  99. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/quick_actions/Stop Vocalize.workflow/Contents/Resources/document.wflow +0 -0
  100. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/speak_options.py +0 -0
  101. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/hooks/speak_url_gate.py +0 -0
  102. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/pyproject.toml +0 -0
  103. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_audio.py +0 -0
  104. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_cache.py +0 -0
  105. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_chain.py +0 -0
  106. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_claude_stop_hook.py +0 -0
  107. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_clipboard.py +0 -0
  108. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_exceptions.py +0 -0
  109. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_google_provider.py +0 -0
  110. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_http.py +0 -0
  111. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_install_hook.py +0 -0
  112. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_install_quick_action.py +0 -0
  113. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_kokoro_manifest.py +0 -0
  114. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_kokoro_provider.py +0 -0
  115. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_kokoro_worker.py +0 -0
  116. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_ledger.py +0 -0
  117. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_listen_check.py +0 -0
  118. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_local_install.py +0 -0
  119. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_openai_provider.py +0 -0
  120. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_polly_provider.py +0 -0
  121. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_preprocess.py +0 -0
  122. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_providers_registry.py +0 -0
  123. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_recorder_build.py +0 -0
  124. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_say_provider.py +0 -0
  125. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_speak_options.py +0 -0
  126. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_speak_url_gate.py +0 -0
  127. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_uv_path.py +0 -0
  128. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_whisper_manifest.py +0 -0
  129. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/tests/test_whisper_worker.py +0 -0
  130. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/__main__.py +0 -0
  131. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/audio.py +0 -0
  132. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/cache.py +0 -0
  133. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/chain.py +0 -0
  134. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/clipboard.py +0 -0
  135. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/interrupted.py +0 -0
  136. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/ledger.py +0 -0
  137. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/local/__init__.py +0 -0
  138. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/local/kokoro_manifest.py +0 -0
  139. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/local/kokoro_worker.py +0 -0
  140. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/local/whisper_manifest.py +0 -0
  141. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/local/whisper_worker.py +0 -0
  142. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/preprocess.py +0 -0
  143. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/providers/__init__.py +0 -0
  144. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/providers/_http.py +0 -0
  145. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/providers/elevenlabs.py +0 -0
  146. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/providers/google.py +0 -0
  147. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/providers/kokoro.py +0 -0
  148. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/providers/openai.py +0 -0
  149. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/providers/polly.py +0 -0
  150. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/providers/say.py +0 -0
  151. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/recorder/Info.plist.in +0 -0
  152. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/recorder/Recorder.entitlements +0 -0
  153. {vocalize_cli-0.10.1 → vocalize_cli-0.11.0}/vocalize/recorder/VocalizeRecorder.swift +0 -0
@@ -11,5 +11,6 @@ dist/
11
11
  venv/
12
12
  *.mp3
13
13
  *.wav
14
+ !vocalize/assets/cues/*.wav
14
15
  .claude/
15
16
  uv.lock
@@ -3,6 +3,128 @@
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.11.0 - 2026-09-05
7
+
8
+ ### Added
9
+
10
+ - **A web portal page** (`vocalize portal`) for the whole config surface: reorder
11
+ the provider chain; set each provider's voice, speed and monthly budget with a
12
+ live preview; store and check API keys (masked, `autocomplete="off"`); watch
13
+ usage; and drive the local Kokoro and whisper installs with progress — no more
14
+ hand-editing `config.toml`. Five tabs (Chain, Providers, Keys, Usage, Local)
15
+ plus a persistent readiness sidebar reading `/api/state`, with buttons on the
16
+ rows that map to a vocalize command the portal already runs. Every save goes
17
+ through the same compare-and-swap the CLI uses, so a change made elsewhere
18
+ while the page was open is refused rather than silently overwritten. Ships as
19
+ static HTML and JS with no framework, no inline script and no external
20
+ resource — the whole surface has to work with the network cable pulled — over
21
+ a loopback-only, one-time-code session (DEC-004).
22
+ - **A voice picker on the Providers tab.** `GET /api/voices/<name>` returns the
23
+ provider's own `list_voices()`, paginated and cached for the life of the
24
+ portal process, so the field is a real dropdown of the account's actual
25
+ voices with the current value pre-selected, and `"Type a voice id…"` for an
26
+ id the list doesn't carry — rather than a hand-typed guess.
27
+
28
+ ### Fixed
29
+
30
+ - **`vocalize voices` and `vocalize usage` printed the ElevenLabs API key to
31
+ stderr when the API quoted it back.** The SDK's `ApiError` renders the whole
32
+ response body and header dict, and both commands printed that text verbatim —
33
+ so a key the API named in an error reached terminal scrollback, any session
34
+ log, and whatever the user pasted into a bug report. `auth.scrub` already
35
+ existed for exactly this, but three callers never reached it. The scrub now
36
+ happens inside `tts` itself, below every caller, against the key the client
37
+ was built with. Found by the 0.11.0 release review (DEC-021).
38
+ - **The portal's `say` voice list failed on the first ask.** The route gave every
39
+ provider the two-second budget meant for network probes, and enumerating the
40
+ system voices takes longer than that on a current Mac — so the one provider
41
+ needing no key, no account and no network was the one that said "couldn't
42
+ fetch the list". Offline lists get their own budget now.
43
+ - **A key of four characters or fewer was shown in full** by `vocalize auth
44
+ status` and on the portal's Keys tab — a truncated paste, or another tool's
45
+ short secret in the same environment variable, reproduced whole in output the
46
+ design treats as safe to screenshot. Anything under eight characters now masks
47
+ to `…` alone.
48
+ - **The portal's Keys tab pointed at Keychain Access** to remove a stored key.
49
+ It now names `vocalize auth logout --provider <name>`, which reads the entry
50
+ back and refuses to claim a removal it cannot verify.
51
+ - **The ElevenLabs SDK client followed a redirect and re-sent the API key
52
+ wherever it pointed, cross-origin, over plain http.** The SDK's httpx client
53
+ follows a 3xx by default; every ElevenLabs endpoint is a fixed URL, so a
54
+ redirect was never legitimate and the key had no business leaving the
55
+ original origin. Present since 0.9.0, when the ElevenLabs provider first
56
+ shipped. Closed by building the client with `follow_redirects=False` — the
57
+ same rule the urllib-based providers already held via `_http._NoRedirects` —
58
+ and proven on two real loopback HTTP servers: the second origin never sees
59
+ the request.
60
+ - **Nothing could rewrite a config file containing an `[stt]` table** —
61
+ which is every config a dictation user has had since 0.10.0 added the
62
+ table. The serialiser wrote flat keys and `[providers.*]` and had no
63
+ case for any other table, so `stt` reached the scalar renderer as a
64
+ `dict` and raised: "The config file has a table under 'stt'. The wizard
65
+ only manages flat keys, so it will not rewrite this file — edit that
66
+ file by hand." That refusal came out of `vocalize wizard` before it
67
+ asked its first question, and out of `vocalize chain google say` instead
68
+ of a write. The only way out was to delete the `[stt]` table, run the
69
+ command, and put it back. `[stt]` is now rendered as its own table, so
70
+ both commands rewrite the file and leave the dictation settings alone.
71
+ The rewritten file puts `[stt]` ahead of `[providers.*]` whatever order
72
+ they were in before — identical TOML, a one-off cosmetic diff.
73
+ - **Two writers saving at the same moment could lose one of the two
74
+ changes.** The config write compared the file against the fingerprint it
75
+ had read and then renamed a temp file over it, with the two steps not
76
+ held together, so two saves that overlapped could both see an unchanged
77
+ file, both write, and both report success while only the second survived.
78
+ The compare and the rename now happen under one lock. Two `vocalize`
79
+ *processes* saving in the same instant can still race — the fingerprint
80
+ check narrows that to a window of microseconds, and closing it entirely
81
+ needs a lock file, which is deliberately not what this is.
82
+ - **A config save rendered through a fixed `config.toml.tmp`**, so two
83
+ writers at once could render through the same temp file and leave one of
84
+ them writing a file the other had already renamed away — reported as
85
+ "Could not write config file", or worse, reported as success for bytes
86
+ that were not in the file. Each writer now gets its own temp file, made
87
+ with `mkstemp` in the same directory, still `0600`, and still renamed
88
+ into place atomically. The predictable name a local process could
89
+ pre-plant as a symlink is gone with it.
90
+
91
+ ### Known limitation
92
+
93
+ - **Any other process on the Mac can close the portal with five wrong or
94
+ missing-origin requests to `/api/session`**, no code and no token needed —
95
+ the same guard that stops a cross-origin web page from doing it has no way
96
+ to tell that traffic apart from a local script's. This is availability
97
+ only: nothing is read or changed, the portal just exits. Rerun
98
+ `vocalize portal` and carry on. Accepted rather than fixed, since anything
99
+ able to send that traffic locally could kill the process outright anyway
100
+ (DEC-018).
101
+
102
+ ## 0.10.2 - 2026-09-02
103
+
104
+ ### Added
105
+
106
+ - **`[stt] cues`** picks what a dictation's feedback sounds — say instead
107
+ of, or alongside, the Tink/Pop/Glass system sounds. `"sounds"` (default)
108
+ is unchanged; `"words"` speaks "Start.", "Stopped.", "Ready." in their
109
+ place; `"both"` speaks the word and then plays the sound. The word files
110
+ ship in `vocalize/assets/cues/`, generated with the local Kokoro voice.
111
+ A spoken "Start." plays *before* the recorder launches — played once the
112
+ microphone was open it would be recorded and transcribed along with the
113
+ dictation. In `"both"` mode the Tink still plays *after* the microphone
114
+ opens, so the two cues keep distinct meanings: the word is "get ready",
115
+ the sound is "talk now". The plain Tink is unaffected.
116
+
117
+ ### Known issue
118
+
119
+ - In `"words"` mode there is no cue for the moment the microphone actually
120
+ opens, which is a second or so after "Start." finishes (LaunchServices
121
+ start-up plus the input device switching on). People start talking too
122
+ soon and lose their first word. The fix — open and warm the microphone
123
+ first, play the cue, and only then capture — is tracked in
124
+ [#2](https://github.com/matthager12-collab/vocalize/issues/2). Until
125
+ then: in `"words"` mode, wait a beat after "Start."; in `"both"` mode,
126
+ talk after the Tink.
127
+
6
128
  ## 0.10.1 - 2026-09-02
7
129
 
8
130
  Three fixes found in the first owner-present run of 0.10.0's dictation.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vocalize-cli
3
- Version: 0.10.1
3
+ Version: 0.11.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
@@ -216,6 +216,9 @@ Hotkeys: `↑`/`↓` or `k`/`j` move, `Enter` selects, `p` previews the
216
216
  highlighted voice, `m` types a value by hand, `q` or `Esc` cancels without
217
217
  writing anything.
218
218
 
219
+ There's also a settings page in the browser — see
220
+ [Settings portal](#settings-portal) below.
221
+
219
222
  | Setting | Flag | Env var | Config file key | Default |
220
223
  |---|---|---|---|---|
221
224
  | API key | `--api-key` | `ELEVENLABS_API_KEY` | not read from the config file | stored via `vocalize auth` |
@@ -295,6 +298,41 @@ configures a TTS provider — see
295
298
  [Dictation](#dictation-speech-to-text) below for every key and its
296
299
  allowlist.
297
300
 
301
+ ## Settings portal
302
+
303
+ A page in your own browser for the whole config surface above, so you don't
304
+ have to hand-edit `config.toml` or click through `vocalize config` one
305
+ setting at a time.
306
+
307
+ ```bash
308
+ vocalize portal # opens your browser at a one-time link
309
+ vocalize portal --no-browser # prints the link instead (headless, SSH)
310
+ ```
311
+
312
+ Five tabs:
313
+
314
+ - **Chain** — reorder providers, or add and remove one.
315
+ - **Providers** — each provider's voice, model, speed, and monthly budget,
316
+ with a live preview of the voice you're looking at.
317
+ - **Keys** — store or check an API key; the field is masked and never
318
+ offers to autocomplete.
319
+ - **Usage** — this month's spend and quota per provider.
320
+ - **Local** — install or update the on-device Kokoro and whisper models,
321
+ with progress.
322
+
323
+ The opening link works once and for 60 seconds; after that, run the command
324
+ again for a new one. The server only ever listens on `127.0.0.1`, and it
325
+ closes itself on Ctrl-C or after fifteen minutes with nothing to do. Nothing
326
+ you change on the page reaches disk until you press Save on that tab — and
327
+ a save made against a config file that's changed on disk since the page
328
+ loaded is refused rather than silently overwritten, the same protection
329
+ `vocalize chain` and `vocalize config` already have.
330
+
331
+ It assumes a single-user machine: everything that reads or changes your
332
+ settings is behind that one-time token, but any other process on the Mac
333
+ can still reach the port and close the portal under you. Nothing leaks if
334
+ that happens — it's a shutdown, not a read — just run the command again.
335
+
298
336
  ## Providers and fallback
299
337
 
300
338
  vocalize tries providers in order — a **chain** — until one speaks. The
@@ -485,6 +523,8 @@ terminal, if you'd rather trigger it that way.
485
523
  - **A third press while transcribing is refused**: a Pop, and "Still
486
524
  transcribing the last dictation." Wait for the clipboard notification, or
487
525
  `--cancel`, before dictating again.
526
+ - Can't tell Tink from Pop from Glass yet? Set `[stt] cues = "words"` and
527
+ vocalize says "Start.", "Stopped.", "Ready." instead.
488
528
 
489
529
  ### `vocalize listen`
490
530
 
@@ -519,6 +559,7 @@ input_device = "" # "" = system default; else an exact name from --list-dev
519
559
  cleanup = false # send the transcript (never audio) to Claude first
520
560
  max_seconds = 120 # 1-600; the recorder self-stops here, dictate backstops it
521
561
  sounds = true # the Tink/Pop/Glass feedback sounds
562
+ cues = "sounds" # "sounds" | "words" | "both" — speak "Start."/"Stopped."/"Ready." instead
522
563
  ```
523
564
 
524
565
  | Key | Allowed values | Default |
@@ -530,6 +571,7 @@ sounds = true # the Tink/Pop/Glass feedback sounds
530
571
  | `paste` | reserved — not implemented in 0.10.0 | `false` |
531
572
  | `max_seconds` | integer, 1–600 | `120` |
532
573
  | `sounds` | `true` / `false` | `true` |
574
+ | `cues` | `sounds`, `words`, `both` | `sounds` |
533
575
 
534
576
  An unknown key warns on stderr; a bad value is a `ConfigError` naming it —
535
577
  every one of these becomes a subprocess argument eventually, so nothing
@@ -180,6 +180,9 @@ Hotkeys: `↑`/`↓` or `k`/`j` move, `Enter` selects, `p` previews the
180
180
  highlighted voice, `m` types a value by hand, `q` or `Esc` cancels without
181
181
  writing anything.
182
182
 
183
+ There's also a settings page in the browser — see
184
+ [Settings portal](#settings-portal) below.
185
+
183
186
  | Setting | Flag | Env var | Config file key | Default |
184
187
  |---|---|---|---|---|
185
188
  | API key | `--api-key` | `ELEVENLABS_API_KEY` | not read from the config file | stored via `vocalize auth` |
@@ -259,6 +262,41 @@ configures a TTS provider — see
259
262
  [Dictation](#dictation-speech-to-text) below for every key and its
260
263
  allowlist.
261
264
 
265
+ ## Settings portal
266
+
267
+ A page in your own browser for the whole config surface above, so you don't
268
+ have to hand-edit `config.toml` or click through `vocalize config` one
269
+ setting at a time.
270
+
271
+ ```bash
272
+ vocalize portal # opens your browser at a one-time link
273
+ vocalize portal --no-browser # prints the link instead (headless, SSH)
274
+ ```
275
+
276
+ Five tabs:
277
+
278
+ - **Chain** — reorder providers, or add and remove one.
279
+ - **Providers** — each provider's voice, model, speed, and monthly budget,
280
+ with a live preview of the voice you're looking at.
281
+ - **Keys** — store or check an API key; the field is masked and never
282
+ offers to autocomplete.
283
+ - **Usage** — this month's spend and quota per provider.
284
+ - **Local** — install or update the on-device Kokoro and whisper models,
285
+ with progress.
286
+
287
+ The opening link works once and for 60 seconds; after that, run the command
288
+ again for a new one. The server only ever listens on `127.0.0.1`, and it
289
+ closes itself on Ctrl-C or after fifteen minutes with nothing to do. Nothing
290
+ you change on the page reaches disk until you press Save on that tab — and
291
+ a save made against a config file that's changed on disk since the page
292
+ loaded is refused rather than silently overwritten, the same protection
293
+ `vocalize chain` and `vocalize config` already have.
294
+
295
+ It assumes a single-user machine: everything that reads or changes your
296
+ settings is behind that one-time token, but any other process on the Mac
297
+ can still reach the port and close the portal under you. Nothing leaks if
298
+ that happens — it's a shutdown, not a read — just run the command again.
299
+
262
300
  ## Providers and fallback
263
301
 
264
302
  vocalize tries providers in order — a **chain** — until one speaks. The
@@ -449,6 +487,8 @@ terminal, if you'd rather trigger it that way.
449
487
  - **A third press while transcribing is refused**: a Pop, and "Still
450
488
  transcribing the last dictation." Wait for the clipboard notification, or
451
489
  `--cancel`, before dictating again.
490
+ - Can't tell Tink from Pop from Glass yet? Set `[stt] cues = "words"` and
491
+ vocalize says "Start.", "Stopped.", "Ready." instead.
452
492
 
453
493
  ### `vocalize listen`
454
494
 
@@ -483,6 +523,7 @@ input_device = "" # "" = system default; else an exact name from --list-dev
483
523
  cleanup = false # send the transcript (never audio) to Claude first
484
524
  max_seconds = 120 # 1-600; the recorder self-stops here, dictate backstops it
485
525
  sounds = true # the Tink/Pop/Glass feedback sounds
526
+ cues = "sounds" # "sounds" | "words" | "both" — speak "Start."/"Stopped."/"Ready." instead
486
527
  ```
487
528
 
488
529
  | Key | Allowed values | Default |
@@ -494,6 +535,7 @@ sounds = true # the Tink/Pop/Glass feedback sounds
494
535
  | `paste` | reserved — not implemented in 0.10.0 | `false` |
495
536
  | `max_seconds` | integer, 1–600 | `120` |
496
537
  | `sounds` | `true` / `false` | `true` |
538
+ | `cues` | `sounds`, `words`, `both` | `sounds` |
497
539
 
498
540
  An unknown key warns on stderr; a bad value is a `ConfigError` naming it —
499
541
  every one of these becomes a subprocess argument eventually, so nothing
@@ -147,6 +147,16 @@ never runs two transcriptions over the same recording, and it never
147
147
  silently drops one either. Wait for the clipboard notification, or use
148
148
  `--cancel`.
149
149
 
150
+ New to the sounds and can't tell Tink from Pop from Glass? Set
151
+ `[stt] cues = "words"` and vocalize says "Start.", "Stopped." and "Ready."
152
+ instead, or `"both"` to hear the word and then its sound — see the `[stt]`
153
+ table below. Timing matters: "Start." is spoken *before* the microphone
154
+ opens (so it is never in your recording), and the microphone is open
155
+ about a second later — the Tink marks that moment. So in `"both"` mode
156
+ talk after the Tink; in `"words"` mode give it a beat after "Start.".
157
+ Closing that gap is
158
+ [#2](https://github.com/matthager12-collab/vocalize/issues/2).
159
+
150
160
  ## `vocalize listen`
151
161
 
152
162
  The terminal-facing primitive behind the hotkey:
@@ -216,6 +226,7 @@ cleanup = false
216
226
  paste = false
217
227
  max_seconds = 120
218
228
  sounds = true
229
+ cues = "sounds" # "sounds" | "words" | "both"
219
230
  ```
220
231
 
221
232
  | Key | Type / allowlist | Default | Notes |
@@ -226,7 +237,8 @@ sounds = true
226
237
  | `cleanup` | `true` / `false` | `false` | see `--cleanup` above |
227
238
  | `paste` | reserved | `false` | not implemented in 0.10.0 — setting it does nothing |
228
239
  | `max_seconds` | integer, 1–600 | `120` | the recorder self-stops here; `dictate` backstops it a few seconds later in case the recorder doesn't |
229
- | `sounds` | `true` / `false` | `true` | the Tink/Pop/Glass feedback; `false` silences all three |
240
+ | `sounds` | `true` / `false` | `true` | the Tink/Pop/Glass feedback; `false` silences all three (words included) |
241
+ | `cues` | `sounds`, `words`, `both` | `sounds` | `"words"` speaks "Start.", "Stopped.", "Ready." instead of the system sounds; `"both"` speaks the word and then plays the sound — for the start cue, the word before the microphone opens and the Tink once it has. Has no effect while `sounds = false`. |
230
242
 
231
243
  Every value here eventually becomes a subprocess argument — the recorder's
232
244
  `--device`, or the whisper worker's `--model`/`--language` — so each one is
@@ -243,6 +255,7 @@ stt.model=small.en
243
255
  stt.language=en
244
256
  stt.cleanup=false
245
257
  stt.max_seconds=120
258
+ stt.cues=sounds
246
259
  ```
247
260
 
248
261
  ### The input-device gotcha
@@ -75,6 +75,19 @@ Smoke test (also proves stdin + preprocessing):
75
75
  echo "vocalize is alive" | vocalize speak-file -
76
76
  ```
77
77
 
78
+ ### Or: finish setup in a browser
79
+
80
+ Everything above — keys, the chain, the local models — can be done from one
81
+ page instead:
82
+
83
+ ```bash
84
+ vocalize portal
85
+ ```
86
+
87
+ It opens a one-time link at `127.0.0.1` (valid for 60 seconds), and writes
88
+ nothing until you save. See [README § Settings portal](../README.md#settings-portal)
89
+ for what each tab does.
90
+
78
91
  ## Layer 2 — the `/speak` command in Claude Code
79
92
 
80
93
  Until the repo ships this (tracked as an open gap), create it by hand at