python-voiceio 1.12.0__tar.gz → 1.14.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 (232) hide show
  1. {python_voiceio-1.12.0/python_voiceio.egg-info → python_voiceio-1.14.0}/PKG-INFO +11 -2
  2. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/README.md +10 -1
  3. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/pyproject.toml +1 -1
  4. {python_voiceio-1.12.0 → python_voiceio-1.14.0/python_voiceio.egg-info}/PKG-INFO +11 -2
  5. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/python_voiceio.egg-info/SOURCES.txt +5 -0
  6. python_voiceio-1.14.0/tests/test_announce.py +84 -0
  7. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_app_wiring.py +1 -1
  8. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_report.py +19 -3
  9. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_sherpa.py +6 -1
  10. python_voiceio-1.14.0/tests/test_spool.py +87 -0
  11. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_tts.py +76 -0
  12. python_voiceio-1.14.0/voiceio/__init__.py +1 -0
  13. python_voiceio-1.14.0/voiceio/announce.py +106 -0
  14. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/app.py +73 -9
  15. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/__init__.py +25 -0
  16. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/models.py +4 -1
  17. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/config.py +2 -1
  18. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/catalog.py +18 -5
  19. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/fetch.py +18 -8
  20. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/feedback.py +4 -0
  21. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/health.py +4 -4
  22. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/socket_backend.py +11 -1
  23. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/recorder.py +11 -2
  24. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/report.py +13 -0
  25. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/server/runtime.py +1 -0
  26. python_voiceio-1.14.0/voiceio/spool.py +116 -0
  27. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/chain.py +4 -1
  28. python_voiceio-1.14.0/voiceio/tts/supertonic_engine.py +91 -0
  29. python_voiceio-1.12.0/voiceio/__init__.py +0 -1
  30. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/LICENSE +0 -0
  31. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
  32. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/python_voiceio.egg-info/entry_points.txt +0 -0
  33. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/python_voiceio.egg-info/requires.txt +0 -0
  34. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/python_voiceio.egg-info/top_level.txt +0 -0
  35. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/setup.cfg +0 -0
  36. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_audio_quality.py +0 -0
  37. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_audio_source.py +0 -0
  38. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_backend_probes.py +0 -0
  39. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_cancel.py +0 -0
  40. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_cli.py +0 -0
  41. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_clipboard_read.py +0 -0
  42. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_clipboard_typer.py +0 -0
  43. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_commands.py +0 -0
  44. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_concurrency_lockdown.py +0 -0
  45. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_config.py +0 -0
  46. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_context_terms.py +0 -0
  47. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_corrections.py +0 -0
  48. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_dead_mic.py +0 -0
  49. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_decoders.py +0 -0
  50. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_engine.py +0 -0
  51. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_evdev_uaccess.py +0 -0
  52. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_fallback.py +0 -0
  53. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_fcitx_bridge.py +0 -0
  54. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_feedback_stream.py +0 -0
  55. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_focus.py +0 -0
  56. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_health.py +0 -0
  57. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_hints.py +0 -0
  58. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_history.py +0 -0
  59. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_hotkey_modes.py +0 -0
  60. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_engine_limits.py +0 -0
  61. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_engine_listener.py +0 -0
  62. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_pending.py +0 -0
  63. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_pids.py +0 -0
  64. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_ping.py +0 -0
  65. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_session.py +0 -0
  66. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_textlimit.py +0 -0
  67. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_ibus_typer.py +0 -0
  68. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_idle_audio.py +0 -0
  69. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_clean.py +0 -0
  70. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_import.py +0 -0
  71. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_language.py +0 -0
  72. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_metrics.py +0 -0
  73. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_pipeline.py +0 -0
  74. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_quiet.py +0 -0
  75. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_resume.py +0 -0
  76. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_review.py +0 -0
  77. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_learn_targets.py +0 -0
  78. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_llm.py +0 -0
  79. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_llm_api.py +0 -0
  80. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_mic_gain.py +0 -0
  81. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_modifier_guard.py +0 -0
  82. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_numbers.py +0 -0
  83. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_paste_last.py +0 -0
  84. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_pipeline.py +0 -0
  85. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_platform.py +0 -0
  86. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_postcorrect.py +0 -0
  87. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_postprocess.py +0 -0
  88. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_prebuffer.py +0 -0
  89. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_presence.py +0 -0
  90. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_prompt.py +0 -0
  91. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_quiet_audio.py +0 -0
  92. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_recorder_integration.py +0 -0
  93. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_retention.py +0 -0
  94. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_robustness.py +0 -0
  95. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_security_hardening.py +0 -0
  96. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_server.py +0 -0
  97. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_service.py +0 -0
  98. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_setup.py +0 -0
  99. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_streaming.py +0 -0
  100. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_suggest.py +0 -0
  101. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_tokens.py +0 -0
  102. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_transcriber.py +0 -0
  103. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_typer_manager.py +0 -0
  104. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_vad.py +0 -0
  105. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_vocabulary.py +0 -0
  106. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_wordfreq.py +0 -0
  107. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/tests/test_worker_decode.py +0 -0
  108. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/__main__.py +0 -0
  109. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/audio_source.py +0 -0
  110. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/backends.py +0 -0
  111. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/configcmd.py +0 -0
  112. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/corrections.py +0 -0
  113. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/doctor.py +0 -0
  114. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/history.py +0 -0
  115. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/learn.py +0 -0
  116. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/servicecmd.py +0 -0
  117. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/uninstall.py +0 -0
  118. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/cli/vocab.py +0 -0
  119. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/clipboard_read.py +0 -0
  120. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/commands.py +0 -0
  121. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/consent.py +0 -0
  122. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/context_terms.py +0 -0
  123. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/__init__.py +0 -0
  124. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/bias.py +0 -0
  125. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/__init__.py +0 -0
  126. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/base.py +0 -0
  127. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
  128. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
  129. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/lanes.py +0 -0
  130. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/sherpa.py +0 -0
  131. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/decoders/whispercpp.py +0 -0
  132. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/engine.py +0 -0
  133. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/core/pipeline.py +0 -0
  134. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/corrections.py +0 -0
  135. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  136. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/data/__init__.py +0 -0
  137. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/demo.py +0 -0
  138. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/display.py +0 -0
  139. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/focus.py +0 -0
  140. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hints.py +0 -0
  141. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/history.py +0 -0
  142. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/__init__.py +0 -0
  143. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/base.py +0 -0
  144. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/chain.py +0 -0
  145. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/evdev.py +0 -0
  146. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/hotkeys/pynput_backend.py +0 -0
  147. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/__init__.py +0 -0
  148. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/engine.py +0 -0
  149. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/install.py +0 -0
  150. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/pending.py +0 -0
  151. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/session.py +0 -0
  152. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ibus/textlimit.py +0 -0
  153. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/idle_audio.py +0 -0
  154. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/__init__.py +0 -0
  155. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/augment.py +0 -0
  156. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/clean.py +0 -0
  157. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/evaluate.py +0 -0
  158. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/importer.py +0 -0
  159. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/metrics.py +0 -0
  160. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/quiet.py +0 -0
  161. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/review.py +0 -0
  162. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/store.py +0 -0
  163. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/teacher.py +0 -0
  164. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/learn/train.py +0 -0
  165. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/llm.py +0 -0
  166. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/llm_api.py +0 -0
  167. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/mic_gain.py +0 -0
  168. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/models/__init__.py +0 -0
  169. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/models/silero_vad.onnx +0 -0
  170. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/numbers.py +0 -0
  171. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/pidlock.py +0 -0
  172. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/platform.py +0 -0
  173. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/postcorrect.py +0 -0
  174. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/postprocess.py +0 -0
  175. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/power.py +0 -0
  176. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/presence.py +0 -0
  177. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/privacy.py +0 -0
  178. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/prompt.py +0 -0
  179. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/quiet_audio.py +0 -0
  180. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/retention.py +0 -0
  181. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/server/__init__.py +0 -0
  182. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/server/__main__.py +0 -0
  183. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/server/app.py +0 -0
  184. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/server/config.py +0 -0
  185. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/service.py +0 -0
  186. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/__init__.py +0 -0
  187. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/configfile.py +0 -0
  188. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/extras.py +0 -0
  189. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/hotkey.py +0 -0
  190. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/noninteractive.py +0 -0
  191. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/speech.py +0 -0
  192. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/steps.py +0 -0
  193. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/system.py +0 -0
  194. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/setup/ui.py +0 -0
  195. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/sounds/__init__.py +0 -0
  196. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/sounds/alert.wav +0 -0
  197. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/sounds/commit.wav +0 -0
  198. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/sounds/start.wav +0 -0
  199. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/sounds/stop.wav +0 -0
  200. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/streaming.py +0 -0
  201. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/suggest.py +0 -0
  202. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tokens.py +0 -0
  203. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tray/__init__.py +0 -0
  204. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tray/_icons.py +0 -0
  205. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tray/_indicator.py +0 -0
  206. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tray/_pystray.py +0 -0
  207. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/__init__.py +0 -0
  208. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/base.py +0 -0
  209. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/edge_engine.py +0 -0
  210. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/espeak.py +0 -0
  211. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/piper_engine.py +0 -0
  212. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/tts/player.py +0 -0
  213. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/__init__.py +0 -0
  214. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/base.py +0 -0
  215. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/chain.py +0 -0
  216. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/clipboard.py +0 -0
  217. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/collecting.py +0 -0
  218. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/fcitx_bridge.py +0 -0
  219. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/ibus.py +0 -0
  220. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/manager.py +0 -0
  221. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/modifier_guard.py +0 -0
  222. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/paste.py +0 -0
  223. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/pynput_type.py +0 -0
  224. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/wtype.py +0 -0
  225. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/xdotool.py +0 -0
  226. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/typers/ydotool.py +0 -0
  227. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/ui.py +0 -0
  228. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/vad.py +0 -0
  229. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/vocab_stats.py +0 -0
  230. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/vocabulary.py +0 -0
  231. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/watchdog.py +0 -0
  232. {python_voiceio-1.12.0 → python_voiceio-1.14.0}/voiceio/wordfreq.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 1.12.0
