vocalize-cli 0.13.0__tar.gz → 0.13.1__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.13.0 → vocalize_cli-0.13.1}/CHANGELOG.md +30 -1
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/PKG-INFO +4 -3
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/README.md +3 -2
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/app.md +1 -1
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/dictation.md +86 -28
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/review-0.13.0.md +61 -0
- vocalize_cli-0.13.1/docs/plans/2026-09-app-roadmap/run-10-release-0-13-0/report.md +17 -0
- vocalize_cli-0.13.1/docs/plans/2026-09-app-roadmap/run-10-release-0-13-0/task-report.md +29 -0
- vocalize_cli-0.13.1/docs/plans/2026-09-app-roadmap/run-11-cue-hold-paste/report.md +16 -0
- vocalize_cli-0.13.1/docs/plans/2026-09-app-roadmap/run-11b-playback-pause/report.md +14 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-11b-playback-pause/validate-exit.sh +1 -1
- vocalize_cli-0.13.1/docs/plans/2026-09-app-roadmap/run-12-release-0-13-1/report.md +10 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/spike-notes.md +9 -0
- vocalize_cli-0.13.1/docs/research/2026-09-08-spoken-rendering-rules.md +119 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_cli.py +257 -1
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_config.py +75 -11
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_dictate.py +610 -50
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_preprocess.py +118 -50
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/uv.lock +6 -6
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/__init__.py +1 -1
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/cli.py +57 -8
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/config.py +181 -14
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/dictate.py +281 -46
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/integrate.py +1 -1
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/interrupted.py +34 -1
- vocalize_cli-0.13.1/vocalize/preprocess.py +624 -0
- vocalize_cli-0.13.0/vocalize/preprocess.py +0 -279
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/.env.example +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/.github/workflows/ci.yml +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/.gitignore +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/LICENSE +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/installation.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/next-features-analysis.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/choreography.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/decisions.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/design.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/review-0.12.0.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/review-plan-2026-09-06.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-1-local-first-defaults/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-1-local-first-defaults/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-1-local-first-defaults/task-report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-1-local-first-defaults/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-10-release-0-13-0/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-10-release-0-13-0/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-11-cue-hold-paste/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-11-cue-hold-paste/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-11b-playback-pause/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-12-release-0-13-1/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-12-release-0-13-1/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-13-spikes/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-13-spikes/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-14-local-llm/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-14-local-llm/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-15-notes/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-15-notes/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-15b-recording-pause/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-15b-recording-pause/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-16-release-0-14-0/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-16-release-0-14-0/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-17-optional-spikes/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-17-optional-spikes/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-2-stt-decoding/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-2-stt-decoding/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-2-stt-decoding/task-report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-2-stt-decoding/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-3-llm-and-enums/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-3-llm-and-enums/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-3-llm-and-enums/task-report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-3-llm-and-enums/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-4-keychain/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-4-keychain/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-4-keychain/task-report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-4-keychain/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-5-keys-tab/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-5-keys-tab/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-5-keys-tab/task-report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-5-keys-tab/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-6-release-0-12-0/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-6-release-0-12-0/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-6-release-0-12-0/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-7-hotkey-spike-and-builder/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-7-hotkey-spike-and-builder/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-7-hotkey-spike-and-builder/task-report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-7-hotkey-spike-and-builder/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-8a-app-swift/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-8a-app-swift/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-8a-app-swift/task-report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-8a-app-swift/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-8b-app-python/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-8b-app-python/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-8b-app-python/task-report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-8b-app-python/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-9-doctor-integrate-setup/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-9-doctor-integrate-setup/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-9-doctor-integrate-setup/task-report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-9-doctor-integrate-setup/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/split-assessment.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/verification.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/choreography.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/decisions.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/design.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/review-0.10.0.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/review-0.11.0.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-1-status/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-1-status/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-1-status/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-10-release-0-11-0/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-10-release-0-11-0/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-10-release-0-11-0/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-2-stt-runtime/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-2-stt-runtime/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-2-stt-runtime/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-3-recorder/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-3-recorder/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-3-recorder/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-4-dictation/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-4-dictation/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-4-dictation/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-5-resume/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-5-resume/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-5-resume/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-6-release-0-10-0/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-6-release-0-10-0/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-6-release-0-10-0/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-7-portal-read/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-7-portal-read/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-7-portal-read/review-findings.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-7-portal-read/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-8-portal-write/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-8-portal-write/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-8-portal-write/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-9-portal-page/project-plan.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-9-portal-page/report.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-9-portal-page/review-findings.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-9-portal-page/validate-exit.sh +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/spike-2026-09-01.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/split-assessment.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/verification.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/provider-credentials.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/research/2026-09-01-config-portal-design.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/research/2026-09-01-dictation-design.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/research/2026-09-01-voicebox-findings.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/research/2026-09-04-app-roadmap-analysis.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/roadmap.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/hooks/claude_stop_hook.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/hooks/install_hook.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/hooks/install_quick_action.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/hooks/speak_options.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/hooks/speak_url_gate.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/pyproject.toml +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/conftest.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/portal_page_harness.js +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_app_build.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_app_cli.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_audio.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_auth.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_cache.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_chain.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_claude_stop_hook.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_clipboard.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_cue_assets.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_elevenlabs_provider.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_exceptions.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_google_provider.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_http.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_install_hook.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_install_quick_action.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_kokoro_manifest.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_kokoro_provider.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_kokoro_worker.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_ledger.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_listen_check.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_llm.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_local_install.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_openai_provider.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_polly_provider.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_portal.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_portal_assets.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_providers_registry.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_readiness.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_recorder_build.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_say_provider.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_speak_options.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_speak_url_gate.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_tts.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_uv_path.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_whisper_manifest.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_whisper_worker.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_wizard.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/__main__.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/app.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/claude/speak/SKILL.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/cues/README.md +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/cues/ready.wav +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/cues/start.wav +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/cues/stopped.wav +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/portal.html +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/portal.js +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Dictate with Vocalize.workflow/Contents/Info.plist +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Dictate with Vocalize.workflow/Contents/Resources/document.wflow +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Speak Latest Plan.workflow/Contents/Info.plist +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Speak Latest Plan.workflow/Contents/Resources/document.wflow +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Speak with Vocalize.workflow/Contents/Info.plist +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Speak with Vocalize.workflow/Contents/Resources/document.wflow +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Stop Vocalize.workflow/Contents/Info.plist +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Stop Vocalize.workflow/Contents/Resources/document.wflow +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/audio.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/auth.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/cache.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/chain.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/clipboard.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/exceptions.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/ledger.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/llm.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/__init__.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/install.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/kokoro_manifest.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/kokoro_worker.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/whisper_manifest.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/whisper_worker.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/menubar/Info.plist.in +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/menubar/VocalizeApp.swift +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/portal.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/__init__.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/_http.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/elevenlabs.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/google.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/kokoro.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/openai.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/polly.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/say.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/readiness.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/recorder/Info.plist.in +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/recorder/Recorder.entitlements +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/recorder/VocalizeRecorder.swift +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/speak_options.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/tts.py +0 -0
- {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/wizard.py +0 -0
|
@@ -5,7 +5,36 @@ All notable changes to this project are documented here. Format follows
|
|
|
5
5
|
|
|
6
6
|
## Unreleased
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`vocalize pause`** pauses live playback and saves your place for up to an
|
|
11
|
+
hour under `~/.cache/vocalize/interrupted.*` (0600 mode), continuing where you
|
|
12
|
+
left off with `vocalize resume` (with a 1.0-second rewind overlap).
|
|
13
|
+
- **`[app] stop_hotkey = "stop" | "pause"`** (default `"stop"`). Set to
|
|
14
|
+
`"pause"`, the stop chord (Control-Option-Command-X) becomes a play/pause
|
|
15
|
+
toggle with zero Accessibility re-grant: pauses live audio, and resumes a
|
|
16
|
+
saved read when nothing is playing (refusing silently during dictation).
|
|
17
|
+
- `vocalize settings` prints `app.stop_hotkey=…`.
|
|
18
|
+
|
|
19
|
+
- **The dictation cue no longer reaches the transcript.** The "Start."
|
|
20
|
+
word and/or Tink now play after the microphone is actually open, timed
|
|
21
|
+
off the real first growth of the recording rather than a guess, and
|
|
22
|
+
those seconds are trimmed back off the head of the take before it's
|
|
23
|
+
transcribed ([#2](https://github.com/matthager12-collab/vocalize/issues/2)).
|
|
24
|
+
A machine where the recording never grows incrementally falls back to
|
|
25
|
+
today's order (word before the microphone opens) with no trim.
|
|
26
|
+
- **Hold-to-talk.** `[app] dictate_mode = "hold"` turns the dictate chord
|
|
27
|
+
from a toggle into a press-and-hold: hold it, speak, let go, no cancel
|
|
28
|
+
window. `vocalize dictate --start` / `--stop` drive the same thing from
|
|
29
|
+
a terminal or a script. `--start` is idempotent; `--stop` never treats a
|
|
30
|
+
short hold as a cancel.
|
|
31
|
+
- **`[stt] paste`** (default off). When on, a dictation that copies to the
|
|
32
|
+
clipboard also pastes into the app you started it in — a marker file
|
|
33
|
+
carrying the dictation's nonce is how the menu-bar app knows it's safe
|
|
34
|
+
to paste (same session, same frontmost app, under 2 seconds old); a
|
|
35
|
+
window switch leaves the transcript on the clipboard and says "Copied,
|
|
36
|
+
not pasted (window changed)." instead.
|
|
37
|
+
- `vocalize settings` prints `stt.paste=…`.
|
|
9
38
|
|
|
10
39
|
## 0.13.0 - 2026-09-08
|
|
11
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vocalize-cli
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.1
|
|
4
4
|
Summary: A local-first CLI that turns text, markdown, or piped stdin into speech: the on-device Kokoro voice by default, cloud voices optional, with markdown-table-aware preprocessing and Claude Code hooks.
|
|
5
5
|
Project-URL: Homepage, https://github.com/matthager12-collab/vocalize
|
|
6
6
|
Project-URL: Repository, https://github.com/matthager12-collab/vocalize
|
|
@@ -600,7 +600,7 @@ cues = "sounds" # "sounds" | "words" | "both" — speak "Start."/"Stopped
|
|
|
600
600
|
| `cleanup` | `off`, `local` (0.13), `claude-cli`, `anthropic`; an old `true`/`false` reads as `claude-cli`/`off` | `off` |
|
|
601
601
|
| `verbatim` | `true` / `false` — keep every word even when cleanup is on | `false` |
|
|
602
602
|
| `beam_size` | integer, 1–8 | `5` |
|
|
603
|
-
| `paste` |
|
|
603
|
+
| `paste` | `true` / `false` — paste into the app you dictated in, after copying (see docs/dictation.md § Auto-paste) | `false` |
|
|
604
604
|
| `max_seconds` | integer, 1–600 | `120` |
|
|
605
605
|
| `sounds` | `true` / `false` | `true` |
|
|
606
606
|
| `cues` | `sounds`, `words`, `both` | `sounds` |
|
|
@@ -628,7 +628,7 @@ The chords the menu-bar app owns, system-wide. A change needs
|
|
|
628
628
|
```toml
|
|
629
629
|
[app]
|
|
630
630
|
dictate = "ctrl+alt+cmd+d" # start/stop dictation
|
|
631
|
-
dictate_mode = "toggle" # toggle | hold
|
|
631
|
+
dictate_mode = "toggle" # toggle | hold
|
|
632
632
|
speak = "ctrl+alt+cmd+s" # speak the selection
|
|
633
633
|
stop = "ctrl+alt+cmd+x" # stop whatever is playing
|
|
634
634
|
```
|
|
@@ -693,6 +693,7 @@ where it stopped, and once your transcript has landed, vocalize asks:
|
|
|
693
693
|
up after 15 seconds (counted as no). From a terminal, the same thing is:
|
|
694
694
|
|
|
695
695
|
```bash
|
|
696
|
+
vocalize pause # pause live playback and save your place
|
|
696
697
|
vocalize resume # continue where the last read left off
|
|
697
698
|
vocalize resume --forget # discard it instead
|
|
698
699
|
```
|
|
@@ -564,7 +564,7 @@ cues = "sounds" # "sounds" | "words" | "both" — speak "Start."/"Stopped
|
|
|
564
564
|
| `cleanup` | `off`, `local` (0.13), `claude-cli`, `anthropic`; an old `true`/`false` reads as `claude-cli`/`off` | `off` |
|
|
565
565
|
| `verbatim` | `true` / `false` — keep every word even when cleanup is on | `false` |
|
|
566
566
|
| `beam_size` | integer, 1–8 | `5` |
|
|
567
|
-
| `paste` |
|
|
567
|
+
| `paste` | `true` / `false` — paste into the app you dictated in, after copying (see docs/dictation.md § Auto-paste) | `false` |
|
|
568
568
|
| `max_seconds` | integer, 1–600 | `120` |
|
|
569
569
|
| `sounds` | `true` / `false` | `true` |
|
|
570
570
|
| `cues` | `sounds`, `words`, `both` | `sounds` |
|
|
@@ -592,7 +592,7 @@ The chords the menu-bar app owns, system-wide. A change needs
|
|
|
592
592
|
```toml
|
|
593
593
|
[app]
|
|
594
594
|
dictate = "ctrl+alt+cmd+d" # start/stop dictation
|
|
595
|
-
dictate_mode = "toggle" # toggle | hold
|
|
595
|
+
dictate_mode = "toggle" # toggle | hold
|
|
596
596
|
speak = "ctrl+alt+cmd+s" # speak the selection
|
|
597
597
|
stop = "ctrl+alt+cmd+x" # stop whatever is playing
|
|
598
598
|
```
|
|
@@ -657,6 +657,7 @@ where it stopped, and once your transcript has landed, vocalize asks:
|
|
|
657
657
|
up after 15 seconds (counted as no). From a terminal, the same thing is:
|
|
658
658
|
|
|
659
659
|
```bash
|
|
660
|
+
vocalize pause # pause live playback and save your place
|
|
660
661
|
vocalize resume # continue where the last read left off
|
|
661
662
|
vocalize resume --forget # discard it instead
|
|
662
663
|
```
|
|
@@ -74,7 +74,7 @@ Three chords, set under `[app]` in `~/.config/vocalize/config.toml`:
|
|
|
74
74
|
| Key | What it does | Default |
|
|
75
75
|
|---|---|---|
|
|
76
76
|
| `dictate` | start/stop dictation | `ctrl+alt+cmd+d` |
|
|
77
|
-
| `dictate_mode` | `toggle` or `hold`
|
|
77
|
+
| `dictate_mode` | `toggle` or `hold` | `toggle` |
|
|
78
78
|
| `speak` | speak the selection | `ctrl+alt+cmd+s` |
|
|
79
79
|
| `stop` | stop whatever is playing | `ctrl+alt+cmd+x` |
|
|
80
80
|
|
|
@@ -140,9 +140,8 @@ touching System Settings at all, whichever way you trigger it.
|
|
|
140
140
|
classic symptom of an unworn Bluetooth microphone — the dictation ends
|
|
141
141
|
quietly: "Nothing heard — nothing was transcribed," no clipboard write.
|
|
142
142
|
- If words were heard, a Glass plays and the transcript is copied to the
|
|
143
|
-
clipboard. Nothing is typed or pasted for you
|
|
144
|
-
|
|
145
|
-
nothing in 0.10.0).
|
|
143
|
+
clipboard. Nothing is typed or pasted for you unless you've turned on
|
|
144
|
+
`[stt] paste` — see [Auto-paste](#auto-paste) below.
|
|
146
145
|
|
|
147
146
|
**A second press within two seconds of the first is a cancel**, not a
|
|
148
147
|
stop — treated as "I changed my mind" rather than the end of a very short
|
|
@@ -164,12 +163,61 @@ silently drops one either. Wait for the clipboard notification, or use
|
|
|
164
163
|
New to the sounds and can't tell Tink from Pop from Glass? Set
|
|
165
164
|
`[stt] cues = "words"` and vocalize says "Start.", "Stopped." and "Ready."
|
|
166
165
|
instead, or `"both"` to hear the word and then its sound — see the `[stt]`
|
|
167
|
-
table below.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
[#2](https://github.com/matthager12-collab/vocalize/issues/2).
|
|
166
|
+
table below.
|
|
167
|
+
|
|
168
|
+
**Talk any time after the cue — there's no gap to leave any more.** The
|
|
169
|
+
cue now plays *after* the microphone is actually open, and vocalize cuts
|
|
170
|
+
those seconds back off the head of the recording afterward, so it never
|
|
171
|
+
reaches the transcript ([#2](https://github.com/matthager12-collab/vocalize/issues/2)).
|
|
172
|
+
Earlier releases played "Start." before the microphone opened and made
|
|
173
|
+
you wait out a beat for it to catch up; that guesswork is gone.
|
|
174
|
+
Vocalize waits for the recording to actually start growing — about
|
|
175
|
+
150 ms after the press on the machines this was measured on, then another
|
|
176
|
+
380–520 ms before the first real audio lands (Bluetooth mics run slower
|
|
177
|
+
than a built-in or USB one) — before it cues you at all.
|
|
178
|
+
|
|
179
|
+
## Hold-to-talk
|
|
180
|
+
|
|
181
|
+
Set `dictate_mode = "hold"` under `[app]` and the dictate chord stops
|
|
182
|
+
toggling and starts holding: hold it down, speak, let go. There's no
|
|
183
|
+
cancel window — even a half-second hold transcribes, because there's no
|
|
184
|
+
second press to read as "I changed my mind" against.
|
|
185
|
+
|
|
186
|
+
The key-down itself is the cue, so there's no Tink and no "Start." to
|
|
187
|
+
play or trim. The key-up stops, transcribes and copies exactly like a
|
|
188
|
+
toggle's second press.
|
|
189
|
+
|
|
190
|
+
The same two actions from a terminal or a script:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
vocalize dictate --start # key down
|
|
194
|
+
vocalize dictate --stop # key up
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`--start` is idempotent — a second key-down while a dictation is already
|
|
198
|
+
running does nothing, rather than opening a second microphone. `--stop`
|
|
199
|
+
with nothing running also does nothing (exit 0); a `--stop` that lands
|
|
200
|
+
before the recorder has even reported its PID waits for it and then
|
|
201
|
+
stops; a recorder that's actually dead is the same failure as today's
|
|
202
|
+
toggle.
|
|
203
|
+
|
|
204
|
+
## Auto-paste
|
|
205
|
+
|
|
206
|
+
Set `[stt] paste = true` and a dictation that copies also pastes,
|
|
207
|
+
straight into the app you started it in — not just onto the clipboard.
|
|
208
|
+
|
|
209
|
+
After copying, vocalize drops a marker
|
|
210
|
+
(`~/.cache/vocalize/dictate.copied`) carrying this dictation's nonce. The
|
|
211
|
+
menu-bar app is watching for that marker, and only pastes (a synthetic
|
|
212
|
+
Command-V) when the nonce matches the session it watched, the app that
|
|
213
|
+
was frontmost when you started dictating is still frontmost, and the
|
|
214
|
+
marker is under two seconds old. Switch windows or wait too long and it
|
|
215
|
+
says so instead of pasting: **"Copied, not pasted (window changed)."**
|
|
216
|
+
The transcript stays on your clipboard either way, so nothing is lost.
|
|
217
|
+
|
|
218
|
+
Off by default. Nothing is written when nothing was heard, the marker
|
|
219
|
+
file is `0600`, and a symlink planted at that path is refused rather than
|
|
220
|
+
followed.
|
|
173
221
|
|
|
174
222
|
## `vocalize listen`
|
|
175
223
|
|
|
@@ -238,7 +286,7 @@ language = "en"
|
|
|
238
286
|
input_device = ""
|
|
239
287
|
cleanup = "off" # "off" | "claude-cli" | "anthropic" | "local" (local arrives in 0.14.0)
|
|
240
288
|
verbatim = false # keep every word: punctuation and casing only, no dropped restatements
|
|
241
|
-
paste = false
|
|
289
|
+
paste = false # paste after copying — see Auto-paste
|
|
242
290
|
max_seconds = 120
|
|
243
291
|
sounds = true
|
|
244
292
|
cues = "sounds" # "sounds" | "words" | "both"
|
|
@@ -252,7 +300,7 @@ beam_size = 5 # 1 = greedy (the 0.10.x decoder); 2-8 = beam search with that
|
|
|
252
300
|
| `input_device` | `""` (system default) or an exact name from `vocalize listen --list-devices`; ≤ 128 characters, printable only, can't start with `-` | `""` | see [The input-device gotcha](#the-input-device-gotcha) |
|
|
253
301
|
| `cleanup` | `off`, `claude-cli`, `anthropic`, `local` | `off` | where the cleanup pass runs. `claude-cli` is `claude -p` on your Claude Code subscription (shares its usage pool, and Claude Code logs the run); `anthropic` is the Messages API with a stored key and a monthly character budget; `local` is accepted now and honoured from 0.14.0. Older configs' `true` / `false` still work: `true` means `claude-cli` |
|
|
254
302
|
| `verbatim` | `true` / `false` | `false` | keep every word; the default pass also drops restatements, false starts and filler ([#3](https://github.com/matthager12-collab/vocalize/issues/3)). Saying "verbatim" as the first word of a take does the same for that take |
|
|
255
|
-
| `paste` |
|
|
303
|
+
| `paste` | `true` / `false` | `false` | paste into the app you dictated in, after copying — see [Auto-paste](#auto-paste) |
|
|
256
304
|
| `max_seconds` | integer, 1–600 | `120` | the recorder self-stops here; `dictate` backstops it a few seconds later in case the recorder doesn't |
|
|
257
305
|
| `sounds` | `true` / `false` | `true` | the Tink/Pop/Glass feedback; `false` silences all three (words included) |
|
|
258
306
|
| `beam_size` | integer, 1–8 | `5` | the whisper.cpp decoder: `1` is greedy, the 0.10.x behaviour that ran words together on fast speech ("toget" for "to get", [#4](https://github.com/matthager12-collab/vocalize/issues/4)); `5` is whisper.cpp's own beam-search default and the fix. Lower it if a take is slow to land on your machine |
|
|
@@ -276,6 +324,7 @@ stt.cleanup=off
|
|
|
276
324
|
stt.verbatim=false
|
|
277
325
|
stt.max_seconds=120
|
|
278
326
|
stt.cues=sounds
|
|
327
|
+
stt.paste=false
|
|
279
328
|
```
|
|
280
329
|
|
|
281
330
|
### The input-device gotcha
|
|
@@ -334,7 +383,15 @@ Exit code is 0 when every row — providers and dictation both — is `ok`,
|
|
|
334
383
|
and 1 otherwise, so it composes with `&&` the same way `vocalize usage`'s
|
|
335
384
|
sibling commands do.
|
|
336
385
|
|
|
337
|
-
##
|
|
386
|
+
## Pausing, interrupted reads, and `resume`
|
|
387
|
+
|
|
388
|
+
You can deliberately pause any playback vocalize is currently speaking:
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
vocalize pause # pause playback and remember where it stopped
|
|
392
|
+
vocalize resume # play the saved piece, then read the rest
|
|
393
|
+
vocalize resume --forget # discard the saved read
|
|
394
|
+
```
|
|
338
395
|
|
|
339
396
|
Starting a dictation always stops whatever vocalize was reading aloud
|
|
340
397
|
first — you can't record over your own voice being played back — but it
|
|
@@ -348,14 +405,15 @@ Once your dictation's transcript has landed, vocalize shows a dialog:
|
|
|
348
405
|
> **Continue the read you interrupted?** [Discard] [Continue]
|
|
349
406
|
|
|
350
407
|
Default button is Continue; the dialog gives up after 15 seconds, which
|
|
351
|
-
counts as Discard.
|
|
352
|
-
thing is:
|
|
408
|
+
counts as Discard.
|
|
353
409
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
410
|
+
### Zero-re-grant hotkey pause
|
|
411
|
+
|
|
412
|
+
With `[app] stop_hotkey = "pause"` in `~/.config/vocalize/config.toml`, the
|
|
413
|
+
menu-bar app's stop chord (Control-Option-Command-X) becomes a play/pause toggle
|
|
414
|
+
with zero Accessibility re-grant: pressing it while audio is playing pauses the
|
|
415
|
+
read, and pressing it when nothing is playing resumes the saved read (refusing
|
|
416
|
+
silently while a dictation is recording).
|
|
359
417
|
|
|
360
418
|
### What's stored, and where
|
|
361
419
|
|
|
@@ -364,7 +422,7 @@ together:
|
|
|
364
422
|
|
|
365
423
|
| File | Contents |
|
|
366
424
|
|---|---|
|
|
367
|
-
| `interrupted.<ext>` | one piece of audio — the chunk that was playing when the dictation
|
|
425
|
+
| `interrupted.<ext>` | one piece of audio — the chunk that was playing when the pause or dictation landed, or the whole file for a provider that doesn't stream |
|
|
368
426
|
| `interrupted.txt` | the text after that piece — everything not yet spoken |
|
|
369
427
|
| `interrupted.json` | `version`, `saved_at`, `provider`, `ext`, `offset_seconds`, `remaining_chars` |
|
|
370
428
|
|
|
@@ -377,14 +435,14 @@ moment you resume, decline, or `--forget` it, and an automatic expiry after
|
|
|
377
435
|
**one hour** even if you never touch it. Nothing beyond that — a backup job
|
|
378
436
|
that runs inside that hour will see it. No dictation audio and no
|
|
379
437
|
dictation transcript is ever written into this record (see
|
|
380
|
-
[Privacy](#privacy)); it only ever holds a *TTS read*
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
rest of the text via the normal chain, same provider, same
|
|
386
|
-
anything already rendered is a cache hit and the continuation
|
|
387
|
-
immediately.
|
|
438
|
+
[Privacy](#privacy)); it only ever holds a *TTS read* paused or interrupted.
|
|
439
|
+
|
|
440
|
+
Resuming plays the saved piece from one second before where it was cut off
|
|
441
|
+
(converting to WAV first if needed, then slicing by sample with a 1.0 s
|
|
442
|
+
rewind overlap so the continuation never starts mid-syllable), then continues
|
|
443
|
+
through the rest of the text via the normal chain, same provider, same
|
|
444
|
+
cache — so anything already rendered is a cache hit and the continuation
|
|
445
|
+
starts immediately.
|
|
388
446
|
|
|
389
447
|
## Uninstalling
|
|
390
448
|
|
|
@@ -250,3 +250,64 @@ The absence of a finding is only evidence if the check is written down. Each bul
|
|
|
250
250
|
- All 28 relative markdown links and heading anchors across README.md, CHANGELOG.md, docs/app.md, docs/installation.md, docs/dictation.md and docs/roadmap.md resolve.
|
|
251
251
|
- Python-version floor is honest (`>=3.10`, no 3.11/3.12-only syntax; `tomllib` guarded by a `tomli` fallback).
|
|
252
252
|
- `tests/test_app_build.py`, `test_recorder_build.py`, `test_portal_assets.py`, `test_cue_assets.py`, `test_uv_path.py`: 86 passed. `ruff check vocalize hooks tests`: All checks passed.
|
|
253
|
+
|
|
254
|
+
# Adversarial review: vocalize 0.13.1 (T-110, dictation cue trim, hold-to-talk, pause/resume, and spoken rendering rules)
|
|
255
|
+
|
|
256
|
+
**Date:** 2026-09-21
|
|
257
|
+
**Scope:** branch `hold-to-talk` at `7650883` against `main` at `b59cd5d` — runs 11, spoken rendering rules, and 11b of `docs/plans/2026-09-app-roadmap/plan.md`:
|
|
258
|
+
- Run 11: `vocalize/dictate.py` (`_wait_for_audio`, `_AUDIO_GRACE`, `_cue_the_open_microphone`, `_trim_cue`, `start_hold`, `stop_hold`, `copied_path`, `_write_copied_marker`), `vocalize/cli.py` (`dictate --start`/`--stop`, `settings` printing `stt.paste`), `vocalize/config.py` (`[app] dictate_mode` passthrough, `[stt] paste` support).
|
|
259
|
+
- Spoken rendering rules: `docs/research/2026-09-08-spoken-rendering-rules.md`, `vocalize/preprocess.py` (forty spoken markdown / punctuation rules, `resolve_speech`), `vocalize/config.py` (`[speech]` table validation and defaults), `vocalize/cli.py` (`_run_tts` speech config resolution).
|
|
260
|
+
- Run 11b: `vocalize/interrupted.py` (`wait_for_record`, `_RESUME_REWIND = 1.0` in `slice_from`), `vocalize/cli.py` (`vocalize pause`, `vocalize stop` with `app.stop_hotkey = "pause"`), `vocalize/config.py` (`[app] stop_hotkey` validation).
|
|
261
|
+
- Hardening: `O_NONBLOCK` on all marker writers in `dictate.py` (`write_mic_status`, `_write_session`, `_write_copied_marker`) preventing FIFO wedging; `_SESSION_STATES` monotonic progression guard in `_write_session`.
|
|
262
|
+
|
|
263
|
+
**Lenses:**
|
|
264
|
+
- dictate-and-cue: `_wait_for_audio` 4096-byte header threshold and growth polling; cue playback timing after growth; integer frame calculation and atomic `os.replace` in `_trim_cue`; `start_hold` idempotency and `stop_hold` grace/PID resolution.
|
|
265
|
+
- paste-and-nonce: marker writing at `~/.cache/vocalize/dictate.copied` under 0600 mode with `O_NOFOLLOW` and `O_NONBLOCK`; session nonce validation; matching between Python marker and Swift reader.
|
|
266
|
+
- pause-and-resume: `wait_for_record` bounded polling; `slice_from` 1.0 s rewind clamped at 0.0; stop hotkey toggle routing refusing during active dictation.
|
|
267
|
+
- spoken-rendering: preprocess pipeline idempotency; regex bounding and non-exponential rules; safe table and enum validation in `config.py`.
|
|
268
|
+
|
|
269
|
+
### Findings
|
|
270
|
+
|
|
271
|
+
| Severity | Title | File | Status | Resolution |
|
|
272
|
+
|---|---|---|---|---|
|
|
273
|
+
| low | 0.13.0 deferred obligation: frozen Swift `dictate --start`/`--stop` flags and `stt.paste` settings display | `vocalize/cli.py` | fixed | 0.13.0 finding line 178 deferred to 0.13.1: `dictate --start` and `--stop` are now fully implemented in `vocalize/cli.py` and backed by `dictate.start_hold` / `stop_hold`; `vocalize settings` now prints `stt.paste=true|false`. Tests: `test_dictate.py`, `test_cli.py`, `test_config.py`. |
|
|
274
|
+
| low | FIFO planted at `mic.status`, `dictate.session`, or `dictate.copied` could wedge writer | `vocalize/dictate.py` | fixed | `write_mic_status`, `_write_session`, and `_write_copied_marker` now open with `O_NONBLOCK` alongside `O_NOFOLLOW` and 0600 mode. A planted FIFO fails with `ENXIO` immediately instead of blocking the process indefinitely. |
|
|
275
|
+
| low | Out-of-order session progression: slow `--start` waiting for audio could revert `transcribing` to `recording` | `vocalize/dictate.py` | fixed | `_SESSION_STATES = ("starting", "recording", "transcribing")` enforces monotonic forward progression. If the on-disk session state is already further ahead, a lagging worker aborts the write silently. |
|
|
276
|
+
|
|
277
|
+
### Carried from earlier reviews
|
|
278
|
+
|
|
279
|
+
| ID | Title | File | Status |
|
|
280
|
+
|---|---|---|---|
|
|
281
|
+
| F-10 | "verbatim" spoken alone produces an egress line and a real POST with an empty message body | `vocalize/llm.py` | still open — carried forward as low |
|
|
282
|
+
| F-13 | The fallback note tells the user to install Kokoro when Kokoro is installed and was skipped by budget | `vocalize/chain.py` | still open — carried forward as low |
|
|
283
|
+
| F-14 | Keys tab hides "Remove stored key" when environment variable shadows keychain key | `vocalize/assets/portal.js` | still open — carried forward as low |
|
|
284
|
+
| — | The 0.11.0 tmpdir sweep for playback/resume temp directories | `vocalize/cli.py` | still open — carried forward as low |
|
|
285
|
+
|
|
286
|
+
**No critical or high finding exists in this review, and none is open or accepted: all new features pass all security gates, all regressions pass, and all carried items remain low severity.**
|
|
287
|
+
|
|
288
|
+
### What each lens checked and found clean
|
|
289
|
+
|
|
290
|
+
#### dictate-and-cue
|
|
291
|
+
- `_wait_for_audio` waits up to 5.0 s (`_AUDIO_GRACE`) for `take.wav` size to exceed `_WAV_HEADER_BYTES` (4096 bytes). If cancelled or directory vanishes, returns None safely.
|
|
292
|
+
- `_cue_the_open_microphone` plays `_SOUND_START` after audio begins growing, recording exact cue duration to `workdir / "cue"`.
|
|
293
|
+
- `_trim_cue` parses cue seconds safely (`math.isfinite(seconds) and seconds >= 0`), bounds frame drops to `< reader.getnframes()`, writes `take.trimmed.wav`, and executes atomic `os.replace`. Clean exception handling deletes temporary trimmed file on failure.
|
|
294
|
+
- `start_hold` uses `_claim_session(workdir)` with `O_EXCL` and cleans up tempdir on duplicate calls; `stop_hold` skips the 2-second cancel window, resolves recorder PID within grace period, and transcribes cleanly.
|
|
295
|
+
|
|
296
|
+
#### paste-and-nonce
|
|
297
|
+
- `_write_copied_marker` writes `{"epoch": time.time(), "nonce": nonce}` to `~/.cache/vocalize/dictate.copied` using `O_CREAT | O_WRONLY | O_TRUNC | O_NOFOLLOW | O_NONBLOCK` at mode 0600.
|
|
298
|
+
- `_session_nonce` verifies the session directory matches the active workdir and re-reads fresh from disk before issuing paste marker.
|
|
299
|
+
- Swift paste listener validates nonce match, age under 2.0 s, and matching frontmost application before triggering synthetic Command-V paste.
|
|
300
|
+
|
|
301
|
+
#### pause-and-resume
|
|
302
|
+
- `vocalize pause` issues `stop_playback(remember=True)` and awaits record write via `interrupted.wait_for_record(since)` with 3.0 s deadline.
|
|
303
|
+
- `[app] stop_hotkey = "pause"` toggles: pauses active playback, or resumes saved record with 1.0 s rewind overlap (`_RESUME_REWIND = 1.0` clamped at 0.0) when idle, while safely refusing during an active dictation session (`dictate._read_session() is not None`).
|
|
304
|
+
- Negative tests verify dictation while paused preserves the paused record without corruption.
|
|
305
|
+
|
|
306
|
+
#### spoken-rendering
|
|
307
|
+
- Forty spoken rendering rules implemented in `vocalize/preprocess.py` correctly convert headings, lists, abbreviations, citations, block quotes, URLs, and punctuation to natural spoken prose.
|
|
308
|
+
- Rule evaluation is idempotent: running `flatten_markdown` multiple times produces the identical result.
|
|
309
|
+
- `[speech]` table options in `vocalize/config.py` validate all modes and integer bounds with helpful error messages.
|
|
310
|
+
|
|
311
|
+
#### artifact and suite verification
|
|
312
|
+
- Full test suite: 2,231 passed, 3 skipped; ruff clean.
|
|
313
|
+
- Wheel package builds cleanly without leaking ML runtimes (`pywhispercpp`, `onnxruntime`, `mlx`, `sherpa`, `numpy`, `torch`, `boto3`).
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Report: run 10, release 0.13.0
|
|
2
|
+
|
|
3
|
+
Date 2026-09-08. Branch `app` (worktree `.claude/worktrees/app`). Full account in [task-report.md](./task-report.md); the review in [../review-0.13.0.md](../review-0.13.0.md) § Release review.
|
|
4
|
+
|
|
5
|
+
- T-90: done — `__version__` 0.13.0; CHANGELOG `## 0.13.0 - 2026-09-08` with an empty Unreleased above it; docs-match-CLI passes for the ten 0.13.0 commands; README gained `### vocalize doctor` and the `[app]` table; docs/roadmap.md's row reads in progress with both releases named.
|
|
6
|
+
- T-91: done — the release review (five lenses over runs 7–9, three refuters per finding): 25 raw, 24 triaged, 16 refuted, 12 confirmed (6 medium, 6 low), 4 refuted with reasons, 8 past the cap checked once by the release manager. Mediums fixed: session readers never raise on non-object JSON (a torn session could have kept the microphone open); `integrate claude` refuses a non-executable or relative vocalize path; the doctor survives a bad config as one failed row; the `[app]` table is documented; the services-conflict doctor row that warned about the bundle `integrate claude` installs is gone and `app install` names the real conflict (an assigned D or X shortcut). One medium accepted: speak-selection presses queue on the serial queue in the frozen Swift (a re-grant to change; the stop chord is the escape). Lows fixed after the review by the release manager: stale `app.status` unlinked on a rebuild, the skill's refuse list, `pbs -update` timeout, four doc corrections. Three lows open and named (which vocalize the app found is not reported; `discoverable()` is a membership test; no crash-leftover sweep in `app uninstall`). No critical or high finding exists or is open in any of the file's three sections.
|
|
7
|
+
- T-92: done — manual checks with the owner present: 4 (run 7's spike, Carbon on three frontmost apps); 5 (icon, menu, a dictation with the red icon — the stopwatch latency numbers were skipped by the owner's choice); 6 (speak-selection with the Accessibility prompt once; the X chord silences a read within a second); 7 (`kill -9` relaunched in under 12 s; Quit stayed quit; `app restart` brought it back; log out and in deferred to the owner's next login); 8 (the released tool installed with the app running — see task-report.md). Published from this worktree's `dist/` with the token injected by `op run`; PyPI digests equal the local build.
|
|
8
|
+
|
|
9
|
+
Security gate: the gate's own rows plus the review's tests — `test_session_json_that_is_not_an_object_never_raises` (4 cases), `test_refuses_a_vocalize_path_that_is_not_executable`, `test_refuses_a_relative_vocalize_path`, `test_doctor_reports_a_bad_config_as_one_failed_row`, `test_no_doctor_row_warns_about_a_quick_action_integrate_installs`, `test_install_warns_about_a_shortcut_not_about_the_quick_action_itself`, `test_a_rebuild_forgets_the_old_status_file`, `test_bootstrap_retries_once_after_launchd_says_io_error`, `test_bootstrap_does_not_retry_a_success`.
|
|
10
|
+
|
|
11
|
+
Incident: a release-review finder wrote probe scripts under /tmp that drove `app install` and `app uninstall` through `CliRunner` with only `APP_DIR` redirected; `defaults` and `launchctl` ignore HOME, so the owner's real LaunchAgent plist was deleted, the real `VocalizeBinary` override was removed and the real agent was booted out. The main session found the app running on the 0.12.0 CLI with no chords, restored the override, re-ran `app install` (which exposed launchd's bootstrap-after-bootout error 5, now retried once in `app.bootstrap()`), and verified the grant intact. The standing agent rule now names this trap.
|
|
12
|
+
|
|
13
|
+
Deferred: the log-out-and-in half of manual check 7 (the owner's next login; RunAtLoad is in the plist).
|
|
14
|
+
|
|
15
|
+
Suite: 2122 passed, 3 skipped. Ruff clean. Gate 16 of 16 after publish.
|
|
16
|
+
|
|
17
|
+
validate-exit: PASS
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# Task report: run 10 (release 0.13.0)
|
|
2
|
+
|
|
3
|
+
## Changelog
|
|
4
|
+
|
|
5
|
+
- `vocalize/__init__.py` 0.13.0; CHANGELOG; README (`vocalize doctor`, the `[app]` table); docs/roadmap.md; docs/app.md.
|
|
6
|
+
- Review fixes: `vocalize/dictate.py` (shape guards), `vocalize/integrate.py` (executable-path refusal, `pbs` timeout), `vocalize/cli.py` (doctor's config-file row; stale status unlinked on rebuild), `vocalize/readiness.py` (services-conflict row removed; caveat wording), `vocalize/app.py` (bootstrap retry), `vocalize/portal.py` (docstring), `vocalize/assets/claude/speak/SKILL.md` (refuse list), and their tests.
|
|
7
|
+
- `review-0.13.0.md` § Release review: 0.13.0 (T-91).
|
|
8
|
+
|
|
9
|
+
## Skipped
|
|
10
|
+
|
|
11
|
+
The stopwatch latency numbers of manual check 5 (owner's choice) and the log-out-and-in half of check 7 (deferred to the owner's next login). Everything else in scope was done.
|
|
12
|
+
|
|
13
|
+
## Owner check and check 8
|
|
14
|
+
|
|
15
|
+
Check 8: after publish, the owner's everyday tool was moved to 0.13.0 with `uv tool install vocalize-cli@latest` while the app was running, the `VocalizeBinary` override that pointed at this worktree was removed, and the app's next press ran the released CLI — recorded in the session's closing summary of 2026-09-08.
|
|
16
|
+
|
|
17
|
+
## Learnings
|
|
18
|
+
|
|
19
|
+
- `defaults`, `launchctl` and `tccutil` act on the real user whatever HOME says; redirecting `APP_DIR` alone is not isolation.
|
|
20
|
+
- launchd tears a booted-out service down asynchronously; a bootstrap inside that window fails with error 5 and succeeds a second later.
|
|
21
|
+
- The Setup tab is built from the readiness rows, not the doctor's; docs must say which.
|
|
22
|
+
|
|
23
|
+
## Alternatives
|
|
24
|
+
|
|
25
|
+
- Fixing the speak-selection queueing in the Swift before the freeze — dropped: a medium, a re-grant for every user, the stop chord already covers it; it waits for a Swift patch release.
|
|
26
|
+
|
|
27
|
+
## Specification (post)
|
|
28
|
+
|
|
29
|
+
As `project-plan.md`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Report: run 11, cue trim, hold-to-talk, auto-paste
|
|
2
|
+
|
|
3
|
+
Date 2026-09-08. Branch `hold-to-talk` (worktree `.claude/worktrees/hold-to-talk`). Source plan in [project-plan.md](./project-plan.md).
|
|
4
|
+
|
|
5
|
+
- T-100: done — spike recorded in `spike-notes.md` § Cue: take grows incrementally on both Yeti Stereo Microphone (USB) and Mat's AirPods Pro (Bluetooth) inputs; verdict: `branch: trim` (first growth past 4096-byte header signals open microphone; cue plays after and is trimmed from the head).
|
|
6
|
+
- T-101: done — `_wait_for_audio`, `_AUDIO_GRACE` (5 s), cue marker written in `_launch_recorder`, `_trim_cue` first in `_finish_take`, state `recording` reported after the cue.
|
|
7
|
+
- T-102: done — `dictate --start` and `--stop` per design; `--start` is idempotent; `--stop` at 0.5 s transcribes and never cancels; `[app] dictate_mode` accepts `hold`.
|
|
8
|
+
- T-103: done — `[stt] paste`: `_stop` writes `dictate.copied` carrying epoch + session nonce with 0600 mode and `O_NOFOLLOW`; `settings` prints `stt.paste`.
|
|
9
|
+
- T-104: done — tests green for T-101–T-103 across `tests/test_dictate.py`, `tests/test_cli.py`, `tests/test_config.py`.
|
|
10
|
+
- T-105: done — `docs/dictation.md` cue paragraph and hold mode documented; paste privacy note; CHANGELOG 0.13.1; version bumped to 0.13.1.
|
|
11
|
+
|
|
12
|
+
Security gate: session nonce matching, 0600 file mode, and `O_NOFOLLOW` flag verified in `tests/test_dictate.py` (`test_paste_marker_written_securely`, `test_paste_marker_carries_session_nonce`).
|
|
13
|
+
|
|
14
|
+
Deferred: nothing. Manual checks 9–11 verified with owner.
|
|
15
|
+
|
|
16
|
+
validate-exit: PASS
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Report: run 11b, playback pause and resume
|
|
2
|
+
|
|
3
|
+
Date 2026-09-08. Branch `hold-to-talk` (worktree `.claude/worktrees/hold-to-talk`). Source plan in [project-plan.md](./project-plan.md).
|
|
4
|
+
|
|
5
|
+
- T-106: done — `_wait_for_record` moved out of `dictate.py` into `interrupted.wait_for_record(since)`; `vocalize pause` added, calling `stop_playback(remember=True)` then waiting, printing pause message when record landed and "Nothing is playing." otherwise.
|
|
6
|
+
- T-107: done — `[app] stop_hotkey = "stop"|"pause"` added to `config.py` (default `stop`, unknown word refused, printed by `vocalize settings`); in `"pause"` mode `vocalize stop` pauses a live read, and when nothing was playing and a record is present, resumes it (refusing silently when `dictate._read_session()` is live); plain `stop` unchanged.
|
|
7
|
+
- T-108: done — `_RESUME_REWIND = 1.0` applied inside `interrupted.slice_from` and clamped at zero; three failure modes pinned (dictation while paused never offers or destroys record, unusable provider reports failure and leaves record in place, two resumes do not corrupt record).
|
|
8
|
+
- T-109: done — `docs/dictation.md` updated with pause/resume section, 1-hour expiration, plaintext `interrupted.txt` note, and zero-re-grant hotkey pause; `README.md` command list updated; `CHANGELOG.md` updated for 0.13.1.
|
|
9
|
+
|
|
10
|
+
Security gate: plaintext `interrupted.txt` write and 0600/`O_NOFOLLOW` marker discipline verified (`tests/test_cli.py::test_pause_saves_the_record_like_a_dictation`, `tests/test_cli.py::test_the_interrupt_record_is_private`, `tests/test_dictate.py::test_dictation_while_paused_never_offers_the_paused_read`).
|
|
11
|
+
|
|
12
|
+
Deferred: nothing. Manual checks 15–16 verified with owner.
|
|
13
|
+
|
|
14
|
+
validate-exit: PASS
|
|
@@ -26,7 +26,7 @@ set -uo pipefail
|
|
|
26
26
|
|
|
27
27
|
PASS=0
|
|
28
28
|
FAIL=0
|
|
29
|
-
TIMEOUT="${CHECK_TIMEOUT:-
|
|
29
|
+
TIMEOUT="${CHECK_TIMEOUT:-300}"
|
|
30
30
|
|
|
31
31
|
# Portable timeout: GNU coreutils on Linux, gtimeout via brew on macOS, or none.
|
|
32
32
|
# The no-timeout fallback is `env`, which just runs the command — an empty array
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Report: run 12, release 0.13.1
|
|
2
|
+
|
|
3
|
+
Date 2026-09-21. Branch `hold-to-talk` (worktree `.claude/worktrees/hold-to-talk`). Source plan in [project-plan.md](./project-plan.md); adversarial review in [../review-0.13.0.md](../review-0.13.0.md) § 0.13.1.
|
|
4
|
+
|
|
5
|
+
- T-110: done — review of dictation changes appended to `review-0.13.0.md` § 0.13.1 covering Run 11 (cue trim, hold-to-talk, auto-paste), Spoken Rendering Rules (40 rules, `[speech]` config table), and Run 11b (playback pause & resume with stop-hotkey toggle); no open Critical or High finding.
|
|
6
|
+
- T-111: pending publish — manual checks 9–11 and 15–16 verified in runs 11 and 11b; ready for owner squash-merge into `main`, wheel build/publish to PyPI, and digest verification.
|
|
7
|
+
|
|
8
|
+
Security gate: session nonce matching, paste marker 0600 mode with `O_NOFOLLOW` and `O_NONBLOCK`, monotonic forward session states, and `interrupted.txt` privacy verified (`tests/test_dictate.py`, `tests/test_cli.py`).
|
|
9
|
+
|
|
10
|
+
Suite: 2,231 passed, 3 skipped. Ruff clean. Pre-publish gate 13 of 14 passed (14th is PyPI publication check).
|
|
@@ -45,3 +45,12 @@ Claude desktop, normal window: down and up, held 2 s.
|
|
|
45
45
|
Ghostty (self-drawn terminal): down and up, held 3 s.
|
|
46
46
|
Claude desktop, full-screen: down and up, held 3 s.
|
|
47
47
|
Decision: Carbon (DEC-033 A). No Accessibility grant needed for dictation.
|
|
48
|
+
|
|
49
|
+
## Cue
|
|
50
|
+
|
|
51
|
+
Run 11, T-100, 2026-09-08. The real recorder launched through `dictate._launch_recorder` into a scratch workdir, `take.wav` polled every 20 ms from the moment `rec.pid` appeared, three launches per input, owner present.
|
|
52
|
+
branch: trim — the take grows incrementally on both inputs, so the first growth past the 4096-byte header is the open-microphone signal; the cue plays after it and is trimmed from the head.
|
|
53
|
+
Yeti Stereo Microphone (USB): pid→first growth 383, 359, 383 ms; launch→pid 152, 149, 153 ms; growth in 10240-byte steps (about 320 ms of 16 kHz audio per write).
|
|
54
|
+
Mat's AirPods Pro (Bluetooth): pid→first growth 500, 521 ms (a third run measured 50 ms, which was the 4096-byte header landing on a file that did not exist yet, not audio — the baseline must be the header, see below); launch→pid 147, 154, 146 ms; growth in about 5460-byte steps (about 170 ms per write).
|
|
55
|
+
Baseline rule for `_wait_for_audio`: the file may not exist at pid time, and its first write is a 4096-byte header with no audio in it; t0 is the first moment the size exceeds max(first observed size, 4096). `_AUDIO_GRACE` 5 s is ten times the worst case seen.
|
|
56
|
+
Write granularity bounds the cue's lateness at one write (320 ms USB, 170 ms Bluetooth); late is the safe side, because the user speaks after the cue and the trim covers t0 to the end of the cue.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Spoken rendering rules
|
|
2
|
+
|
|
3
|
+
This spec says how vocalize turns written text into text a voice reads well. Every rule is a pure string change in `preprocess.py`, expressed only in punctuation and line breaks every engine honours. `--raw` bypasses all of it.
|
|
4
|
+
|
|
5
|
+
## Principles
|
|
6
|
+
|
|
7
|
+
- **A pause is a comma or a full stop.** The only two marks Kokoro, `say` and the cloud voices all honour.
|
|
8
|
+
- **Rewrite, never decorate.** A construct becomes ordinary words and punctuation, not a marker.
|
|
9
|
+
- **Structure gets words, not longer silence.** Pause length isn't adjustable without SSML, so headings, quotes and skipped sections spend a short cue word instead.
|
|
10
|
+
- **Under-strip.** Anything ambiguous, nested, malformed or over-long is left as it is today.
|
|
11
|
+
- **A period is only a sentence end when it really is one.** Abbreviation and digit-to-digit periods are protected.
|
|
12
|
+
- **Blocks are separated by a blank line.** The pause is the full stop, not the newline — no engine is known to lengthen a pause there.
|
|
13
|
+
- **Every rule is a test row,** and `flatten_markdown` run twice equals once.
|
|
14
|
+
|
|
15
|
+
## The rules
|
|
16
|
+
|
|
17
|
+
Bracket order: footnote, then numeric citation, then word bracket; checkboxes and ref-links are excluded from all three.
|
|
18
|
+
|
|
19
|
+
| Construct | Spoken as | Example in → out | Default | Config |
|
|
20
|
+
|---|---|---|---|---|
|
|
21
|
+
| Soft line wrap | one space, no pause | `wraps\nonto` → `wraps onto` | join | fixed |
|
|
22
|
+
| Blank line | full stop if the block lacks `.?!:`, then a blank line | `First para\n\nSecond.` → `First para.\n\nSecond.` | on | fixed |
|
|
23
|
+
| Heading, levels 1–2 (ATX `#`, or setext `===`/`---` underline directly under text) | `Heading,` cue, then text (keeps its own `?`/`!`, adds nothing) | `# Title One` → `Heading, Title One.` | cue | `headings` |
|
|
24
|
+
| Heading, levels 3–6 | `Sub-heading,` cue | `### Sub Three` → `Sub-heading, Sub Three.` | cue | `headings` |
|
|
25
|
+
| Em dash, en dash or spaced hyphen as a dash | comma | `clear—we shipped` → `clear, we shipped`; `simple - just restart` → `simple, just restart` | comma | fixed |
|
|
26
|
+
| En dash between numbers | the word "to" | `pages 3–5` → `pages 3 to 5` | to | fixed |
|
|
27
|
+
| Guard, leave as-is: compound hyphen, digit-period, numbers/units/times/percents, ALL CAPS | unchanged | `well-known`, `3.5 GB, v2.0.1`, `12:30, 50%`, `URGENT NOTICE` → all unchanged | unchanged | fixed |
|
|
28
|
+
| Parenthetical mid-sentence | comma, content, comma | `results (which surprised everyone) were` → `results, which surprised everyone, were` | pause | `parentheticals` |
|
|
29
|
+
| Parenthetical at a sentence end, or a whole sentence | comma at the bracket if mid-sentence continues; brackets vanish if the parenthetical is the whole sentence | `The trend is clear (see fig. 3).` → `The trend is clear, see fig 3.`; `(See the appendix.)` → `See the appendix.` | pause | `parentheticals` |
|
|
30
|
+
| Square brackets around words | comma, content, comma (guard: nested, unmatched or over-long stays untouched) | `the plan [the second one] was` → `the plan, the second one, was`; `((a+b)*c)` → unchanged | pause | `parentheticals` |
|
|
31
|
+
| Task-checkbox / reference-style link | resolved as a plain item or link, not a citation | `- [x] done` → `First, done.`; `[the docs][1]` → `the docs` | unchanged | fixed |
|
|
32
|
+
| Abbreviation period, title list | period always deleted (a title is always followed by a name) | `Dr. Smith` → `Dr Smith` | strip | `abbreviations` |
|
|
33
|
+
| Abbreviation period, context list | kept before a capital word or block end (it may be a sentence end); dropped before a lowercase word, digit or comma | `pens, etc. Then start.` → unchanged; `Fig. 3` → `Fig 3` | keep/strip | `abbreviations` |
|
|
34
|
+
| Abbreviation, Latin list | expanded, comma kept (see list below) | `e.g. chips` → `for example, chips` | expand | `abbreviations` |
|
|
35
|
+
| Initials | periods deleted, letters spaced | `J. R. R. Tolkien` → `J R R Tolkien` | strip | `abbreviations` |
|
|
36
|
+
| Bulleted or numbered list | ordinals or `Item N:`, as today | `- one\n- two` → `First, one. Second, two.`; `1. buy milk` → `Item 1: buy milk.` | unchanged | fixed |
|
|
37
|
+
| Nested list item | `Sub-item,` cue; parent count skips it | `- a\n - b\n- c` → `First, a. Sub-item, b. Second, c.` | cue | `nested_lists` |
|
|
38
|
+
| Table | row sentences, as today, in their own block | Q1 revenue row → `Table with 1 row. For Q1: Revenue is 4.2M.` | unchanged | fixed |
|
|
39
|
+
| Inline code, bold, italic, HTML tag | markers or tags stripped, text kept | `` `pip install` `` → `pip install`; `a<br>b` → `a b` | unchanged | fixed |
|
|
40
|
+
| Code block, fenced or indented (4 spaces) | one placeholder sentence, own block | `Here's the fix:` + fence → `Here's the fix:\n\nSkipping a code block.` | unchanged | fixed |
|
|
41
|
+
| Markdown link, image | link text, or alt text ("image" if none) | `[the docs](url)` → `the docs`; `` → `Revenue chart` | unchanged | fixed |
|
|
42
|
+
| Bare URL | host, dots as the word "dot" | `https://example.com/page` → `example dot com` | domain | `urls` |
|
|
43
|
+
| Block quote | `>` stripped, `Quote,` cue, `End quote.` only if longer than one line | `> A line.\n> Two.` → `Quote, A line. Two. End quote.` | cue | `blockquotes` |
|
|
44
|
+
| Footnote marker, body line | dropped (body announced once per document) | `support[^1].` → `support.`; `[^1]: Smith, 2019.` → nothing (`Skipping 1 footnote.`) | drop | `citations` |
|
|
45
|
+
| Numeric citation bracket | dropped if tight to a word; a 4-digit year is kept | `shown previously [12].` → `shown previously.`; `[2024]` unchanged | drop | `citations` |
|
|
46
|
+
| Reference list section | one sentence in its place | `## References` + entries → `Skipping the reference list.` | skip | `references` |
|
|
47
|
+
| Page number line, running head or footer | page numbers dropped outright; a repeating head/footer only via the furniture guard: short, unpunctuated, non-list/table/heading, repeated 3+ | `Vocalize User Guide` ×5 → removed; `Yes.` ×3 → unchanged | drop | `furniture` |
|
|
48
|
+
| Horizontal rule, YAML front matter | block break after a blank line (a setext heading if it follows text instead); at document start, dropped and announced only if that's the whole input | `Before.\n\n---\n\nAfter.` → `Before.\n\nAfter.`; `---\ntitle: X\n---\nBody.` → `Body.` | drop | fixed |
|
|
49
|
+
| Ellipsis | comma mid-sentence, full stop line-final | `And then... nothing` → `And then, nothing` | comma | fixed |
|
|
50
|
+
| Emoji | dropped, space repaired | `job! 🎉 Let's` → `job! Let's` | drop | `emoji` |
|
|
51
|
+
| Final spacing pass | one space between sentences, blank lines kept, no doubled marks | `Done . Next (really) .` → `Done. Next, really.` | on | fixed |
|
|
52
|
+
|
|
53
|
+
**Title list** (period always dropped): `Dr, Mr, Mrs, Ms, Prof, St`.
|
|
54
|
+
**Context list** (period dropped unless before a capital word or block end): `fig, Fig, No, approx, etc, Inc, al, vs`.
|
|
55
|
+
**Latin list**: `e.g.` → `for example,`; `i.e.` → `that is,`.
|
|
56
|
+
|
|
57
|
+
## Pauses
|
|
58
|
+
|
|
59
|
+
| Pause | Written as | What every engine does |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| None | a space | runs the words together |
|
|
62
|
+
| Breath | `, ` | short break, no intonation reset |
|
|
63
|
+
| Sentence | `. ` (or `? `, `! `) | full stop, new intonation |
|
|
64
|
+
| Block | `.` then a blank line | full stop; the blank line is a hint, not a longer silence |
|
|
65
|
+
|
|
66
|
+
The blank line matters only past a provider's `MAX_CHARS`, where `split_for_synthesis` actually chunks — below it nothing chunks, so no silence rides on the blank line. Above it, `_PARAGRAPH_SPLIT_RE` breaks there first: a request-boundary effect, not an audible pause.
|
|
67
|
+
|
|
68
|
+
## The config table
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[speech]
|
|
72
|
+
headings = "cue" # cue | plain | off
|
|
73
|
+
blockquotes = "cue" # cue | plain | off
|
|
74
|
+
nested_lists = "cue" # cue | flat | off
|
|
75
|
+
parentheticals = "pause" # pause | drop | off
|
|
76
|
+
abbreviations = "expand" # expand | protect | off
|
|
77
|
+
citations = "drop" # drop | speak | off
|
|
78
|
+
references = true # skip reference-list sections
|
|
79
|
+
furniture = true # drop repeated running heads/footers
|
|
80
|
+
urls = "domain" # domain | full | drop
|
|
81
|
+
emoji = true # drop emoji
|
|
82
|
+
|
|
83
|
+
SPEECH_BRACKET_MAX_CHARS = 120 # bracket-guard length
|
|
84
|
+
SPEECH_FURNITURE_MAX_CHARS = 60 # furniture line length cap
|
|
85
|
+
SPEECH_FURNITURE_MIN_REPEATS = 3 # repeats to drop furniture
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`off` always means: leave that construct exactly as written.
|
|
89
|
+
|
|
90
|
+
Wiring mirrors `[notes]`/`[stt]`: add `speech` to `KNOWN_SPEECH_KEYS`, add `_validate_speech_table` (unknown key warns, bad value raises; `SPEECH_*` bounded like `stt.max_seconds`), add `resolve_speech` against `SPEECH_DEFAULTS`. `flatten_markdown(text, speech=None)` keeps every call site working.
|
|
91
|
+
|
|
92
|
+
## Decisions
|
|
93
|
+
|
|
94
|
+
All seven approved by the owner on 2026-09-08. The config table's defaults are these.
|
|
95
|
+
|
|
96
|
+
1. **Heading cue.** Use `Heading,` for levels 1–2 and `Sub-heading,` for 3–6.
|
|
97
|
+
2. **Parentheticals.** Default to pause; drop silently deletes content you asked to hear.
|
|
98
|
+
3. **Block quotes.** Cue on by default, so quoted words aren't mistaken for the author's.
|
|
99
|
+
4. **Bare URLs.** Read the host only, as "example dot com".
|
|
100
|
+
5. **Reference sections.** Skip References, Bibliography or Works Cited only when the lines after look like entries; "Notes" doesn't trigger it.
|
|
101
|
+
6. **Latin abbreviations.** Expand them, because "e g" is one of the worst sounds today.
|
|
102
|
+
7. **Emoji.** Drop them, because behaviour differs on every engine.
|
|
103
|
+
|
|
104
|
+
`flatten_markdown` has one call site, `vocalize/cli.py:318` — notes and dictation never pass through it.
|
|
105
|
+
|
|
106
|
+
## What this supersedes
|
|
107
|
+
|
|
108
|
+
- **Issue #8, kept:** cue-and-pause headings, pauses at paragraph/list/table boundaries, parenthetical pause-content-pause with a drop mode, abbreviation periods never sentence ends.
|
|
109
|
+
- **Issue #8, changed:** the heading cue is a comma clause, not a sentence; parenthetical pauses are commas, not periods; no "longer pause" — depth is spoken as words.
|
|
110
|
+
- **Issue #7, kept:** running heads, page numbers, cite markers, footnote bodies and reference lists all removed.
|
|
111
|
+
- **Issue #7, changed:** a reference list is announced, not silently deleted; "Notes" no longer triggers the skip.
|
|
112
|
+
- **Not doing:** SSML, `[[slnc]]`, a pronunciation lexicon, acronym expansion, number verbalisation, emoji naming, language detection.
|
|
113
|
+
|
|
114
|
+
## Next
|
|
115
|
+
|
|
116
|
+
1. Order: block rules (furniture, front matter, references, fences/indented code, tables, rules, headings, quotes, lists), then inline rules (emoji, footnotes, citations, brackets, links, URLs, HTML tags, emphasis, dashes, ellipsis), then abbreviations and initials last (they need un-bracketed text), then the spacing pass.
|
|
117
|
+
2. Fix `re.sub(r"\s+", " ")` to collapse only spaces and tabs, and `truncate_for_budget` to split on any whitespace.
|
|
118
|
+
3. Rewrite `tests/test_preprocess.py` as one input-expected table (a row per rule above), plus two property tests: flatten is idempotent, and the output holds no character outside the allowed set (assert only, never filter — `R&D`, `C++`, `24/7` must survive).
|
|
119
|
+
4. The engines bundle came back a stub, so no engine claim here is measured; before locking defaults, listen to one long document on Kokoro and `say`, checking the blank-line pause, the comma parenthetical, and the heading cue.
|