python-voiceio 1.0.0__tar.gz → 1.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. {python_voiceio-1.0.0/python_voiceio.egg-info → python_voiceio-1.2.0}/PKG-INFO +10 -10
  2. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/README.md +9 -9
  3. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/pyproject.toml +1 -1
  4. {python_voiceio-1.0.0 → python_voiceio-1.2.0/python_voiceio.egg-info}/PKG-INFO +10 -10
  5. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/SOURCES.txt +1 -0
  6. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_app_wiring.py +5 -5
  7. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_concurrency_lockdown.py +2 -2
  8. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_engine.py +1 -0
  9. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_health.py +18 -0
  10. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_pipeline.py +16 -0
  11. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_postcorrect.py +12 -0
  12. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_postprocess.py +1 -1
  13. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_retention.py +8 -8
  14. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_robustness.py +20 -20
  15. python_voiceio-1.2.0/tests/test_service.py +62 -0
  16. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_setup.py +26 -0
  17. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_streaming.py +95 -98
  18. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_transcriber.py +11 -0
  19. python_voiceio-1.2.0/voiceio/__init__.py +1 -0
  20. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/app.py +22 -7
  21. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/learn.py +45 -2
  22. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/servicecmd.py +26 -0
  23. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/uninstall.py +3 -0
  24. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/config.py +13 -4
  25. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/faster_whisper/__init__.py +3 -1
  26. python_voiceio-1.2.0/voiceio/display.py +150 -0
  27. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/health.py +31 -1
  28. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/train.py +10 -6
  29. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/service.py +71 -0
  30. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/configfile.py +4 -0
  31. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/noninteractive.py +3 -0
  32. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/steps.py +26 -0
  33. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/streaming.py +28 -116
  34. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/manager.py +1 -1
  35. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/watchdog.py +15 -15
  36. python_voiceio-1.0.0/tests/test_service.py +0 -25
  37. python_voiceio-1.0.0/voiceio/__init__.py +0 -1
  38. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/LICENSE +0 -0
  39. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
  40. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/entry_points.txt +0 -0
  41. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/requires.txt +0 -0
  42. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/top_level.txt +0 -0
  43. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/setup.cfg +0 -0
  44. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_audio_quality.py +0 -0
  45. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_audio_source.py +0 -0
  46. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_backend_probes.py +0 -0
  47. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_cli.py +0 -0
  48. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_clipboard_read.py +0 -0
  49. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_commands.py +0 -0
  50. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_config.py +0 -0
  51. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_corrections.py +0 -0
  52. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_decoders.py +0 -0
  53. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_evdev_uaccess.py +0 -0
  54. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_fallback.py +0 -0
  55. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_fcitx_bridge.py +0 -0
  56. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_hints.py +0 -0
  57. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_history.py +0 -0
  58. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_engine_limits.py +0 -0
  59. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_engine_listener.py +0 -0
  60. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_pending.py +0 -0
  61. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_pids.py +0 -0
  62. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_ping.py +0 -0
  63. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_session.py +0 -0
  64. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_textlimit.py +0 -0
  65. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_ibus_typer.py +0 -0
  66. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_clean.py +0 -0
  67. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_import.py +0 -0
  68. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_metrics.py +0 -0
  69. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_review.py +0 -0
  70. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_learn_targets.py +0 -0
  71. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_llm.py +0 -0
  72. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_llm_api.py +0 -0
  73. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_numbers.py +0 -0
  74. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_pipeline.py +0 -0
  75. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_platform.py +0 -0
  76. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_prebuffer.py +0 -0
  77. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_prompt.py +0 -0
  78. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_recorder_integration.py +0 -0
  79. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_security_hardening.py +0 -0
  80. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_server.py +0 -0
  81. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_suggest.py +0 -0
  82. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_tokens.py +0 -0
  83. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_tts.py +0 -0
  84. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_typer_manager.py +0 -0
  85. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_vad.py +0 -0
  86. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_vocabulary.py +0 -0
  87. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_wordfreq.py +0 -0
  88. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/tests/test_worker_decode.py +0 -0
  89. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/__main__.py +0 -0
  90. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/audio_source.py +0 -0
  91. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/backends.py +0 -0
  92. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/__init__.py +0 -0
  93. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/configcmd.py +0 -0
  94. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/corrections.py +0 -0
  95. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/doctor.py +0 -0
  96. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/history.py +0 -0
  97. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/models.py +0 -0
  98. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/cli/vocab.py +0 -0
  99. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/clipboard_read.py +0 -0
  100. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/commands.py +0 -0
  101. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/consent.py +0 -0
  102. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/__init__.py +0 -0
  103. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/bias.py +0 -0
  104. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/__init__.py +0 -0
  105. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/base.py +0 -0
  106. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/catalog.py +0 -0
  107. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
  108. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/lanes.py +0 -0
  109. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/sherpa.py +0 -0
  110. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/decoders/whispercpp.py +0 -0
  111. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/engine.py +0 -0
  112. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/core/pipeline.py +0 -0
  113. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/corrections.py +0 -0
  114. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  115. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/data/__init__.py +0 -0
  116. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/demo.py +0 -0
  117. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/feedback.py +0 -0
  118. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hints.py +0 -0
  119. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/history.py +0 -0
  120. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/__init__.py +0 -0
  121. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/base.py +0 -0
  122. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/chain.py +0 -0
  123. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/evdev.py +0 -0
  124. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/pynput_backend.py +0 -0
  125. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/hotkeys/socket_backend.py +0 -0
  126. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/__init__.py +0 -0
  127. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/engine.py +0 -0
  128. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/install.py +0 -0
  129. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/pending.py +0 -0
  130. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/session.py +0 -0
  131. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ibus/textlimit.py +0 -0
  132. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/__init__.py +0 -0
  133. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/augment.py +0 -0
  134. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/clean.py +0 -0
  135. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/evaluate.py +0 -0
  136. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/importer.py +0 -0
  137. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/metrics.py +0 -0
  138. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/review.py +0 -0
  139. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/store.py +0 -0
  140. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/learn/teacher.py +0 -0
  141. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/llm.py +0 -0
  142. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/llm_api.py +0 -0
  143. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/models/__init__.py +0 -0
  144. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/models/silero_vad.onnx +0 -0
  145. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/numbers.py +0 -0
  146. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/pidlock.py +0 -0
  147. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/platform.py +0 -0
  148. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/postcorrect.py +0 -0
  149. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/postprocess.py +0 -0
  150. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/privacy.py +0 -0
  151. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/prompt.py +0 -0
  152. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/recorder.py +0 -0
  153. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/retention.py +0 -0
  154. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/server/__init__.py +0 -0
  155. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/server/__main__.py +0 -0
  156. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/server/app.py +0 -0
  157. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/server/config.py +0 -0
  158. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/server/runtime.py +0 -0
  159. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/__init__.py +0 -0
  160. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/extras.py +0 -0
  161. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/hotkey.py +0 -0
  162. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/speech.py +0 -0
  163. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/system.py +0 -0
  164. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/setup/ui.py +0 -0
  165. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/sounds/__init__.py +0 -0
  166. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/sounds/commit.wav +0 -0
  167. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/sounds/start.wav +0 -0
  168. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/sounds/stop.wav +0 -0
  169. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/suggest.py +0 -0
  170. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tokens.py +0 -0
  171. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tray/__init__.py +0 -0
  172. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tray/_icons.py +0 -0
  173. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tray/_indicator.py +0 -0
  174. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tray/_pystray.py +0 -0
  175. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/__init__.py +0 -0
  176. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/base.py +0 -0
  177. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/chain.py +0 -0
  178. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/edge_engine.py +0 -0
  179. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/espeak.py +0 -0
  180. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/piper_engine.py +0 -0
  181. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/tts/player.py +0 -0
  182. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/__init__.py +0 -0
  183. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/base.py +0 -0
  184. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/chain.py +0 -0
  185. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/clipboard.py +0 -0
  186. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/collecting.py +0 -0
  187. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/fcitx_bridge.py +0 -0
  188. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/ibus.py +0 -0
  189. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/pynput_type.py +0 -0
  190. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/wtype.py +0 -0
  191. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/xdotool.py +0 -0
  192. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/typers/ydotool.py +0 -0
  193. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/ui.py +0 -0
  194. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/vad.py +0 -0
  195. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/vocab_stats.py +0 -0
  196. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/vocabulary.py +0 -0
  197. {python_voiceio-1.0.0 → python_voiceio-1.2.0}/voiceio/wordfreq.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -140,7 +140,7 @@ uv run voiceio setup # also links voiceio into ~/.local/bin
