python-voiceio 1.0.0__tar.gz → 1.1.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.1.0}/PKG-INFO +8 -8
  2. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/README.md +7 -7
  3. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/pyproject.toml +1 -1
  4. {python_voiceio-1.0.0 → python_voiceio-1.1.0/python_voiceio.egg-info}/PKG-INFO +8 -8
  5. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/python_voiceio.egg-info/SOURCES.txt +1 -0
  6. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_app_wiring.py +5 -5
  7. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_concurrency_lockdown.py +2 -2
  8. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_engine.py +1 -0
  9. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_postcorrect.py +12 -0
  10. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_postprocess.py +1 -1
  11. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_retention.py +8 -8
  12. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_robustness.py +20 -20
  13. python_voiceio-1.1.0/tests/test_service.py +45 -0
  14. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_setup.py +11 -0
  15. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_streaming.py +95 -98
  16. python_voiceio-1.1.0/voiceio/__init__.py +1 -0
  17. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/app.py +7 -7
  18. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/learn.py +29 -0
  19. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/uninstall.py +3 -0
  20. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/config.py +7 -4
  21. python_voiceio-1.1.0/voiceio/display.py +150 -0
  22. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/service.py +71 -0
  23. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/configfile.py +4 -0
  24. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/noninteractive.py +3 -0
  25. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/steps.py +6 -0
  26. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/streaming.py +28 -116
  27. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/manager.py +1 -1
  28. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/watchdog.py +15 -15
  29. python_voiceio-1.0.0/tests/test_service.py +0 -25
  30. python_voiceio-1.0.0/voiceio/__init__.py +0 -1
  31. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/LICENSE +0 -0
  32. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
  33. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/python_voiceio.egg-info/entry_points.txt +0 -0
  34. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/python_voiceio.egg-info/requires.txt +0 -0
  35. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/python_voiceio.egg-info/top_level.txt +0 -0
  36. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/setup.cfg +0 -0
  37. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_audio_quality.py +0 -0
  38. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_audio_source.py +0 -0
  39. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_backend_probes.py +0 -0
  40. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_cli.py +0 -0
  41. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_clipboard_read.py +0 -0
  42. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_commands.py +0 -0
  43. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_config.py +0 -0
  44. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_corrections.py +0 -0
  45. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_decoders.py +0 -0
  46. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_evdev_uaccess.py +0 -0
  47. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_fallback.py +0 -0
  48. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_fcitx_bridge.py +0 -0
  49. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_health.py +0 -0
  50. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_hints.py +0 -0
  51. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_history.py +0 -0
  52. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_engine_limits.py +0 -0
  53. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_engine_listener.py +0 -0
  54. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_pending.py +0 -0
  55. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_pids.py +0 -0
  56. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_ping.py +0 -0
  57. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_session.py +0 -0
  58. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_textlimit.py +0 -0
  59. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_ibus_typer.py +0 -0
  60. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_clean.py +0 -0
  61. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_import.py +0 -0
  62. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_metrics.py +0 -0
  63. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_pipeline.py +0 -0
  64. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_review.py +0 -0
  65. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_learn_targets.py +0 -0
  66. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_llm.py +0 -0
  67. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_llm_api.py +0 -0
  68. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_numbers.py +0 -0
  69. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_pipeline.py +0 -0
  70. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_platform.py +0 -0
  71. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_prebuffer.py +0 -0
  72. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_prompt.py +0 -0
  73. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_recorder_integration.py +0 -0
  74. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_security_hardening.py +0 -0
  75. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_server.py +0 -0
  76. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_suggest.py +0 -0
  77. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_tokens.py +0 -0
  78. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_transcriber.py +0 -0
  79. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_tts.py +0 -0
  80. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_typer_manager.py +0 -0
  81. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_vad.py +0 -0
  82. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_vocabulary.py +0 -0
  83. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_wordfreq.py +0 -0
  84. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/tests/test_worker_decode.py +0 -0
  85. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/__main__.py +0 -0
  86. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/audio_source.py +0 -0
  87. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/backends.py +0 -0
  88. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/__init__.py +0 -0
  89. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/configcmd.py +0 -0
  90. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/corrections.py +0 -0
  91. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/doctor.py +0 -0
  92. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/history.py +0 -0
  93. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/models.py +0 -0
  94. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/servicecmd.py +0 -0
  95. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/cli/vocab.py +0 -0
  96. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/clipboard_read.py +0 -0
  97. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/commands.py +0 -0
  98. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/consent.py +0 -0
  99. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/__init__.py +0 -0
  100. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/bias.py +0 -0
  101. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/__init__.py +0 -0
  102. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/base.py +0 -0
  103. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/catalog.py +0 -0
  104. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
  105. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
  106. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/lanes.py +0 -0
  107. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/sherpa.py +0 -0
  108. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/decoders/whispercpp.py +0 -0
  109. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/engine.py +0 -0
  110. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/core/pipeline.py +0 -0
  111. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/corrections.py +0 -0
  112. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  113. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/data/__init__.py +0 -0
  114. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/demo.py +0 -0
  115. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/feedback.py +0 -0
  116. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/health.py +0 -0
  117. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hints.py +0 -0
  118. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/history.py +0 -0
  119. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/__init__.py +0 -0
  120. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/base.py +0 -0
  121. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/chain.py +0 -0
  122. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/evdev.py +0 -0
  123. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/pynput_backend.py +0 -0
  124. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/hotkeys/socket_backend.py +0 -0
  125. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/__init__.py +0 -0
  126. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/engine.py +0 -0
  127. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/install.py +0 -0
  128. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/pending.py +0 -0
  129. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/session.py +0 -0
  130. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ibus/textlimit.py +0 -0
  131. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/__init__.py +0 -0
  132. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/augment.py +0 -0
  133. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/clean.py +0 -0
  134. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/evaluate.py +0 -0
  135. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/importer.py +0 -0
  136. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/metrics.py +0 -0
  137. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/review.py +0 -0
  138. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/store.py +0 -0
  139. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/teacher.py +0 -0
  140. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/learn/train.py +0 -0
  141. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/llm.py +0 -0
  142. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/llm_api.py +0 -0
  143. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/models/__init__.py +0 -0
  144. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/models/silero_vad.onnx +0 -0
  145. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/numbers.py +0 -0
  146. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/pidlock.py +0 -0
  147. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/platform.py +0 -0
  148. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/postcorrect.py +0 -0
  149. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/postprocess.py +0 -0
  150. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/privacy.py +0 -0
  151. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/prompt.py +0 -0
  152. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/recorder.py +0 -0
  153. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/retention.py +0 -0
  154. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/server/__init__.py +0 -0
  155. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/server/__main__.py +0 -0
  156. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/server/app.py +0 -0
  157. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/server/config.py +0 -0
  158. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/server/runtime.py +0 -0
  159. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/__init__.py +0 -0
  160. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/extras.py +0 -0
  161. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/hotkey.py +0 -0
  162. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/speech.py +0 -0
  163. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/system.py +0 -0
  164. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/setup/ui.py +0 -0
  165. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/sounds/__init__.py +0 -0
  166. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/sounds/commit.wav +0 -0
  167. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/sounds/start.wav +0 -0
  168. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/sounds/stop.wav +0 -0
  169. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/suggest.py +0 -0
  170. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tokens.py +0 -0
  171. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tray/__init__.py +0 -0
  172. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tray/_icons.py +0 -0
  173. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tray/_indicator.py +0 -0
  174. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tray/_pystray.py +0 -0
  175. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/__init__.py +0 -0
  176. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/base.py +0 -0
  177. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/chain.py +0 -0
  178. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/edge_engine.py +0 -0
  179. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/espeak.py +0 -0
  180. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/piper_engine.py +0 -0
  181. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/tts/player.py +0 -0
  182. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/__init__.py +0 -0
  183. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/base.py +0 -0
  184. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/chain.py +0 -0
  185. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/clipboard.py +0 -0
  186. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/collecting.py +0 -0
  187. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/fcitx_bridge.py +0 -0
  188. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/ibus.py +0 -0
  189. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/pynput_type.py +0 -0
  190. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/wtype.py +0 -0
  191. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/xdotool.py +0 -0
  192. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/typers/ydotool.py +0 -0
  193. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/ui.py +0 -0
  194. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/vad.py +0 -0
  195. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/vocab_stats.py +0 -0
  196. {python_voiceio-1.0.0 → python_voiceio-1.1.0}/voiceio/vocabulary.py +0 -0
  197. {python_voiceio-1.0.0 → python_voiceio-1.1.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.1.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`.
@@ -301,7 +301,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
301
301
  ## Roadmap
302
302
 
303
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.
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`.
@@ -248,7 +248,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
248
248
  ## Roadmap
249
249
 
250
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.
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.1.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.1.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`.
@@ -301,7 +301,7 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
301
301
  ## Roadmap
302
302
 
303
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.
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"
@@ -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.
@@ -0,0 +1,45 @@
1
+ import sys
2
+ from pathlib import Path
3
+
4
+ from voiceio import service
5
+
6
+
7
+ def test_install_symlinks_relinks_and_drops_legacy(monkeypatch, tmp_path):
8
+ venv_bin = tmp_path / "venv" / "bin"
9
+ venv_bin.mkdir(parents=True)
10
+ for name in service.SCRIPT_NAMES:
11
+ (venv_bin / name).write_text("#!/bin/sh\n")
12
+ old = tmp_path / "old"
13
+ old.mkdir()
14
+ local_bin = tmp_path / "local-bin"
15
+ local_bin.mkdir()
16
+ (local_bin / "voiceio").symlink_to(old / "voiceio") # stale install
17
+ (local_bin / "voiceio-doctor").symlink_to(old / "voiceio-doctor")
18
+ monkeypatch.setattr(service, "LOCAL_BIN", local_bin)
19
+ monkeypatch.setattr(sys, "prefix", str(tmp_path / "venv"))
20
+ monkeypatch.setattr(service, "_is_pipx_install", lambda: False)
21
+ monkeypatch.setattr(service, "_local_bin_on_path", lambda: True)
22
+
23
+ assert service.install_symlinks() == service.SCRIPT_NAMES
24
+ assert Path(local_bin / "voiceio").resolve() == (venv_bin / "voiceio").resolve()
25
+ assert not (local_bin / "voiceio-doctor").is_symlink()
26
+
27
+
28
+ def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
29
+ calls = []
30
+ monkeypatch.setattr(service, "has_systemd", lambda: True)
31
+ monkeypatch.setattr(service, "_systemctl", lambda *a: calls.append(a) or True)
32
+ monkeypatch.setattr(service, "_find_voiceio_bin", lambda: "/opt/v/bin/voiceio")
33
+ monkeypatch.setattr(service, "SERVICE_DIR", tmp_path)
34
+ monkeypatch.setattr(service, "LEARN_SERVICE_PATH", tmp_path / service.LEARN_SERVICE_NAME)
35
+ monkeypatch.setattr(service, "LEARN_TIMER_PATH", tmp_path / service.LEARN_TIMER_NAME)
36
+
37
+ assert service.install_learn_timer()
38
+ unit = service.LEARN_SERVICE_PATH.read_text()
39
+ assert "ExecStart=/opt/v/bin/voiceio learn maintain" in unit
40
+ assert "CPUWeight=idle" in unit and "VOICEIO_IDLE_SCOPE=1" in unit
41
+ assert "Persistent=true" in service.LEARN_TIMER_PATH.read_text()
42
+ assert ("enable", "--now", service.LEARN_TIMER_NAME) in calls
43
+
44
+ service.uninstall_learn_timer()
45
+ assert not service.LEARN_TIMER_PATH.exists() and not service.LEARN_SERVICE_PATH.exists()
@@ -254,3 +254,14 @@ def test_write_config_with_api_key_records_consent_and_is_private(tmp_path, monk
254
254
  configfile._record_cloud_consent()
255
255
  assert consent.has_cloud_consent()
256
256
  assert stat.S_IMODE(config.CONFIG_PATH.stat().st_mode) == 0o600
257
+
258
+
259
+ def test_retention_is_written_only_when_asked(cfg_paths):
260
+ from voiceio import config
261
+ kw = dict(model="small", language="en", hotkey="ctrl+alt+v", method="auto",
262
+ streaming=True, backend="auto", quiet=True)
263
+ configfile._write_config(**kw)
264
+ assert "retain_audio" not in configfile._load_raw_config().get("data", {})
265
+ assert config.load(cfg_paths).data.retain_audio is False # off unless chosen
266
+ configfile._write_config(**kw, retain_audio=True)
267
+ assert config.load(cfg_paths).data.retain_audio is True