python-voiceio 1.8.2__tar.gz → 1.9.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 (216) hide show
  1. {python_voiceio-1.8.2/python_voiceio.egg-info → python_voiceio-1.9.0}/PKG-INFO +1 -1
  2. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/pyproject.toml +1 -1
  3. {python_voiceio-1.8.2 → python_voiceio-1.9.0/python_voiceio.egg-info}/PKG-INFO +1 -1
  4. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/python_voiceio.egg-info/SOURCES.txt +3 -0
  5. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_cancel.py +5 -2
  6. python_voiceio-1.9.0/tests/test_context_terms.py +253 -0
  7. python_voiceio-1.9.0/tests/test_learn_language.py +39 -0
  8. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_postprocess.py +15 -2
  9. python_voiceio-1.9.0/voiceio/__init__.py +1 -0
  10. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/app.py +31 -5
  11. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/learn.py +14 -1
  12. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/config.py +4 -0
  13. python_voiceio-1.9.0/voiceio/context_terms.py +253 -0
  14. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/bias.py +52 -10
  15. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/engine.py +10 -4
  16. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/health.py +6 -0
  17. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/teacher.py +1 -1
  18. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/postprocess.py +32 -3
  19. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/streaming.py +8 -0
  20. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tokens.py +17 -0
  21. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/vocabulary.py +6 -15
  22. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/wordfreq.py +15 -3
  23. python_voiceio-1.8.2/voiceio/__init__.py +0 -1
  24. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/LICENSE +0 -0
  25. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/README.md +0 -0
  26. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
  27. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/python_voiceio.egg-info/entry_points.txt +0 -0
  28. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/python_voiceio.egg-info/requires.txt +0 -0
  29. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/python_voiceio.egg-info/top_level.txt +0 -0
  30. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/setup.cfg +0 -0
  31. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_app_wiring.py +0 -0
  32. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_audio_quality.py +0 -0
  33. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_audio_source.py +0 -0
  34. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_backend_probes.py +0 -0
  35. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_cli.py +0 -0
  36. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_clipboard_read.py +0 -0
  37. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_clipboard_typer.py +0 -0
  38. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_commands.py +0 -0
  39. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_concurrency_lockdown.py +0 -0
  40. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_config.py +0 -0
  41. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_corrections.py +0 -0
  42. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_decoders.py +0 -0
  43. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_engine.py +0 -0
  44. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_evdev_uaccess.py +0 -0
  45. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_fallback.py +0 -0
  46. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_fcitx_bridge.py +0 -0
  47. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_feedback_stream.py +0 -0
  48. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_focus.py +0 -0
  49. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_health.py +0 -0
  50. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_hints.py +0 -0
  51. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_history.py +0 -0
  52. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_hotkey_modes.py +0 -0
  53. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_engine_limits.py +0 -0
  54. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_engine_listener.py +0 -0
  55. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_pending.py +0 -0
  56. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_pids.py +0 -0
  57. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_ping.py +0 -0
  58. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_session.py +0 -0
  59. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_textlimit.py +0 -0
  60. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_ibus_typer.py +0 -0
  61. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_idle_audio.py +0 -0
  62. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_clean.py +0 -0
  63. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_import.py +0 -0
  64. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_metrics.py +0 -0
  65. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_pipeline.py +0 -0
  66. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_quiet.py +0 -0
  67. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_review.py +0 -0
  68. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_learn_targets.py +0 -0
  69. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_llm.py +0 -0
  70. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_llm_api.py +0 -0
  71. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_modifier_guard.py +0 -0
  72. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_numbers.py +0 -0
  73. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_paste_last.py +0 -0
  74. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_pipeline.py +0 -0
  75. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_platform.py +0 -0
  76. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_postcorrect.py +0 -0
  77. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_prebuffer.py +0 -0
  78. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_prompt.py +0 -0
  79. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_quiet_audio.py +0 -0
  80. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_recorder_integration.py +0 -0
  81. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_retention.py +0 -0
  82. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_robustness.py +0 -0
  83. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_security_hardening.py +0 -0
  84. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_server.py +0 -0
  85. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_service.py +0 -0
  86. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_setup.py +0 -0
  87. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_streaming.py +0 -0
  88. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_suggest.py +0 -0
  89. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_tokens.py +0 -0
  90. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_transcriber.py +0 -0
  91. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_tts.py +0 -0
  92. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_typer_manager.py +0 -0
  93. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_vad.py +0 -0
  94. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_vocabulary.py +0 -0
  95. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_wordfreq.py +0 -0
  96. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/tests/test_worker_decode.py +0 -0
  97. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/__main__.py +0 -0
  98. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/audio_source.py +0 -0
  99. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/backends.py +0 -0
  100. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/__init__.py +0 -0
  101. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/configcmd.py +0 -0
  102. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/corrections.py +0 -0
  103. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/doctor.py +0 -0
  104. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/history.py +0 -0
  105. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/models.py +0 -0
  106. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/servicecmd.py +0 -0
  107. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/uninstall.py +0 -0
  108. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/cli/vocab.py +0 -0
  109. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/clipboard_read.py +0 -0
  110. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/commands.py +0 -0
  111. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/consent.py +0 -0
  112. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/__init__.py +0 -0
  113. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/__init__.py +0 -0
  114. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/base.py +0 -0
  115. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/catalog.py +0 -0
  116. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
  117. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
  118. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/lanes.py +0 -0
  119. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/sherpa.py +0 -0
  120. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/decoders/whispercpp.py +0 -0
  121. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/core/pipeline.py +0 -0
  122. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/corrections.py +0 -0
  123. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  124. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/data/__init__.py +0 -0
  125. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/demo.py +0 -0
  126. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/display.py +0 -0
  127. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/feedback.py +0 -0
  128. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/focus.py +0 -0
  129. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hints.py +0 -0
  130. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/history.py +0 -0
  131. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/__init__.py +0 -0
  132. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/base.py +0 -0
  133. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/chain.py +0 -0
  134. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/evdev.py +0 -0
  135. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/pynput_backend.py +0 -0
  136. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/hotkeys/socket_backend.py +0 -0
  137. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/__init__.py +0 -0
  138. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/engine.py +0 -0
  139. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/install.py +0 -0
  140. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/pending.py +0 -0
  141. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/session.py +0 -0
  142. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ibus/textlimit.py +0 -0
  143. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/idle_audio.py +0 -0
  144. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/__init__.py +0 -0
  145. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/augment.py +0 -0
  146. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/clean.py +0 -0
  147. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/evaluate.py +0 -0
  148. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/importer.py +0 -0
  149. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/metrics.py +0 -0
  150. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/quiet.py +0 -0
  151. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/review.py +0 -0
  152. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/store.py +0 -0
  153. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/learn/train.py +0 -0
  154. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/llm.py +0 -0
  155. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/llm_api.py +0 -0
  156. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/models/__init__.py +0 -0
  157. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/models/silero_vad.onnx +0 -0
  158. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/numbers.py +0 -0
  159. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/pidlock.py +0 -0
  160. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/platform.py +0 -0
  161. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/postcorrect.py +0 -0
  162. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/power.py +0 -0
  163. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/privacy.py +0 -0
  164. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/prompt.py +0 -0
  165. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/quiet_audio.py +0 -0
  166. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/recorder.py +0 -0
  167. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/retention.py +0 -0
  168. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/server/__init__.py +0 -0
  169. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/server/__main__.py +0 -0
  170. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/server/app.py +0 -0
  171. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/server/config.py +0 -0
  172. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/server/runtime.py +0 -0
  173. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/service.py +0 -0
  174. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/__init__.py +0 -0
  175. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/configfile.py +0 -0
  176. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/extras.py +0 -0
  177. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/hotkey.py +0 -0
  178. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/noninteractive.py +0 -0
  179. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/speech.py +0 -0
  180. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/steps.py +0 -0
  181. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/system.py +0 -0
  182. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/setup/ui.py +0 -0
  183. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/sounds/__init__.py +0 -0
  184. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/sounds/commit.wav +0 -0
  185. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/sounds/start.wav +0 -0
  186. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/sounds/stop.wav +0 -0
  187. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/suggest.py +0 -0
  188. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tray/__init__.py +0 -0
  189. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tray/_icons.py +0 -0
  190. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tray/_indicator.py +0 -0
  191. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tray/_pystray.py +0 -0
  192. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/__init__.py +0 -0
  193. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/base.py +0 -0
  194. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/chain.py +0 -0
  195. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/edge_engine.py +0 -0
  196. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/espeak.py +0 -0
  197. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/piper_engine.py +0 -0
  198. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/tts/player.py +0 -0
  199. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/__init__.py +0 -0
  200. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/base.py +0 -0
  201. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/chain.py +0 -0
  202. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/clipboard.py +0 -0
  203. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/collecting.py +0 -0
  204. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/fcitx_bridge.py +0 -0
  205. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/ibus.py +0 -0
  206. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/manager.py +0 -0
  207. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/modifier_guard.py +0 -0
  208. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/paste.py +0 -0
  209. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/pynput_type.py +0 -0
  210. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/wtype.py +0 -0
  211. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/xdotool.py +0 -0
  212. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/typers/ydotool.py +0 -0
  213. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/ui.py +0 -0
  214. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/vad.py +0 -0
  215. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/vocab_stats.py +0 -0
  216. {python_voiceio-1.8.2 → python_voiceio-1.9.0}/voiceio/watchdog.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 1.8.2