3
+ Version: 1.14.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, 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.
68
+ - **Runs on your machine.** Whisper, or NVIDIA's Parakeet (text lands ~0.7 s after you stop talking instead of ~4.4 s, 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
@@ -109,6 +109,15 @@ voiceio learn status # what it learned and how each model score
109
109
 
110
110
  Scheduled learning runs daily or weekly at a time you pick, on mains power and only while you're away from the computer: it stops within seconds when you come back and resumes later where it was. You get a notification with a button to stop it. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
111
111
 
112
+ ## Let your agents talk to you
113
+
114
+ ```bash
115
+ voiceio say --from build-bot "tests are green" # chime, notification, then it's read aloud
116
+ voiceio say --from deploy --urgent "I need your approval"
117
+ ```
118
+
119
+ Running several coding agents? Have each one call `voiceio say` when it needs you, so you hear *which* one. It never talks over you: while you dictate, announcements wait (speech into an open mic would get typed). They are read in your [tts] voice, offline.
120
+
112
121
  ## Configure
113
122
 
114
123
  Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](config.example.toml) documents every option. `voiceio config set section.key value` edits it in place.
@@ -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, 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.
15
+ - **Runs on your machine.** Whisper, or NVIDIA's Parakeet (text lands ~0.7 s after you stop talking instead of ~4.4 s, 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
@@ -56,6 +56,15 @@ voiceio learn status # what it learned and how each model score
56
56
 
