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.
Files changed (240) hide show
  1. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/CHANGELOG.md +30 -1
  2. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/PKG-INFO +4 -3
  3. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/README.md +3 -2
  4. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/app.md +1 -1
  5. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/dictation.md +86 -28
  6. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/review-0.13.0.md +61 -0
  7. vocalize_cli-0.13.1/docs/plans/2026-09-app-roadmap/run-10-release-0-13-0/report.md +17 -0
  8. vocalize_cli-0.13.1/docs/plans/2026-09-app-roadmap/run-10-release-0-13-0/task-report.md +29 -0
  9. vocalize_cli-0.13.1/docs/plans/2026-09-app-roadmap/run-11-cue-hold-paste/report.md +16 -0
  10. vocalize_cli-0.13.1/docs/plans/2026-09-app-roadmap/run-11b-playback-pause/report.md +14 -0
  11. {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
  12. vocalize_cli-0.13.1/docs/plans/2026-09-app-roadmap/run-12-release-0-13-1/report.md +10 -0
  13. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/spike-notes.md +9 -0
  14. vocalize_cli-0.13.1/docs/research/2026-09-08-spoken-rendering-rules.md +119 -0
  15. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_cli.py +257 -1
  16. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_config.py +75 -11
  17. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_dictate.py +610 -50
  18. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_preprocess.py +118 -50
  19. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/uv.lock +6 -6
  20. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/__init__.py +1 -1
  21. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/cli.py +57 -8
  22. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/config.py +181 -14
  23. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/dictate.py +281 -46
  24. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/integrate.py +1 -1
  25. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/interrupted.py +34 -1
  26. vocalize_cli-0.13.1/vocalize/preprocess.py +624 -0
  27. vocalize_cli-0.13.0/vocalize/preprocess.py +0 -279
  28. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/.env.example +0 -0
  29. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/.github/workflows/ci.yml +0 -0
  30. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/.gitignore +0 -0
  31. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/LICENSE +0 -0
  32. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/installation.md +0 -0
  33. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/next-features-analysis.md +0 -0
  34. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/choreography.md +0 -0
  35. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/decisions.md +0 -0
  36. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/design.md +0 -0
  37. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/plan.md +0 -0
  38. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/review-0.12.0.md +0 -0
  39. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/review-plan-2026-09-06.md +0 -0
  40. {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
  41. {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
  42. {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
  43. {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
  44. {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
  45. {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
  46. {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
  47. {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
  48. {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
  49. {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
  50. {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
  51. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-13-spikes/project-plan.md +0 -0
  52. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-13-spikes/validate-exit.sh +0 -0
  53. {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
  54. {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
  55. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-15-notes/project-plan.md +0 -0
  56. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-15-notes/validate-exit.sh +0 -0
  57. {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
  58. {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
  59. {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
  60. {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
  61. {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
  62. {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
  63. {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
  64. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-2-stt-decoding/report.md +0 -0
  65. {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
  66. {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
  67. {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
  68. {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
  69. {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
  70. {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
  71. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-4-keychain/project-plan.md +0 -0
  72. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-4-keychain/report.md +0 -0
  73. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-4-keychain/task-report.md +0 -0
  74. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-4-keychain/validate-exit.sh +0 -0
  75. {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
  76. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-5-keys-tab/report.md +0 -0
  77. {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
  78. {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
  79. {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
  80. {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
  81. {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
  82. {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
  83. {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
  84. {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
  85. {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
  86. {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
  87. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-8a-app-swift/report.md +0 -0
  88. {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
  89. {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
  90. {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
  91. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/run-8b-app-python/report.md +0 -0
  92. {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
  93. {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
  94. {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
  95. {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
  96. {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
  97. {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
  98. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/split-assessment.md +0 -0
  99. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-app-roadmap/verification.md +0 -0
  100. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/choreography.md +0 -0
  101. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/decisions.md +0 -0
  102. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/design.md +0 -0
  103. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/plan.md +0 -0
  104. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/review-0.10.0.md +0 -0
  105. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/review-0.11.0.md +0 -0
  106. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-1-status/project-plan.md +0 -0
  107. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-1-status/report.md +0 -0
  108. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-1-status/validate-exit.sh +0 -0
  109. {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
  110. {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
  111. {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
  112. {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
  113. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-2-stt-runtime/report.md +0 -0
  114. {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
  115. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-3-recorder/project-plan.md +0 -0
  116. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-3-recorder/report.md +0 -0
  117. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-3-recorder/validate-exit.sh +0 -0
  118. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-4-dictation/project-plan.md +0 -0
  119. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-4-dictation/report.md +0 -0
  120. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-4-dictation/validate-exit.sh +0 -0
  121. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-5-resume/project-plan.md +0 -0
  122. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-5-resume/report.md +0 -0
  123. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-5-resume/validate-exit.sh +0 -0
  124. {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
  125. {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
  126. {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
  127. {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
  128. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-7-portal-read/report.md +0 -0
  129. {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
  130. {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
  131. {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
  132. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-8-portal-write/report.md +0 -0
  133. {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
  134. {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
  135. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/run-9-portal-page/report.md +0 -0
  136. {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
  137. {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
  138. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/spike-2026-09-01.md +0 -0
  139. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/split-assessment.md +0 -0
  140. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/plans/2026-09-next-features/verification.md +0 -0
  141. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/provider-credentials.md +0 -0
  142. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/research/2026-09-01-config-portal-design.md +0 -0
  143. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/research/2026-09-01-dictation-design.md +0 -0
  144. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/research/2026-09-01-voicebox-findings.md +0 -0
  145. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/research/2026-09-04-app-roadmap-analysis.md +0 -0
  146. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/docs/roadmap.md +0 -0
  147. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/hooks/claude_stop_hook.py +0 -0
  148. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/hooks/install_hook.py +0 -0
  149. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/hooks/install_quick_action.py +0 -0
  150. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/hooks/speak_options.py +0 -0
  151. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/hooks/speak_url_gate.py +0 -0
  152. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/pyproject.toml +0 -0
  153. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/conftest.py +0 -0
  154. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/portal_page_harness.js +0 -0
  155. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_app_build.py +0 -0
  156. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_app_cli.py +0 -0
  157. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_audio.py +0 -0
  158. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_auth.py +0 -0
  159. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_cache.py +0 -0
  160. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_chain.py +0 -0
  161. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_claude_stop_hook.py +0 -0
  162. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_clipboard.py +0 -0
  163. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_cue_assets.py +0 -0
  164. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_elevenlabs_provider.py +0 -0
  165. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_exceptions.py +0 -0
  166. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_google_provider.py +0 -0
  167. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_http.py +0 -0
  168. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_install_hook.py +0 -0
  169. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_install_quick_action.py +0 -0
  170. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_kokoro_manifest.py +0 -0
  171. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_kokoro_provider.py +0 -0
  172. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_kokoro_worker.py +0 -0
  173. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_ledger.py +0 -0
  174. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_listen_check.py +0 -0
  175. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_llm.py +0 -0
  176. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_local_install.py +0 -0
  177. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_openai_provider.py +0 -0
  178. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_polly_provider.py +0 -0
  179. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_portal.py +0 -0
  180. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_portal_assets.py +0 -0
  181. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_providers_registry.py +0 -0
  182. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_readiness.py +0 -0
  183. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_recorder_build.py +0 -0
  184. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_say_provider.py +0 -0
  185. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_speak_options.py +0 -0
  186. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_speak_url_gate.py +0 -0
  187. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_tts.py +0 -0
  188. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_uv_path.py +0 -0
  189. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_whisper_manifest.py +0 -0
  190. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_whisper_worker.py +0 -0
  191. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/tests/test_wizard.py +0 -0
  192. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/__main__.py +0 -0
  193. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/app.py +0 -0
  194. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/claude/speak/SKILL.md +0 -0
  195. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/cues/README.md +0 -0
  196. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/cues/ready.wav +0 -0
  197. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/cues/start.wav +0 -0
  198. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/cues/stopped.wav +0 -0
  199. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/portal.html +0 -0
  200. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/portal.js +0 -0
  201. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Dictate with Vocalize.workflow/Contents/Info.plist +0 -0
  202. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Dictate with Vocalize.workflow/Contents/Resources/document.wflow +0 -0
  203. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Speak Latest Plan.workflow/Contents/Info.plist +0 -0
  204. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Speak Latest Plan.workflow/Contents/Resources/document.wflow +0 -0
  205. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Speak with Vocalize.workflow/Contents/Info.plist +0 -0
  206. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Speak with Vocalize.workflow/Contents/Resources/document.wflow +0 -0
  207. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Stop Vocalize.workflow/Contents/Info.plist +0 -0
  208. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/assets/quick_actions/Stop Vocalize.workflow/Contents/Resources/document.wflow +0 -0
  209. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/audio.py +0 -0
  210. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/auth.py +0 -0
  211. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/cache.py +0 -0
  212. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/chain.py +0 -0
  213. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/clipboard.py +0 -0
  214. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/exceptions.py +0 -0
  215. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/ledger.py +0 -0
  216. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/llm.py +0 -0
  217. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/__init__.py +0 -0
  218. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/install.py +0 -0
  219. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/kokoro_manifest.py +0 -0
  220. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/kokoro_worker.py +0 -0
  221. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/whisper_manifest.py +0 -0
  222. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/local/whisper_worker.py +0 -0
  223. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/menubar/Info.plist.in +0 -0
  224. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/menubar/VocalizeApp.swift +0 -0
  225. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/portal.py +0 -0
  226. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/__init__.py +0 -0
  227. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/_http.py +0 -0
  228. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/elevenlabs.py +0 -0
  229. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/google.py +0 -0
  230. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/kokoro.py +0 -0
  231. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/openai.py +0 -0
  232. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/polly.py +0 -0
  233. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/providers/say.py +0 -0
  234. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/readiness.py +0 -0
  235. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/recorder/Info.plist.in +0 -0
  236. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/recorder/Recorder.entitlements +0 -0
  237. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/recorder/VocalizeRecorder.swift +0 -0
  238. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/speak_options.py +0 -0
  239. {vocalize_cli-0.13.0 → vocalize_cli-0.13.1}/vocalize/tts.py +0 -0
  240. {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
- Nothing yet.
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.0
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` | reserved — not implemented in 0.10.0 | `false` |
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 ("hold" arrives in 0.13.1)
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` | reserved — not implemented in 0.10.0 | `false` |
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 ("hold" arrives in 0.13.1)
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` (`hold` arrives in 0.13.1) | `toggle` |
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; that's a deliberate
144
- narrowing (`[stt] paste` is reserved for a future release and does
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. Timing matters: "Start." is spoken *before* the microphone
168
- opens (so it is never in your recording), and the microphone is open
169
- about a second later — the Tink marks that moment. So in `"both"` mode
170
- talk after the Tink; in `"words"` mode give it a beat after "Start.".
171
- Closing that gap is
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` | reserved | `false` | not implemented in 0.10.0 — setting it does nothing |
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
- ## Interrupted reads and `resume`
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. From a terminal (or if you missed the dialog), the same
352
- thing is:
408
+ counts as Discard.
353
409
 
354
- ```bash
355
- vocalize resume # play the saved piece, then read the rest
356
- vocalize resume --forget # discard the interrupted read
357
- vocalize resume # "Nothing to resume." if there's nothing saved
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 started, or the whole file for a provider that doesn't stream |
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* your dictation
381
- interrupted.
382
-
383
- Resuming plays the saved piece from where it was cut off (converting to
384
- WAV first if needed, then slicing by sample), then continues through the
385
- rest of the text via the normal chain, same provider, same cache — so
386
- anything already rendered is a cache hit and the continuation starts
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:-120}"
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](c.png)` → `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.