python-voiceio 1.1.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 (196) hide show
  1. {python_voiceio-1.1.0/python_voiceio.egg-info → python_voiceio-1.2.0}/PKG-INFO +3 -3
  2. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/README.md +2 -2
  3. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/pyproject.toml +1 -1
  4. {python_voiceio-1.1.0 → python_voiceio-1.2.0/python_voiceio.egg-info}/PKG-INFO +3 -3
  5. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_health.py +18 -0
  6. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_pipeline.py +16 -0
  7. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_service.py +17 -0
  8. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_setup.py +15 -0
  9. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_transcriber.py +11 -0
  10. python_voiceio-1.2.0/voiceio/__init__.py +1 -0
  11. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/app.py +15 -0
  12. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/learn.py +16 -2
  13. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/servicecmd.py +26 -0
  14. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/config.py +6 -0
  15. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/faster_whisper/__init__.py +3 -1
  16. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/health.py +31 -1
  17. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/train.py +10 -6
  18. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/steps.py +20 -0
  19. python_voiceio-1.1.0/voiceio/__init__.py +0 -1
  20. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/LICENSE +0 -0
  21. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/SOURCES.txt +0 -0
  22. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
  23. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/entry_points.txt +0 -0
  24. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/requires.txt +0 -0
  25. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/python_voiceio.egg-info/top_level.txt +0 -0
  26. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/setup.cfg +0 -0
  27. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_app_wiring.py +0 -0
  28. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_audio_quality.py +0 -0
  29. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_audio_source.py +0 -0
  30. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_backend_probes.py +0 -0
  31. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_cli.py +0 -0
  32. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_clipboard_read.py +0 -0
  33. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_commands.py +0 -0
  34. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_concurrency_lockdown.py +0 -0
  35. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_config.py +0 -0
  36. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_corrections.py +0 -0
  37. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_decoders.py +0 -0
  38. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_engine.py +0 -0
  39. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_evdev_uaccess.py +0 -0
  40. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_fallback.py +0 -0
  41. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_fcitx_bridge.py +0 -0
  42. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_hints.py +0 -0
  43. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_history.py +0 -0
  44. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_engine_limits.py +0 -0
  45. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_engine_listener.py +0 -0
  46. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_pending.py +0 -0
  47. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_pids.py +0 -0
  48. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_ping.py +0 -0
  49. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_session.py +0 -0
  50. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_textlimit.py +0 -0
  51. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_ibus_typer.py +0 -0
  52. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_clean.py +0 -0
  53. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_import.py +0 -0
  54. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_metrics.py +0 -0
  55. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_review.py +0 -0
  56. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_learn_targets.py +0 -0
  57. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_llm.py +0 -0
  58. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_llm_api.py +0 -0
  59. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_numbers.py +0 -0
  60. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_pipeline.py +0 -0
  61. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_platform.py +0 -0
  62. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_postcorrect.py +0 -0
  63. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_postprocess.py +0 -0
  64. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_prebuffer.py +0 -0
  65. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_prompt.py +0 -0
  66. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_recorder_integration.py +0 -0
  67. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_retention.py +0 -0
  68. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_robustness.py +0 -0
  69. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_security_hardening.py +0 -0
  70. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_server.py +0 -0
  71. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_streaming.py +0 -0
  72. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_suggest.py +0 -0
  73. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_tokens.py +0 -0
  74. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_tts.py +0 -0
  75. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_typer_manager.py +0 -0
  76. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_vad.py +0 -0
  77. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_vocabulary.py +0 -0
  78. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_wordfreq.py +0 -0
  79. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/tests/test_worker_decode.py +0 -0
  80. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/__main__.py +0 -0
  81. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/audio_source.py +0 -0
  82. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/backends.py +0 -0
  83. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/__init__.py +0 -0
  84. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/configcmd.py +0 -0
  85. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/corrections.py +0 -0
  86. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/doctor.py +0 -0
  87. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/history.py +0 -0
  88. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/models.py +0 -0
  89. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/uninstall.py +0 -0
  90. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/cli/vocab.py +0 -0
  91. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/clipboard_read.py +0 -0
  92. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/commands.py +0 -0
  93. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/consent.py +0 -0
  94. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/__init__.py +0 -0
  95. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/bias.py +0 -0
  96. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/__init__.py +0 -0
  97. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/base.py +0 -0
  98. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/catalog.py +0 -0
  99. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
  100. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/lanes.py +0 -0
  101. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/sherpa.py +0 -0
  102. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/decoders/whispercpp.py +0 -0
  103. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/engine.py +0 -0
  104. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/core/pipeline.py +0 -0
  105. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/corrections.py +0 -0
  106. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  107. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/data/__init__.py +0 -0
  108. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/demo.py +0 -0
  109. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/display.py +0 -0
  110. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/feedback.py +0 -0
  111. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hints.py +0 -0
  112. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/history.py +0 -0
  113. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/__init__.py +0 -0
  114. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/base.py +0 -0
  115. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/chain.py +0 -0
  116. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/evdev.py +0 -0
  117. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/pynput_backend.py +0 -0
  118. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/hotkeys/socket_backend.py +0 -0
  119. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/__init__.py +0 -0
  120. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/engine.py +0 -0
  121. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/install.py +0 -0
  122. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/pending.py +0 -0
  123. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/session.py +0 -0
  124. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ibus/textlimit.py +0 -0
  125. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/__init__.py +0 -0
  126. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/augment.py +0 -0
  127. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/clean.py +0 -0
  128. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/evaluate.py +0 -0
  129. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/importer.py +0 -0
  130. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/metrics.py +0 -0
  131. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/review.py +0 -0
  132. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/store.py +0 -0
  133. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/learn/teacher.py +0 -0
  134. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/llm.py +0 -0
  135. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/llm_api.py +0 -0
  136. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/models/__init__.py +0 -0
  137. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/models/silero_vad.onnx +0 -0
  138. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/numbers.py +0 -0
  139. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/pidlock.py +0 -0
  140. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/platform.py +0 -0
  141. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/postcorrect.py +0 -0
  142. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/postprocess.py +0 -0
  143. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/privacy.py +0 -0
  144. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/prompt.py +0 -0
  145. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/recorder.py +0 -0
  146. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/retention.py +0 -0
  147. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/server/__init__.py +0 -0
  148. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/server/__main__.py +0 -0
  149. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/server/app.py +0 -0
  150. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/server/config.py +0 -0
  151. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/server/runtime.py +0 -0
  152. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/service.py +0 -0
  153. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/__init__.py +0 -0
  154. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/configfile.py +0 -0
  155. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/extras.py +0 -0
  156. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/hotkey.py +0 -0
  157. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/noninteractive.py +0 -0
  158. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/speech.py +0 -0
  159. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/system.py +0 -0
  160. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/setup/ui.py +0 -0
  161. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/sounds/__init__.py +0 -0
  162. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/sounds/commit.wav +0 -0
  163. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/sounds/start.wav +0 -0
  164. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/sounds/stop.wav +0 -0
  165. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/streaming.py +0 -0
  166. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/suggest.py +0 -0
  167. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tokens.py +0 -0
  168. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tray/__init__.py +0 -0
  169. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tray/_icons.py +0 -0
  170. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tray/_indicator.py +0 -0
  171. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tray/_pystray.py +0 -0
  172. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/__init__.py +0 -0
  173. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/base.py +0 -0
  174. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/chain.py +0 -0
  175. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/edge_engine.py +0 -0
  176. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/espeak.py +0 -0
  177. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/piper_engine.py +0 -0
  178. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/tts/player.py +0 -0
  179. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/__init__.py +0 -0
  180. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/base.py +0 -0
  181. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/chain.py +0 -0
  182. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/clipboard.py +0 -0
  183. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/collecting.py +0 -0
  184. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/fcitx_bridge.py +0 -0
  185. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/ibus.py +0 -0
  186. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/manager.py +0 -0
  187. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/pynput_type.py +0 -0
  188. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/wtype.py +0 -0
  189. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/xdotool.py +0 -0
  190. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/typers/ydotool.py +0 -0
  191. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/ui.py +0 -0
  192. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/vad.py +0 -0
  193. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/vocab_stats.py +0 -0
  194. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/vocabulary.py +0 -0
  195. {python_voiceio-1.1.0 → python_voiceio-1.2.0}/voiceio/watchdog.py +0 -0
  196. {python_voiceio-1.1.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.1.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
@@ -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,7 +300,7 @@ 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.
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
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.
@@ -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,7 +247,7 @@ 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.
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
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.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "1.1.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.1.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
@@ -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,7 +300,7 @@ 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.
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
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.
@@ -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")
@@ -43,3 +43,20 @@ def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
43
43
 
44
44
  service.uninstall_learn_timer()
45
45
  assert not service.LEARN_TIMER_PATH.exists() and not service.LEARN_SERVICE_PATH.exists()
46
+
47
+
48
+ def test_daemon_publishes_state_and_restart_waits_for_idle(monkeypatch):
49
+ import threading
50
+
51
+ from voiceio import config
52
+ from voiceio.app import VoiceIO, _State
53
+ from voiceio.cli.servicecmd import wait_until_idle
54
+
55
+ assert wait_until_idle(timeout=1) # not running: no wait
56
+ vio = VoiceIO.__new__(VoiceIO)
57
+ vio._state = _State.RECORDING
58
+ assert config.STATE_PATH.read_text() == "recording"
59
+ threading.Timer(0.3, lambda: setattr(vio, "_state", _State.IDLE)).start()
60
+ assert wait_until_idle(timeout=5, poll=0.05) # waited for the dictation
61
+ vio._state = _State.FINALIZING
62
+ assert not wait_until_idle(timeout=0.2, poll=0.05) # gives up, says so
@@ -265,3 +265,18 @@ def test_retention_is_written_only_when_asked(cfg_paths):
265
265
  assert config.load(cfg_paths).data.retain_audio is False # off unless chosen
266
266
  configfile._write_config(**kw, retain_audio=True)
267
267
  assert config.load(cfg_paths).data.retain_audio is True
268
+
269
+
270
+ def test_setup_offers_nightly_learning_once(monkeypatch):
271
+ from voiceio import service
272
+ from voiceio.setup import steps
273
+ installed = []
274
+ monkeypatch.setattr(service, "has_systemd", lambda: True)
275
+ monkeypatch.setattr(service, "learn_timer_enabled", lambda: bool(installed))
276
+ monkeypatch.setattr(service, "install_learn_timer", lambda: installed.append(1) or True)
277
+ asked = []
278
+ monkeypatch.setattr(steps, "_ask_yn", lambda q, default=True: asked.append(q) or True)
279
+ state = steps.SetupState(checks={"is_linux": True})
280
+ steps._offer_nightly_learning(state)
281
+ steps._offer_nightly_learning(state) # already on: not asked again
282
+ assert installed == [1] and len(asked) == 1
@@ -313,3 +313,14 @@ def test_no_log_call_formats_the_transcript_itself():
313
313
  and any(leaks(a) for a in node.args[1:])):