57
57
  Scheduled learning runs daily or weekly at a time you pick, on mains power and only while you're away from the computer: it stops within seconds when you come back and resumes later where it was. You get a notification with a button to stop it. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
58
58
 
59
+ ## Let your agents talk to you
60
+
61
+ ```bash
62
+ voiceio say --from build-bot "tests are green" # chime, notification, then it's read aloud
63
+ voiceio say --from deploy --urgent "I need your approval"
64
+ ```
65
+
66
+ Running several coding agents? Have each one call `voiceio say` when it needs you, so you hear *which* one. It never talks over you: while you dictate, announcements wait (speech into an open mic would get typed). They are read in your [tts] voice, offline.
67
+
59
68
  ## Configure
60
69
 
61
70
  Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](config.example.toml) documents every option. `voiceio config set section.key value` edits it in place.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "1.12.0"
7
+ version = "1.14.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.12.0
3
+ Version: 1.14.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, 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.
68
+ - **Runs on your machine.** Whisper, or NVIDIA's Parakeet (text lands ~0.7 s after you stop talking instead of ~4.4 s, 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
@@ -109,6 +109,15 @@ voiceio learn status # what it learned and how each model score
109
109
 
110
110
  Scheduled learning runs daily or weekly at a time you pick, on mains power and only while you're away from the computer: it stops within seconds when you come back and resumes later where it was. You get a notification with a button to stop it. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
111
111
 
112
+ ## Let your agents talk to you
113
+
114
+ ```bash
115
+ voiceio say --from build-bot "tests are green" # chime, notification, then it's read aloud
116
+ voiceio say --from deploy --urgent "I need your approval"
117
+ ```
118
+
119
+ Running several coding agents? Have each one call `voiceio say` when it needs you, so you hear *which* one. It never talks over you: while you dictate, announcements wait (speech into an open mic would get typed). They are read in your [tts] voice, offline.
120
+
112
121
  ## Configure
113
122
 
114
123
  Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](config.example.toml) documents every option. `voiceio config set section.key value` edits it in place.