140
140
 
141
141
  ## Your data
142
142
 
143
- Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded.
143
+ Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded. Recordings, decode traces and window titles are **off unless you turn them on** — `voiceio setup` asks whether to keep recordings so voiceio can learn your voice. Logs record sizes and timings, never your words.
144
144
 
145
145
  | Path | Holds | Retention |
146
146
  |---|---|---|
@@ -150,14 +150,13 @@ Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`,
150
150
  | `vocab_stats.json` | Per-term usage counts used to rank the vocabulary | Rebuilt as you dictate |
151
151
  | `corrections.json` | Find/replace rules (`post grass` → `PostgreSQL`) | — |
152
152
  | `flagged.txt` | Words flagged with the "correct that" voice command, for review | — |
153
- | `snapshots/` | Copies of corrections + vocabulary from before 1.0's cleanup (safe to delete) | Last 8 kept |
153
+ | `snapshots/` | Copies of corrections + vocabulary from before 1.0 (safe to delete) | — |
154
154
  | `consent.json` | Whether you allowed cloud LLM calls (text only) | — |
155
155
  | `hints.json` | Which CLI tips you have already seen | — |
156
156
  | **`~/.local/state/voiceio/`** | *What you said* | |
157
- | `history.jsonl` | Every committed transcript: time, raw and final text, focused-window title | `[history] enabled`, `max_entries`, `max_age_days` (0 = keep forever) |
158
- | `recordings/*.wav` | The audio of each utterance, for `voiceio learn` | `[data] retain_audio`, `max_audio_mb` (default 4096), `min_free_gb` (default 5) |
159
- | `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode traces and LLM before/after pairs | `[data] capture_intermediates`; each capped at 64 MB |
160
- | `corrections_audit.jsonl` | When each correction rule fired (shows which rules earn their keep) | — |
157
+ | `history.jsonl` | Every committed transcript: time, raw and final text (and the focused-window title with `[data] capture_context`) | `[history] enabled`, `max_entries`, `max_age_days` (0 = keep forever) |
158
+ | `recordings/*.wav` | The audio of each utterance, for `voiceio learn` — **opt-in** | `[data] retain_audio` (default off), `max_audio_mb` (default 4096), `min_free_gb` (default 5) |
159
+ | `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode text and timings, LLM before/after pairs — **opt-in**, for debugging | `[data] capture_intermediates` (default off); each capped at 64 MB |
161
160
  | `voiceio.log`, `worker.log`, `ibus-engine-stderr.log` | Logs | `voiceio.log` rotates at 2 MB × 3 |
162
161
  | **`~/.local/share/voiceio/learn/`** | *Personalization corpus* (only exists once you run `voiceio learn`) | Never pruned; delete freely |
163
162
  | `labels.jsonl` | Reference transcripts from the teacher model or your review | — |
@@ -191,6 +190,7 @@ voiceio learn eval # score the current model on your held-out clips: WER a
191
190
  voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
192
191
  voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
193
192
  voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
193
+ voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
194
194
  ```
195
195
 
196
196
  Labeling, training and offline eval run in an idle-priority cgroup, so they never slow dictation down. Everything stays on your machine unless you choose `train --remote`.
@@ -292,7 +292,7 @@ There is a best-effort pynput path for both (hotkeys and typing, no live preview
292
292
  | Hotkey does nothing on Wayland | `voiceio doctor --fix` installs the keyboard udev rule, or bind `voiceio toggle` in your compositor |
293
293
  | `sounddevice is not installed` | You installed without the desktop extra: `pipx install --force 'python-voiceio[desktop]'` |
294
294
  | evdev fails to build | Install a C toolchain and Python headers (see Quickstart), then reinstall |
295
- | Too slow | `voiceio doctor` shows your real stop→text; turn off cloud postcorrect, or `voiceio models use small` (or a GPU with `large-v3-turbo`) |
295
+ | Too slow | `voiceio doctor` shows your real stop→text. Turn off cloud postcorrect; use `voiceio models use small` (or a GPU with `large-v3-turbo`); or narrow the beam of the passes you keep: on the author's 44 clips `[model] beam_size = 2` matched the default 5 (WER 4.9% vs 5.4%, vocabulary 28/32 vs 29/32) while decoding faster; greedy (`1`) lost vocabulary (26/32). Measure on yours: `voiceio learn eval --beam 2` |
296
296
  | Words come out wrong in a pattern | `voiceio learn eval --pipeline` and `voiceio corrections audit` show which stage or rule did it |
297
297
  | Start over | `voiceio uninstall`, then `voiceio setup` |
298
298
 
@@ -300,8 +300,8 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
300
300
 
301
301
  ## Roadmap
302
302
 
303
- - **A landing page** — voiceio as local, open-source, private dictation that learns from you.
304
- - **Continuous learning** — `learn maintain` on a timer: label new dictation while idle, retrain when enough piles up, and promote only what wins on your held-out clips.
303
+ - **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
304
+ - **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
305
305
  - **Unbounded vocabulary** — move to a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
306
306
  - **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
307
307
 
@@ -87,7 +87,7 @@ uv run voiceio setup # also links voiceio into ~/.local/bin
87
87
 
88
88
  ## Your data
89
89
 
90
- Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded.
90
+ Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded. Recordings, decode traces and window titles are **off unless you turn them on** — `voiceio setup` asks whether to keep recordings so voiceio can learn your voice. Logs record sizes and timings, never your words.
91
91
 
92
92
  | Path | Holds | Retention |
93
93
  |---|---|---|
@@ -97,14 +97,13 @@ Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`,
97
97
  | `vocab_stats.json` | Per-term usage counts used to rank the vocabulary | Rebuilt as you dictate |
98
98
  | `corrections.json` | Find/replace rules (`post grass` → `PostgreSQL`) | — |
99
99
  | `flagged.txt` | Words flagged with the "correct that" voice command, for review | — |
100
- | `snapshots/` | Copies of corrections + vocabulary from before 1.0's cleanup (safe to delete) | Last 8 kept |
100
+ | `snapshots/` | Copies of corrections + vocabulary from before 1.0 (safe to delete) | — |
101
101
  | `consent.json` | Whether you allowed cloud LLM calls (text only) | — |
102
102
  | `hints.json` | Which CLI tips you have already seen | — |
103
103
  | **`~/.local/state/voiceio/`** | *What you said* | |
104
- | `history.jsonl` | Every committed transcript: time, raw and final text, focused-window title | `[history] enabled`, `max_entries`, `max_age_days` (0 = keep forever) |
105
- | `recordings/*.wav` | The audio of each utterance, for `voiceio learn` | `[data] retain_audio`, `max_audio_mb` (default 4096), `min_free_gb` (default 5) |
106
- | `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode traces and LLM before/after pairs | `[data] capture_intermediates`; each capped at 64 MB |
107
- | `corrections_audit.jsonl` | When each correction rule fired (shows which rules earn their keep) | — |
104
+ | `history.jsonl` | Every committed transcript: time, raw and final text (and the focused-window title with `[data] capture_context`) | `[history] enabled`, `max_entries`, `max_age_days` (0 = keep forever) |
105
+ | `recordings/*.wav` | The audio of each utterance, for `voiceio learn` — **opt-in** | `[data] retain_audio` (default off), `max_audio_mb` (default 4096), `min_free_gb` (default 5) |
106
+ | `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode text and timings, LLM before/after pairs — **opt-in**, for debugging | `[data] capture_intermediates` (default off); each capped at 64 MB |
108
107
  | `voiceio.log`, `worker.log`, `ibus-engine-stderr.log` | Logs | `voiceio.log` rotates at 2 MB × 3 |
109
108
  | **`~/.local/share/voiceio/learn/`** | *Personalization corpus* (only exists once you run `voiceio learn`) | Never pruned; delete freely |
110
109
  | `labels.jsonl` | Reference transcripts from the teacher model or your review | — |
@@ -138,6 +137,7 @@ voiceio learn eval # score the current model on your held-out clips: WER a
138
137
  voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
139
138
  voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
140
139
  voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
140
+ voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
141
141
  ```
142
142
 
143
143
  Labeling, training and offline eval run in an idle-priority cgroup, so they never slow dictation down. Everything stays on your machine unless you choose `train --remote`.
@@ -239,7 +239,7 @@ There is a best-effort pynput path for both (hotkeys and typing, no live preview
239
239
  | Hotkey does nothing on Wayland | `voiceio doctor --fix` installs the keyboard udev rule, or bind `voiceio toggle` in your compositor |
240
240
  | `sounddevice is not installed` | You installed without the desktop extra: `pipx install --force 'python-voiceio[desktop]'` |
241
241
  | evdev fails to build | Install a C toolchain and Python headers (see Quickstart), then reinstall |
242
- | Too slow | `voiceio doctor` shows your real stop→text; turn off cloud postcorrect, or `voiceio models use small` (or a GPU with `large-v3-turbo`) |
242
+ | Too slow | `voiceio doctor` shows your real stop→text. Turn off cloud postcorrect; use `voiceio models use small` (or a GPU with `large-v3-turbo`); or narrow the beam of the passes you keep: on the author's 44 clips `[model] beam_size = 2` matched the default 5 (WER 4.9% vs 5.4%, vocabulary 28/32 vs 29/32) while decoding faster; greedy (`1`) lost vocabulary (26/32). Measure on yours: `voiceio learn eval --beam 2` |
243
243
  | Words come out wrong in a pattern | `voiceio learn eval --pipeline` and `voiceio corrections audit` show which stage or rule did it |
244
244
  | Start over | `voiceio uninstall`, then `voiceio setup` |
245
245
 
@@ -247,8 +247,8 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
247
247
 
248
248
  ## Roadmap
249
249
 
250
- - **A landing page** — voiceio as local, open-source, private dictation that learns from you.
251
- - **Continuous learning** — `learn maintain` on a timer: label new dictation while idle, retrain when enough piles up, and promote only what wins on your held-out clips.
250
+ - **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
251
+ - **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
252
252
  - **Unbounded vocabulary** — move to a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
253
253
  - **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
254
254
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "1.0.0"
7
+ version = "1.2.0"
8
8
  description = "Voice dictation for Linux. Speak → text, locally, instantly."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 1.0.0
3
+ Version: 1.2.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -140,7 +140,7 @@ uv run voiceio setup # also links voiceio into ~/.local/bin
140
140
 
141
141
  ## Your data
142
142
 
143
- Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded.
143
+ Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`, `XDG_STATE_HOME` and `XDG_DATA_HOME`). Files are created `0600`, directories `0700`. Nothing is uploaded. Recordings, decode traces and window titles are **off unless you turn them on** — `voiceio setup` asks whether to keep recordings so voiceio can learn your voice. Logs record sizes and timings, never your words.
144
144
 
145
145
  | Path | Holds | Retention |
146
146
  |---|---|---|
@@ -150,14 +150,13 @@ Everything voiceio keeps is in three directories (they follow `XDG_CONFIG_HOME`,
150
150
  | `vocab_stats.json` | Per-term usage counts used to rank the vocabulary | Rebuilt as you dictate |
151
151
  | `corrections.json` | Find/replace rules (`post grass` → `PostgreSQL`) | — |
152
152
  | `flagged.txt` | Words flagged with the "correct that" voice command, for review | — |
153
- | `snapshots/` | Copies of corrections + vocabulary from before 1.0's cleanup (safe to delete) | Last 8 kept |
153
+ | `snapshots/` | Copies of corrections + vocabulary from before 1.0 (safe to delete) | — |
154
154
  | `consent.json` | Whether you allowed cloud LLM calls (text only) | — |
155
155
  | `hints.json` | Which CLI tips you have already seen | — |
156
156
  | **`~/.local/state/voiceio/`** | *What you said* | |
157
- | `history.jsonl` | Every committed transcript: time, raw and final text, focused-window title | `[history] enabled`, `max_entries`, `max_age_days` (0 = keep forever) |
158
- | `recordings/*.wav` | The audio of each utterance, for `voiceio learn` | `[data] retain_audio`, `max_audio_mb` (default 4096), `min_free_gb` (default 5) |
159
- | `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode traces and LLM before/after pairs | `[data] capture_intermediates`; each capped at 64 MB |
160
- | `corrections_audit.jsonl` | When each correction rule fired (shows which rules earn their keep) | — |
157
+ | `history.jsonl` | Every committed transcript: time, raw and final text (and the focused-window title with `[data] capture_context`) | `[history] enabled`, `max_entries`, `max_age_days` (0 = keep forever) |
158
+ | `recordings/*.wav` | The audio of each utterance, for `voiceio learn` — **opt-in** | `[data] retain_audio` (default off), `max_audio_mb` (default 4096), `min_free_gb` (default 5) |
159
+ | `streaming_trace.jsonl`, `postcorrect_pairs.jsonl` | Per-pass decode text and timings, LLM before/after pairs — **opt-in**, for debugging | `[data] capture_intermediates` (default off); each capped at 64 MB |
161
160
  | `voiceio.log`, `worker.log`, `ibus-engine-stderr.log` | Logs | `voiceio.log` rotates at 2 MB × 3 |
162
161
  | **`~/.local/share/voiceio/learn/`** | *Personalization corpus* (only exists once you run `voiceio learn`) | Never pruned; delete freely |
163
162
  | `labels.jsonl` | Reference transcripts from the teacher model or your review | — |
@@ -191,6 +190,7 @@ voiceio learn eval # score the current model on your held-out clips: WER a
191
190
  voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
192
191
  voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
193
192
  voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
193
+ voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
194
194
  ```
195
195
 
196
196
  Labeling, training and offline eval run in an idle-priority cgroup, so they never slow dictation down. Everything stays on your machine unless you choose `train --remote`.
@@ -292,7 +292,7 @@ There is a best-effort pynput path for both (hotkeys and typing, no live preview
292
292
  | Hotkey does nothing on Wayland | `voiceio doctor --fix` installs the keyboard udev rule, or bind `voiceio toggle` in your compositor |
293
293
  | `sounddevice is not installed` | You installed without the desktop extra: `pipx install --force 'python-voiceio[desktop]'` |
294
294
  | evdev fails to build | Install a C toolchain and Python headers (see Quickstart), then reinstall |
295
- | Too slow | `voiceio doctor` shows your real stop→text; turn off cloud postcorrect, or `voiceio models use small` (or a GPU with `large-v3-turbo`) |
295
+ | Too slow | `voiceio doctor` shows your real stop→text. Turn off cloud postcorrect; use `voiceio models use small` (or a GPU with `large-v3-turbo`); or narrow the beam of the passes you keep: on the author's 44 clips `[model] beam_size = 2` matched the default 5 (WER 4.9% vs 5.4%, vocabulary 28/32 vs 29/32) while decoding faster; greedy (`1`) lost vocabulary (26/32). Measure on yours: `voiceio learn eval --beam 2` |
296
296
  | Words come out wrong in a pattern | `voiceio learn eval --pipeline` and `voiceio corrections audit` show which stage or rule did it |
297
297
  | Start over | `voiceio uninstall`, then `voiceio setup` |
298
298
 
@@ -300,8 +300,8 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
300
300
 
301
301
  ## Roadmap
302
302
 
303
- - **A landing page** — voiceio as local, open-source, private dictation that learns from you.
304
- - **Continuous learning** — `learn maintain` on a timer: label new dictation while idle, retrain when enough piles up, and promote only what wins on your held-out clips.
303
+ - **A landing page** — voiceio as local, open-source, private dictation that learns from you. A draft is in [`site/index.html`](site/index.html), ready for GitHub Pages.
304
+ - **Hands-off promotion** — `learn schedule` already retrains nightly and tells you when a model wins; next is switching to it automatically, with one-command rollback.
305
305
  - **Unbounded vocabulary** — move to a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
306
306
  - **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
307
307
 
@@ -76,6 +76,7 @@ voiceio/config.py
76
76
  voiceio/consent.py
77
77
  voiceio/corrections.py
78
78
  voiceio/demo.py
79
+ voiceio/display.py
79
80
  voiceio/feedback.py
80
81
  voiceio/health.py
81
82
  voiceio/hints.py
@@ -13,18 +13,18 @@ import pytest
13
13
  from voiceio.config import Config
14
14
 
15
15
 
16
- def _make_vio(mock_transcriber=None):
16
+ def _make_vio(mock_decoder=None):
17
17
  """Create a VoiceIO instance with mocked backends."""
18
18
  mock_hotkey = MagicMock()
19
19
  mock_hotkey.name = "socket"
20
20
  mock_typer = MagicMock()
21
21
  mock_typer.name = "clipboard"
22
- if mock_transcriber is None:
23
- mock_transcriber = FakeDecoder()
22
+ if mock_decoder is None:
23
+ mock_decoder = FakeDecoder()
24
24
 
25
25
  with patch("voiceio.app.hotkey_chain.select", return_value=mock_hotkey), \
26
26
  patch("voiceio.app.typer_chain.select", return_value=mock_typer), \
27
- patch("voiceio.app.build_decoder", return_value=mock_transcriber), \
27
+ patch("voiceio.app.build_decoder", return_value=mock_decoder), \
28
28
  patch("voiceio.app.plat.detect") as mock_detect:
29
29
  mock_detect.return_value = MagicMock(display_server="wayland", desktop="gnome")
30
30
 
@@ -39,7 +39,7 @@ def _make_vio(mock_transcriber=None):
39
39
  mock_stream.stopped = False
40
40
  vio.recorder._stream = mock_stream # skip real audio
41
41
  vio.recorder._last_callback_time = time.monotonic() # healthy heartbeat
42
- return vio, mock_typer, mock_transcriber
42
+ return vio, mock_typer, mock_decoder
43
43
 
44
44
 
45
45
  def _feed_audio(vio, chunks=20):
@@ -98,7 +98,7 @@ class TestGuardedSwaps:
98
98
  from voiceio.app import _State
99
99
  vio = _make_app()
100
100
  vio._state = _State.RECORDING
101
- vio.transcriber.healthy.return_value = True
101
+ vio.decoder.healthy.return_value = True
102
102
  vio.recorder.stream_health = MagicMock(return_value=(True, ""))
103
103
  vio.cfg.tray.enabled = False
104
104
  vio._typers.health_upkeep = MagicMock()
@@ -153,7 +153,7 @@ class TestIBusGoodCitizen:
153
153
  from voiceio.app import _State
154
154
  vio = _make_app(typer_name="ibus")
155
155
  vio._state = _State.IDLE
156
- vio.transcriber.healthy.return_value = True
156
+ vio.decoder.healthy.return_value = True
157
157
  vio.recorder.stream_health = MagicMock(return_value=(True, ""))
158
158
  vio.cfg.tray.enabled = False
159
159
  vio._typers.health_upkeep = MagicMock()
@@ -67,6 +67,7 @@ def test_commit_strips_prefix_learns_and_keeps_history():
67
67
 
68
68
  from voiceio import config
69
69
  eng, _ = _engine(voice_input_prefix="[voice]")
70
+ eng.cfg.data.retain_audio = True
70
71
  stored = eng.commit("[voice] we deployed the Kalshi bot", raw="we deployed the kalshi bot",
71
72
  duration=2.0, audio=np.zeros(16000, dtype=np.float32))
72
73
  assert stored == "we deployed the Kalshi bot"
@@ -105,3 +105,21 @@ def test_usage_decoder_counts_incidents_on_the_current_release(tmp_path, monkeyp
105
105
  assert ok
106
106
  assert finding.startswith(f"decoder: 0 hangs, 1 crashes in the last 7 days on {__version__}")
107
107
  assert f"(1 more before {__version__})" in finding
108
+
109
+
110
+ def test_learning_status_and_tips(monkeypatch):
111
+ from voiceio import config, service
112
+ from voiceio.health import _accuracy_tips, _learning_status
113
+ from voiceio.learn.store import learn_dir
114
+ monkeypatch.setattr(service, "learn_timer_enabled", lambda: False)
115
+ cfg = config.Config()
116
+ st = _learning_status(cfg)
117
+ assert st["Voice model"] == "stock (small)" and st["Recordings"].startswith("off")
118
+ assert any("retain_audio true" in t for t in _accuracy_tips(st))
119
+
120
+ cfg.data.retain_audio = True
121
+ cfg.model.name = str(learn_dir() / "models" / "v0001-small-x" / "ct2")
122
+ st = _learning_status(cfg)
123
+ assert st["Voice model"] == "fine-tuned on you (v0001-small-x)"
124
+ assert st["Recordings"].startswith("on (") and st["Nightly learning"] == "off"
125
+ assert any("learn schedule on" in t for t in _accuracy_tips(st))
@@ -230,11 +230,27 @@ def test_maintain_waits_for_enough_new_clips(monkeypatch, capsys):
230
230
  store.add_label("new.wav", [Segment(0, 3, "a new one")], "teacher:fake")
231
231
 
232
232
  monkeypatch.setattr(cli, "cmd_label", lambda args: None)
233
+ monkeypatch.setattr(train, "missing_train_deps", lambda: [])
233
234
  monkeypatch.setattr(cli, "_train", lambda *a, **k: pytest.fail("must not retrain"))
234
235
  cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1))
235
236
  assert "1 new labeled clips since v0001" in capsys.readouterr().out
236
237
 
237
238
 
239
+ def test_maintain_without_train_extra_labels_and_exits_cleanly(monkeypatch, capsys):
240
+ """A nightly timer must not fail every night, or spend an eval, over an
241
+ optional install."""
242
+ import argparse
243
+
244
+ from voiceio.cli import learn as cli
245
+ labeled = []
246
+ monkeypatch.setattr(cli, "cmd_label", lambda args: labeled.append(True))
247
+ monkeypatch.setattr(train, "missing_train_deps", lambda: ["torch", "peft"])
248
+ monkeypatch.setattr(store, "build_dataset", lambda: pytest.fail("must not build"))
249
+ monkeypatch.setattr(cli, "_run_eval", lambda *a, **k: pytest.fail("must not eval"))
250
+ cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1))
251
+ assert labeled and "python-voiceio[train]" in capsys.readouterr().out
252
+
253
+
238
254
  def test_dev_mix_is_stable_per_row():
239
255
  assert train._row_draws("a.wav") == train._row_draws("a.wav")
240
256
  assert train._row_draws("a.wav") != train._row_draws("b.wav")
@@ -17,6 +17,7 @@ def _cfg(*, enabled=True, api_key="test-key", min_words=4, model="",
17
17
  )
18
18
  cfg.output.remove_disfluencies = remove_disfluencies
19
19
  cfg.cloud.api_key = api_key
20
+ cfg.data.capture_intermediates = True # most tests here check the pairs log
20
21
  return cfg
21
22
 
22
23
 
@@ -364,3 +365,14 @@ def test_guard_rejects_meaning_changes(original, corrected, reason):
364
365
  def test_guard_allows_filler_repeats_and_false_starts(original, corrected):
365
366
  pc = PostCorrector(_cfg(remove_disfluencies=True))
366
367
  assert pc._guard(original, corrected) == (True, "")
368
+
369
+
370
+ def test_nothing_recorded_unless_opted_in(tmp_path, monkeypatch):
371
+ from voiceio import config as cfgmod
372
+ pairs = tmp_path / "pairs.jsonl"
373
+ monkeypatch.setattr(cfgmod, "POSTCORRECT_PAIRS_PATH", pairs)
374
+ cfg = _cfg()
375
+ cfg.data.capture_intermediates = False
376
+ with patch("voiceio.llm_api.chat", return_value="Crisp chat is the tool"):
377
+ PostCorrector(cfg).correct("Chris chat is the tool")
378
+ assert not pairs.exists()
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
  from voiceio.config import Config
5
5
  from voiceio.core.pipeline import Pipeline
6
6
  from voiceio.postprocess import cleanup, strip_disfluencies
7
- from voiceio.streaming import _word_match_len
7
+ from voiceio.display import _word_match_len
8
8
 
9
9
 
10
10
  def _run(text: str, *, final: bool = False, **output) -> tuple[str, bool]:
@@ -12,7 +12,7 @@ from voiceio.config import DataConfig
12
12
 
13
13
  class TestSaveAudio:
14
14
  def test_saves_wav(self, tmp_path):
15
- cfg = DataConfig()
15
+ cfg = DataConfig(retain_audio=True)
16
16
  audio = np.full(16000, 0.1, dtype=np.float32)
17
17
  with patch.object(retention, "RECORDINGS_DIR", tmp_path):
18
18
  name = retention.save_audio(audio, ts=1700000000.5, cfg=cfg)
@@ -29,13 +29,13 @@ class TestSaveAudio:
29
29
  assert retention.save_audio(audio, ts=0, cfg=cfg) is None
30
30
 
31
31
  def test_none_and_empty_audio(self, tmp_path):
32
- cfg = DataConfig()
32
+ cfg = DataConfig(retain_audio=True)
33
33
  with patch.object(retention, "RECORDINGS_DIR", tmp_path):
34
34
  assert retention.save_audio(None, ts=0, cfg=cfg) is None
35
35
  assert retention.save_audio(np.zeros(0, dtype=np.float32), ts=0, cfg=cfg) is None
36
36
 
37
37
  def test_roundtrip_amplitude(self, tmp_path):
38
- cfg = DataConfig()
38
+ cfg = DataConfig(retain_audio=True)
39
39
  audio = np.full(1600, 0.5, dtype=np.float32)
40
40
  with patch.object(retention, "RECORDINGS_DIR", tmp_path):
41
41
  name = retention.save_audio(audio, ts=1700000001.0, cfg=cfg)
@@ -47,7 +47,7 @@ class TestSaveAudio:
47
47
  class TestPrune:
48
48
  def test_prunes_oldest_over_cap(self, tmp_path):
49
49
  import os
50
- cfg = DataConfig(max_audio_mb=1)
50
+ cfg = DataConfig(retain_audio=True, max_audio_mb=1)
51
51
  # Three ~600KB files: total ~1.8MB > 1MB cap → oldest goes
52
52
  for i, name in enumerate(["a.wav", "b.wav", "c.wav"]):
53
53
  p = tmp_path / name
@@ -60,7 +60,7 @@ class TestPrune:
60
60
  assert "c.wav" in remaining
61
61
 
62
62
  def test_noop_under_cap(self, tmp_path):
63
- cfg = DataConfig(max_audio_mb=100)
63
+ cfg = DataConfig(retain_audio=True, max_audio_mb=100)
64
64
  (tmp_path / "a.wav").write_bytes(b"\0" * 1000)
65
65
  with patch.object(retention, "RECORDINGS_DIR", tmp_path):
66
66
  retention.prune(cfg)
@@ -122,7 +122,7 @@ class TestSaveTrace:
122
122
  from voiceio import retention
123
123
  from voiceio.config import DataConfig
124
124
  monkeypatch.setattr(retention, "TRACES_PATH", tmp_path / "trace.jsonl")
125
- retention.save_trace(DataConfig(), {"ts": 1, "passes": []})
125
+ retention.save_trace(DataConfig(capture_intermediates=True), {"ts": 1, "passes": []})
126
126
  assert (tmp_path / "trace.jsonl").exists()
127
127
 
128
128
  def test_skips_when_disabled(self, tmp_path, monkeypatch):
@@ -140,7 +140,7 @@ class TestDiskFloor:
140
140
  from voiceio.config import DataConfig
141
141
  monkeypatch.setattr(retention, "_free_gb", lambda p: 1.0)
142
142
  name = retention.save_audio(
143
- np.zeros(1600, dtype=np.float32), 0.0, DataConfig(min_free_gb=5.0),
143
+ np.zeros(1600, dtype=np.float32), 0.0, DataConfig(retain_audio=True, min_free_gb=5.0),
144
144
  )
145
145
  assert name is None
146
146
 
@@ -151,7 +151,7 @@ class TestDiskFloor:
151
151
  monkeypatch.setattr(retention, "RECORDINGS_DIR", tmp_path)
152
152
  monkeypatch.setattr(retention, "_free_gb", lambda p: 100.0)
153
153
  name = retention.save_audio(
154
- np.zeros(1600, dtype=np.float32), 0.0, DataConfig(min_free_gb=5.0),
154
+ np.zeros(1600, dtype=np.float32), 0.0, DataConfig(retain_audio=True, min_free_gb=5.0),
155
155
  )
156
156
  assert name is not None and (tmp_path / name).exists()
157
157
 
@@ -17,18 +17,18 @@ from voiceio.config import Config
17
17
  # Helpers (same pattern as test_app_wiring.py)
18
18
  # ---------------------------------------------------------------------------
19
19
 
20
- def _make_vio(mock_transcriber=None):
20
+ def _make_vio(mock_decoder=None):
21
21
  """Create a VoiceIO instance with mocked backends."""
22
22
  mock_hotkey = MagicMock()
23
23
  mock_hotkey.name = "socket"
24
24
  mock_typer = MagicMock()
25
25
  mock_typer.name = "clipboard"
26
- if mock_transcriber is None:
27
- mock_transcriber = FakeDecoder()
26
+ if mock_decoder is None:
27
+ mock_decoder = FakeDecoder()
28
28
 
29
29
  with patch("voiceio.app.hotkey_chain.select", return_value=mock_hotkey), \
30
30
  patch("voiceio.app.typer_chain.select", return_value=mock_typer), \
31
- patch("voiceio.app.build_decoder", return_value=mock_transcriber), \
31
+ patch("voiceio.app.build_decoder", return_value=mock_decoder), \
32
32
  patch("voiceio.app.plat.detect") as mock_detect:
33
33
  mock_detect.return_value = MagicMock(display_server="wayland", desktop="gnome")
34
34
 
@@ -40,7 +40,7 @@ def _make_vio(mock_transcriber=None):
40
40
  mock_stream.stopped = False
41
41
  vio.recorder._stream = mock_stream
42
42
  vio.recorder._last_callback_time = time.monotonic()
43
- return vio, mock_typer, mock_transcriber
43
+ return vio, mock_typer, mock_decoder
44
44
 
45
45
 
46
46
  def _feed_audio(vio, chunks=20):
@@ -421,7 +421,7 @@ class TestHealthWatchdogAudioBackoff:
421
421
  vio, _, _ = _make_vio()
422
422
  vio.recorder.stream_health = MagicMock(return_value=(False, "stream not active"))
423
423
  vio.recorder.reopen_stream = MagicMock()
424
- vio.transcriber.healthy = MagicMock(return_value=True)
424
+ vio.decoder.healthy = MagicMock(return_value=True)
425
425
 
426
426
  with patch("voiceio.watchdog.tray"):
427
427
  vio._watchdog.check()
@@ -434,7 +434,7 @@ class TestHealthWatchdogAudioBackoff:
434
434
  vio._watchdog._next_stream_retry = 0 # allow retry
435
435
  vio.recorder.stream_health = MagicMock(return_value=(False, "stream not active"))
436
436
  vio.recorder.reopen_stream = MagicMock() # succeeds (no exception)
437
- vio.transcriber.healthy = MagicMock(return_value=True)
437
+ vio.decoder.healthy = MagicMock(return_value=True)
438
438
 
439
439
  with patch("voiceio.watchdog.tray"):
440
440
  vio._watchdog.check()
@@ -448,7 +448,7 @@ class TestHealthWatchdogAudioBackoff:
448
448
  vio._watchdog._next_stream_retry = 0
449
449
  vio.recorder.stream_health = MagicMock(return_value=(False, "stream not active"))
450
450
  vio.recorder.reopen_stream = MagicMock(side_effect=OSError("device gone"))
451
- vio.transcriber.healthy = MagicMock(return_value=True)
451
+ vio.decoder.healthy = MagicMock(return_value=True)
452
452
 
453
453
  with patch("voiceio.watchdog.tray"):
454
454
  vio._watchdog.check()
@@ -463,7 +463,7 @@ class TestHealthWatchdogAudioBackoff:
463
463
  vio._watchdog._next_stream_retry = time.monotonic() + 999 # far future
464
464
  vio.recorder.stream_health = MagicMock(return_value=(False, "stream not active"))
465
465
  vio.recorder.reopen_stream = MagicMock()
466
- vio.transcriber.healthy = MagicMock(return_value=True)
466
+ vio.decoder.healthy = MagicMock(return_value=True)
467
467
 
468
468
  with patch("voiceio.watchdog.tray"):
469
469
  vio._watchdog.check()
@@ -476,7 +476,7 @@ class TestHealthWatchdogAudioBackoff:
476
476
  vio._watchdog._stream_fail_count = 3
477
477
  vio._watchdog._next_stream_retry = time.monotonic() + 100
478
478
  vio.recorder.stream_health = MagicMock(return_value=(True, ""))
479
- vio.transcriber.healthy = MagicMock(return_value=True)
479
+ vio.decoder.healthy = MagicMock(return_value=True)
480
480
 
481
481
  with patch("voiceio.watchdog.tray"):
482
482
  vio._watchdog.check()
@@ -489,7 +489,7 @@ class TestHealthWatchdogAudioBackoff:
489
489
  vio, _, _ = _make_vio()
490
490
  vio.recorder.stream_health = MagicMock(return_value=(False, "stream not active"))
491
491
  vio.recorder.reopen_stream = MagicMock(side_effect=OSError("device gone"))
492
- vio.transcriber.healthy = MagicMock(return_value=True)
492
+ vio.decoder.healthy = MagicMock(return_value=True)
493
493
 
494
494
  delays = []
495
495
  with patch("voiceio.watchdog.tray"):
@@ -551,7 +551,7 @@ class TestPreFlightStreamCheck:
551
551
  class TestStreamingWorkerExceptionHandling:
552
552
  def test_worker_catches_exception_and_continues(self):
553
553
  """Worker loop should catch exceptions in _transcribe_and_apply and keep running."""
554
- mock_transcriber = FakeDecoder()
554
+ mock_decoder = FakeDecoder()
555
555
  mock_typer = MagicMock()
556
556
  mock_typer.name = "clipboard"
557
557
  mock_recorder = MagicMock()
@@ -561,7 +561,7 @@ class TestStreamingWorkerExceptionHandling:
561
561
  from voiceio.streaming import StreamingSession
562
562
 
563
563
  session = StreamingSession(
564
- mock_transcriber, mock_typer, mock_recorder,
564
+ mock_decoder, mock_typer, mock_recorder,
565
565
  generation=0,
566
566
  )
567
567
 
@@ -593,8 +593,8 @@ class TestStreamingWorkerExceptionHandling:
593
593
 
594
594
  def test_final_transcription_exception_does_not_crash(self):
595
595
  """Exception during final transcription should be caught."""
596
- mock_transcriber = FakeDecoder()
597
- mock_transcriber.transcribe.side_effect = RuntimeError("model crash")
596
+ mock_decoder = FakeDecoder()
597
+ mock_decoder.transcribe.side_effect = RuntimeError("model crash")
598
598
  mock_typer = MagicMock()
599
599
  mock_typer.name = "clipboard"
600
600
  mock_recorder = MagicMock()
@@ -604,7 +604,7 @@ class TestStreamingWorkerExceptionHandling:
604
604
  from voiceio.streaming import StreamingSession
605
605
 
606
606
  session = StreamingSession(
607
- mock_transcriber, mock_typer, mock_recorder,
607
+ mock_decoder, mock_typer, mock_recorder,
608
608
  generation=0,
609
609
  )
610
610
 
@@ -629,7 +629,7 @@ class TestTrayWatchdog:
629
629
  vio, _, _ = _make_vio()
630
630
  vio.cfg.tray.enabled = True
631
631
  vio.recorder.stream_health = MagicMock(return_value=(True, ""))
632
- vio.transcriber.healthy = MagicMock(return_value=True)
632
+ vio.decoder.healthy = MagicMock(return_value=True)
633
633
 
634
634
  with patch("voiceio.watchdog.tray") as mock_tray:
635
635
  mock_tray.is_alive.return_value = False
@@ -640,7 +640,7 @@ class TestTrayWatchdog:
640
640
  vio, _, _ = _make_vio()
641
641
  vio.cfg.tray.enabled = True
642
642
  vio.recorder.stream_health = MagicMock(return_value=(True, ""))
643
- vio.transcriber.healthy = MagicMock(return_value=True)
643
+ vio.decoder.healthy = MagicMock(return_value=True)
644
644
 
645
645
  with patch("voiceio.watchdog.tray") as mock_tray:
646
646
  mock_tray.is_alive.return_value = True
@@ -651,7 +651,7 @@ class TestTrayWatchdog:
651
651
  vio, _, _ = _make_vio()
652
652
  vio.cfg.tray.enabled = False
653
653
  vio.recorder.stream_health = MagicMock(return_value=(True, ""))
654
- vio.transcriber.healthy = MagicMock(return_value=True)
654
+ vio.decoder.healthy = MagicMock(return_value=True)
655
655
 
656
656
  with patch("voiceio.watchdog.tray") as mock_tray:
657
657
  vio._watchdog.check()
@@ -810,7 +810,7 @@ class TestBootRaceAndReProbe:
810
810
  vio._typers.typer = MagicMock()
811
811
  vio._typers.typer.name = "clipboard"
812
812
  vio.recorder.stream_health = MagicMock(return_value=(True, ""))
813
- vio.transcriber.healthy = MagicMock(return_value=True)
813
+ vio.decoder.healthy = MagicMock(return_value=True)
814
814
  vio.cfg.tray.enabled = False
815
815
 
816
816
  # Env arrives; health check should re-import, re-detect, and upgrade.