314
314
  hits.append(f"{path.name}:{node.lineno}")
315
315
  assert not hits, hits
316
+
317
+
318
+ def test_non_default_beam_is_scored_under_its_own_name():
319
+ """An eval at beam 1 must not stand in for the model's default score."""
320
+ mock_proc = MagicMock()
321
+ mock_proc.stdout.readline.return_value = "READY\n"
322
+ mock_proc.poll.return_value = None
323
+ with patch("voiceio.core.decoders.faster_whisper.subprocess.Popen", return_value=mock_proc):
324
+ from voiceio.core.decoders.faster_whisper import Transcriber
325
+ assert Transcriber(ModelConfig()).name == "faster-whisper:small"
326
+ assert Transcriber(ModelConfig(beam_size=1)).name == "faster-whisper:small@beam1"
@@ -0,0 +1 @@
1
+ __version__ = "1.2.0"
@@ -36,6 +36,20 @@ class _State(enum.Enum):
36
36
 
37
37
 
38
38
  class VoiceIO:
39
+ @property
40
+ def _state(self) -> _State:
41
+ return self.__state
42
+
43
+ @_state.setter
44
+ def _state(self, value: _State) -> None:
45
+ self.__state = value
46
+ try: # best-effort: only `service restart` reads it
47
+ tmp = config.STATE_PATH.with_suffix(".tmp")
48
+ tmp.write_text(value.value)
49
+ tmp.replace(config.STATE_PATH)
50
+ except OSError:
51
+ pass
52
+
39
53
  def __init__(self, cfg: config.Config):