@@ -7,6 +7,7 @@ python_voiceio.egg-info/dependency_links.txt
7
7
  python_voiceio.egg-info/entry_points.txt
8
8
  python_voiceio.egg-info/requires.txt
9
9
  python_voiceio.egg-info/top_level.txt
10
+ tests/test_announce.py
10
11
  tests/test_app_wiring.py
11
12
  tests/test_audio_quality.py
12
13
  tests/test_audio_source.py
@@ -73,6 +74,7 @@ tests/test_server.py
73
74
  tests/test_service.py
74
75
  tests/test_setup.py
75
76
  tests/test_sherpa.py
77
+ tests/test_spool.py
76
78
  tests/test_streaming.py
77
79
  tests/test_suggest.py
78
80
  tests/test_tokens.py
@@ -85,6 +87,7 @@ tests/test_wordfreq.py
85
87
  tests/test_worker_decode.py
86
88
  voiceio/__init__.py
87
89
  voiceio/__main__.py
90
+ voiceio/announce.py
88
91
  voiceio/app.py
89
92
  voiceio/audio_source.py
90
93
  voiceio/backends.py
@@ -119,6 +122,7 @@ voiceio/recorder.py
119
122
  voiceio/report.py
120
123
  voiceio/retention.py
121
124
  voiceio/service.py
125
+ voiceio/spool.py
122
126
  voiceio/streaming.py
123
127
  voiceio/suggest.py
124
128
  voiceio/tokens.py
@@ -208,6 +212,7 @@ voiceio/tts/edge_engine.py
208
212
  voiceio/tts/espeak.py
209
213
  voiceio/tts/piper_engine.py
210
214
  voiceio/tts/player.py
215
+ voiceio/tts/supertonic_engine.py
211
216
  voiceio/typers/__init__.py
212
217
  voiceio/typers/base.py
213
218
  voiceio/typers/chain.py
@@ -0,0 +1,84 @@
1
+ """`voiceio say`: the message format, the queue, the socket and the CLI."""
2
+ import threading
3
+ import time
4
+ from unittest.mock import patch
5
+
6
+ from voiceio import announce
7
+ from voiceio.announce import Message
8
+
9
+
10
+ def test_encode_parse_round_trip():
11
+ msg = Message("tests are green", "build bot", urgent=True)
12
+ cmd = announce.encode(msg)
13
+ assert cmd.startswith("say:")
14
+ assert announce.parse(cmd[len("say:"):]) == msg
15
+ assert msg.spoken == "build bot: tests are green"
16
+ assert Message("hi").spoken == "hi"
17
+
18
+
19
+ def test_parse_rejects_junk_and_caps_length():
20
+ assert announce.parse("not json") is None
21
+ assert announce.parse('["a list"]') is None
22
+ assert announce.parse('{"text": " "}') is None
23
+ long = announce.parse('{"text": "%s", "from": "x\\n y"}' % ("a" * 5000))
24
+ assert len(long.text) == announce.MAX_TEXT and long.sender == "x y"
25
+
26
+
27
+ def test_announcer_holds_while_busy_then_speaks_in_order():
28
+ spoken, busy = [], threading.Event()
29
+ busy.set()
30
+ done = threading.Event()
31
+
32
+ def speak(text):
33
+ spoken.append(text)
34
+ if len(spoken) == 2:
35
+ done.set()
36
+
37
+ with patch.object(announce, "cue_and_notify"):
38
+ a = announce.Announcer(speak=speak, busy=busy.is_set)
39
+ a.submit(Message("one", "a"))
40
+ a.submit(Message("two", "b"))
41
+ time.sleep(0.4)
42
+ assert spoken == [] # held while the user dictates
43
+ busy.clear()
44
+ assert done.wait(3)
45
+ assert spoken == ["a: one", "b: two"]
46
+
47
+
48
+ def test_announcer_drops_when_full():
49
+ hold, spoken = threading.Event(), []
50
+ with patch.object(announce, "cue_and_notify"):
51
+ a = announce.Announcer(speak=lambda t: (hold.wait(5), spoken.append(t)), busy=lambda: False)
52
+ results = [a.submit(Message(str(i))) for i in range(announce._QUEUE + 3)]
53
+ hold.set()
54
+ deadline = time.monotonic() + 3 # drain inside the patch: no real notifications
55
+ while a._queue.unfinished_tasks and len(spoken) < sum(results) and time.monotonic() < deadline:
56
+ time.sleep(0.05)
57
+ assert not all(results)
58
+
59
+
60
+ def test_socket_routes_payload_commands(tmp_path, monkeypatch):
61
+ from voiceio.hotkeys import socket_backend as sb
62
+ monkeypatch.setattr(sb, "SOCKET_PATH", tmp_path / "v.sock")
63
+ got = threading.Event()
64
+ payloads = []
65
+ hk = sb.SocketHotkey()
66
+ hk.handle_payload("say", lambda p: (payloads.append(p), got.set()))
67
+ hk.start("x", lambda: None)
68
+ try:
69
+ assert sb.send_command(announce.encode(Message("hello", "agent")))
70
+ assert got.wait(3)
71
+ finally:
72
+ hk.stop()
73
+ assert announce.parse(payloads[0]) == Message("hello", "agent")
74
+
75
+
76
+ def test_cli_speaks_here_without_a_daemon(tmp_path, monkeypatch, capsys):
77
+ import sys
78
+ from voiceio import cli
79
+ from voiceio.hotkeys import socket_backend as sb
80
+ monkeypatch.setattr(sb, "SOCKET_PATH", tmp_path / "absent.sock")
81
+ monkeypatch.setattr(sys, "argv", ["voiceio", "say", "--from", "bot", "hi", "there"])
82
+ with patch.object(announce, "say_here", return_value=True) as here:
83
+ cli.main()
84
+ here.assert_called_once_with(Message("hi there", "bot", False))
@@ -366,7 +366,7 @@ class TestStartCue:
366
366
 
