python-voiceio 1.3.0__tar.gz → 1.5.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 (198) hide show
  1. {python_voiceio-1.3.0/python_voiceio.egg-info → python_voiceio-1.5.0}/PKG-INFO +19 -7
  2. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/README.md +17 -5
  3. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/pyproject.toml +2 -2
  4. {python_voiceio-1.3.0 → python_voiceio-1.5.0/python_voiceio.egg-info}/PKG-INFO +19 -7
  5. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/python_voiceio.egg-info/SOURCES.txt +2 -0
  6. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_health.py +1 -1
  7. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_pipeline.py +119 -0
  8. python_voiceio-1.5.0/tests/test_learn_quiet.py +102 -0
  9. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_service.py +25 -2
  10. python_voiceio-1.5.0/voiceio/__init__.py +1 -0
  11. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/learn.py +136 -16
  12. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/config.py +27 -0
  13. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/feedback.py +34 -0
  14. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/health.py +2 -3
  15. python_voiceio-1.5.0/voiceio/learn/quiet.py +54 -0
  16. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/train.py +11 -1
  17. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/service.py +49 -10
  18. python_voiceio-1.3.0/voiceio/__init__.py +0 -1
  19. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/LICENSE +0 -0
  20. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
  21. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/python_voiceio.egg-info/entry_points.txt +0 -0
  22. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/python_voiceio.egg-info/requires.txt +0 -0
  23. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/python_voiceio.egg-info/top_level.txt +0 -0
  24. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/setup.cfg +0 -0
  25. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_app_wiring.py +0 -0
  26. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_audio_quality.py +0 -0
  27. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_audio_source.py +0 -0
  28. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_backend_probes.py +0 -0
  29. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_cli.py +0 -0
  30. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_clipboard_read.py +0 -0
  31. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_commands.py +0 -0
  32. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_concurrency_lockdown.py +0 -0
  33. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_config.py +0 -0
  34. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_corrections.py +0 -0
  35. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_decoders.py +0 -0
  36. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_engine.py +0 -0
  37. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_evdev_uaccess.py +0 -0
  38. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_fallback.py +0 -0
  39. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_fcitx_bridge.py +0 -0
  40. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_hints.py +0 -0
  41. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_history.py +0 -0
  42. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_engine_limits.py +0 -0
  43. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_engine_listener.py +0 -0
  44. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_pending.py +0 -0
  45. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_pids.py +0 -0
  46. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_ping.py +0 -0
  47. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_session.py +0 -0
  48. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_textlimit.py +0 -0
  49. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_ibus_typer.py +0 -0
  50. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_clean.py +0 -0
  51. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_import.py +0 -0
  52. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_metrics.py +0 -0
  53. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_review.py +0 -0
  54. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_learn_targets.py +0 -0
  55. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_llm.py +0 -0
  56. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_llm_api.py +0 -0
  57. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_numbers.py +0 -0
  58. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_pipeline.py +0 -0
  59. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_platform.py +0 -0
  60. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_postcorrect.py +0 -0
  61. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_postprocess.py +0 -0
  62. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_prebuffer.py +0 -0
  63. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_prompt.py +0 -0
  64. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_recorder_integration.py +0 -0
  65. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_retention.py +0 -0
  66. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_robustness.py +0 -0
  67. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_security_hardening.py +0 -0
  68. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_server.py +0 -0
  69. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_setup.py +0 -0
  70. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_streaming.py +0 -0
  71. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_suggest.py +0 -0
  72. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_tokens.py +0 -0
  73. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_transcriber.py +0 -0
  74. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_tts.py +0 -0
  75. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_typer_manager.py +0 -0
  76. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_vad.py +0 -0
  77. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_vocabulary.py +0 -0
  78. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_wordfreq.py +0 -0
  79. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/tests/test_worker_decode.py +0 -0
  80. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/__main__.py +0 -0
  81. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/app.py +0 -0
  82. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/audio_source.py +0 -0
  83. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/backends.py +0 -0
  84. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/__init__.py +0 -0
  85. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/configcmd.py +0 -0
  86. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/corrections.py +0 -0
  87. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/doctor.py +0 -0
  88. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/history.py +0 -0
  89. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/models.py +0 -0
  90. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/servicecmd.py +0 -0
  91. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/uninstall.py +0 -0
  92. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/cli/vocab.py +0 -0
  93. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/clipboard_read.py +0 -0
  94. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/commands.py +0 -0
  95. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/consent.py +0 -0
  96. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/__init__.py +0 -0
  97. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/bias.py +0 -0
  98. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/__init__.py +0 -0
  99. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/base.py +0 -0
  100. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/catalog.py +0 -0
  101. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
  102. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
  103. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/lanes.py +0 -0
  104. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/sherpa.py +0 -0
  105. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/decoders/whispercpp.py +0 -0
  106. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/engine.py +0 -0
  107. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/core/pipeline.py +0 -0
  108. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/corrections.py +0 -0
  109. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  110. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/data/__init__.py +0 -0
  111. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/demo.py +0 -0
  112. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/display.py +0 -0
  113. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hints.py +0 -0
  114. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/history.py +0 -0
  115. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/__init__.py +0 -0
  116. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/base.py +0 -0
  117. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/chain.py +0 -0
  118. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/evdev.py +0 -0
  119. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/pynput_backend.py +0 -0
  120. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/hotkeys/socket_backend.py +0 -0
  121. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/__init__.py +0 -0
  122. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/engine.py +0 -0
  123. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/install.py +0 -0
  124. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/pending.py +0 -0
  125. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/session.py +0 -0
  126. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ibus/textlimit.py +0 -0
  127. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/__init__.py +0 -0
  128. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/augment.py +0 -0
  129. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/clean.py +0 -0
  130. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/evaluate.py +0 -0
  131. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/importer.py +0 -0
  132. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/metrics.py +0 -0
  133. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/review.py +0 -0
  134. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/store.py +0 -0
  135. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/learn/teacher.py +0 -0
  136. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/llm.py +0 -0
  137. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/llm_api.py +0 -0
  138. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/models/__init__.py +0 -0
  139. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/models/silero_vad.onnx +0 -0
  140. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/numbers.py +0 -0
  141. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/pidlock.py +0 -0
  142. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/platform.py +0 -0
  143. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/postcorrect.py +0 -0
  144. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/postprocess.py +0 -0
  145. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/privacy.py +0 -0
  146. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/prompt.py +0 -0
  147. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/recorder.py +0 -0
  148. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/retention.py +0 -0
  149. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/server/__init__.py +0 -0
  150. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/server/__main__.py +0 -0
  151. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/server/app.py +0 -0
  152. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/server/config.py +0 -0
  153. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/server/runtime.py +0 -0
  154. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/__init__.py +0 -0
  155. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/configfile.py +0 -0
  156. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/extras.py +0 -0
  157. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/hotkey.py +0 -0
  158. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/noninteractive.py +0 -0
  159. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/speech.py +0 -0
  160. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/steps.py +0 -0
  161. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/system.py +0 -0
  162. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/setup/ui.py +0 -0
  163. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/sounds/__init__.py +0 -0
  164. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/sounds/commit.wav +0 -0
  165. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/sounds/start.wav +0 -0
  166. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/sounds/stop.wav +0 -0
  167. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/streaming.py +0 -0
  168. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/suggest.py +0 -0
  169. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tokens.py +0 -0
  170. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tray/__init__.py +0 -0
  171. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tray/_icons.py +0 -0
  172. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tray/_indicator.py +0 -0
  173. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tray/_pystray.py +0 -0
  174. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/__init__.py +0 -0
  175. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/base.py +0 -0
  176. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/chain.py +0 -0
  177. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/edge_engine.py +0 -0
  178. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/espeak.py +0 -0
  179. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/piper_engine.py +0 -0
  180. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/tts/player.py +0 -0
  181. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/__init__.py +0 -0
  182. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/base.py +0 -0
  183. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/chain.py +0 -0
  184. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/clipboard.py +0 -0
  185. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/collecting.py +0 -0
  186. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/fcitx_bridge.py +0 -0
  187. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/ibus.py +0 -0
  188. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/manager.py +0 -0
  189. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/pynput_type.py +0 -0
  190. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/wtype.py +0 -0
  191. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/xdotool.py +0 -0
  192. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/typers/ydotool.py +0 -0
  193. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/ui.py +0 -0
  194. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/vad.py +0 -0
  195. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/vocab_stats.py +0 -0
  196. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/vocabulary.py +0 -0
  197. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/watchdog.py +0 -0
  198. {python_voiceio-1.3.0 → python_voiceio-1.5.0}/voiceio/wordfreq.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/Hugo0/voiceio