40
54
  self.cfg = cfg
41
55
 
@@ -713,4 +727,5 @@ class VoiceIO:
713
727
  self._ibus.shutdown()
714
728
  self._pid_fd.close()
715
729
  PID_PATH.unlink(missing_ok=True)
730
+ config.STATE_PATH.unlink(missing_ok=True)
716
731
  log.info("voiceio stopped")
@@ -63,6 +63,8 @@ def register(sub: argparse._SubParsersAction) -> None:
63
63
  e.add_argument("--pipeline", action="store_true",
64
64
  help="no decoding: score what each text stage (corrections, numbers, "
65
65
  "cloud postcorrect…) did to past dictations")
66
+ e.add_argument("--beam", type=int, default=None,
67
+ help="beam width of the passes you keep (default: [model] beam_size)")
66
68
  e.add_argument("--speed", type=float, default=1.0,
67
69
  help="streaming replay speed; >1 is faster but skews latency")
68
70
  e.set_defaults(func=cmd_eval)
@@ -334,12 +336,12 @@ def cmd_eval(args: argparse.Namespace) -> None:
334
336
  if ds is None or not ds.exists():
335
337
  raise SystemExit("no dataset — run `voiceio learn build` first")
336
338
  _run_eval(args.model, ds, limit=args.limit, streaming=args.streaming, lanes=args.lanes,
337
- freeze=args.freeze, speed=args.speed)
339
+ freeze=args.freeze, speed=args.speed, beam=args.beam)
338
340
 