367
367
  vio.recorder.stream_health = health
368
368
  real_start = vio.recorder.start
369
- vio.recorder.start = lambda: (events.append("record"), real_start())
369
+ vio.recorder.start = lambda **kw: (events.append("record"), real_start(**kw))
370
370
  vio._quiet = MagicMock()
371
371
  return events
372
372
 
@@ -121,7 +121,23 @@ class _Resp(io.BytesIO):
121
121
  return False
122
122
 
123
123
 
124
- def test_post_success_reads_receipt(monkeypatch):
124
+ @pytest.fixture
125
+ def outside_pytest(monkeypatch):
126
+ """post() refuses under pytest; these tests check what it does past that.
127
+ (pytest sets PYTEST_CURRENT_TEST again after fixtures, so patch the check.)"""
128
+ monkeypatch.setattr(report, "_test_artifact", lambda text: "")
129
+
130
+
131
+ def test_post_refuses_test_runs_and_mock_text(monkeypatch):
132
+ monkeypatch.setattr(report, "_urlopen", lambda url, timeout: pytest.fail("posted"))
133
+ ok, why = report.post("voiceio 1.13.0 report")
134
+ assert not ok and "pytest" in why
135
+ monkeypatch.delenv("PYTEST_CURRENT_TEST", raising=False)
136
+ ok, why = report.post("system: Ubuntu · <MagicMock name='mock.platform.desktop'>")
137
+ assert not ok and "mock" in why
138
+
139
+
140
+ def test_post_success_reads_receipt(monkeypatch, outside_pytest):
125
141
  seen = []
126
142
 
127
143
  def fake(url, timeout):
@@ -138,13 +154,13 @@ def test_post_success_reads_receipt(monkeypatch):
138
154
 
139
155
  @pytest.mark.parametrize("body", [b'{"ok": false, "error": "rate limited"}', b"[]", b"not json",
140
156
  b'{"ok": true}'])
141
- def test_post_bad_response_is_failure(monkeypatch, body):
157
+ def test_post_bad_response_is_failure(monkeypatch, outside_pytest, body):
142
158
  monkeypatch.setattr(report, "_urlopen", lambda url, timeout: _Resp(body))
143
159
  ok, why = report.post("x")
144
160
  assert not ok and why
145
161
 
146
162
 
147
- def test_post_network_error_never_raises():
163
+ def test_post_network_error_never_raises(outside_pytest):
148
164
  # conftest refuses every connection: this is the offline path.
149
165
  ok, why = report.post("x")
150
166
  assert not ok and "ConnectionRefused" in why
@@ -195,7 +195,8 @@ def test_default_is_parakeet_only_when_sherpa_has_the_fix(monkeypatch):
195
195
  assert catalog.default_model("en") == "small"
196
196
  monkeypatch.setattr(sherpa, "features", lambda: SherpaFeatures(True, True, False))
197
197
  assert catalog.default_model("en") == catalog.PARAKEET
198
- assert catalog.default_model("es") == "small" # Parakeet v2 is English only
198
+ assert catalog.default_model("es") == catalog.PARAKEET_V3
199
+ assert catalog.default_model("ja") == "small" # outside Parakeet v3's languages
199
200
 