3
+ Version: 1.9.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "1.8.2"
7
+ version = "1.9.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.8.2
3
+ Version: 1.9.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -18,6 +18,7 @@ tests/test_clipboard_typer.py
18
18
  tests/test_commands.py
19
19
  tests/test_concurrency_lockdown.py
20
20
  tests/test_config.py
21
+ tests/test_context_terms.py
21
22
  tests/test_corrections.py
22
23
  tests/test_decoders.py
23
24
  tests/test_engine.py
@@ -41,6 +42,7 @@ tests/test_ibus_typer.py
41
42
  tests/test_idle_audio.py
42
43
  tests/test_learn_clean.py
43
44
  tests/test_learn_import.py
45
+ tests/test_learn_language.py
44
46
  tests/test_learn_metrics.py
45
47
  tests/test_learn_pipeline.py
46
48
  tests/test_learn_quiet.py
@@ -84,6 +86,7 @@ voiceio/clipboard_read.py
84
86
  voiceio/commands.py
85
87
  voiceio/config.py
86
88
  voiceio/consent.py
89
+ voiceio/context_terms.py
87
90
  voiceio/corrections.py
88
91
  voiceio/demo.py
89
92
  voiceio/display.py
@@ -156,14 +156,17 @@ def test_cancel_too_late_changes_nothing(tmp_path):
156
156
  session = MagicMock()