339
341
 
340
342
  def _run_eval(model: str | None, ds: Path, *, limit: int | None = None,
341
343
  streaming: bool = False, lanes: int | None = None,
342
- freeze: float | None = None, speed: float = 1.0):
344
+ freeze: float | None = None, speed: float = 1.0, beam: int | None = None):
343
345
  """Score `model` (default: the configured one) on `ds`; print and save."""
344
346
  from voiceio.core.bias import BiasProvider
345
347
  from voiceio.core.decoders import build_decoder
@@ -348,6 +350,8 @@ def _run_eval(model: str | None, ds: Path, *, limit: int | None = None,
348
350
  cfg = _cfg()
349
351
  if model:
350
352
  cfg.model.name = model
353
+ if beam:
354
+ cfg.model.beam_size = beam
351
355
  print(f"Loading {cfg.model.name}…", flush=True)
352
356
  # Offline scoring needs one lane; streaming mirrors the live setup.
353
357
  decoder = build_decoder(cfg.model, lanes=lanes if lanes is not None else
@@ -428,6 +432,12 @@ def cmd_maintain(args: argparse.Namespace) -> None:
428
432
  _yield_cpu()
429
433
  from voiceio.learn import evaluate
430
434
  cmd_label(argparse.Namespace(teacher=None, limit=None, relabel=False))
435
+ from voiceio.learn import train
436
+ if missing := train.missing_train_deps():
437
+ # Labeling alone is worth it; retraining waits for the extra. Exit 0:
438
+ # a nightly timer must not fail every night over an optional install.
439
+ print(f"labeled; fine-tuning needs {', '.join(missing)}: {train.TRAIN_HINT}")
440
+ return
431
441
  labels = store.load_labels()
432
442
  labeled = {c.audio for c in store.clips() if c.audio in labels}
433
443
  ds = store.latest_dataset()
@@ -457,6 +467,10 @@ def cmd_schedule(args: argparse.Namespace) -> None:
457
467
  if not config.load().data.retain_audio:
458
468
  print("note: [data] retain_audio is off, so there is nothing new to learn from;\n"
459
469
  " turn it on with: voiceio config set data.retain_audio true")
470
+ from voiceio.learn import train
471
+ if missing := train.missing_train_deps():
472
+ print(f"note: until {', '.join(missing)} are installed it only labels new clips;\n"
473
+ f" to also retrain: {train.TRAIN_HINT}")
460
474
  if not service.install_learn_timer():
461
475
  raise SystemExit("could not enable the timer (needs systemd --user)")
462
476
  print("on: every night ~03:30, `voiceio learn maintain` labels new dictation at idle "
@@ -10,6 +10,8 @@ def register(sub: argparse._SubParsersAction) -> None:
10
10
  p.add_argument("action", nargs="?", default="status",
11
11
  choices=["install", "uninstall", "start", "stop", "restart",
12
12
  "status", "logs"])
13
+ p.add_argument("--now", action="store_true",
14
+ help="stop/restart: don't wait for a dictation in progress to finish")
13
15
  p.add_argument("-f", "--follow", action="store_true", help="logs: follow")
14
16
  p.add_argument("-n", "--lines", type=int, default=50, help="logs: line count")
15
17
  p.set_defaults(func=_cmd_service)
@@ -61,6 +63,8 @@ def _cmd_service(args: argparse.Namespace) -> None:
61
63
  sys.exit(1)
62
64
 
63
65
  elif action in ("stop", "restart"):
66
+ if not getattr(args, "now", False):
67
+ wait_until_idle()
64
68
  try:
65
69
  subprocess.run(["systemctl", "--user", action, "voiceio.service"],
66
70
  capture_output=True, timeout=10, check=True)
@@ -79,3 +83,25 @@ def _cmd_service(args: argparse.Namespace) -> None:
79
83
  subprocess.call(cmd)
80
84
  except KeyboardInterrupt:
81
85
  pass
86
+
87
+
88
+ def wait_until_idle(timeout: float = 300.0, poll: float = 0.25) -> bool:
89
+ """Wait for the daemon to finish any dictation in progress (stopping it
90
+ mid-recording loses what was said). True once idle or not running."""
91
+ import time
92
+
93
+ from voiceio.config import STATE_PATH
94
+ deadline, told = time.monotonic() + timeout, False
95
+ while time.monotonic() < deadline:
96
+ try:
97
+ state = STATE_PATH.read_text().strip()
98
+ except OSError:
99
+ return True # not running (or pre-1.1): nothing to wait for
100
+ if state in ("idle", "error"):
101
+ return True
102
+ if not told:
103
+ print(f"Waiting for the dictation in progress to finish ({state})…", flush=True)
104
+ told = True
105
+ time.sleep(poll)
106
+ print("Still busy; going ahead. (--now skips the wait.)", file=sys.stderr)
107
+ return False
@@ -34,6 +34,9 @@ HISTORY_PATH = LOG_DIR / "history.jsonl"
34
34
  RECORDINGS_DIR = LOG_DIR / "recordings"
35
35
  LOG_PATH = LOG_DIR / "voiceio.log"
36
36
  PID_PATH = LOG_DIR / "voiceio.pid"
37
+ # The daemon's state (idle/recording/finalizing), so `voiceio service
38
+ # restart` can wait for a dictation to finish instead of killing it.
39
+ STATE_PATH = Path(os.environ.get("XDG_RUNTIME_DIR", "/tmp")) / "voiceio.state"
37
40
  # Intermediate pipeline data kept for debugging/profiling and future training:
38
41
  # per-pass streaming decode traces and postcorrect before/after pairs. These
39
42
  # live in their own JSONL files (not the rotating log) so they survive.
@@ -76,6 +79,9 @@ class ModelConfig:
76
79
  # "modified_beam_search" (required for hotwords).
77
80
  decoding: str = ""
78
81
  vocabulary_file: str = "" # path to vocabulary.txt
82
+ # Beam width of the passes whose text you keep (freezes and the final
83
+ # tail); interim previews are always greedy. 1 is greedy: faster stop.
84
+ beam_size: int = 5
79
85
  # Other languages you dictate in: a correction rule that would rewrite a
80
86
  # real word in one of these is flagged (e.g. ["es"] protects "harina").
81
87
  protect_languages: list[str] = field(default_factory=list)
@@ -93,6 +93,8 @@ class Transcriber:
93
93
  def __init__(self, cfg: ModelConfig):
94
94
  self._cfg = cfg
95
95
  self.name = f"faster-whisper:{cfg.name}"
96
+ if cfg.beam_size != 5: # scored separately from the default
97
+ self.name += f"@beam{cfg.beam_size}"
96
98
  if _PROMPTLESS.search(cfg.name):
97
99
  self.capabilities = Capabilities(bias=BiasMode.NONE)
98
100
  self._proc: subprocess.Popen | None = None
@@ -205,7 +207,7 @@ class Transcriber:
205
207
 
206
208
  audio = normalize_audio(audio)
207
209
  audio_b64 = base64.b64encode(audio.tobytes()).decode("ascii")
208
- req = {"audio_b64": audio_b64, "options": {"beam_size": 5 if final else 1}}
210
+ req = {"audio_b64": audio_b64, "options": {"beam_size": self._cfg.beam_size if final else 1}}
209
211
  # Cap the combined prompt. initial_prompt (recent transcripts) and
210
212
  # `context` (streaming's frozen-text tail) are both bounded in CHARS
211
213
  # by their callers, but nothing bounded their SUM in tokens — and
@@ -217,6 +217,8 @@ def _check_features() -> FeatureGroups:
217
217
  else:
218
218
  groups.accuracy["Vocabulary"] = "none"
219
219
 
220
+ groups.accuracy.update(_learning_status(cfg))
221
+
220
222
  if cfg.postcorrect.enabled:
221
223
  model = cfg.postcorrect.model or cfg.cloud.model
222
224
  groups.accuracy["Cloud postcorrect"] = f"on ({model}), adds up to {cfg.postcorrect.timeout_secs:g}s"
@@ -225,6 +227,30 @@ def _check_features() -> FeatureGroups:
225
227
  return groups
226
228
 
227
229
 
230
+ def _learning_status(cfg) -> dict[str, str]:
231
+ """Whether voiceio is learning your voice: the model in use, whether
232
+ recordings are kept to learn from, and whether it retrains nightly."""
233
+ from pathlib import Path
234
+
235
+ from voiceio import config, service
236
+ from voiceio.learn.store import learn_dir
237
+ out: dict[str, str] = {}
238
+ models = learn_dir() / "models"
239
+ name = cfg.model.name
240
+ if models in Path(name).expanduser().parents:
241
+ out["Voice model"] = f"fine-tuned on you ({Path(name).parent.name})"
242
+ else:
243
+ out["Voice model"] = f"stock ({name})"
244
+ if cfg.data.retain_audio:
245
+ n = sum(1 for _ in config.RECORDINGS_DIR.glob("*.wav")) \
246
+ if config.RECORDINGS_DIR.exists() else 0
247
+ out["Recordings"] = f"on ({n} kept to learn from)"
248
+ else:
249
+ out["Recordings"] = "off (nothing to learn from)"
250
+ out["Nightly learning"] = "on" if service.learn_timer_enabled() else "off"
251
+ return out
252
+
253
+
228
254
  def _check_ibus() -> IBusStatus:
229
255
  """Run detailed IBus health checks."""
230
256
  from pathlib import Path
@@ -325,6 +351,10 @@ def _accuracy_tips(accuracy: dict[str, str]) -> list[str]:
325
351
  tips.append("voiceio corrections add WRONG RIGHT — fix a word that keeps being misheard")
326
352
  if vocab == "none":
327
353
  tips.append("Add names/jargon to ~/.config/voiceio/vocabulary.txt")
354
+ if accuracy.get("Recordings", "").startswith("off"):
355
+ tips.append("voiceio config set data.retain_audio true — keep recordings so voiceio can learn your voice")
356
+ elif accuracy.get("Nightly learning") == "off":
357
+ tips.append("voiceio learn schedule on — retrain on your voice every night")
328
358
  return tips
329
359
 
330
360
 
@@ -391,7 +421,7 @@ def format_report(report: HealthReport) -> str:
391
421
  # Accuracy / learning stack
392
422
  if report.features.accuracy:
393
423
  lines.append(_section("Accuracy"))
394
- lines.append(f" {DIM}Learns from your dictation history to fix recurring mistakes.{RESET}")
424
+ lines.append(f" {DIM}What voiceio knows about your words, and whether it keeps learning.{RESET}")
395
425
  for name, status in report.features.accuracy.items():
396
426
  lines.append(_feature_line(name, status))
397
427
  # Show actionable tips at the bottom
@@ -76,13 +76,17 @@ _DECODER_TARGETS = (r".*decoder\.layers\.\d+\.(self_attn\.(q_proj|k_proj|v_proj|
76
76
  r"|encoder_attn\.(q_proj|out_proj)|fc1|fc2)")
77
77
 
78
78
 
79
+ TRAIN_HINT = "pip install 'python-voiceio[train]'"
80
+
81
+
82
+ def missing_train_deps() -> list[str]:
83
+ """Training packages that aren't installed (the `train` extra)."""
84
+ import importlib.util
85
+ return [m for m in ("torch", "transformers", "peft") if importlib.util.find_spec(m) is None]
86
+
87
+
79
88
  def require_train_deps() -> None:
80
- missing = []
81
- for mod in ("torch", "transformers", "peft"):
82
- try:
83
- __import__(mod)
84
- except ImportError:
85
- missing.append(mod)
89
+ missing = missing_train_deps()
86
90
  if missing:
87
91
  raise RuntimeError(
88
92
  f"training needs {', '.join(missing)}: pip install 'python-voiceio[train]'")
@@ -504,6 +504,8 @@ def _step_finalize(state: SetupState) -> None:
504
504
  state.retain_audio = _ask_yn("Keep recordings so voiceio can learn your voice?",
505
505
  default=state.cur.data.retain_audio if state.cur else True)
506
506
  _write_config(**state.config_kwargs())
507
+ if state.retain_audio:
508
+ _offer_nightly_learning(state)
507
509
  _setup_desktop_shortcut(state)
508
510
  _install_autostart(state)
509
511
  _verify_hotkey(state)
@@ -511,6 +513,24 @@ def _step_finalize(state: SetupState) -> None:
511
513
  _start_service()
512
514
 
513
515
 
516
+ def _offer_nightly_learning(state: SetupState) -> None:
517
+ """The recordings are only worth keeping if something learns from them."""
518
+ from voiceio import service
519
+ if not state.checks["is_linux"] or not service.has_systemd() or service.learn_timer_enabled():
520
+ return
521
+ print(f" {DIM}Every night at idle priority: transcribe new recordings with a bigger model,")
522
+ print(f" retrain once enough are new, and tell you if the result is better.{RESET}")
523
+ if not _ask_yn("Learn from them every night?", default=True):
524
+ return
525
+ if service.install_learn_timer():
526
+ print(f" {GREEN}✓{RESET} {DIM}Nightly learning on (voiceio learn schedule off to stop){RESET}")
527
+ from voiceio.learn import train
528
+ if missing := train.missing_train_deps():
529
+ print(f" {DIM}Retraining also needs {', '.join(missing)}: {train.TRAIN_HINT}{RESET}")
530
+ else:
531
+ print(f" {YELLOW}⚠{RESET} {DIM}Could not enable it; try: voiceio learn schedule on{RESET}")
532
+
533
+
514
534
  def _setup_desktop_shortcut(state: SetupState) -> None:
515
535
  """Set up DE shortcut if on GNOME + socket backend (Linux only)."""
516
536
  if not state.checks["is_linux"]:
@@ -1 +0,0 @@
1
- __version__ = "1.1.0"
File without changes
File without changes