200
201
 
201
202
  def test_config_resolves_unset_or_auto_name_but_an_explicit_choice_wins(monkeypatch, tmp_path):
@@ -343,6 +344,10 @@ def test_doctor_reports_the_decoder(monkeypatch):
343
344
  assert "aren't used" in _learning_status(cfg)["Voice model"]
344
345
  cfg.model.name = "small"
345
346
  assert catalog.PARAKEET in _decoder_status(cfg)["Decoder"]
347
+ cfg.model.language = "es"
348
+ assert catalog.PARAKEET_V3 in _decoder_status(cfg)["Decoder"]
349
+ cfg.model.language = "ja"
350
+ assert _decoder_status(cfg) == {}
346
351
 
347
352
 
348
353
  def test_eval_results_are_found_for_sherpa_models(monkeypatch, tmp_path):
@@ -0,0 +1,87 @@
1
+ """The crash spool: a dictation's audio survives a killed daemon."""
2
+ import os
3
+
4
+ import numpy as np
5
+
6
+ from voiceio import spool
7
+
8
+
9
+ def _wait(s):
10
+ s.flush()
11
+
12
+
13
+ def test_recording_is_spooled_then_removed(tmp_path):
14
+ s = spool.Spool(tmp_path)
15
+ s.begin(7, 16000, prebuffer=np.ones(160, np.float32))
16
+ s.write(7, np.full((320, 1), 0.5, np.float32))
17
+ _wait(s)
18
+ path = s.path(7, 16000)
19
+ audio = np.fromfile(path, dtype=np.float32)
20
+ assert len(audio) == 480 and audio[-1] == 0.5
21
+ assert oct(path.stat().st_mode & 0o777) == "0o600"
22
+ s.finish(7)
23
+ _wait(s)
24
+ assert not path.exists()
25
+
26
+
27
+ def test_writes_for_unknown_or_finished_recordings_are_ignored(tmp_path):
28
+ s = spool.Spool(tmp_path)
29
+ s.write(1, np.ones(10, np.float32))
30
+ s.finish(1)
31
+ _wait(s)
32
+ assert list(tmp_path.iterdir()) == []
33
+
34
+
35
+ def test_orphans_are_files_of_dead_processes(tmp_path, monkeypatch):
36
+ long = np.zeros(32000, np.float32)
37
+ (tmp_path / "recording-999999-3-16000.f32").write_bytes(long.tobytes()) # dead
38
+ (tmp_path / f"recording-{os.getpid()}-4-16000.f32").write_bytes(long.tobytes()) # us
39
+ (tmp_path / "recording-999998-1-16000.f32").write_bytes(np.zeros(100, np.float32).tobytes())
40
+ (tmp_path / "notes.txt").write_text("x")
41
+ monkeypatch.setattr(spool, "_alive", lambda pid: pid != 999999 and pid != 999998)
42
+ found = spool.orphans(tmp_path)
43
+ assert [(p.name, len(a), r) for p, a, r in found] == [("recording-999999-3-16000.f32", 32000, 16000)]
44
+ assert not (tmp_path / "recording-999998-1-16000.f32").exists() # too short: dropped
45
+ assert (tmp_path / f"recording-{os.getpid()}-4-16000.f32").exists()
46
+
47
+
48
+ def test_missing_dir_has_no_orphans(tmp_path):
49
+ assert spool.orphans(tmp_path / "nope") == []
50
+
51
+
52
+ def test_recorder_taps_every_chunk_of_a_recording():
53
+ from unittest.mock import MagicMock
54
+ from voiceio.config import AudioConfig
55
+ from voiceio.recorder import AudioRecorder
56
+ rec = AudioRecorder(AudioConfig())
57
+ rec._stream = MagicMock()
58
+ rec._ring.append(np.ones((80, 1), np.float32))
59
+ got = []
60
+ rec.start(tap=got.append)
61
+ rec._callback(np.full((160, 1), 0.2, np.float32), 160, None, None)
62
+ rec.stop()
63
+ rec._callback(np.full((160, 1), 0.3, np.float32), 160, None, None) # after stop: not tapped
64
+ assert [len(c) for c in got] == [80, 160]
65
+
66
+
67
+ def test_daemon_recovers_an_orphan_into_history(tmp_path, monkeypatch):
68
+ from types import SimpleNamespace
69
+ from unittest.mock import MagicMock
70
+ from voiceio import app, feedback, history
71
+ orphan = tmp_path / "recording-999999-1-16000.f32"
72
+ orphan.write_bytes(np.zeros(32000, np.float32).tobytes())
73
+ monkeypatch.setattr(spool, "spool_dir", lambda: tmp_path)
74
+ monkeypatch.setattr(spool, "_alive", lambda pid: False)
75
+ saved, told = [], []
76
+ monkeypatch.setattr(history, "append", lambda text, **kw: saved.append((text, kw)))
77
+ monkeypatch.setattr(feedback, "notify", lambda title, body, urgent=False: told.append(body))
78
+ vio = app.VoiceIO.__new__(app.VoiceIO)
79
+ vio.recorder = SimpleNamespace(sample_rate=16000)
80
+ vio.engine = MagicMock()
81
+ vio.engine.transcribe.return_value = SimpleNamespace(text="the words that were cut off", raw="r")
82
+ vio.cfg = SimpleNamespace(history=None)
83
+ vio._recover_orphans()
84
+ assert saved[0][0] == "the words that were cut off"
85
+ assert saved[0][1]["extra"] == {"recovered": True}
86
+ assert "paste-last" in told[0]
87
+ assert not orphan.exists()
@@ -423,3 +423,79 @@ def test_piper_shutdown_releases_voice():
423
423
  engine._voice = MagicMock()