157
157
  session.cancel.return_value = False # final text already typed
158
158
  session.stop.side_effect = lambda audio: (in_stop.set(), release.wait(5), "words")[2]
159
+ # The finalizer goes on to copy the text from its own thread; left real, it
160
+ # lands on whatever clipboard a later test has patched in.
161
+ vio._copy_result_async = MagicMock()
159
162
  _finalizing(vio, tmp_path, session)
160
163
  assert in_stop.wait(5)
161
164
  gen = vio._generation
162
165
  vio.cancel()
163
166
  assert vio._state == _State.FINALIZING and vio._generation == gen
164
167
  release.set()
165
- for _ in range(200):
166
- if vio._state == _State.IDLE:
168
+ for _ in range(500):
169
+ if vio._state == _State.IDLE and not vio._outputs:
167
170
  break
168
171
  time.sleep(0.01)
169
172
  vio.engine.commit.assert_called_once()
@@ -0,0 +1,253 @@
1
+ """On-screen vocabulary: term extraction, budget share, capture deadline, and
2
+ that capture never sits on the hotkey path."""
3
+ from __future__ import annotations
4
+
5
+ import threading
6
+ import time
7
+ from unittest.mock import patch
8
+
9
+ import pytest
10
+
11
+ from voiceio import context_terms
12
+ from voiceio.context_terms import ScreenContext, capture, extract_terms
13
+ from voiceio.core.bias import SCREEN_TOKEN_SHARE, BiasProvider
14
+ from voiceio.core.decoders.base import BiasMode, Capabilities
15
+ from voiceio.tokens import count_tokens
16
+
17
+ HOTWORDS = Capabilities(bias=BiasMode.HOTWORDS | BiasMode.PROMPT, max_bias_tokens=120)
18
+
19
+
20
+ # ── extraction ────────────────────────────────────────────────────────────
21
+
22
+
23
+ class TestExtract:
24
+ def test_rare_names_camelcase_acronyms(self):
25
+ terms = extract_terms(["Kalshi pricing - Discord",
26
+ "The HyperNEAT paper uses OpenRouter and the OKR doc."])
27
+ assert {"Kalshi", "HyperNEAT", "OpenRouter", "OKR"} <= set(terms)
28
+
29
+ def test_common_words_and_app_names_are_not_terms(self):
30
+ terms = extract_terms(["Inbox - Google Chrome",
31
+ "Please check the API before the meeting with Hugo."])
32
+ assert terms == []
33
+
34
+ def test_multiword_name_but_not_a_greeting(self):
35
+ terms = extract_terms(["", "Hey Kosh, ask Tarrant Fnordwick about it."])
36
+ assert "Tarrant Fnordwick" in terms
37
+ assert "Kosh" in terms
38
+ assert not any(t.startswith("Hey") for t in terms)
39
+ # A name and its own parts would spend tokens twice on one word.
40
+ assert "Tarrant" not in terms and "Fnordwick" not in terms
41
+
42
+ def test_identifiers_split_numbers_dropped(self):
43
+ terms = extract_terms(["", "see github.com/Hugo0/voiceio and v2 of x86_64 build 2026"])
44
+ assert "voiceio" in terms
45
+ assert not any(any(c.isdigit() for c in t) for t in terms)
46
+
47
+ def test_exclude_is_case_insensitive_and_dedupes_spellings(self):
48
+ terms = extract_terms(["", "Kalshi kalshi KALSHI Zorblax"], exclude=["zorblax"])
49
+ assert [t.lower() for t in terms] == ["kalshi"]
50
+
51
+ def test_vocabulary_long_tail_ranks_first(self):
52
+ terms = extract_terms(["", "Zorblax met Quuxley at Wibblecorp."],
53
+ prefer=["Wibblecorp"])
54
+ assert terms[0] == "Wibblecorp"
55
+
56
+ def test_title_beats_selection_on_ties(self):
57
+ terms = extract_terms(["Quuxley", "Zorblax"])
58
+ assert terms == ["Quuxley", "Zorblax"]
59
+
60
+ def test_repeats_outrank_one_offs(self):
61
+ terms = extract_terms(["", "Zorblax once. Quuxley, Quuxley, Quuxley."])
62
+ assert terms[0] == "Quuxley"
63
+
64
+ def test_capped(self):
65
+ text = " ".join(f"Zorb{chr(97 + i)}lax" for i in range(20))
66
+ assert len(extract_terms(["", text], max_terms=5)) == 5
67
+
68
+ def test_huge_selection_is_cheap(self):
69
+ text = "the quick brown fox Zorblax jumps " * 20000
70
+ t0 = time.monotonic()
71
+ assert extract_terms(["", text]) == ["Zorblax"]
72
+ assert time.monotonic() - t0 < 1.0
73
+
74
+ def test_empty(self):
75
+ assert extract_terms(["", ""]) == []
76
+
77
+
78
+ # ── budget share in the hotwords ──────────────────────────────────────────
79
+
80
+
81
+ def _provider(terms: list[str]) -> BiasProvider:
82
+ from voiceio import config
83
+ from voiceio.vocabulary import VocabularyLoader
84
+ config.CONFIG_DIR.mkdir(parents=True, exist_ok=True)
85
+ (config.CONFIG_DIR / "vocabulary.txt").write_text("\n".join(terms) + "\n")
86
+ return BiasProvider(VocabularyLoader(config.ModelConfig()))
87
+
88
+
89
+ def _vocab(n: int) -> list[str]:
90
+ # Distinct pronounceable made-up names, in file (= rank) order.
91
+ syll = ["ka", "zor", "bli", "mun", "tep", "vas", "quo", "rin", "lud", "fey"]
92
+ return [(syll[i % 10] + syll[(i // 10) % 10] + syll[(i * 7) % 10]).capitalize() + "x"
93
+ for i in range(n)]
94
+
95
+
96
+ class TestBudget:
97
+ def test_no_screen_terms_is_unchanged(self):
98
+ bp = _provider(_vocab(60))
99
+ assert bp.bias_for(HOTWORDS).hotwords == bp.bias_for(HOTWORDS, []).hotwords
100
+
101
+ def test_screen_terms_displace_only_the_vocabulary_tail(self):
102
+ vocab = _vocab(60)
103
+ bp = _provider(vocab)
104
+ base = bp.bias_for(HOTWORDS).hotwords.split(", ")
105
+ merged = bp.bias_for(HOTWORDS, ["Quuxley", "Wibblecorp"]).hotwords.split(", ")
106
+ assert "Quuxley" in merged and "Wibblecorp" in merged
107
+ kept = [t for t in merged if t in vocab]
108
+ assert kept == base[:len(kept)], "the best-ranked terms survive, in order"
109
+ assert len(kept) < len(base)
110
+ assert count_tokens(", ".join(merged), "small") <= 120 + 2 # join slack
111
+
112
+ def test_screen_share_is_capped(self):
113
+ bp = _provider(_vocab(60))
114
+ many = [f"Zorb{chr(97 + i)}laxian" for i in range(20)]
115
+ merged = bp.bias_for(HOTWORDS, many).hotwords.split(", ")
116
+ screen = [t for t in merged if t in many]
117
+ assert 0 < len(screen) < len(many)
118
+ assert count_tokens(", ".join(screen), "small") <= SCREEN_TOKEN_SHARE
119
+ # The vocabulary keeps the rest of the budget.
120
+ assert len(merged) - len(screen) >= 10
121
+
122
+ def test_screen_terms_skip_what_hotwords_carry(self):
123
+ bp = _provider(["Kalshi", "HyperNEAT"])
124
+ assert bp.screen_terms(["Kalshi - Discord", "Quuxley and HyperNEAT"], HOTWORDS) == ["Quuxley"]
125
+
126
+ def test_unbounded_appends(self):
127
+ bp = _provider(_vocab(60))
128
+ caps = Capabilities(bias=BiasMode.UNBOUNDED)
129
+ merged = bp.bias_for(caps, ["Quuxley"]).hotwords.split(", ")
130
+ assert merged[-1] == "Quuxley" and len(merged) == 61
131
+
132
+ def test_decoder_without_hotwords_gets_nothing(self):
133
+ bp = _provider(["Kalshi"])
134
+ assert bp.screen_terms(["Quuxley"], Capabilities(bias=BiasMode.PROMPT)) == []
135
+
136
+ def test_extraction_failure_degrades_to_nothing(self):
137
+ bp = _provider(["Kalshi"])
138
+ with patch("voiceio.context_terms.extract_terms", side_effect=RuntimeError):
139
+ assert bp.screen_terms(["Quuxley"], HOTWORDS) == []
140
+
141
+
142
+ # ── capture: deadline and degrade ─────────────────────────────────────────
143
+
144
+
145
+ class TestCapture:
146
+ def test_slow_source_is_dropped_at_the_deadline(self):
147
+ gate = threading.Event()
148
+
149
+ def slow(_t):
150
+ gate.wait(5)
151
+ return "Zorblax"
152
+
153
+ t0 = time.monotonic()
154
+ ctx = capture(0.1, readers={"title": lambda _t: "Kalshi", "selection": slow})
155
+ elapsed = time.monotonic() - t0
156
+ gate.set()
157
+ assert elapsed < 0.5
158
+ assert ctx.title == "Kalshi" and ctx.selection == ""
159
+
160
+ def test_failing_source_degrades_silently(self):
161
+ def boom(_t):
162
+ raise OSError("no display")
163
+ ctx = capture(0.1, readers={"title": boom, "selection": lambda _t: "x"})
164
+ assert ctx == ScreenContext(title="", selection="x", secs=ctx.secs)
165
+
166
+ def test_no_tools_no_selection(self, monkeypatch):
167
+ monkeypatch.setattr(context_terms.shutil, "which", lambda _n: None)
168
+ assert context_terms._read_primary(0.1) == ""
169
+
170
+ def test_gnome_wayland_never_uses_wl_paste(self, monkeypatch):
171
+ """wl-paste can take focus on GNOME and drop the IBus preedit."""
172
+ from types import SimpleNamespace
173
+ monkeypatch.delenv("DISPLAY", raising=False)
174
+ monkeypatch.setattr("voiceio.platform.detect",
175
+ lambda: SimpleNamespace(is_wayland=True, desktop="gnome"))
176
+ monkeypatch.setattr(context_terms.shutil, "which", lambda n: f"/usr/bin/{n}")
177
+ with patch.object(context_terms.subprocess, "run") as run:
178
+ assert context_terms._read_primary(0.1) == ""
179
+ run.assert_not_called()
180
+
181
+ def test_selection_read_has_the_deadline_as_timeout(self, monkeypatch):
182
+ monkeypatch.setenv("DISPLAY", ":0")
183
+ monkeypatch.setattr(context_terms.shutil, "which", lambda n: f"/usr/bin/{n}")
184
+ with patch.object(context_terms.subprocess, "run") as run:
185
+ run.return_value.returncode = 0
186
+ run.return_value.stdout = "Zorblax".encode()
187
+ assert context_terms._read_primary(0.15) == "Zorblax"
188
+ assert run.call_args.kwargs["timeout"] == 0.15
189
+ assert run.call_args.args[0][:2] == ["xclip", "-o"]
190
+
191
+
192
+ # ── the daemon: never on the hotkey path ──────────────────────────────────
193
+
194
+
195
+ @pytest.fixture
196
+ def vio():
197
+ from test_app_wiring import _make_vio
198
+ from fakes import FakeDecoder
199
+ dec = FakeDecoder()
200
+ dec.transcribe.return_value = "hello"
201
+ v, _, _ = _make_vio(dec)
202
+ v.cfg.model.screen_vocabulary = True
203
+ yield v, dec
204
+ if v._session is not None:
205
+ v._session.cancel()
206
+
207
+
208
+ class TestDaemon:
209
+ def test_capture_does_not_block_recording_start(self, vio):
210
+ v, dec = vio
211
+ gate = threading.Event()
212
+ captured = threading.Event()
213
+
214
+ def blocked_capture(*_a, **_k):
215
+ gate.wait(5)
216
+ captured.set()
217
+ return ScreenContext(title="Quuxley - Discord")
218
+
219
+ with patch("voiceio.context_terms.capture", side_effect=blocked_capture):
220
+ t0 = time.monotonic()
221
+ v.on_hotkey()
222
+ assert time.monotonic() - t0 < 1.0
223
+ assert v.recorder.is_recording, "recording started while capture blocks"
224
+ assert "Quuxley" not in (v._recording_bias.hotwords or "")
225
+ gate.set()
226
+ assert captured.wait(5)
227
+ for _ in range(100):
228
+ if "Quuxley" in (v._recording_bias.hotwords or ""):
229
+ break
230
+ time.sleep(0.02)
231
+ assert "Quuxley" in (v._recording_bias.hotwords or "")
232
+ assert "Quuxley" in (v._session._bias.hotwords or ""), "applied to later passes"
233
+
234
+ def test_late_capture_never_touches_the_next_recording(self, vio):
235
+ v, _ = vio
236
+ gate = threading.Event()
237
+ with patch("voiceio.context_terms.capture",
238
+ side_effect=lambda *a, **k: (gate.wait(5), ScreenContext(title="Quuxley"))[1]):
239
+ v.on_hotkey()
240
+ rid = v._recording
241
+ v._recording += 1 # as if this recording ended and another began
242
+ gate.set()
243
+ time.sleep(0.3)
244
+ v._recording = rid
245
+ assert "Quuxley" not in (v._recording_bias.hotwords or "")
246
+
247
+ def test_off_reads_nothing(self, vio):
248
+ v, _ = vio
249
+ v.cfg.model.screen_vocabulary = False
250
+ with patch("voiceio.context_terms.capture") as cap:
251
+ v.on_hotkey()
252
+ time.sleep(0.1)
253
+ cap.assert_not_called()
@@ -0,0 +1,39 @@
1
+ """Fine-tuning and labeling follow the dictation language."""
2
+ from __future__ import annotations
3
+
4
+ import pytest
5
+
6
+ from voiceio.cli import learn as learn_cli
7
+ from voiceio.config import Config
8
+
9
+
10
+ def _with_language(monkeypatch, lang):
11
+ cfg = Config()
12
+ cfg.model.language = lang
13
+ monkeypatch.setattr(learn_cli, "_cfg", lambda: cfg)
14
+
15
+
16
+ def test_training_uses_the_dictation_language(monkeypatch):
17
+ """A Spanish voice trained under the "en" token learned the wrong prompt."""
18
+ _with_language(monkeypatch, "es")
19
+ assert learn_cli._train_language() == "es"
20
+
21
+
22
+ def test_training_refuses_auto_with_a_hint(monkeypatch):
23
+ _with_language(monkeypatch, "auto")
24
+ with pytest.raises(SystemExit, match="model.language"):
25
+ learn_cli._train_language()
26
+
27
+
28
+ def test_teacher_detects_when_language_is_auto(monkeypatch):
29
+ from voiceio.learn import teacher
30
+ monkeypatch.setattr("voiceio.core.decoders.faster_whisper.worker.load_model", lambda *a, **k: object())
31
+ assert teacher.WhisperTeacher("tiny", language="auto")._language is None
32
+ assert teacher.WhisperTeacher("tiny", language="es")._language == "es"
33
+
34
+
35
+ def test_word_frequency_survives_auto_and_unknown_languages():
36
+ """language = "auto" raised LookupError in vocabulary biasing."""
37
+ from voiceio.wordfreq import is_common, is_known
38
+ assert is_common("house", "auto") and is_known("casa", "es")
39
+ assert not is_common("house", "xx")
@@ -102,7 +102,20 @@ class TestCapitalization:
102
102
  assert cleanup("hello world") == "Hello world"
103
103
 
104
104
  def test_after_period(self):
105
- assert cleanup("hello. world") == "Hello. World"
105
+ assert cleanup("hello? world") == "Hello? World"
106
+ assert cleanup("Hello. World") == "Hello. World"
107
+
108
+ def test_lowercase_after_a_segment_stop_is_a_comma(self):
109
+ # Whisper ends each segment with "." and continues the sentence in
110
+ # lowercase; capitalizing it made a false stop permanent.
111
+ assert cleanup("a bit bigger. and remove the latch.") == "A bit bigger, and remove the latch."
112
+ assert cleanup("hello. world") == "Hello, world"
113
+
114
+ def test_abbreviations_and_dotted_names_keep_their_dot(self):
115
+ assert cleanup("pears etc. and more") == "Pears etc. and more"
116
+ assert cleanup("use e.g. this one") == "Use e.g. this one"
117
+ assert cleanup("go to hugo0.com. and then") == "Go to hugo0.com. and then"
118
+ assert cleanup("I paused... and then") == "I paused... and then"
106
119
 
107
120
  def test_after_question_mark(self):
108
121
  assert cleanup("really? yes") == "Really? Yes"
@@ -184,7 +197,7 @@ class TestEdgeCases:
184
197
  assert result == "..."
185
198
 
186
199
  def test_unicode_accents(self):
187
- assert cleanup("café. résumé") == "Café. Résumé"
200
+ assert cleanup("café? résumé") == "Café? Résumé"
188
201
 
189
202
 
190
203
  class TestWordMatchCompatibility:
@@ -0,0 +1 @@
1
+ __version__ = "1.9.0"
@@ -168,6 +168,10 @@ class VoiceIO:
168
168
  threading.Thread(
169
169
  target=retention.prune, args=(cfg.data,), daemon=True,
170
170
  ).start()
171
+ if cfg.model.screen_vocabulary:
172
+ # Word frequency tables load in ~1s; not on the first dictation.
173
+ from voiceio.context_terms import warm
174
+ threading.Thread(target=warm, args=(cfg.model.language,), daemon=True).start()
171
175
 
172
176
  # Hotkey deduplication
173
177
  self._hotkey_lock = threading.Lock()
@@ -557,10 +561,12 @@ class VoiceIO:
557
561
  # `voiceio vocab`/`corrections` apply from the next recording.
558
562
  bias = self.engine.bias()
559
563
  self._recording_bias = bias
560
- if self.cfg.data.capture_context:
561
- # Snapshot the dictation target now — focus may change by finalize
564
+ if self.cfg.data.capture_context or self.cfg.model.screen_vocabulary:
565
+ # Snapshot the dictation target now — focus may change by finalize.
566
+ # Off this thread: the chime and the mic never wait on it.
562
567
  self._context_title = None
563
- threading.Thread(target=self._capture_context, daemon=True).start()
568
+ threading.Thread(target=self._capture_context, args=(self._recording,),
569
+ daemon=True, name="screen-context").start()
564
570
  self._record_start = time.monotonic()
565
571
  self.recorder.start()
566
572
  self.recorder.set_on_auto_stop(self._on_auto_stop)
@@ -627,9 +633,29 @@ class VoiceIO:
627
633
  # No finalizer will run to release the input source, so do it here.
628
634
  self._typers.end(rid)
629
635
 
630
- def _capture_context(self) -> None:
636
+ def _capture_context(self, rid: int) -> None:
631
637
  from voiceio import retention
632
- self._context_title = retention.active_window_title()
638
+ if not self.cfg.model.screen_vocabulary:
639
+ self._context_title = retention.active_window_title()
640
+ return
641
+ from voiceio import context_terms
642
+ t0 = time.monotonic()
643
+ screen = context_terms.capture()
644
+ if self.cfg.data.capture_context:
645
+ self._context_title = screen.title or None
646
+ terms = self.engine.screen_terms([screen.title, screen.selection])
647
+ if not terms:
648
+ return
649
+ bias = self.engine.bias(screen_terms=terms)
650
+ with self._hotkey_lock:
651
+ if self._recording != rid or self._state != _State.RECORDING:
652
+ return
653
+ self._recording_bias = bias
654
+ if self._session is not None:
655
+ self._session.set_bias(bias)
656
+ # A count only: these are words from the user's screen.
657
+ log.debug("Screen vocabulary: %d term(s) from %s, ready %.0f ms after start",
658
+ len(terms), "+".join(screen.sources()), (time.monotonic() - t0) * 1000)
633
659
 
634
660
  def _retention_extra(self, audio: np.ndarray | None) -> dict:
635
661
  """Save the utterance audio (before transcription, so it survives
@@ -417,6 +417,16 @@ def cmd_train(args: argparse.Namespace) -> None:
417
417
  f" (compare with the current model, then `voiceio learn promote {run.name}`)")
418
418
 
419
419
 
420
+ def _train_language() -> str:
421
+ """The language a fine-tune is trained under: the one dictation decodes
422
+ with. "auto" has no single answer, so it has to be set first."""
423
+ lang = _cfg().model.language or "en"
424
+ if lang == "auto":
425
+ raise SystemExit("Fine-tuning needs one language: set it with "
426
+ "`voiceio config set model.language es` (your dictation language).")
427
+ return lang
428
+
429
+
420
430
  def _train(ds: Path, *, base: str = "small", epochs: int = 4, synthetic_share: float = 0.3,
421
431
  full: bool = False, remote: bool = False) -> Path | None:
422
432
  """Export `ds` and fine-tune on it; returns the run dir (None for --remote)."""
@@ -433,8 +443,11 @@ def _train(ds: Path, *, base: str = "small", epochs: int = 4, synthetic_share: f
433
443
  return None
434
444
  train.require_train_deps()
435
445
  hotwords = (folder / train.HOTWORDS_FILE).read_text(encoding="utf-8")
446
+ # The language token the model is trained under must be the one it decodes
447
+ # with: training a Spanish voice under "en" taught it the wrong prompt.
436
448
  hf = train.train(folder, run, train.TrainConfig(
437
- base=base, epochs=epochs, decoder_only=False if full else None, hotwords=hotwords))
449
+ base=base, epochs=epochs, decoder_only=False if full else None, hotwords=hotwords,
450
+ language=_train_language()))
438
451
  train.convert(hf, run / "ct2")
439
452
  return run
440
453
 
@@ -90,6 +90,10 @@ class ModelConfig:
90
90
  # "modified_beam_search" (required for hotwords).
91
91
  decoding: str = ""
92
92
  vocabulary_file: str = "" # path to vocabulary.txt
93
+ # Bias each dictation toward rare names on screen: the focused window's
94
+ # title and the selected text (voiceio/context_terms.py). Read locally
95
+ # when recording starts; never stored or sent anywhere.
96
+ screen_vocabulary: bool = True
93
97
  # Beam width of the passes whose text you keep (freezes and the final
94
98
  # tail); interim previews are always greedy. 1 is greedy: faster stop.
95
99
  beam_size: int = 5