python-voiceio 1.11.0__tar.gz → 1.12.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 (228) hide show
  1. {python_voiceio-1.11.0/python_voiceio.egg-info → python_voiceio-1.12.0}/PKG-INFO +4 -4
  2. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/README.md +3 -3
  3. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/pyproject.toml +1 -1
  4. {python_voiceio-1.11.0 → python_voiceio-1.12.0/python_voiceio.egg-info}/PKG-INFO +4 -4
  5. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/python_voiceio.egg-info/SOURCES.txt +2 -0
  6. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_config.py +6 -1
  7. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_decoders.py +0 -56
  8. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_postprocess.py +13 -0
  9. python_voiceio-1.12.0/tests/test_sherpa.py +369 -0
  10. python_voiceio-1.12.0/voiceio/__init__.py +1 -0
  11. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/learn.py +20 -0
  12. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/models.py +85 -8
  13. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/config.py +16 -6
  14. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/__init__.py +32 -11
  15. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/catalog.py +51 -4
  16. python_voiceio-1.12.0/voiceio/core/decoders/fetch.py +198 -0
  17. python_voiceio-1.12.0/voiceio/core/decoders/sherpa.py +304 -0
  18. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/health.py +26 -0
  19. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/evaluate.py +8 -3
  20. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/postprocess.py +20 -4
  21. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/service.py +1 -1
  22. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/noninteractive.py +5 -4
  23. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/speech.py +42 -1
  24. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/steps.py +11 -7
  25. python_voiceio-1.11.0/voiceio/__init__.py +0 -1
  26. python_voiceio-1.11.0/voiceio/core/decoders/sherpa.py +0 -108
  27. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/LICENSE +0 -0
  28. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
  29. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/python_voiceio.egg-info/entry_points.txt +0 -0
  30. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/python_voiceio.egg-info/requires.txt +0 -0
  31. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/python_voiceio.egg-info/top_level.txt +0 -0
  32. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/setup.cfg +0 -0
  33. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_app_wiring.py +0 -0
  34. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_audio_quality.py +0 -0
  35. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_audio_source.py +0 -0
  36. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_backend_probes.py +0 -0
  37. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_cancel.py +0 -0
  38. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_cli.py +0 -0
  39. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_clipboard_read.py +0 -0
  40. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_clipboard_typer.py +0 -0
  41. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_commands.py +0 -0
  42. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_concurrency_lockdown.py +0 -0
  43. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_context_terms.py +0 -0
  44. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_corrections.py +0 -0
  45. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_dead_mic.py +0 -0
  46. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_engine.py +0 -0
  47. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_evdev_uaccess.py +0 -0
  48. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_fallback.py +0 -0
  49. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_fcitx_bridge.py +0 -0
  50. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_feedback_stream.py +0 -0
  51. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_focus.py +0 -0
  52. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_health.py +0 -0
  53. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_hints.py +0 -0
  54. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_history.py +0 -0
  55. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_hotkey_modes.py +0 -0
  56. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_engine_limits.py +0 -0
  57. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_engine_listener.py +0 -0
  58. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_pending.py +0 -0
  59. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_pids.py +0 -0
  60. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_ping.py +0 -0
  61. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_session.py +0 -0
  62. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_textlimit.py +0 -0
  63. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_ibus_typer.py +0 -0
  64. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_idle_audio.py +0 -0
  65. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_clean.py +0 -0
  66. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_import.py +0 -0
  67. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_language.py +0 -0
  68. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_metrics.py +0 -0
  69. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_pipeline.py +0 -0
  70. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_quiet.py +0 -0
  71. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_resume.py +0 -0
  72. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_review.py +0 -0
  73. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_learn_targets.py +0 -0
  74. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_llm.py +0 -0
  75. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_llm_api.py +0 -0
  76. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_mic_gain.py +0 -0
  77. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_modifier_guard.py +0 -0
  78. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_numbers.py +0 -0
  79. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_paste_last.py +0 -0
  80. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_pipeline.py +0 -0
  81. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_platform.py +0 -0
  82. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_postcorrect.py +0 -0
  83. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_prebuffer.py +0 -0
  84. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_presence.py +0 -0
  85. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_prompt.py +0 -0
  86. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_quiet_audio.py +0 -0
  87. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_recorder_integration.py +0 -0
  88. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_report.py +0 -0
  89. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_retention.py +0 -0
  90. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_robustness.py +0 -0
  91. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_security_hardening.py +0 -0
  92. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_server.py +0 -0
  93. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_service.py +0 -0
  94. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_setup.py +0 -0
  95. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_streaming.py +0 -0
  96. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_suggest.py +0 -0
  97. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_tokens.py +0 -0
  98. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_transcriber.py +0 -0
  99. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_tts.py +0 -0
  100. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_typer_manager.py +0 -0
  101. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_vad.py +0 -0
  102. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_vocabulary.py +0 -0
  103. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_wordfreq.py +0 -0
  104. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/tests/test_worker_decode.py +0 -0
  105. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/__main__.py +0 -0
  106. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/app.py +0 -0
  107. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/audio_source.py +0 -0
  108. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/backends.py +0 -0
  109. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/__init__.py +0 -0
  110. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/configcmd.py +0 -0
  111. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/corrections.py +0 -0
  112. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/doctor.py +0 -0
  113. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/history.py +0 -0
  114. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/servicecmd.py +0 -0
  115. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/uninstall.py +0 -0
  116. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/cli/vocab.py +0 -0
  117. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/clipboard_read.py +0 -0
  118. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/commands.py +0 -0
  119. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/consent.py +0 -0
  120. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/context_terms.py +0 -0
  121. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/__init__.py +0 -0
  122. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/bias.py +0 -0
  123. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/base.py +0 -0
  124. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
  125. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
  126. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/lanes.py +0 -0
  127. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/decoders/whispercpp.py +0 -0
  128. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/engine.py +0 -0
  129. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/core/pipeline.py +0 -0
  130. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/corrections.py +0 -0
  131. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  132. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/data/__init__.py +0 -0
  133. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/demo.py +0 -0
  134. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/display.py +0 -0
  135. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/feedback.py +0 -0
  136. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/focus.py +0 -0
  137. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hints.py +0 -0
  138. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/history.py +0 -0
  139. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/__init__.py +0 -0
  140. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/base.py +0 -0
  141. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/chain.py +0 -0
  142. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/evdev.py +0 -0
  143. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/pynput_backend.py +0 -0
  144. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/hotkeys/socket_backend.py +0 -0
  145. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/__init__.py +0 -0
  146. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/engine.py +0 -0
  147. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/install.py +0 -0
  148. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/pending.py +0 -0
  149. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/session.py +0 -0
  150. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ibus/textlimit.py +0 -0
  151. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/idle_audio.py +0 -0
  152. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/__init__.py +0 -0
  153. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/augment.py +0 -0
  154. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/clean.py +0 -0
  155. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/importer.py +0 -0
  156. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/metrics.py +0 -0
  157. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/quiet.py +0 -0
  158. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/review.py +0 -0
  159. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/store.py +0 -0
  160. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/teacher.py +0 -0
  161. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/learn/train.py +0 -0
  162. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/llm.py +0 -0
  163. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/llm_api.py +0 -0
  164. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/mic_gain.py +0 -0
  165. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/models/__init__.py +0 -0
  166. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/models/silero_vad.onnx +0 -0
  167. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/numbers.py +0 -0
  168. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/pidlock.py +0 -0
  169. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/platform.py +0 -0
  170. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/postcorrect.py +0 -0
  171. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/power.py +0 -0
  172. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/presence.py +0 -0
  173. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/privacy.py +0 -0
  174. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/prompt.py +0 -0
  175. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/quiet_audio.py +0 -0
  176. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/recorder.py +0 -0
  177. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/report.py +0 -0
  178. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/retention.py +0 -0
  179. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/server/__init__.py +0 -0
  180. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/server/__main__.py +0 -0
  181. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/server/app.py +0 -0
  182. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/server/config.py +0 -0
  183. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/server/runtime.py +0 -0
  184. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/__init__.py +0 -0
  185. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/configfile.py +0 -0
  186. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/extras.py +0 -0
  187. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/hotkey.py +0 -0
  188. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/system.py +0 -0
  189. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/setup/ui.py +0 -0
  190. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/sounds/__init__.py +0 -0
  191. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/sounds/alert.wav +0 -0
  192. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/sounds/commit.wav +0 -0
  193. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/sounds/start.wav +0 -0
  194. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/sounds/stop.wav +0 -0
  195. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/streaming.py +0 -0
  196. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/suggest.py +0 -0
  197. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tokens.py +0 -0
  198. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tray/__init__.py +0 -0
  199. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tray/_icons.py +0 -0
  200. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tray/_indicator.py +0 -0
  201. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tray/_pystray.py +0 -0
  202. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/__init__.py +0 -0
  203. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/base.py +0 -0
  204. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/chain.py +0 -0
  205. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/edge_engine.py +0 -0
  206. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/espeak.py +0 -0
  207. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/piper_engine.py +0 -0
  208. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/tts/player.py +0 -0
  209. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/__init__.py +0 -0
  210. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/base.py +0 -0
  211. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/chain.py +0 -0
  212. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/clipboard.py +0 -0
  213. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/collecting.py +0 -0
  214. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/fcitx_bridge.py +0 -0
  215. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/ibus.py +0 -0
  216. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/manager.py +0 -0
  217. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/modifier_guard.py +0 -0
  218. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/paste.py +0 -0
  219. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/pynput_type.py +0 -0
  220. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/wtype.py +0 -0
  221. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/xdotool.py +0 -0
  222. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/typers/ydotool.py +0 -0
  223. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/ui.py +0 -0
  224. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/vad.py +0 -0
  225. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/vocab_stats.py +0 -0
  226. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/vocabulary.py +0 -0
  227. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/watchdog.py +0 -0
  228. {python_voiceio-1.11.0 → python_voiceio-1.12.0}/voiceio/wordfreq.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 1.11.0