424
424
  engine.shutdown()
425
425
  assert engine._voice is None
426
+
427
+
428
+ # ── supertonic tests ─────────────────────────────────────────────────
429
+
430
+ class _FakeGenerated:
431
+ samples = [0.0, 0.5, -1.5]
432
+ sample_rate = 44100
433
+
434
+
435
+ def _fake_sherpa(calls):
436
+ so = MagicMock()
437
+
438
+ class Tts:
439
+ def __init__(self, config):
440
+ calls["config"] = config
441
+
442
+ def generate(self, text, gen):
443
+ calls["text"], calls["gen"] = text, gen
444
+ return _FakeGenerated()
445
+
446
+ so.OfflineTts = Tts
447
+ so.GenerationConfig = lambda: type("G", (), {})()
448
+ return so
449
+
450
+
451
+ def _supertonic_dir(path):
452
+ from voiceio.tts import supertonic_engine as st
453
+ for stem in st._STEMS:
454
+ (path / f"{stem}.int8.onnx").write_bytes(b"")
455
+ for f in st._FILES:
456
+ (path / f).write_bytes(b"")
457
+ return path
458
+
459
+
460
+ def test_supertonic_probe_needs_supertonic_in_sherpa():
461
+ from voiceio.tts.supertonic_engine import SupertonicEngine
462
+ old = MagicMock(spec=[]) # a sherpa-onnx without OfflineTtsSupertonicModelConfig
463
+ with patch.dict("sys.modules", {"sherpa_onnx": old}):
464
+ assert not SupertonicEngine().probe().ok
465
+ with patch.dict("sys.modules", {"sherpa_onnx": MagicMock()}):
466
+ assert SupertonicEngine().probe().ok
467
+
468
+
469
+ def test_supertonic_reads_in_its_language_and_returns_int16(tmp_path):
470
+ from voiceio.tts import supertonic_engine as st
471
+ calls = {}
472
+ model = _supertonic_dir(tmp_path)
473
+ with patch.dict("sys.modules", {"sherpa_onnx": _fake_sherpa(calls)}), \
474
+ patch("voiceio.core.decoders.fetch.download_archive", return_value=model) as dl:
475
+ engine = st.SupertonicEngine(language="es")
476
+ audio, rate = engine.synthesize("hola", voice="", speed=0)
477
+ engine.synthesize("otra vez", voice="2", speed=1.2)
478
+ dl.assert_called_once() # loaded once, then reused
479
+ assert dl.call_args.args[0] == st.MODEL
480
+ assert calls["gen"].extra == {"lang": "es"}
481
+ assert calls["gen"].sid == 2 and calls["gen"].speed == 1.2
482
+ assert rate == 44100 and audio.dtype == np.int16
483
+ assert audio.tolist()[2] == -32767 # clipped, not wrapped
484
+
485
+
486
+ def test_supertonic_auto_language_reads_english():
487
+ from voiceio.tts.supertonic_engine import SupertonicEngine
488
+ assert SupertonicEngine(language="auto")._language == "en"
489
+ assert SupertonicEngine()._language == "en"
490
+
491
+
492
+ def test_supertonic_completeness_check(tmp_path):
493
+ from voiceio.tts import supertonic_engine as st
494
+ assert not st._complete(tmp_path)
495
+ assert st._complete(_supertonic_dir(tmp_path))
496
+
497
+
498
+ def test_chain_passes_language_to_supertonic():
499
+ from voiceio.tts import chain
500
+ engine = chain._create("supertonic", TTSConfig(engine="supertonic", language="de"))
501
+ assert engine.name == "supertonic" and engine._language == "de"
@@ -0,0 +1 @@
1
+ __version__ = "1.14.0"
@@ -0,0 +1,106 @@
1
+ """`voiceio say`: let an agent or a script speak to you, and say who it is.
2
+
3
+ A chime, a desktop notification titled with the sender, then the sender's
4
+ name and the text read aloud. The daemon queues announcements and holds them
5
+ while you dictate: speech played into an open mic would be typed back out,
6
+ and talking over you is rude anyway. Without a daemon, `voiceio say` speaks
7
+ in its own process instead (slower: it loads the voice first).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import logging
13
+ import queue
14
+ import threading
15
+ import time
16
+ from collections.abc import Callable
17
+ from dataclasses import dataclass
18
+
19
+ log = logging.getLogger(__name__)
20
+
21
+ MAX_TEXT = 600 # characters read aloud; an announcement, not an essay
22
+ _QUEUE = 8 # waiting announcements beyond this are dropped
23
+ _HOLD_SECS = 600 # wait at most this long for a dictation to finish
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Message:
28
+ text: str
29
+ sender: str = ""
30
+ urgent: bool = False
31
+
32
+ @property
33
+ def spoken(self) -> str:
34
+ return f"{self.sender}: {self.text}" if self.sender else self.text
35
+
36
+
37
+ def encode(msg: Message) -> str:
38
+ """The control-socket command for `msg`."""
39
+ return "say:" + json.dumps({"text": msg.text, "from": msg.sender, "urgent": msg.urgent})
40
+
41
+
42
+ def parse(payload: str) -> Message | None:
43
+ try:
44
+ d = json.loads(payload)
45
+ text = " ".join(str(d.get("text", "")).split())[:MAX_TEXT]
46
+ sender = " ".join(str(d.get("from", "")).split())[:60]
47
+ except (ValueError, AttributeError):
48
+ return None
49
+ return Message(text, sender, bool(d.get("urgent"))) if text else None
50
+
51
+
52
+ def cue_and_notify(msg: Message) -> None:
53
+ """The chime and the notification: who is talking, even with no voice."""
54
+ from voiceio import feedback
55
+ if feedback.open_cue_stream():
56
+ (feedback.play_alert_sound if msg.urgent else feedback.play_commit_sound)()
57
+ time.sleep(0.4) # let the chime land before the voice starts
58
+ feedback.notify(msg.sender or "voiceio", msg.text, urgent=msg.urgent)
59
+
60
+
61
+ class Announcer:
62
+ """One announcement at a time, in order, on its own thread."""
63
+
64
+ def __init__(self, speak: Callable[[str], None], busy: Callable[[], bool],
65
+ hold_secs: float = _HOLD_SECS) -> None:
66
+ self._speak = speak
67
+ self._busy = busy
68
+ self._hold_secs = hold_secs
69
+ self._queue: queue.Queue[Message] = queue.Queue(maxsize=_QUEUE)
70
+ threading.Thread(target=self._run, name="announcer", daemon=True).start()
71
+
72
+ def submit(self, msg: Message) -> bool:
73
+ try:
74
+ self._queue.put_nowait(msg)
75
+ return True
76
+ except queue.Full:
77
+ log.warning("say: %d announcements waiting, dropped one from %s",
78
+ _QUEUE, msg.sender or "?")
79
+ return False
80
+
81
+ def _run(self) -> None:
82
+ while True:
83
+ msg = self._queue.get()
84
+ deadline = time.monotonic() + self._hold_secs
85
+ while self._busy() and time.monotonic() < deadline:
86
+ time.sleep(0.25)
87
+ try:
88
+ cue_and_notify(msg)
89
+ self._speak(msg.spoken)
90
+ except Exception:
91
+ log.exception("say: announcement from %s failed", msg.sender or "?")
92
+
93
+
94
+ def say_here(msg: Message) -> bool:
95
+ """No daemon: chime, notify and speak in this process. False if no TTS
96
+ engine works here (the chime and notification still happened)."""
97
+ from voiceio import config, tts
98
+ from voiceio.tts.player import TTSPlayer
99
+ cue_and_notify(msg)
100
+ cfg = config.load().tts
101
+ engine = tts.select(cfg, allow_network=False)
102
+ if engine is None:
103
+ return False
104
+ audio, rate = engine.synthesize(msg.spoken, cfg.voice, cfg.speed)
105
+ TTSPlayer().play(audio, rate)
106
+ return True