7
+ Project-URL: Homepage, https://voiceio.dev/
8
8
  Project-URL: Repository, https://github.com/Hugo0/voiceio
9
9
  Project-URL: Issues, https://github.com/Hugo0/voiceio/issues
10
10
  Project-URL: Changelog, https://github.com/Hugo0/voiceio/releases
@@ -58,7 +58,7 @@ Dynamic: license-file
58
58
  [![Python](https://img.shields.io/pypi/pyversions/python-voiceio)](https://pypi.org/project/python-voiceio/)
59
59
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
60
60
 
61
- **Voice dictation that learns you.** Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
61
+ **Voice dictation that learns you.** ([website](https://voiceio.dev/)) Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
62
62
 
63
63
  Off-the-shelf speech models stumble on exactly the words you say most: your colleagues' names, your project, your jargon. voiceio keeps (with your permission) the audio of what you dictate, has a bigger model transcribe it overnight, and trains a personal model from it — nothing uploaded, no GPU required. On the author's dictation, one overnight run on a laptop CPU cut word errors from **12.1% to 5.0%** on held-out clips and recognized more of the author's vocabulary, at the same speed. Every model is scored on clips it never trained on, and it only replaces the current one when it wins.
64
64
 
@@ -190,11 +190,19 @@ voiceio learn eval # score the current model on your held-out clips: WER a
190
190
  # --streaming replays them through live dictation and times stop→text
191
191
  voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
192
192
  voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
193
- voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
194
- voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
195
- # (--auto-promote: switch to a winner by itself; `learn rollback` undoes it)
193
+ voiceio learn maintain # later: label what's new, and once 50+ clips piled up ([learn] min_new), retrain and compare
194
+ voiceio learn schedule on # …or let it run `maintain` on a schedule, and notify you when a model wins
195
+ # --at 02:00 --every week: when (default daily at 03:30)
196
+ # --auto-promote: switch to a winner by itself; `learn rollback` undoes it
197
+ voiceio learn stop # stop a run in progress (it goes again next time)
196
198
  ```
197
199
 
200
+ Scheduled runs wait for a quiet machine: they start only when the 1-minute load per CPU is at most `[learn] max_load` (0.25) and you're on mains power, and give up after `wait_hours` (3) until the next run. When one starts you get a notification with **Stop for now** and **Weekly instead**.
201
+
202
+ What a scheduled run trains comes from `[learn]` too: `base` (default `small`; any Whisper size, `.en` included, or a Hugging Face checkpoint), `teacher` (default `large-v3-turbo`), `epochs` (4) and `min_new` (50); `maintain`'s flags override them. To fine-tune the English-only medium model: `voiceio config set learn.base medium.en` (on CPU, medium trains and dictates roughly three times slower than small).
203
+
204
+ To keep a second machine (say, the server your phone dictates through) on the same model, set `[learn] on_promote = "scripts/push_model.sh HOST"`: after every switch it copies the model there and flips a `current` link the server points at.
205
+
198
206
  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`.
199
207
 
200
208
  `voiceio learn status` shows where you are, each model's scores, and suggests `promote` only when a fine-tune wins on everything. `promote` itself refuses a model that hasn't, and reports how many clips got better or worse with a confidence interval — a few dozen test clips can move an average by luck.
@@ -302,7 +310,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
302
310
 
303
311
  ## Roadmap
304
312
 
305
- - **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.
306
313
  - **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)).
307
314
  - **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
308
315
 
@@ -310,6 +317,11 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
310
317
 
311
318
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the architecture, conventions and the reasoning behind the model choice. Please open an issue before a large PR.
312
319
 
320
+ ## Credits
321
+
322
+ - Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models. The in-browser demo on [voiceio.dev](https://voiceio.dev) runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
323
+ - The landing page's look (dithered art, typewriter heading, hairline bento) takes its inspiration from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
324
+
313
325
  ## License
314
326
 
315
327
  MIT. Model weights are not part of voiceio and carry their own licenses (see [Choose a model](#choose-a-model)).
@@ -5,7 +5,7 @@
5
5
  [![Python](https://img.shields.io/pypi/pyversions/python-voiceio)](https://pypi.org/project/python-voiceio/)
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
7
 
8
- **Voice dictation that learns you.** Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
8
+ **Voice dictation that learns you.** ([website](https://voiceio.dev/)) Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
9
9
 
10
10
  Off-the-shelf speech models stumble on exactly the words you say most: your colleagues' names, your project, your jargon. voiceio keeps (with your permission) the audio of what you dictate, has a bigger model transcribe it overnight, and trains a personal model from it — nothing uploaded, no GPU required. On the author's dictation, one overnight run on a laptop CPU cut word errors from **12.1% to 5.0%** on held-out clips and recognized more of the author's vocabulary, at the same speed. Every model is scored on clips it never trained on, and it only replaces the current one when it wins.
11
11
 
@@ -137,11 +137,19 @@ voiceio learn eval # score the current model on your held-out clips: WER a
137
137
  # --streaming replays them through live dictation and times stop→text
138
138
  voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
139
139
  voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
140
- voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
141
- voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
142
- # (--auto-promote: switch to a winner by itself; `learn rollback` undoes it)
140
+ voiceio learn maintain # later: label what's new, and once 50+ clips piled up ([learn] min_new), retrain and compare
141
+ voiceio learn schedule on # …or let it run `maintain` on a schedule, and notify you when a model wins
142
+ # --at 02:00 --every week: when (default daily at 03:30)
143
+ # --auto-promote: switch to a winner by itself; `learn rollback` undoes it
144
+ voiceio learn stop # stop a run in progress (it goes again next time)
143
145
  ```
144
146
 
147
+ Scheduled runs wait for a quiet machine: they start only when the 1-minute load per CPU is at most `[learn] max_load` (0.25) and you're on mains power, and give up after `wait_hours` (3) until the next run. When one starts you get a notification with **Stop for now** and **Weekly instead**.
148
+
149
+ What a scheduled run trains comes from `[learn]` too: `base` (default `small`; any Whisper size, `.en` included, or a Hugging Face checkpoint), `teacher` (default `large-v3-turbo`), `epochs` (4) and `min_new` (50); `maintain`'s flags override them. To fine-tune the English-only medium model: `voiceio config set learn.base medium.en` (on CPU, medium trains and dictates roughly three times slower than small).
150
+
151
+ To keep a second machine (say, the server your phone dictates through) on the same model, set `[learn] on_promote = "scripts/push_model.sh HOST"`: after every switch it copies the model there and flips a `current` link the server points at.
152
+
145
153
  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`.
146
154
 
147
155
  `voiceio learn status` shows where you are, each model's scores, and suggests `promote` only when a fine-tune wins on everything. `promote` itself refuses a model that hasn't, and reports how many clips got better or worse with a confidence interval — a few dozen test clips can move an average by luck.
@@ -249,7 +257,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
249
257
 
250
258
  ## Roadmap
251
259
 
252
- - **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.
253
260
  - **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)).
254
261
  - **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
255
262
 
@@ -257,6 +264,11 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
257
264
 
258
265
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the architecture, conventions and the reasoning behind the model choice. Please open an issue before a large PR.
259
266
 
267
+ ## Credits
268
+
269
+ - Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models. The in-browser demo on [voiceio.dev](https://voiceio.dev) runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
270
+ - The landing page's look (dithered art, typewriter heading, hairline bento) takes its inspiration from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
271
+
260
272
  ## License
261
273
 
262
274
  MIT. Model weights are not part of voiceio and carry their own licenses (see [Choose a model](#choose-a-model)).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "1.3.0"
7
+ version = "1.5.0"
8
8
  description = "Voice dictation for Linux. Speak → text, locally, instantly."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -58,7 +58,7 @@ sherpa = ["sherpa-onnx>=1.12"]
58
58
  dev = ["pytest>=7.0", "pytest-mock", "ruff>=0.15,<0.18"]
59
59
 
60
60
  [project.urls]
61
- Homepage = "https://github.com/Hugo0/voiceio"
61
+ Homepage = "https://voiceio.dev/"
62
62
  Repository = "https://github.com/Hugo0/voiceio"
63
63
  Issues = "https://github.com/Hugo0/voiceio/issues"
64
64
  Changelog = "https://github.com/Hugo0/voiceio/releases"
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 1.3.0
3
+ Version: 1.5.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/Hugo0/voiceio
7
+ Project-URL: Homepage, https://voiceio.dev/
8
8
  Project-URL: Repository, https://github.com/Hugo0/voiceio
9
9
  Project-URL: Issues, https://github.com/Hugo0/voiceio/issues
10
10
  Project-URL: Changelog, https://github.com/Hugo0/voiceio/releases
@@ -58,7 +58,7 @@ Dynamic: license-file
58
58
  [![Python](https://img.shields.io/pypi/pyversions/python-voiceio)](https://pypi.org/project/python-voiceio/)
59
59
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
60
60
 
61
- **Voice dictation that learns you.** Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
61
+ **Voice dictation that learns you.** ([website](https://voiceio.dev/)) Local, open source, private — and it fine-tunes itself on your own voice, on your own machine.
62
62
 
63
63
  Off-the-shelf speech models stumble on exactly the words you say most: your colleagues' names, your project, your jargon. voiceio keeps (with your permission) the audio of what you dictate, has a bigger model transcribe it overnight, and trains a personal model from it — nothing uploaded, no GPU required. On the author's dictation, one overnight run on a laptop CPU cut word errors from **12.1% to 5.0%** on held-out clips and recognized more of the author's vocabulary, at the same speed. Every model is scored on clips it never trained on, and it only replaces the current one when it wins.
64
64
 
@@ -190,11 +190,19 @@ voiceio learn eval # score the current model on your held-out clips: WER a
190
190
  # --streaming replays them through live dictation and times stop→text
191
191
  voiceio learn train # LoRA fine-tune (needs the `train` extra; decoder-only on CPU, --full on a GPU)
192
192
  voiceio learn promote <run> # dictate with the fine-tune; `voiceio learn rollback` undoes it
193
- voiceio learn maintain # later: label what's new, and once 50+ clips piled up, retrain and compare
194
- voiceio learn schedule on # …or let it run `maintain` every night, and notify you when a model wins
195
- # (--auto-promote: switch to a winner by itself; `learn rollback` undoes it)
193
+ voiceio learn maintain # later: label what's new, and once 50+ clips piled up ([learn] min_new), retrain and compare
194
+ voiceio learn schedule on # …or let it run `maintain` on a schedule, and notify you when a model wins
195
+ # --at 02:00 --every week: when (default daily at 03:30)
196
+ # --auto-promote: switch to a winner by itself; `learn rollback` undoes it
197
+ voiceio learn stop # stop a run in progress (it goes again next time)
196
198
  ```
197
199
 
200
+ Scheduled runs wait for a quiet machine: they start only when the 1-minute load per CPU is at most `[learn] max_load` (0.25) and you're on mains power, and give up after `wait_hours` (3) until the next run. When one starts you get a notification with **Stop for now** and **Weekly instead**.
201
+
202
+ What a scheduled run trains comes from `[learn]` too: `base` (default `small`; any Whisper size, `.en` included, or a Hugging Face checkpoint), `teacher` (default `large-v3-turbo`), `epochs` (4) and `min_new` (50); `maintain`'s flags override them. To fine-tune the English-only medium model: `voiceio config set learn.base medium.en` (on CPU, medium trains and dictates roughly three times slower than small).
203
+
204
+ To keep a second machine (say, the server your phone dictates through) on the same model, set `[learn] on_promote = "scripts/push_model.sh HOST"`: after every switch it copies the model there and flips a `current` link the server points at.
205
+
198
206
  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`.
199
207
 
200
208
  `voiceio learn status` shows where you are, each model's scores, and suggests `promote` only when a fine-tune wins on everything. `promote` itself refuses a model that hasn't, and reports how many clips got better or worse with a confidence interval — a few dozen test clips can move an average by luck.
@@ -302,7 +310,6 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
302
310
 
303
311
  ## Roadmap
304
312
 
305
- - **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.
306
313
  - **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)).
307
314
  - **One model everywhere** — sync your fine-tune to your phone server and laptops, peer to peer.
308
315
 
@@ -310,6 +317,11 @@ Logs: `journalctl --user -u voiceio` or `~/.local/state/voiceio/voiceio.log`.
310
317
 
311
318
  See [CONTRIBUTING.md](CONTRIBUTING.md) for the architecture, conventions and the reasoning behind the model choice. Please open an issue before a large PR.
312
319
 
320
+ ## Credits
321
+
322
+ - Speech recognition: [faster-whisper](https://github.com/SYSTRAN/faster-whisper) and [CTranslate2](https://github.com/OpenNMT/CTranslate2), with OpenAI's [Whisper](https://github.com/openai/whisper) models. The in-browser demo on [voiceio.dev](https://voiceio.dev) runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
323
+ - The landing page's look (dithered art, typewriter heading, hairline bento) takes its inspiration from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
324
+
313
325
  ## License
314
326
 
315
327
  MIT. Model weights are not part of voiceio and carry their own licenses (see [Choose a model](#choose-a-model)).
@@ -37,6 +37,7 @@ tests/test_learn_clean.py
37
37
  tests/test_learn_import.py
38
38
  tests/test_learn_metrics.py
39
39
  tests/test_learn_pipeline.py
40
+ tests/test_learn_quiet.py
40
41
  tests/test_learn_review.py
41
42
  tests/test_learn_targets.py
42
43
  tests/test_llm.py
@@ -144,6 +145,7 @@ voiceio/learn/clean.py
144
145
  voiceio/learn/evaluate.py
145
146
  voiceio/learn/importer.py
146
147
  voiceio/learn/metrics.py
148
+ voiceio/learn/quiet.py
147
149
  voiceio/learn/review.py
148
150
  voiceio/learn/store.py
149
151
  voiceio/learn/teacher.py
@@ -121,5 +121,5 @@ def test_learning_status_and_tips(monkeypatch):
121
121
  cfg.model.name = str(learn_dir() / "models" / "v0001-small-x" / "ct2")
122
122
  st = _learning_status(cfg)
123
123
  assert st["Voice model"] == "fine-tuned on you (v0001-small-x)"
124
- assert st["Recordings"].startswith("on (") and st["Nightly learning"] == "off"
124
+ assert st["Recordings"].startswith("on (") and st["Scheduled learning"] == "off"
125
125
  assert any("learn schedule on" in t for t in _accuracy_tips(st))
@@ -343,6 +343,7 @@ def test_maintain_auto_promote_switches_restarts_and_says_how_to_undo(monkeypatc
343
343
  run = store.learn_dir() / "models" / "v0001-small-x"
344
344
  calls = []
345
345
  monkeypatch.setattr(cli, "cmd_label", lambda a: None)
346
+ monkeypatch.setattr(cli, "_announce_learning", lambda min_new: None)
346
347
  monkeypatch.setattr(train, "missing_train_deps", lambda: [])
347
348
  monkeypatch.setattr(cli, "_run_eval", lambda *a, **k: SimpleNamespace(
348
349
  wer=0.1, term_recall=0.5, repetitions=0, per_clip=[]))
@@ -366,3 +367,121 @@ def test_maintain_auto_promote_switches_restarts_and_says_how_to_undo(monkeypatc
366
367
  monkeypatch.setattr(cli, "cmd_promote", refuse)
367
368
  cli.cmd_maintain(argparse.Namespace(min_new=1, base="small", epochs=1, auto_promote=True))
368
369
  assert calls == []
370
+
371
+
372
+ def test_promote_and_rollback_run_on_promote_with_the_model(monkeypatch, tmp_path):
373
+ import argparse
374
+
375
+ from voiceio import config
376
+ from voiceio.cli import learn as cli
377
+ run = store.learn_dir() / "models" / "v0002-small-x" / "ct2"
378
+ run.mkdir(parents=True)
379
+ (run / "model.bin").write_text("")
380
+ out = tmp_path / "hook.txt"
381
+ config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
382
+ config.CONFIG_PATH.write_text(f'[learn]\non_promote = "echo $VOICEIO_MODEL >> {out}"\n')
383
+ cli.cmd_promote(argparse.Namespace(run="v0002-small-x", force=True))
384
+ cli.cmd_rollback(argparse.Namespace())
385
+ assert out.read_text().split() == [str(run), "small"]
386
+
387
+
388
+ def _maintain_calls(monkeypatch, min_new_clips: int = 3) -> dict:
389
+ """Run-ready maintain with labeling/training/eval faked; records what they got."""
390
+ from types import SimpleNamespace
391
+
392
+ from voiceio.cli import learn as cli
393
+ for i in range(min_new_clips):
394
+ _clip(f"{i}.wav", 3, "x")
395
+ store.add_label(f"{i}.wav", [Segment(0, 3, "hello there")], "teacher:fake")
396
+ got: dict = {}
397
+ monkeypatch.setattr(cli, "cmd_label", lambda a: got.update(teacher=a.teacher))
398
+ monkeypatch.setattr(cli, "_announce_learning", lambda min_new: got.update(announced=min_new))
399
+ monkeypatch.setattr(train, "missing_train_deps", lambda: [])
400
+ monkeypatch.setattr(cli, "_run_eval", lambda *a, **k: SimpleNamespace(
401
+ wer=0.1, term_recall=0.5, repetitions=0, per_clip=[]))
402
+ monkeypatch.setattr(cli, "_train", lambda ds, **k: got.update(k) or store.learn_dir() / "r")
403
+ monkeypatch.setattr(evaluate, "promotion_verdict", lambda c, b: (False, "table"))
404
+ return got
405
+
406
+
407
+ def _learn_config(**values) -> None:
408
+ for key, value in values.items():
409
+ config.set_value("learn", key, value)
410
+
411
+
412
+ def test_maintain_trains_what_learn_config_says(monkeypatch):
413
+ import argparse
414
+
415
+ from voiceio.cli import learn as cli
416
+ _learn_config(base="medium.en", teacher="large-v3", epochs=2, min_new=3)
417
+ got = _maintain_calls(monkeypatch)
418
+ cli.cmd_maintain(argparse.Namespace(min_new=None, base=None, epochs=None))
419
+ assert got == {"teacher": "large-v3", "announced": 3, "base": "medium.en", "epochs": 2}
420
+
421
+
422
+ def test_maintain_config_min_new_gates_retraining(monkeypatch, capsys):
423
+ import argparse
424
+
425
+ from voiceio.cli import learn as cli
426
+ _learn_config(min_new=4)
427
+ got = _maintain_calls(monkeypatch)
428
+ cli.cmd_maintain(argparse.Namespace(min_new=None, base=None, epochs=None))
429
+ assert "base" not in got and "retraining waits for 4" in capsys.readouterr().out
430
+ assert got["teacher"] is None # empty: the default teacher
431
+
432
+
433
+ def test_maintain_flags_override_learn_config(monkeypatch):
434
+ import argparse
435
+
436
+ from voiceio.cli import learn as cli
437
+ _learn_config(base="medium.en", epochs=2, min_new=100)
438
+ got = _maintain_calls(monkeypatch)
439
+ cli.cmd_maintain(argparse.Namespace(min_new=3, base="tiny", epochs=7))
440
+ assert (got["announced"], got["base"], got["epochs"]) == (3, "tiny", 7)
441
+
442
+
443
+ @pytest.mark.parametrize("values, message", [
444
+ ({"epochs": 0}, "epochs must be at least 1"),
445
+ ({"min_new": 0}, "min_new must be at least 1"),
446
+ ({"base": ""}, "base must name a model"),
447
+ ])
448
+ def test_maintain_rejects_bad_learn_config(monkeypatch, values, message):
449
+ import argparse
450
+
451
+ from voiceio.cli import learn as cli
452
+ _learn_config(**values)
453
+ got = _maintain_calls(monkeypatch)
454
+ with pytest.raises(SystemExit, match=message):
455
+ cli.cmd_maintain(argparse.Namespace(min_new=None, base=None, epochs=None))
456
+ assert got == {} # refused before any work
457
+
458
+
459
+ def test_english_only_base_needs_english(monkeypatch):
460
+ import argparse
461
+
462
+ from voiceio.cli import learn as cli
463
+ _learn_config(base="small.en")
464
+ config.set_value("model", "language", "de")
465
+ _maintain_calls(monkeypatch)
466
+ with pytest.raises(SystemExit, match="English-only"):
467
+ cli.cmd_maintain(argparse.Namespace(min_new=None, base=None, epochs=None))
468
+
469
+
470
+ def test_english_only_bases_train_without_language_prefix():
471
+ assert train.HF_BASE["medium.en"] == "openai/whisper-medium.en"
472
+ cfg = train.TrainConfig(language="en")
473
+ assert train._prefix(51865, cfg) == {"language": "en", "task": "transcribe"}
474
+ assert train._prefix(51864, cfg) == {} # *.en: <|startoftranscript|> only
475
+
476
+
477
+ def test_schedule_on_states_the_configured_threshold(monkeypatch, capsys):
478
+ import argparse
479
+
480
+ from voiceio import service
481
+ from voiceio.cli import learn as cli
482
+ _learn_config(min_new=20)
483
+ monkeypatch.setattr(train, "missing_train_deps", lambda: [])
484
+ monkeypatch.setattr(service, "install_learn_timer", lambda auto_promote: True)
485
+ monkeypatch.setattr(service, "learn_schedule_summary", lambda: "daily at 03:30")
486
+ cli.cmd_schedule(argparse.Namespace(state="on", at=None, every=None, auto_promote=False))
487
+ assert "retrains once 20+ clips are new" in capsys.readouterr().out
@@ -0,0 +1,102 @@
1
+ import argparse
2
+
3
+ from voiceio.learn import quiet
4
+
5
+
6
+ def _supply(root, name, kind, online):
7
+ d = root / name
8
+ d.mkdir()
9
+ (d / "type").write_text(kind + "\n")
10
+ (d / "online").write_text(online + "\n")
11
+
12
+
13
+ def test_on_battery_needs_a_mains_supply_that_is_offline(tmp_path):
14
+ assert not quiet.on_battery(tmp_path) # desktop: no supplies listed
15
+ _supply(tmp_path, "BAT0", "Battery", "0")
16
+ _supply(tmp_path, "AC", "Mains", "0")
17
+ assert quiet.on_battery(tmp_path)
18
+ (tmp_path / "AC" / "online").write_text("1\n")
19
+ assert not quiet.on_battery(tmp_path)
20
+
21
+
22
+ def test_busy_reason_is_load_per_cpu_then_battery(monkeypatch):
23
+ monkeypatch.setattr(quiet.os, "cpu_count", lambda: 8)
24
+ monkeypatch.setattr(quiet, "on_battery", lambda: False)
25
+ monkeypatch.setattr(quiet.os, "getloadavg", lambda: (4.0, 0, 0)) # 0.5 per CPU
26
+ assert "load 0.50 per CPU" in quiet.busy_reason(0.25)
27
+ monkeypatch.setattr(quiet.os, "getloadavg", lambda: (1.0, 0, 0))
28
+ assert quiet.busy_reason(0.25) is None
29
+ monkeypatch.setattr(quiet, "on_battery", lambda: True)
30
+ assert quiet.busy_reason(0.25) == "on battery"
31
+
32
+
33
+ def test_wait_until_quiet_waits_then_gives_up(monkeypatch):
34
+ now = [0.0]
35
+ sleeps = []
36
+ reasons = iter(["busy", "busy", None])
37
+ monkeypatch.setattr(quiet, "busy_reason", lambda _: next(reasons))
38
+ args = dict(poll=300, sleep=lambda s: (sleeps.append(s), now.__setitem__(0, now[0] + s)),
39
+ clock=lambda: now[0], say=lambda _: None)
40
+ assert quiet.wait_until_quiet(0.25, 3, **args)
41
+ assert sleeps == [300, 300]
42
+
43
+ monkeypatch.setattr(quiet, "busy_reason", lambda _: "on battery")
44
+ said = []
45
+ now[0] = 0.0
46
+ assert not quiet.wait_until_quiet(0.25, 1, **{**args, "say": said.append})
47
+ assert now[0] <= 3600 and said[-1].startswith("still on battery after 1 h")
48
+
49
+
50
+ def test_scheduled_maintain_waits_for_a_quiet_machine(monkeypatch):
51
+ from voiceio.cli import learn as cli
52
+ monkeypatch.setattr(cli, "_yield_cpu", lambda: None)
53
+ monkeypatch.setattr(quiet, "wait_until_quiet", lambda *a, **k: False)
54
+ ran = []
55
+ monkeypatch.setattr(cli, "cmd_label", lambda a: ran.append("label"))
56
+ cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1, when_idle=True))
57
+ assert ran == [] # busy all night: nothing ran
58
+
59
+
60
+ def test_learning_announcement_offers_stop_and_weekly(monkeypatch, tmp_path):
61
+ from voiceio import feedback
62
+ from voiceio.cli import learn as cli
63
+ sent = []
64
+ monkeypatch.setattr(feedback, "notify_actions", lambda *a: sent.append(a))
65
+ monkeypatch.setattr(cli.store, "load_labels", lambda: {})
66
+ monkeypatch.setattr(cli.store, "clips", lambda: [argparse.Namespace(audio=f"a{i}.wav") for i in range(3)])
67
+ monkeypatch.setattr(cli.store, "latest_dataset", lambda: None)
68
+ cli._announce_learning(min_new=50)
69
+ (title, body, actions, _), = sent
70
+ assert "learning" in title and body.startswith("Labeling 3 new recordings,")
71
+ assert "on this computer" in body and set(actions) == {"stop", "weekly"}
72
+
73
+ sent.clear()
74
+ monkeypatch.setattr(cli.store, "load_labels", lambda: {f"a{i}.wav": 1 for i in range(3)})
75
+ monkeypatch.setattr(cli.store, "latest_dataset", lambda: tmp_path)
76
+ monkeypatch.setattr(cli.store, "dataset_clips", lambda _: {f"a{i}.wav" for i in range(3)})
77
+ cli._announce_learning(min_new=50)
78
+ assert sent == [] # nothing to do: no notification
79
+
80
+
81
+ def test_notify_actions_runs_the_picked_action(monkeypatch):
82
+ import subprocess
83
+
84
+ from voiceio import feedback
85
+ monkeypatch.setattr("sys.platform", "linux")
86
+ monkeypatch.setattr(feedback, "_which", lambda _: "/usr/bin/notify-send")
87
+ monkeypatch.setattr(feedback, "_notify_send_has_actions", lambda: True)
88
+ calls = []
89
+
90
+ def fake_run(args, **kw):
91
+ calls.append(args)
92
+ return subprocess.CompletedProcess(args, 0, stdout="weekly\n")
93
+ monkeypatch.setattr(feedback.subprocess, "run", fake_run)
94
+ picked = []
95
+
96
+ class Now: # run the waiting thread inline
97
+ def __init__(self, target, daemon): self.target = target
98
+ def start(self): self.target()
99
+ monkeypatch.setattr(feedback.threading, "Thread", Now)
100
+ feedback.notify_actions("t", "b", {"stop": "Stop", "weekly": "Weekly"}, picked.append)
101
+ assert picked == ["weekly"]
102
+ assert "--wait" in calls[0] and "--action=stop=Stop" in calls[0]
@@ -36,7 +36,7 @@ def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
36
36
 
37
37
  assert service.install_learn_timer()
38
38
  unit = service.LEARN_SERVICE_PATH.read_text()
39
- assert "ExecStart=/opt/v/bin/voiceio learn maintain" in unit
39
+ assert "ExecStart=/opt/v/bin/voiceio learn maintain --when-idle" in unit
40
40
  assert "CPUWeight=idle" in unit and "VOICEIO_IDLE_SCOPE=1" in unit
41
41
  assert "Persistent=true" in service.LEARN_TIMER_PATH.read_text()
42
42
  assert ("enable", "--now", service.LEARN_TIMER_NAME) in calls
@@ -89,5 +89,28 @@ def test_learn_timer_auto_promote_is_opt_in(monkeypatch, tmp_path):
89
89
  service.install_learn_timer()
90
90
  assert not service.learn_timer_auto_promotes()
91
91
  service.install_learn_timer(auto_promote=True)
92
- assert "learn maintain --auto-promote" in service.LEARN_SERVICE_PATH.read_text()
92
+ assert "learn maintain --when-idle --auto-promote" in service.LEARN_SERVICE_PATH.read_text()
93
93
  assert service.learn_timer_auto_promotes()
94
+
95
+
96
+ def test_learn_schedule_comes_from_config(monkeypatch, tmp_path):
97
+ import pytest
98
+
99
+ from voiceio import config
100
+ monkeypatch.setattr(service, "has_systemd", lambda: True)
101
+ monkeypatch.setattr(service, "_systemctl", lambda *a: True)
102
+ monkeypatch.setattr(service, "_find_voiceio_bin", lambda: "voiceio")
103
+ monkeypatch.setattr(service, "LEARN_SERVICE_PATH", tmp_path / "s.service")
104
+ monkeypatch.setattr(service, "LEARN_TIMER_PATH", tmp_path / "s.timer")
105
+ service.install_learn_timer()
106
+ assert "OnCalendar=*-*-* 03:30" in service.LEARN_TIMER_PATH.read_text()
107
+
108
+ config.set_value("learn", "at", "2:05")
109
+ config.set_value("learn", "every", "week")
110
+ service.install_learn_timer()
111
+ assert "OnCalendar=Sun *-*-* 02:05" in service.LEARN_TIMER_PATH.read_text()
112
+ assert service.learn_schedule_summary() == "weekly on Sunday at 2:05"
113
+
114
+ for at, every in (("25:00", "day"), ("3pm", "day"), ("03:30", "month")):
115
+ with pytest.raises(ValueError):
116
+ service.learn_calendar(at, every)
@@ -0,0 +1 @@
1
+ __version__ = "1.5.0"