3
+ Version: 1.12.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -65,7 +65,7 @@ Dynamic: license-file
65
65
  **[Try it in your browser at voiceio.dev](https://voiceio.dev)**: no install, the model runs in the tab.
66
66
 
67
67
  - **Types anywhere.** Press your hotkey, speak, and the words stream into the focused app with a live underlined preview (IBus), on Wayland and X11: GNOME, KDE, Hyprland, sway, i3.
68
- - **Runs on your machine.** Whisper decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
68
+ - **Runs on your machine.** Whisper, or NVIDIA's Parakeet (about 2x faster, with every name you use as a hotword), decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
69
69
  - **Learns your words.** Keep your recordings (opt-in, on your disk) and voiceio fine-tunes its model on them while the machine is idle. It switches only when the new model wins on clips it never trained on. On the author's voice: 12.1% → 4.9% word errors after one night on a laptop CPU.
70
70
 
71
71
  ## Install
@@ -127,7 +127,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
127
127
 
128
128
  ## Roadmap
129
129
 
130
- - **Unbounded vocabulary**: a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
130
+ - **Parakeet by default for everyone**: it needs our sherpa-onnx fix ([#4084](https://github.com/k2-fsa/sherpa-onnx/pull/4084)) in an upstream release; until then `voiceio models sherpa && voiceio models use parakeet-tdt-0.6b-v2` installs a patched build ([why](CONTRIBUTING.md#model-choice-parakeet-where-it-can-be-biased-whisper-otherwise)).
131
131
  - **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
132
132
  - **Typing without IBus on GNOME and KDE**: a libei backend.
133
133
  - **Distro packages**: AUR (packaged, not yet published) and .rpm.
@@ -139,7 +139,7 @@ Open an issue before a large PR. [CONTRIBUTING.md](CONTRIBUTING.md) covers the a
139
139
 
140
140
  ## Credits
141
141
 
142
- - 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 runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
142
+ - 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; [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) with NVIDIA's [Parakeet-TDT](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v2). The in-browser demo runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
143
143
  - Ideas borrowed with thanks: modifier release, paste keys and media pausing from [Voxtype](https://github.com/peteonrails/voxtype); the landing page's look from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
144
144
 
145
145
  ## License
@@ -12,7 +12,7 @@
12
12
  **[Try it in your browser at voiceio.dev](https://voiceio.dev)**: no install, the model runs in the tab.
13
13
 
14
14
  - **Types anywhere.** Press your hotkey, speak, and the words stream into the focused app with a live underlined preview (IBus), on Wayland and X11: GNOME, KDE, Hyprland, sway, i3.
15
- - **Runs on your machine.** Whisper decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
15
+ - **Runs on your machine.** Whisper, or NVIDIA's Parakeet (about 2x faster, with every name you use as a hotword), decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
16
16
  - **Learns your words.** Keep your recordings (opt-in, on your disk) and voiceio fine-tunes its model on them while the machine is idle. It switches only when the new model wins on clips it never trained on. On the author's voice: 12.1% → 4.9% word errors after one night on a laptop CPU.
17
17
 
18
18
  ## Install
@@ -74,7 +74,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
74
74
 
75
75
  ## Roadmap
76
76
 
77
- - **Unbounded vocabulary**: a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
77
+ - **Parakeet by default for everyone**: it needs our sherpa-onnx fix ([#4084](https://github.com/k2-fsa/sherpa-onnx/pull/4084)) in an upstream release; until then `voiceio models sherpa && voiceio models use parakeet-tdt-0.6b-v2` installs a patched build ([why](CONTRIBUTING.md#model-choice-parakeet-where-it-can-be-biased-whisper-otherwise)).
78
78
  - **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
79
79
  - **Typing without IBus on GNOME and KDE**: a libei backend.
80
80
  - **Distro packages**: AUR (packaged, not yet published) and .rpm.
@@ -86,7 +86,7 @@ Open an issue before a large PR. [CONTRIBUTING.md](CONTRIBUTING.md) covers the a
86
86
 
87
87
  ## Credits
88
88
 
89
- - 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 runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
89
+ - 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; [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) with NVIDIA's [Parakeet-TDT](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v2). The in-browser demo runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
90
90
  - Ideas borrowed with thanks: modifier release, paste keys and media pausing from [Voxtype](https://github.com/peteonrails/voxtype); the landing page's look from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
91
91
 
92
92
  ## License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "1.11.0"
7
+ version = "1.12.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.11.0
3
+ Version: 1.12.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -65,7 +65,7 @@ Dynamic: license-file
65
65
  **[Try it in your browser at voiceio.dev](https://voiceio.dev)**: no install, the model runs in the tab.
66
66
 
67
67
  - **Types anywhere.** Press your hotkey, speak, and the words stream into the focused app with a live underlined preview (IBus), on Wayland and X11: GNOME, KDE, Hyprland, sway, i3.
68
- - **Runs on your machine.** Whisper decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
68
+ - **Runs on your machine.** Whisper, or NVIDIA's Parakeet (about 2x faster, with every name you use as a hotword), decodes your speech locally. No account, no server, no telemetry; logs never contain your words.
69
69
  - **Learns your words.** Keep your recordings (opt-in, on your disk) and voiceio fine-tunes its model on them while the machine is idle. It switches only when the new model wins on clips it never trained on. On the author's voice: 12.1% → 4.9% word errors after one night on a laptop CPU.
70
70
 
71
71
  ## Install
@@ -127,7 +127,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
127
127
 
128
128
  ## Roadmap
129
129
 
130
- - **Unbounded vocabulary**: a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
130
+ - **Parakeet by default for everyone**: it needs our sherpa-onnx fix ([#4084](https://github.com/k2-fsa/sherpa-onnx/pull/4084)) in an upstream release; until then `voiceio models sherpa && voiceio models use parakeet-tdt-0.6b-v2` installs a patched build ([why](CONTRIBUTING.md#model-choice-parakeet-where-it-can-be-biased-whisper-otherwise)).
131
131
  - **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
132
132
  - **Typing without IBus on GNOME and KDE**: a libei backend.
133
133
  - **Distro packages**: AUR (packaged, not yet published) and .rpm.
@@ -139,7 +139,7 @@ Open an issue before a large PR. [CONTRIBUTING.md](CONTRIBUTING.md) covers the a
139
139
 
140
140
  ## Credits
141
141
 
142
- - 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 runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
142
+ - 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; [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx) with NVIDIA's [Parakeet-TDT](https://huggingface.co/nvidia/parakeet-tdt-0.6b-v2). The in-browser demo runs [Moonshine](https://github.com/usefulsensors/moonshine) through [transformers.js](https://github.com/huggingface/transformers.js).
143
143
  - Ideas borrowed with thanks: modifier release, paste keys and media pausing from [Voxtype](https://github.com/peteonrails/voxtype); the landing page's look from Romario Kavin's [KOLlateral](https://github.com/RomarioKavin1/kollateral).
144
144
 
145
145
  ## License
@@ -72,6 +72,7 @@ tests/test_security_hardening.py
72
72
  tests/test_server.py
73
73
  tests/test_service.py
74
74
  tests/test_setup.py
75
+ tests/test_sherpa.py
75
76
  tests/test_streaming.py
76
77
  tests/test_suggest.py
77
78
  tests/test_tokens.py
@@ -144,6 +145,7 @@ voiceio/core/pipeline.py
144
145
  voiceio/core/decoders/__init__.py
145
146
  voiceio/core/decoders/base.py
146
147
  voiceio/core/decoders/catalog.py
148
+ voiceio/core/decoders/fetch.py
147
149
  voiceio/core/decoders/lanes.py
148
150
  voiceio/core/decoders/sherpa.py
149
151
  voiceio/core/decoders/whispercpp.py
@@ -98,7 +98,7 @@ def test_example_config_matches_defaults():
98
98
  import dataclasses
99
99
  from pathlib import Path
100
100
 
101
- from voiceio.config import Config
101
+ from voiceio.config import Config, ModelConfig
102
102
  example = tomllib.loads(
103
103
  (Path(__file__).parent.parent / "config.example.toml").read_text())
104
104
  cfg = Config()
@@ -107,6 +107,11 @@ def test_example_config_matches_defaults():
107
107
  assert sec.name in example, f"[{sec.name}] missing from example"
108
108
  for f in dataclasses.fields(defaults):
109
109
  assert f.name in example[sec.name], f"{sec.name}.{f.name} missing"
110
+ if (sec.name, f.name) == ("model", "name"):
111
+ # "auto" resolves to the default decoder on construction
112
+ assert example["model"]["name"] == "auto"
113
+ assert ModelConfig(name="auto").name == defaults.name
114
+ continue
110
115
  assert example[sec.name][f.name] == getattr(defaults, f.name), \
111
116
  f"{sec.name}.{f.name}: example differs from default"
112
117
  for name, section in example.items():
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
 
4
4
  import io
5
5
  import json
6
- import sys
7
6
  import types
8
7
 
9
8
  import numpy as np
@@ -54,61 +53,6 @@ def test_whispercpp_errors_are_decode_errors(monkeypatch):
54
53
  dec.decode(np.zeros(1600, dtype=np.float32))
55
54
 
56
55
 
57
- def _fake_sherpa(monkeypatch, created):
58
- class Stream:
59
- def accept_waveform(self, sr, a):
60
- self.n = len(a)
61
-
62
- @property
63
- def result(self):
64
- return types.SimpleNamespace(text=" hello world ")
65
-
66
- class Rec:
67
- def create_stream(self):
68
- return Stream()
69
-
70
- def decode_stream(self, s):
71
- pass
72
-
73
- class OfflineRecognizer:
74
- @staticmethod
75
- def from_transducer(**kw):
76
- created.append(kw)
77
- return Rec()
78
-
79
- monkeypatch.setitem(sys.modules, "sherpa_onnx",
80
- types.SimpleNamespace(OfflineRecognizer=OfflineRecognizer))
81
-
82
-
83
- def _model_dir(tmp_path):
84
- for f in ("encoder.int8.onnx", "decoder.int8.onnx", "joiner.int8.onnx", "tokens.txt"):
85
- (tmp_path / f).write_text("x")
86
- return tmp_path
87
-
88
-
89
- def test_sherpa_greedy_declares_no_bias(monkeypatch, tmp_path):
90
- created = []
91
- _fake_sherpa(monkeypatch, created)
92
- dec = build_decoder(ModelConfig(name=f"sherpa:{_model_dir(tmp_path)}"))
93
- assert dec.capabilities.bias == BiasMode.NONE
94
- assert dec.decode(np.zeros(16000, dtype=np.float32),
95
- bias=Bias(hotwords="Kalshi")).text == "hello world"
96
- assert "hotwords_file" not in created[-1]
97
-
98
-
99
- def test_sherpa_mbs_passes_unbounded_hotwords(monkeypatch, tmp_path):
100
- created = []
101
- _fake_sherpa(monkeypatch, created)
102
- dec = build_decoder(ModelConfig(name=f"sherpa:{_model_dir(tmp_path)}",
103
- decoding="modified_beam_search"))
104
- assert dec.capabilities.bias == BiasMode.UNBOUNDED
105
- dec.decode(np.zeros(16000, dtype=np.float32), bias=Bias(hotwords="Kalshi, HyperNEAT"))
106
- kw = created[-1]
107
- assert kw["decoding_method"] == "modified_beam_search"
108
- with open(kw["hotwords_file"]) as f:
109
- assert f.read().split() == ["Kalshi", "HyperNEAT"]
110
-
111
-
112
56
  def test_lanes_share_the_thread_budget(monkeypatch):
113
57
  import voiceio.core.decoders.faster_whisper as tr
114
58
  from voiceio.core.decoders.faster_whisper.worker import auto_cpu_threads
@@ -70,6 +70,19 @@ class TestStripDisfluencies:
70
70
  "First para.\n\nSecond para."
71
71
  assert "\n\n" in strip_disfluencies("First para.\n\num\n\nSecond para.")
72
72
 
73
+ def test_filler_sentence_goes_with_its_full_stop(self):
74
+ # Parakeet writes a filler as a sentence of its own.
75
+ assert strip_disfluencies("escape them. Um. So on") == "escape them. So on"
76
+ assert strip_disfluencies("the scope of this? Um. I want") == "the scope of this? I want"
77
+ assert strip_disfluencies("Um. I want it") == "I want it"
78
+
79
+ def test_word_after_a_sentence_opening_filler_is_capitalized(self):
80
+ # Else "those. so I" reads as a false stop and cleanup makes it a comma.
81
+ assert strip_disfluencies("most of those. Um so I") == "most of those. So I"
82
+ assert strip_disfluencies("might be. Um uh yeah, and") == "might be. Yeah, and"
83
+ assert cleanup(strip_disfluencies("most of those. Um so I")) == "Most of those. So I"
84
+ assert strip_disfluencies("it was um uh good") == "it was good"
85
+
73
86
  def test_uh_huh_removed_whole(self):
74
87
  # Regression: ordering bug once stranded "-huh".
75
88
  assert strip_disfluencies("uh-huh right") == "right"
@@ -0,0 +1,369 @@
1
+ """Parakeet / sherpa-onnx: one recognizer, per-stream hotwords, chunking,
2
+ default selection, model download. sherpa-onnx is faked: no weights."""
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import io
7
+ import sys
8
+ import tarfile
9
+ import threading
10
+ import types
11
+
12
+ import numpy as np
13
+ import pytest
14
+
15
+ from voiceio import config
16
+ from voiceio.config import ModelConfig
17
+ from voiceio.core.decoders import BiasMode, build_decoder, catalog, fetch, sherpa
18
+ from voiceio.core.decoders.base import Bias, DecodeError
19
+ from voiceio.core.decoders.sherpa import SherpaFeatures, SherpaTransducer, Tokenizer
20
+
21
+ FIXED = SherpaFeatures(installed=True, length_reward=True, pretokenized_hotwords=True)
22
+ TOKENS = "\n".join(f"{p} {i}" for i, p in enumerate(
23
+ ["<unk>", "▁K", "al", "▁Kal", "sh", "hi", "shi", "▁", "K", "a", "l", "s", "h", "i",
24
+ "▁H", "▁Hy", "per", "p", "e", "r", "y", "H", "▁k", "k", "er"])) + "\n"
25
+
26
+
27
+ class FakeSherpa:
28
+ """Records recognizers built and the hotwords of every stream."""
29
+
30
+ def __init__(self, text="hello world", block: threading.Event | None = None):
31
+ self.created: list[dict] = []
32
+ self.streams: list[dict] = []
33
+ self.text = text
34
+ self.block = block
35
+ self.running = 0
36
+ self.max_running = 0
37
+ fake = self
38
+
39
+ class Stream:
40
+ def __init__(self, hotwords):
41
+ self.rec = {"hotwords": hotwords, "samples": 0}
42
+ fake.streams.append(self.rec)
43
+
44
+ def accept_waveform(self, sr, a):
45
+ assert sr == 16000 and a.dtype == np.float32
46
+ self.rec["samples"] = len(a)
47
+
48
+ @property
49
+ def result(self):
50
+ return types.SimpleNamespace(text=f" {fake.text} ")
51
+
52
+ class Rec:
53
+ def create_stream(self, hotwords=None):
54
+ return Stream(hotwords)
55
+
56
+ def decode_stream(self, s):
57
+ fake.running += 1
58
+ fake.max_running = max(fake.max_running, fake.running)
59
+ if fake.block is not None:
60
+ fake.block.wait(5)
61
+ fake.running -= 1
62
+
63
+ class OfflineRecognizer:
64
+ @staticmethod
65
+ def from_transducer(**kw):
66
+ fake.created.append(kw)
67
+ return Rec()
68
+
69
+ self.module = types.SimpleNamespace(OfflineRecognizer=OfflineRecognizer)
70
+
71
+
72
+ @pytest.fixture
73
+ def fake(monkeypatch):
74
+ f = FakeSherpa()
75
+ monkeypatch.setitem(sys.modules, "sherpa_onnx", f.module)
76
+ monkeypatch.setattr(sherpa, "features", lambda: FIXED)
77
+ return f
78
+
79
+
80
+ def _model_dir(tmp_path):
81
+ for name in ("encoder.int8.onnx", "decoder.int8.onnx", "joiner.int8.onnx"):
82
+ (tmp_path / name).write_text("x")
83
+ (tmp_path / "tokens.txt").write_text(TOKENS, encoding="utf-8")
84
+ return tmp_path
85
+
86
+
87
+ def _audio(secs: float) -> np.ndarray:
88
+ return np.full(int(secs * 16000), 0.1, dtype=np.float32)
89
+
90
+
91
+ # ── tokenizer ────────────────────────────────────────────────────────────
92
+
93
+ def test_tokenizer_merges_by_rank_like_sentencepiece_bpe(tmp_path):
94
+ tok = Tokenizer(_model_dir(tmp_path) / "tokens.txt")
95
+ assert tok.encode("Kalshi") == ["▁Kal", "shi"]
96
+ assert tok.encode("Hyper Kalshi") == ["▁Hy", "per", "▁Kal", "shi"]
97
+ assert tok.encode("Kalshi™") is None # a character with no piece
98
+ assert "<unk>" not in tok.bpe_vocab() and "▁Kal\t-3\n" in tok.bpe_vocab()
99
+
100
+
101
+ # ── the decoder ──────────────────────────────────────────────────────────
102
+
103
+ def test_one_recognizer_and_per_stream_pretokenized_proper_nouns(fake, tmp_path):
104
+ dec = SherpaTransducer(_model_dir(tmp_path), label="parakeet-tdt-0.6b-v2")
105
+ assert dec.capabilities.bias == BiasMode.UNBOUNDED
106
+ assert dec.name == "sherpa:parakeet-tdt-0.6b-v2"
107
+ kw = fake.created[0]
108
+ assert kw["decoding_method"] == "modified_beam_search"
109
+ assert (kw["length_reward"], kw["max_active_paths"], kw["hotwords_score"]) == (0.5, 4, 2.0)
110
+ assert "hotwords_file" not in kw and kw["modeling_unit"] == "bpe"
111
+ assert (tmp_path / "bpe.vocab").exists()
112
+
113
+ out = dec.decode(_audio(2), final=True, bias=Bias(hotwords="Kalshi, kalshi, Hyper, a/b"))
114
+ assert out.text == "hello world"
115
+ # Lowercase (mined jargon) and unusable terms never become hotwords.
116
+ assert fake.streams[-1]["hotwords"] == "▁Kal shi/▁Hy per"
117
+ dec.decode(_audio(2), bias=Bias(hotwords="Hyper"))
118
+ dec.decode(_audio(2), bias=Bias())
119
+ assert [s["hotwords"] for s in fake.streams[1:]] == ["▁Hy per", None]
120
+ assert len(fake.created) == 1, "a new hotword set must never reload the model"
121
+
122
+
123
+ def test_sherpa_tokenization_fallback_passes_terms(fake, monkeypatch, tmp_path):
124
+ monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, True, False))
125
+ dec = SherpaTransducer(_model_dir(tmp_path))
126
+ dec.decode(_audio(1), bias=Bias(hotwords="Kalshi, Hyper Kalshi"))
127
+ assert fake.streams[-1]["hotwords"] == "Kalshi/Hyper Kalshi"
128
+
129
+
130
+ def test_without_the_tdt_fix_decodes_greedy_and_unbiased(fake, monkeypatch, tmp_path):
131
+ monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, False, False))
132
+ dec = build_decoder(ModelConfig(name=f"sherpa:{_model_dir(tmp_path)}"))
133
+ assert dec.capabilities.bias == BiasMode.NONE
134
+ assert dec.name.endswith("@greedy_search")
135
+ assert dec.decode(_audio(1), bias=Bias(hotwords="Kalshi")).text == "hello world"
136
+ kw = fake.created[-1]
137
+ assert kw["decoding_method"] == "greedy_search" and "length_reward" not in kw
138
+ assert fake.streams[-1]["hotwords"] is None
139
+
140
+
141
+ def test_long_audio_is_decoded_in_pieces_of_at_most_30s(fake, tmp_path):
142
+ dec = SherpaTransducer(_model_dir(tmp_path))
143
+ audio = _audio(70)
144
+ audio[int(25 * 16000):int(25.4 * 16000)] = 0.0 # a pause to cut at
145
+ out = dec.decode(audio, final=True)
146
+ sizes = [s["samples"] for s in fake.streams]
147
+ assert sum(sizes) == len(audio) and all(n <= 30 * 16000 for n in sizes)
148
+ assert len(sizes) == 3 and 24.5 * 16000 < sizes[0] <= 25.5 * 16000
149
+ assert out.text == "hello world hello world hello world"
150
+
151
+
152
+ def test_empty_audio_is_silence_not_an_error(fake, tmp_path):
153
+ dec = SherpaTransducer(_model_dir(tmp_path))
154
+ assert dec.decode(np.zeros(0, dtype=np.float32)).text == ""
155
+
156
+
157
+ def test_decodes_run_concurrently_so_stop_never_queues(monkeypatch, tmp_path):
158
+ gate = threading.Event()
159
+ f = FakeSherpa(block=gate)
160
+ monkeypatch.setitem(sys.modules, "sherpa_onnx", f.module)
161
+ monkeypatch.setattr(sherpa, "features", lambda: FIXED)
162
+ dec = SherpaTransducer(_model_dir(tmp_path))
163
+ assert dec.draft is dec # lets stop decode past a pending freeze
164
+ threads = [threading.Thread(target=dec.decode, args=(_audio(1),)) for _ in range(2)]
165
+ for t in threads:
166
+ t.start()
167
+ for _ in range(200):
168
+ if f.running == 2:
169
+ break
170
+ threading.Event().wait(0.01)
171
+ assert f.max_running == 2
172
+ assert not dec.wait_idle(timeout=0.05)
173
+ gate.set()
174
+ for t in threads:
175
+ t.join(5)
176
+ assert dec.wait_idle(timeout=1)
177
+
178
+
179
+ def test_engine_errors_are_decode_errors(fake, tmp_path):
180
+ dec = SherpaTransducer(_model_dir(tmp_path))
181
+
182
+ def boom(s):
183
+ raise RuntimeError("ort")
184
+ dec._rec.decode_stream = boom
185
+ with pytest.raises(DecodeError):
186
+ dec.decode(_audio(1))
187
+ assert dec.wait_idle(timeout=1)
188
+
189
+
190
+ # ── default selection ────────────────────────────────────────────────────
191
+
192
+ def test_default_is_parakeet_only_when_sherpa_has_the_fix(monkeypatch):
193
+ assert catalog.default_model("en") == "small" # conftest: no sherpa
194
+ monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, False, False))
195
+ assert catalog.default_model("en") == "small"
196
+ monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, True, False))
197
+ assert catalog.default_model("en") == catalog.PARAKEET
198
+ assert catalog.default_model("es") == "small" # Parakeet v2 is English only
199
+
200
+
201
+ def test_config_resolves_unset_or_auto_name_but_an_explicit_choice_wins(monkeypatch, tmp_path):
202
+ monkeypatch.setattr(sherpa, "features", lambda: FIXED)
203
+ path = tmp_path / "c.toml"
204
+ path.write_text('[model]\nlanguage = "en"\n')
205
+ assert config.load(path).model.name == catalog.PARAKEET
206
+ path.write_text('[model]\nname = "auto"\n')
207
+ assert config.load(path).model.name == catalog.PARAKEET
208
+ for explicit in ("small", "/home/u/.local/share/voiceio/learn/models/v0001-small-x/ct2"):
209
+ path.write_text(f'[model]\nname = "{explicit}"\n')
210
+ assert config.load(path).model.name == explicit
211
+ monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures())
212
+ path.write_text('[model]\nlanguage = "en"\n')
213
+ assert config.load(path).model.name == "small"
214
+
215
+
216
+ def test_runtime_and_whisper_detection():
217
+ assert catalog.runtime_of(catalog.PARAKEET) == "sherpa-onnx"
218
+ assert catalog.runtime_of("sherpa:/models/x") == "sherpa-onnx"
219
+ assert catalog.is_whisper("small") and catalog.is_whisper("/learn/models/v1/ct2")
220
+ assert not catalog.is_whisper(catalog.PARAKEET)
221
+ m = catalog.info(catalog.PARAKEET)
222
+ assert m.url.startswith("https://github.com/k2-fsa/sherpa-onnx/releases/")
223
+ assert len(m.sha256) == 64 and "huggingface" not in m.url
224
+
225
+
226
+ def test_catalog_model_is_fetched_on_first_use(fake, monkeypatch, tmp_path):
227
+ got = []
228
+ (tmp_path / "m").mkdir()
229
+ model = _model_dir(tmp_path / "m")
230
+ monkeypatch.setattr(fetch, "download", lambda name, **k: got.append(name) or model)
231
+ dec = build_decoder(ModelConfig(name=catalog.PARAKEET))
232
+ assert got == [catalog.PARAKEET] and dec.name == f"sherpa:{catalog.PARAKEET}"
233
+
234
+
235
+ def test_no_download_when_sherpa_is_missing(monkeypatch):
236
+ monkeypatch.setattr(fetch, "download", lambda *a, **k: pytest.fail("downloaded"))
237
+ with pytest.raises(DecodeError, match="sherpa-onnx not installed"):
238
+ build_decoder(ModelConfig(name=catalog.PARAKEET))
239
+
240
+
241
+ # ── download ─────────────────────────────────────────────────────────────
242
+
243
+ def _tarball() -> bytes:
244
+ buf = io.BytesIO()
245
+ with tarfile.open(fileobj=buf, mode="w:bz2") as tar:
246
+ for name, data in (("encoder.int8.onnx", b"e"), ("decoder.int8.onnx", b"d"),
247
+ ("joiner.int8.onnx", b"j"), ("tokens.txt", TOKENS.encode()),
248
+ ("test_wavs/0.wav", b"w")):
249
+ info = tarfile.TarInfo(f"sherpa-onnx-nemo-parakeet-tdt-0.6b-v2-int8/{name}")
250
+ info.size = len(data)
251
+ tar.addfile(info, io.BytesIO(data))
252
+ return buf.getvalue()
253
+
254
+
255
+ def _serve(monkeypatch, body: bytes):
256
+ class Resp(io.BytesIO):
257
+ headers = {"Content-Length": str(len(body))}
258
+
259
+ def __enter__(self):
260
+ return self
261
+
262
+ def __exit__(self, *a):
263
+ return False
264
+
265
+ monkeypatch.setattr("urllib.request.urlopen", lambda req, timeout=0: Resp(body))
266
+
267
+
268
+ def _entry(monkeypatch, body: bytes):
269
+ m = catalog.ModelInfo("pk-test", "sherpa-onnx", "1", "x", BiasMode.UNBOUNDED,
270
+ url="https://github.com/x.tar.bz2",
271
+ sha256=hashlib.sha256(body).hexdigest(), size_mb=1)
272
+ monkeypatch.setattr(fetch, "info", lambda name: m if name == "pk-test" else None)
273
+
274
+
275
+ def test_download_verifies_and_unpacks_without_sample_wavs(monkeypatch, tmp_path):
276
+ monkeypatch.setattr(config, "MODELS_DIR", tmp_path)
277
+ body = _tarball()
278
+ _entry(monkeypatch, body)
279
+ _serve(monkeypatch, body)
280
+ seen = []
281
+ path = fetch.download("pk-test", progress=lambda d, t: seen.append((d, t)))
282
+ assert path == tmp_path / "pk-test" and fetch.is_downloaded("pk-test")
283
+ assert not (path / "test_wavs").exists()
284
+ assert seen[-1] == (len(body), len(body))
285
+ assert sorted(p.name for p in tmp_path.iterdir()) == ["pk-test"] # no leftovers
286
+
287
+
288
+ def test_download_with_a_bad_checksum_leaves_nothing(monkeypatch, tmp_path):
289
+ monkeypatch.setattr(config, "MODELS_DIR", tmp_path)
290
+ body = _tarball()
291
+ _entry(monkeypatch, body)
292
+ _serve(monkeypatch, body[:-10] + b"0123456789")
293
+ with pytest.raises(OSError, match="checksum"):
294
+ fetch.download("pk-test")
295
+ assert list(tmp_path.iterdir()) == []
296
+
297
+
298
+ def test_patched_wheel_is_picked_for_this_python_and_cpu():
299
+ sums = ("aa sherpa_onnx-1.13.8-cp312-cp312-linux_x86_64.whl\n"
300
+ "bb sherpa_onnx-1.13.8-cp313-cp313-manylinux_2_28_x86_64.whl\n"
301
+ "cc sherpa_onnx-1.13.8-cp313-cp313-linux_aarch64.whl\n")
302
+ assert fetch.patched_sherpa_wheel(sums, (3, 13), "x86_64") == (
303
+ "sherpa_onnx-1.13.8-cp313-cp313-manylinux_2_28_x86_64.whl", "bb")
304
+ assert fetch.patched_sherpa_wheel(sums, (3, 11), "x86_64") is None
305
+
306
+
307
+ # ── learn / doctor / eval ────────────────────────────────────────────────
308
+
309
+ def test_learn_says_fine_tunes_are_unused_while_parakeet_decodes(monkeypatch):
310
+ from voiceio.cli import learn
311
+ monkeypatch.setattr(learn, "_cfg", lambda: types.SimpleNamespace(
312
+ model=ModelConfig(name=catalog.PARAKEET)))
313
+ assert "isn't used" in learn._whisper_unused()
314
+ monkeypatch.setattr(learn, "_cfg", lambda: types.SimpleNamespace(
315
+ model=ModelConfig(name="small")))
316
+ assert learn._whisper_unused() == ""
317
+
318
+
319
+ def test_maintain_labels_but_does_not_train_for_an_unused_whisper(monkeypatch, capsys):
320
+ import argparse
321
+
322
+ from voiceio.cli import learn
323
+ from voiceio.config import LearnConfig
324
+ monkeypatch.setattr(learn, "_cfg", lambda: types.SimpleNamespace(
325
+ model=ModelConfig(name=catalog.PARAKEET)))
326
+ labeled = []
327
+ monkeypatch.setattr(learn, "cmd_label", lambda a: labeled.append(a))
328
+ monkeypatch.setattr(learn, "_announce_learning", lambda n: None)
329
+ monkeypatch.setattr(learn, "_train", lambda *a, **k: pytest.fail("trained"))
330
+ learn._maintain(argparse.Namespace(), LearnConfig())
331
+ assert labeled and "not retraining" in capsys.readouterr().out
332
+
333
+
334
+ def test_doctor_reports_the_decoder(monkeypatch):
335
+ from voiceio.health import _decoder_status, _learning_status
336
+ cfg = config.Config()
337
+ cfg.model.name = catalog.PARAKEET
338
+ assert "not installed" in _decoder_status(cfg)["Decoder"]
339
+ monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, False, False))
340
+ assert "NO vocabulary biasing" in _decoder_status(cfg)["Decoder"]
341
+ monkeypatch.setattr(sherpa, "features", lambda: FIXED)
342
+ assert "pre-tokenized" in _decoder_status(cfg)["Decoder"]
343
+ assert "aren't used" in _learning_status(cfg)["Voice model"]
344
+ cfg.model.name = "small"
345
+ assert catalog.PARAKEET in _decoder_status(cfg)["Decoder"]
346
+
347
+
348
+ def test_eval_results_are_found_for_sherpa_models(monkeypatch, tmp_path):
349
+ import json
350
+
351
+ from voiceio.learn import evaluate, metrics
352
+ runs = tmp_path / "runs"
353
+ runs.mkdir()
354
+ (runs / "eval-1.json").write_text(json.dumps({
355
+ "decoder": f"sherpa:{catalog.PARAKEET}", "dataset": "v1", "mode": "offline",
356
+ "metrics": metrics.METRICS_VERSION, "wer": 0.07}))
357
+ monkeypatch.setattr(evaluate, "learn_dir", lambda: tmp_path)
358
+ assert evaluate.latest(catalog.PARAKEET, "v1", beam=2)["wer"] == 0.07
359
+ assert evaluate.latest("small", "v1") is None
360
+
361
+
362
+ def test_wizard_offers_parakeet_first_only_where_it_can_bias(monkeypatch):
363
+ from voiceio.setup import speech
364
+ assert speech.model_choices("en") is speech.MODELS
365
+ monkeypatch.setattr(sherpa, "features", lambda: FIXED)
366
+ choices = speech.model_choices("en")
367
+ assert choices[0][0] == catalog.PARAKEET and speech.default_choice(choices) == 0
368
+ assert not any("recommended" in c[2] for c in choices[1:])
369
+ assert speech.model_choices("es") is speech.MODELS
@@ -0,0 +1 @@
1
+ __version__ = "1.12.0"
@@ -159,6 +159,17 @@ def _cfg():
159
159
  return config.load()
160
160
 
161
161
 
162
+ def _whisper_unused() -> str:
163
+ """Why a Whisper fine-tune does nothing right now, or "": `learn` only
164
+ trains Whisper, and dictation may decode with something else."""
165
+ from voiceio.core.decoders.catalog import is_whisper
166
+ active = _cfg().model.name
167
+ if is_whisper(active):
168
+ return ""
169
+ return (f"dictation decodes with {active}, not Whisper: a fine-tune trains Whisper and "
170
+ f"isn't used unless you switch back (voiceio learn promote RUN)")
171
+
172
+
162
173
  def _vocabulary() -> list[str]:
163
174
  from voiceio.vocabulary import VocabularyLoader
164
175
  return VocabularyLoader(_cfg().model).get_all()
@@ -196,6 +207,8 @@ def cmd_status(args: argparse.Namespace) -> None:
196
207
  print(f"models {', '.join(runs)}")
197
208
  active = _cfg().model.name
198
209
  print(f"active {active}")
210
+ if unused := _whisper_unused():
211
+ print(f"note {unused}")
199
212
  print(f"nightly {_schedule_state()}")
200
213
  promotable = _print_scores(ds, active, runs) if ds else None
201
214
  if promotable:
@@ -409,6 +422,8 @@ def _eval_pipeline() -> None:
409
422
 
410
423
  def cmd_train(args: argparse.Namespace) -> None:
411
424
  _yield_cpu()
425
+ if unused := _whisper_unused():
426
+ print(f"note: {unused}")
412
427
  ds = store.latest_dataset()
413
428
  if ds is None:
414
429
  raise SystemExit("no dataset — run `voiceio learn build` first")
@@ -535,6 +550,11 @@ def _maintain(args: argparse.Namespace, learn_cfg) -> None:
535
550
  if not os.environ.get(quiet.RESUMED_ENV):
536
551
  _announce_learning(learn_cfg.min_new)
537
552
  cmd_label(argparse.Namespace(teacher=learn_cfg.teacher or None, limit=None, relabel=False))
553
+ if unused := _whisper_unused():
554
+ # Labels still pay off (evals, a later switch back); nightly training
555
+ # of a model dictation doesn't use would only burn the CPU.
556
+ print(f"labeled; not retraining — {unused}. `voiceio learn train` trains anyway.")
557
+ return
538
558
  if missing := train.missing_train_deps():
539
559
  # Labeling alone is worth it; retraining waits for the extra. Exit 0:
540
560
  # a nightly timer must not fail every night over an optional install.