python-voiceio 1.15.2__tar.gz → 1.16.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 (234) hide show
  1. {python_voiceio-1.15.2/python_voiceio.egg-info → python_voiceio-1.16.0}/PKG-INFO +1 -1
  2. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/pyproject.toml +1 -1
  3. {python_voiceio-1.15.2 → python_voiceio-1.16.0/python_voiceio.egg-info}/PKG-INFO +1 -1
  4. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/SOURCES.txt +1 -0
  5. python_voiceio-1.16.0/tests/test_gpu_fallback.py +80 -0
  6. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_health.py +13 -0
  7. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_report.py +33 -4
  8. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_service.py +26 -0
  9. python_voiceio-1.16.0/voiceio/__init__.py +1 -0
  10. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/doctor.py +8 -5
  11. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/faster_whisper/__init__.py +5 -1
  12. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/faster_whisper/worker.py +35 -9
  13. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/health.py +58 -0
  14. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/report.py +38 -7
  15. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/service.py +28 -29
  16. python_voiceio-1.15.2/voiceio/__init__.py +0 -1
  17. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/LICENSE +0 -0
  18. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/README.md +0 -0
  19. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
  20. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/entry_points.txt +0 -0
  21. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/requires.txt +0 -0
  22. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/python_voiceio.egg-info/top_level.txt +0 -0
  23. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/setup.cfg +0 -0
  24. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_announce.py +0 -0
  25. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_app_wiring.py +0 -0
  26. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_audio_quality.py +0 -0
  27. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_audio_source.py +0 -0
  28. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_backend_probes.py +0 -0
  29. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_cancel.py +0 -0
  30. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_cli.py +0 -0
  31. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_clipboard_read.py +0 -0
  32. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_clipboard_typer.py +0 -0
  33. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_commands.py +0 -0
  34. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_concurrency_lockdown.py +0 -0
  35. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_config.py +0 -0
  36. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_context_terms.py +0 -0
  37. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_corrections.py +0 -0
  38. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_dead_mic.py +0 -0
  39. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_decoders.py +0 -0
  40. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_engine.py +0 -0
  41. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_evdev_uaccess.py +0 -0
  42. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_fallback.py +0 -0
  43. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_fcitx_bridge.py +0 -0
  44. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_feedback_stream.py +0 -0
  45. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_focus.py +0 -0
  46. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_hints.py +0 -0
  47. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_history.py +0 -0
  48. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_hotkey_modes.py +0 -0
  49. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_engine_limits.py +0 -0
  50. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_engine_listener.py +0 -0
  51. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_pending.py +0 -0
  52. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_pids.py +0 -0
  53. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_ping.py +0 -0
  54. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_session.py +0 -0
  55. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_textlimit.py +0 -0
  56. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_ibus_typer.py +0 -0
  57. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_idle_audio.py +0 -0
  58. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_clean.py +0 -0
  59. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_import.py +0 -0
  60. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_language.py +0 -0
  61. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_metrics.py +0 -0
  62. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_pipeline.py +0 -0
  63. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_quiet.py +0 -0
  64. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_resume.py +0 -0
  65. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_review.py +0 -0
  66. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_learn_targets.py +0 -0
  67. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_llm.py +0 -0
  68. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_llm_api.py +0 -0
  69. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_mic_gain.py +0 -0
  70. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_modifier_guard.py +0 -0
  71. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_numbers.py +0 -0
  72. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_paste_last.py +0 -0
  73. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_pipeline.py +0 -0
  74. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_platform.py +0 -0
  75. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_postcorrect.py +0 -0
  76. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_postprocess.py +0 -0
  77. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_prebuffer.py +0 -0
  78. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_presence.py +0 -0
  79. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_prompt.py +0 -0
  80. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_quiet_audio.py +0 -0
  81. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_recorder_integration.py +0 -0
  82. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_retention.py +0 -0
  83. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_robustness.py +0 -0
  84. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_security_hardening.py +0 -0
  85. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_server.py +0 -0
  86. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_setup.py +0 -0
  87. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_sherpa.py +0 -0
  88. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_spool.py +0 -0
  89. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_streaming.py +0 -0
  90. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_suggest.py +0 -0
  91. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_tokens.py +0 -0
  92. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_transcriber.py +0 -0
  93. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_tray.py +0 -0
  94. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_tts.py +0 -0
  95. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_typer_manager.py +0 -0
  96. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_vad.py +0 -0
  97. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_vocabulary.py +0 -0
  98. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_wordfreq.py +0 -0
  99. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/tests/test_worker_decode.py +0 -0
  100. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/__main__.py +0 -0
  101. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/announce.py +0 -0
  102. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/app.py +0 -0
  103. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/audio_source.py +0 -0
  104. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/backends.py +0 -0
  105. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/__init__.py +0 -0
  106. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/configcmd.py +0 -0
  107. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/corrections.py +0 -0
  108. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/history.py +0 -0
  109. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/learn.py +0 -0
  110. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/models.py +0 -0
  111. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/servicecmd.py +0 -0
  112. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/uninstall.py +0 -0
  113. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/cli/vocab.py +0 -0
  114. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/clipboard_read.py +0 -0
  115. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/commands.py +0 -0
  116. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/config.py +0 -0
  117. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/consent.py +0 -0
  118. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/context_terms.py +0 -0
  119. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/__init__.py +0 -0
  120. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/bias.py +0 -0
  121. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/__init__.py +0 -0
  122. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/base.py +0 -0
  123. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/catalog.py +0 -0
  124. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/fetch.py +0 -0
  125. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/lanes.py +0 -0
  126. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/sherpa.py +0 -0
  127. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/decoders/whispercpp.py +0 -0
  128. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/engine.py +0 -0
  129. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/core/pipeline.py +0 -0
  130. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/corrections.py +0 -0
  131. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  132. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/data/__init__.py +0 -0
  133. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/demo.py +0 -0
  134. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/display.py +0 -0
  135. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/feedback.py +0 -0
  136. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/focus.py +0 -0
  137. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hints.py +0 -0
  138. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/history.py +0 -0
  139. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/__init__.py +0 -0
  140. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/base.py +0 -0
  141. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/chain.py +0 -0
  142. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/evdev.py +0 -0
  143. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/pynput_backend.py +0 -0
  144. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/hotkeys/socket_backend.py +0 -0
  145. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/__init__.py +0 -0
  146. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/engine.py +0 -0
  147. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/install.py +0 -0
  148. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/pending.py +0 -0
  149. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/session.py +0 -0
  150. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ibus/textlimit.py +0 -0
  151. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/idle_audio.py +0 -0
  152. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/__init__.py +0 -0
  153. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/augment.py +0 -0
  154. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/clean.py +0 -0
  155. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/evaluate.py +0 -0
  156. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/importer.py +0 -0
  157. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/metrics.py +0 -0
  158. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/quiet.py +0 -0
  159. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/review.py +0 -0
  160. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/store.py +0 -0
  161. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/teacher.py +0 -0
  162. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/learn/train.py +0 -0
  163. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/llm.py +0 -0
  164. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/llm_api.py +0 -0
  165. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/mic_gain.py +0 -0
  166. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/models/__init__.py +0 -0
  167. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/models/silero_vad.onnx +0 -0
  168. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/numbers.py +0 -0
  169. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/pidlock.py +0 -0
  170. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/platform.py +0 -0
  171. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/postcorrect.py +0 -0
  172. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/postprocess.py +0 -0
  173. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/power.py +0 -0
  174. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/presence.py +0 -0
  175. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/privacy.py +0 -0
  176. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/prompt.py +0 -0
  177. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/quiet_audio.py +0 -0
  178. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/recorder.py +0 -0
  179. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/retention.py +0 -0
  180. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/server/__init__.py +0 -0
  181. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/server/__main__.py +0 -0
  182. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/server/app.py +0 -0
  183. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/server/config.py +0 -0
  184. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/server/runtime.py +0 -0
  185. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/__init__.py +0 -0
  186. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/configfile.py +0 -0
  187. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/extras.py +0 -0
  188. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/hotkey.py +0 -0
  189. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/noninteractive.py +0 -0
  190. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/speech.py +0 -0
  191. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/steps.py +0 -0
  192. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/system.py +0 -0
  193. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/setup/ui.py +0 -0
  194. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/sounds/__init__.py +0 -0
  195. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/sounds/alert.wav +0 -0
  196. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/sounds/commit.wav +0 -0
  197. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/sounds/start.wav +0 -0
  198. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/sounds/stop.wav +0 -0
  199. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/spool.py +0 -0
  200. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/streaming.py +0 -0
  201. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/suggest.py +0 -0
  202. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tokens.py +0 -0
  203. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tray/__init__.py +0 -0
  204. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tray/_icons.py +0 -0
  205. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tray/_indicator.py +0 -0
  206. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tray/_pystray.py +0 -0
  207. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/__init__.py +0 -0
  208. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/base.py +0 -0
  209. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/chain.py +0 -0
  210. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/edge_engine.py +0 -0
  211. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/espeak.py +0 -0
  212. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/piper_engine.py +0 -0
  213. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/player.py +0 -0
  214. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/tts/supertonic_engine.py +0 -0
  215. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/__init__.py +0 -0
  216. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/base.py +0 -0
  217. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/chain.py +0 -0
  218. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/clipboard.py +0 -0
  219. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/collecting.py +0 -0
  220. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/fcitx_bridge.py +0 -0
  221. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/ibus.py +0 -0
  222. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/manager.py +0 -0
  223. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/modifier_guard.py +0 -0
  224. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/paste.py +0 -0
  225. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/pynput_type.py +0 -0
  226. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/wtype.py +0 -0
  227. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/xdotool.py +0 -0
  228. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/typers/ydotool.py +0 -0
  229. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/ui.py +0 -0
  230. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/vad.py +0 -0
  231. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/vocab_stats.py +0 -0
  232. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/vocabulary.py +0 -0
  233. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/watchdog.py +0 -0
  234. {python_voiceio-1.15.2 → python_voiceio-1.16.0}/voiceio/wordfreq.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 1.15.2
3
+ Version: 1.16.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "1.15.2"
7
+ version = "1.16.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.15.2
3
+ Version: 1.16.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -29,6 +29,7 @@ tests/test_fallback.py
29
29
  tests/test_fcitx_bridge.py
30
30
  tests/test_feedback_stream.py
31
31
  tests/test_focus.py
32
+ tests/test_gpu_fallback.py
32
33
  tests/test_health.py
33
34
  tests/test_hints.py
34
35
  tests/test_history.py
@@ -0,0 +1,80 @@
1
+ """device = "auto" with a GPU whose CUDA libraries are missing: CPU, not a crash loop."""
2
+ from __future__ import annotations
3
+
4
+ import sys
5
+ from types import SimpleNamespace
6
+ from unittest.mock import patch
7
+
8
+ import pytest
9
+
10
+ worker = pytest.importorskip("voiceio.core.decoders.faster_whisper.worker")
11
+
12
+ ARGS = {"model": "small", "compute_type": "int8", "language": "en"}
13
+
14
+
15
+ def _loader(fail_on: set[str]):
16
+ devices = []
17
+
18
+ def load(args, device):
19
+ devices.append(device)
20
+ if device in fail_on:
21
+ raise RuntimeError("Library libcublas.so.12 is not found or cannot be loaded")
22
+ return SimpleNamespace(device=device)
23
+ return load, devices
24
+
25
+
26
+ def test_auto_falls_back_to_cpu_when_cuda_fails_at_first_encode(capsys):
27
+ load, devices = _loader({"auto"})
28
+ with patch.object(worker, "_load_and_warm", load):
29
+ model, why = worker.load_ready({**ARGS, "device": "auto"})
30
+ assert model.device == "cpu" and devices == ["auto", "cpu"]
31
+ assert "libcublas.so.12" in why
32
+ assert "decoding on CPU" in capsys.readouterr().err
33
+
34
+
35
+ def test_explicit_cuda_still_fails_loudly():
36
+ load, _ = _loader({"cuda"})
37
+ with patch.object(worker, "_load_and_warm", load), pytest.raises(RuntimeError):
38
+ worker.load_ready({**ARGS, "device": "cuda"})
39
+
40
+
41
+ def test_working_device_reports_no_fallback():
42
+ load, devices = _loader(set())
43
+ with patch.object(worker, "_load_and_warm", load):
44
+ assert worker.load_ready({**ARGS, "device": "auto"})[1] == ""
45
+ assert devices == ["auto"]
46
+
47
+
48
+ def test_doctor_names_missing_cuda_libs(monkeypatch):
49
+ from voiceio import health
50
+ from voiceio.config import Config
51
+ cfg = Config()
52
+ cfg.model.name, cfg.model.device = "small", "auto"
53
+ monkeypatch.setattr("voiceio.config.load", lambda: cfg)
54
+ monkeypatch.setitem(sys.modules, "ctranslate2",
55
+ SimpleNamespace(get_cuda_device_count=lambda: 1))
56
+ real = __import__("ctypes").CDLL
57
+
58
+ def cdll(name, *a, **k):
59
+ if name == "libcublas.so.12":
60
+ raise OSError(name)
61
+ return real("libc.so.6")
62
+ monkeypatch.setattr("ctypes.CDLL", cdll)
63
+ msg = health._gpu_libs_warning()
64
+ assert "libcublas.so.12" in msg and "libcudnn" not in msg and 'model.device = "cpu"' in msg
65
+ cfg.model.device = "cpu"
66
+ assert health._gpu_libs_warning() == ""
67
+
68
+
69
+ def test_daemon_accepts_the_fallback_handshake_and_logs_it(caplog):
70
+ from unittest.mock import MagicMock
71
+
72
+ from voiceio.config import ModelConfig
73
+ from voiceio.core.decoders.faster_whisper import Transcriber
74
+ proc = MagicMock()
75
+ proc.stdout.readline.return_value = "READY cpu-fallback: libcublas.so.12 missing\n"
76
+ proc.poll.return_value = None
77
+ with patch("voiceio.core.decoders.faster_whisper.subprocess.Popen", return_value=proc), \
78
+ caplog.at_level("WARNING"):
79
+ Transcriber(ModelConfig())
80
+ assert "decoding on CPU instead: libcublas.so.12 missing" in caplog.text
@@ -149,3 +149,16 @@ def test_check_ibus_asks_about_voiceio_only_when_the_engine_is_silent():
149
149
  alive.reset_mock()
150
150
  assert health._check_ibus().voiceio_running is True
151
151
  alive.assert_not_called()
152
+
153
+
154
+ def test_kde_wayland_without_ibus_input_method_warns(tmp_path):
155
+ from voiceio.health import _kde_input_method_warning
156
+ kde = Platform(os="linux", display_server="wayland", desktop="kde")
157
+ rc = tmp_path / "kwinrc"
158
+ rc.write_text("[Wayland]\nInputMethod[$e]=/usr/share/applications/com.github.maliit.keyboard.desktop\n")
159
+ assert "Virtual Keyboard" in _kde_input_method_warning(kde, rc)
160
+ rc.write_text("[Wayland]\nInputMethod[$e]=/usr/share/applications/org.freedesktop.IBus.Panel.Wayland.Gtk3.desktop\n")
161
+ assert _kde_input_method_warning(kde, rc) == ""
162
+ assert "Virtual Keyboard" in _kde_input_method_warning(kde, tmp_path / "missing")
163
+ gnome = Platform(os="linux", display_server="wayland", desktop="gnome")
164
+ assert _kde_input_method_warning(gnome, tmp_path / "missing") == ""
@@ -4,6 +4,7 @@ from __future__ import annotations
4
4
  import io
5
5
  import json
6
6
  import sys
7
+ from pathlib import Path
7
8
 
8
9
  import pytest
9
10
 
@@ -96,12 +97,12 @@ def test_report_contains_only_backend_and_model_names(identity):
96
97
  def test_report_lists_failures_and_warnings():
97
98
  health = _health(typer_backends=[BackendStatus("ibus", False, "daemon down")],
98
99
  cli_in_path=False, tray_ok=False, tray_reason="no AppIndicator")
99
- text = report.build_report(health, missing_deps=["portaudio"], perm_issues=2, cfg=Config())
100
+ text = report.build_report(health, missing_deps=["portaudio"], perm_issues=[Path(FAKE_HOME) / ".config/voiceio/config.toml"], cfg=Config())
100
101
  assert "typer none" in text
101
102
  assert "typer ibus: daemon down" in text
102
103
  assert "missing system deps" in text
103
104
  assert "cli not in PATH" in text and "tray: no AppIndicator" in text
104
- assert "insecure permissions on 2 file(s)" in text
105
+ assert "insecure permissions on ~/.config/voiceio/config.toml" in text
105
106
 
106
107
 
107
108
  def test_silent_ibus_engine_is_expected_while_voiceio_is_stopped():
@@ -109,9 +110,9 @@ def test_silent_ibus_engine_is_expected_while_voiceio_is_stopped():
109
110
  ib = IBusStatus(component_installed=True, daemon_running=True, env_persisted=True)
110
111
  stopped = report.build_report(_health(ibus_checks=IBusStatus(**{**vars(ib), "voiceio_running": False})),
111
112
  cfg=Config())
112
- assert "engine ping" not in stopped and "voiceio not running" in stopped
113
+ assert "engine ping" not in stopped and "voiceio was not running when this report was made" in stopped
113
114
  running = report.build_report(_health(ibus_checks=ib), cfg=Config())
114
- assert "ibus engine ping" in running and "voiceio not running" not in running
115
+ assert "ibus engine ping" in running and "not running" not in running
115
116
 
116
117
 
117
118
  def test_comment_is_one_bounded_line():
@@ -121,6 +122,34 @@ def test_comment_is_one_bounded_line():
121
122
  assert len(line) <= len("comment: ") + report.MAX_COMMENT
122
123
 
123
124
 
125
+ def test_long_comment_ends_with_an_ellipsis_at_a_word():
126
+ text = report.build_report(_health(), comment="word " * 400, cfg=Config())
127
+ line = next(ln for ln in text.splitlines() if ln.startswith("comment:"))
128
+ assert line.endswith("word…") and len(line) <= len("comment: ") + report.MAX_COMMENT
129
+ short = report.build_report(_health(), comment="fine " * 100, cfg=Config())
130
+ assert "…" not in short
131
+
132
+
133
+ def test_works_line_only_for_a_known_answer():
134
+ assert "works: no" in report.build_report(_health(), works="no", cfg=Config()).splitlines()
135
+ assert "works:" not in report.build_report(_health(), works="maybe", cfg=Config())
136
+
137
+
138
+ def test_many_insecure_paths_are_counted(identity):
139
+ paths = [Path(FAKE_HOME) / f"f{i}" for i in range(7)]
140
+ text = report.build_report(_health(), perm_issues=paths, cfg=Config())
141
+ assert "~/f0, ~/f1, ~/f2, ~/f3, ~/f4 and 2 more" in text
142
+ _assert_private(text)
143
+
144
+
145
+ def test_ask_details_maps_answers(monkeypatch):
146
+ answers = iter([" it typed twice ", "Y"])
147
+ monkeypatch.setattr(report, "_ask", lambda prompt: next(answers))
148
+ assert report.ask_details() == ("it typed twice", "yes")
149
+ answers = iter(["", "whatever"])
150
+ assert report.ask_details() == ("", "")
151
+
152
+
124
153
  def test_comment_drops_terminal_escapes():
125
154
  # A real 1.15.0 report arrived with "comment: \x1bv" (Alt+V at the prompt).
126
155
  text = report.build_report(_health(), comment="\x1bv", cfg=Config())
@@ -168,3 +168,29 @@ def test_reinstalling_the_service_refreshes_learning_units_that_are_on(monkeypat
168
168
  monkeypatch.setattr(service, "refresh_learn_timer", lambda: calls.append("refresh"))
169
169
  monkeypatch.setattr(service.subprocess, "run", lambda *a, **k: None)
170
170
  assert service.install_service() and calls == ["refresh"]
171
+
172
+
173
+ def test_slow_daemon_reload_still_enables_the_service(tmp_path, monkeypatch):
174
+ # Mint report: daemon-reload hit its 5s timeout and setup never ran enable.
175
+ import subprocess
176
+ from voiceio import service
177
+ monkeypatch.setattr(service, "has_systemd", lambda: True)
178
+ monkeypatch.setattr(service, "_find_voiceio_bin", lambda: "voiceio")
179
+ monkeypatch.setattr(service, "SERVICE_DIR", tmp_path)
180
+ monkeypatch.setattr(service, "SERVICE_PATH", tmp_path / service.SERVICE_NAME)
181
+ for name in ("install_symlinks", "uninstall_correct_timer", "refresh_learn_timer"):
182
+ monkeypatch.setattr(service, name, lambda: None)
183
+ ran = []
184
+
185
+ def run(cmd, **kw):
186
+ ran.append(cmd[2])
187
+ assert kw["timeout"] == service.SYSTEMCTL_TIMEOUT
188
+ if cmd[2] == "daemon-reload":
189
+ raise subprocess.TimeoutExpired(cmd, kw["timeout"])
190
+ return subprocess.CompletedProcess(cmd, 1 if cmd[2] == enable_fails else 0)
191
+ monkeypatch.setattr(service.subprocess, "run", run)
192
+ enable_fails = None
193
+ assert service.install_service()
194
+ assert ran == ["daemon-reload", "disable", "enable"]
195
+ enable_fails = "enable"
196
+ assert not service.install_service()
@@ -0,0 +1 @@
1
+ __version__ = "1.16.0"
@@ -87,7 +87,7 @@ def _cmd_doctor(args: argparse.Namespace) -> None:
87
87
  from voiceio.hints import hint
88
88
  hint("vocab_suggest", "Run 'voiceio vocab suggest' to find names it keeps mishearing")
89
89
  if args.report:
90
- _share_report(args, report, missing_deps, len(perm_issues))
90
+ _share_report(args, report, missing_deps, [p for p, _, _ in perm_issues])
91
91
  sys.exit(0 if (report.all_ok and not perm_issues and not missing_deps) else 1)
92
92
 
93
93
  # Auto-fix mode
@@ -199,16 +199,19 @@ def _cmd_doctor(args: argparse.Namespace) -> None:
199
199
 
200
200
  if args.report:
201
201
  _share_report(args, report, plat.check_system_deps(),
202
- len(_config.check_permissions()))
202
+ [p for p, _, _ in _config.check_permissions()])
203
203
  sys.exit(0 if report.all_ok else 1)
204
204
 
205
205
 
206
206
  def _share_report(args: argparse.Namespace, report, missing_deps: list[str],
207
- perm_issues: int) -> None:
208
- from voiceio.report import build_report, share
207
+ perm_issues: list) -> None:
208
+ from voiceio.report import _interactive, ask_details, build_report, share
209
+ comment, works = args.comment or "", ""
210
+ if not comment and not args.yes and _interactive():
211
+ comment, works = ask_details()
209
212
  try:
210
213
  text = build_report(report, missing_deps=missing_deps, perm_issues=perm_issues,
211
- comment=args.comment or "")
214
+ comment=comment, works=works)
212
215
  except Exception as e: # noqa: BLE001 — the report must not take doctor down
213
216
  print(f"\nCould not build the report ({type(e).__name__}); nothing was posted.")
214
217
  return
@@ -134,12 +134,16 @@ class Transcriber:
134
134
 
135
135
  t0 = time.monotonic()
136
136
  ready = self._read_with_timeout(WORKER_START_TIMEOUT)
137
- if ready is None or ready.strip() != "READY":
137
+ if ready is None or ready.split(" ", 1)[0].strip() != "READY":
138
138
  self._kill_worker()
139
139
  detail = "timed out" if ready is None else f"said {ready.strip()!r}"
140
140
  raise DecodeError(
141
141
  f"worker failed to start ({detail} after {time.monotonic() - t0:.0f}s)"
142
142
  )
143
+ if fallback := ready.strip().partition("cpu-fallback: ")[2]:
144
+ log.warning("GPU unusable, decoding on CPU instead: %s "
145
+ "(voiceio doctor shows the fix; model.device = \"cpu\" silences this)",
146
+ fallback)
143
147
  elapsed = time.monotonic() - t0
144
148
  log.info("Model ready (%.1fs)", elapsed)
145
149
 
@@ -135,6 +135,39 @@ def decode(model: WhisperModel, audio: np.ndarray, kwargs: dict) -> list:
135
135
  kwargs.get("initial_prompt"))})
136
136
 
137
137
 
138
+ def _load_and_warm(args: dict, device: str) -> WhisperModel:
139
+ model = load_model(
140
+ args["model"], device=device, compute_type=args["compute_type"],
141
+ cpu_threads=int(args.get("cpu_threads") or 0),
142
+ )
143
+ # Warmup: the first transcription is always slow. It also runs the first
144
+ # encode, where a CUDA build missing libcublas/libcudnn actually fails.
145
+ segs, _ = model.transcribe(np.zeros(16000, dtype=np.float32),
146
+ language=args.get("language"), beam_size=1)
147
+ list(segs)
148
+ return model
149
+
150
+
151
+ def load_ready(args: dict) -> tuple[WhisperModel, str]:
152
+ """The warmed-up model, and why it fell back to CPU ("" if it didn't).
153
+
154
+ device "auto" picks CUDA whenever a GPU is visible, even when the CUDA
155
+ libraries CTranslate2 needs are missing (a torch for another CUDA major
156
+ pulled in by the train extra): that failed every start and crash-looped
157
+ the service. On "auto" a GPU that can't run is a CPU decode instead;
158
+ an explicit "cuda" still fails loudly.
159
+ """
160
+ device = args["device"]
161
+ try:
162
+ return _load_and_warm(args, device), ""
163
+ except Exception as e: # noqa: BLE001
164
+ if device != "auto":
165
+ raise
166
+ reason = " ".join(str(e).split())[:200] or type(e).__name__
167
+ print(f"voiceio: GPU unusable ({reason}); decoding on CPU", file=sys.stderr, flush=True)
168
+ return _load_and_warm(args, "cpu"), reason
169
+
170
+
138
171
  def main() -> None:
139
172
  # A hung decode is killed by the parent after a timeout; SIGUSR1 first
140
173
  # dumps every thread's stack to stderr (worker.log) so hangs are diagnosable.
@@ -142,15 +175,8 @@ def main() -> None:
142
175
  faulthandler.register(signal.SIGUSR1, all_threads=True)
143
176
 
144
177
  args = json.loads(sys.argv[1])
145
- model = load_model(
146
- args["model"], device=args["device"], compute_type=args["compute_type"],
147
- cpu_threads=int(args.get("cpu_threads") or 0),
148
- )
149
-
150
- # Warmup: first transcription is always slow
151
- segs, _ = model.transcribe(np.zeros(16000, dtype=np.float32), language=args.get("language"), beam_size=1)
152
- list(segs)
153
- print("READY", flush=True)
178
+ model, fallback = load_ready(args)
179
+ print(f"READY cpu-fallback: {fallback}" if fallback else "READY", flush=True)
154
180
 
155
181
  for line in sys.stdin:
156
182
  if line.strip() == "QUIT":
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import logging
5
5
  from dataclasses import dataclass, field
6
+ from pathlib import Path
6
7
 
7
8
  from voiceio import platform as plat
8
9
  from voiceio.hotkeys import chain as hotkey_chain
@@ -125,9 +126,66 @@ def check_health(p: plat.Platform | None = None) -> HealthReport:
125
126
  "Grant it in System Settings → Privacy & Security → Accessibility."
126
127
  )
127
128
 
129
+ if gpu := _gpu_libs_warning():
130
+ report.warnings.append(gpu)
131
+ if kde := _kde_input_method_warning(report.platform):
132
+ report.warnings.append(kde)
133
+
128
134
  return report
129
135
 
130
136
 
137
+ def _kde_input_method_warning(p: plat.Platform, kwinrc: Path | None = None) -> str:
138
+ """KWin on Wayland only hands text to the input method picked under
139
+ Virtual Keyboard; without IBus there, voiceio has no live preview."""
140
+ if not (p.desktop == "kde" and p.display_server == "wayland"):
141
+ return ""
142
+ import configparser
143
+ rc = configparser.ConfigParser(interpolation=None, strict=False)
144
+ try:
145
+ rc.read(kwinrc or Path.home() / ".config" / "kwinrc")
146
+ except configparser.Error:
147
+ return ""
148
+ # KWin writes it as "InputMethod[$e]" (a KConfig flag), so match the prefix.
149
+ section = rc["Wayland"] if rc.has_section("Wayland") else {}
150
+ if any(k.startswith("inputmethod") and "ibus" in v.lower() for k, v in section.items()):
151
+ return ""
152
+ return ("KDE isn't using IBus as its Wayland input method, so there's no live preview. "
153
+ "System Settings → Keyboard → Virtual Keyboard → IBus Wayland, then log out and in.")
154
+
155
+
156
+ # What CTranslate2 4.x's CUDA build loads at the first encode.
157
+ _CUDA_LIBS = ("libcublas.so.12", "libcudnn.so.9")
158
+
159
+
160
+ def _gpu_libs_warning() -> str:
161
+ """Why Whisper can't use the GPU it sees, when that's a missing CUDA
162
+ library (a torch for another CUDA major, pulled in by the train extra,
163
+ is the usual way). The daemon then decodes on CPU instead."""
164
+ try:
165
+ from voiceio.config import load
166
+ cfg = load().model
167
+ from voiceio.core.decoders.catalog import is_whisper
168
+ if cfg.device not in ("auto", "cuda") or not is_whisper(cfg.name):
169
+ return ""
170
+ import ctranslate2
171
+ if ctranslate2.get_cuda_device_count() == 0:
172
+ return ""
173
+ except Exception: # noqa: BLE001 — no faster-whisper, or no CUDA build
174
+ return ""
175
+ import ctypes
176
+ missing = []
177
+ for lib in _CUDA_LIBS:
178
+ try:
179
+ ctypes.CDLL(lib)
180
+ except OSError:
181
+ missing.append(lib)
182
+ if not missing:
183
+ return ""
184
+ return (f"A GPU is present but {', '.join(missing)} can't be loaded, so Whisper "
185
+ "decodes on CPU. Set model.device = \"cpu\" in config.toml to stay on CPU, "
186
+ "or install the CUDA 12 runtime (cuBLAS 12 and cuDNN 9) to use the GPU.")
187
+
188
+
131
189
  def _check_features() -> FeatureGroups:
132
190
  """Check which features are active based on config and files."""
133
191
  groups = FeatureGroups()
@@ -19,6 +19,7 @@ import urllib.parse
19
19
  import urllib.request
20
20
  import uuid
21
21
  from pathlib import Path
22
+ from collections.abc import Sequence
22
23
  from typing import TYPE_CHECKING
23
24
 
24
25
  if TYPE_CHECKING:
@@ -27,7 +28,10 @@ if TYPE_CHECKING:
27
28
  BOARD_URL = "https://voiceio.dev/#feedback"
28
29
  POST_URL = "https://swarmmemo.com/w/voiceio/feedback"
29
30
  TIMEOUT_SECS = 8.0
30
- MAX_COMMENT = 300
31
+ MAX_COMMENT = 1000
32
+ # Insecure paths named in a report; more are counted.
33
+ MAX_PERM_PATHS = 5
34
+ WORKS = ("yes", "no", "not tried")
31
35
  MAX_DETAIL = 90
32
36
 
33
37
  # A terminal escape: CSI sequences (arrow keys) or ESC plus one key (Alt+key).
@@ -126,8 +130,9 @@ def build_report(
126
130
  health: HealthReport,
127
131
  *,
128
132
  missing_deps: list[str] | None = None,
129
- perm_issues: int = 0,
133
+ perm_issues: Sequence[Path] = (),
130
134
  comment: str = "",
135
+ works: str = "",
131
136
  cfg=None,
132
137
  ) -> str:
133
138
  """The exact public text: short, plain, and scrubbed."""
@@ -168,7 +173,9 @@ def build_report(
168
173
  if not health.tray_ok:
169
174
  warned.append(f"tray: {_short(health.tray_reason)}" if health.tray_reason else "tray")
170
175
  if perm_issues:
171
- warned.append(f"insecure permissions on {perm_issues} file(s)")
176
+ named = ", ".join(str(p) for p in perm_issues[:MAX_PERM_PATHS])
177
+ more = len(perm_issues) - MAX_PERM_PATHS
178
+ warned.append(f"insecure permissions on {named}" + (f" and {more} more" if more > 0 else ""))
172
179
  unavailable = [b.name for chosen, chain in ((hotkey, health.hotkey_backends),
173
180
  (typer, health.typer_backends))
174
181
  if chosen is not None for b in chain if not b.ok]
@@ -181,8 +188,8 @@ def build_report(
181
188
  f"backends: hotkey {hotkey.name if hotkey else 'none'} · "
182
189
  f"typer {typer.name if typer else 'none'} · audio {audio} "
183
190
  f"(vad {cfg.audio.vad_backend})"
184
- + (" · voiceio not running" if health.ibus_checks is not None
185
- and not health.ibus_checks.voiceio_running else ""),
191
+ + (" · voiceio was not running when this report was made"
192
+ if health.ibus_checks is not None and not health.ibus_checks.voiceio_running else ""),
186
193
  f"model: {_model_label(cfg.model.name)}",
187
194
  ]
188
195
  if unavailable:
@@ -191,7 +198,9 @@ def build_report(
191
198
  if warned:
192
199
  lines.append("warnings: " + "; ".join(warned))
193
200
  comment = "".join(c if c.isprintable() else " " for c in _ESCAPE.sub("", comment))
194
- comment = " ".join(comment.split())[:MAX_COMMENT]
201
+ if works in WORKS:
202
+ lines.append(f"works: {works}")
203
+ comment = _clip(" ".join(comment.split()), MAX_COMMENT)
195
204
  if comment:
196
205
  lines.append(f"comment: {comment}")
197
206
  return scrub("\n".join(lines))
@@ -233,6 +242,28 @@ def _urlopen(url: str, *, timeout: float):
233
242
  return urllib.request.urlopen(req, timeout=timeout)
234
243
 
235
244
 
245
+ def _clip(text: str, limit: int) -> str:
246
+ """At most `limit` characters, cut at a word with an ellipsis so a cut
247
+ comment doesn't read as the user's own last word."""
248
+ if len(text) <= limit:
249
+ return text
250
+ cut = text[:limit - 1]
251
+ word = cut.rsplit(" ", 1)[0]
252
+ return (word if len(word) >= len(cut) - 40 else cut) + "…"
253
+
254
+
255
+ def ask_details() -> tuple[str, str]:
256
+ """Two questions for a report filed from a terminal: what happened, and
257
+ whether dictation types into apps. Enter skips either."""
258
+ comment = _ask("\n What happened? One or two sentences for the public board "
259
+ "(Enter to skip): ").strip()
260
+ answer = _ask(" Does dictation type text into your apps? [y/n/not tried, Enter to skip]: ")
261
+ answer = answer.strip().lower()
262
+ works = {"y": "yes", "yes": "yes", "n": "no", "no": "no",
263
+ "not tried": "not tried", "t": "not tried", "nt": "not tried"}.get(answer, "")
264
+ return comment, works
265
+
266
+
236
267
  def _interactive() -> bool:
237
268
  return sys.stdin.isatty() and sys.stdout.isatty()
238
269
 
@@ -279,7 +310,7 @@ def gather(comment: str = "") -> tuple[HealthReport, str]:
279
310
  return health, build_report(
280
311
  health,
281
312
  missing_deps=plat.check_system_deps(),
282
- perm_issues=len(config.check_permissions()),
313
+ perm_issues=[p for p, _, _ in config.check_permissions()],
283
314
  comment=comment,
284
315
  )
285
316
 
@@ -149,10 +149,16 @@ WantedBy=timers.target
149
149
  return service, timer
150
150
 
151
151
 
152
+ # systemd --user can take well over 5s to answer on a busy or freshly booted
153
+ # session (a Mint report: daemon-reload timed out and setup never enabled the
154
+ # service).
155
+ SYSTEMCTL_TIMEOUT = 30
156
+
157
+
152
158
  def _systemctl(*args: str) -> bool:
153
159
  try:
154
160
  return subprocess.run(["systemctl", "--user", *args], capture_output=True,
155
- timeout=10).returncode == 0
161
+ timeout=SYSTEMCTL_TIMEOUT).returncode == 0
156
162
  except (FileNotFoundError, subprocess.TimeoutExpired):
157
163
  return False
158
164
 
@@ -233,7 +239,7 @@ def uninstall_correct_timer() -> None:
233
239
  return
234
240
  try:
235
241
  subprocess.run(["systemctl", "--user", "disable", "--now", CORRECT_TIMER_NAME],
236
- capture_output=True, timeout=5)
242
+ capture_output=True, timeout=SYSTEMCTL_TIMEOUT)
237
243
  except (FileNotFoundError, subprocess.TimeoutExpired):
238
244
  pass
239
245
  for p in (CORRECT_TIMER_PATH, CORRECT_SERVICE_PATH):
@@ -291,30 +297,23 @@ def install_service() -> bool:
291
297
  SERVICE_PATH.write_text(_service_unit(bin_path))
292
298
  log.info("Installed systemd service to %s", SERVICE_PATH)
293
299
 
294
- try:
295
- subprocess.run(
296
- ["systemctl", "--user", "daemon-reload"],
297
- capture_output=True, timeout=5,
298
- )
299
- # disable before enable so a prior [Install] target (older versions
300
- # wired WantedBy=default.target, which never restarts the service on a
301
- # GNOME logout/login) is unlinked instead of left as a stale want.
302
- subprocess.run(
303
- ["systemctl", "--user", "disable", SERVICE_NAME],
304
- capture_output=True, timeout=5,
305
- )
306
- subprocess.run(
307
- ["systemctl", "--user", "enable", SERVICE_NAME],
308
- capture_output=True, timeout=5,
309
- )
310
- log.info("Enabled %s", SERVICE_NAME)
311
- uninstall_correct_timer()
312
- refresh_learn_timer()
313
- install_symlinks() # the CLI must run the same install as the service
314
- return True
315
- except (FileNotFoundError, subprocess.TimeoutExpired) as e:
316
- log.warning("Could not enable service: %s", e)
300
+ # Each step on its own: a slow daemon-reload is worth a warning, not a
301
+ # reason to skip enabling (enable reloads the unit it links anyway).
302
+ if not _systemctl("daemon-reload"):
303
+ log.warning("systemctl --user daemon-reload failed or timed out; enabling anyway")
304
+ # disable before enable so a prior [Install] target (older versions
305
+ # wired WantedBy=default.target, which never restarts the service on a
306
+ # GNOME logout/login) is unlinked instead of left as a stale want.
307
+ _systemctl("disable", SERVICE_NAME)
308
+ if not _systemctl("enable", SERVICE_NAME):
309
+ log.warning("Could not enable %s: run `systemctl --user enable %s`",
310
+ SERVICE_NAME, SERVICE_NAME)
317
311
  return False
312
+ log.info("Enabled %s", SERVICE_NAME)
313
+ uninstall_correct_timer()
314
+ refresh_learn_timer()
315
+ install_symlinks() # the CLI must run the same install as the service
316
+ return True
318
317
 
319
318
 
320
319
  def uninstall_service() -> bool:
@@ -325,11 +324,11 @@ def uninstall_service() -> bool:
325
324
  try:
326
325
  subprocess.run(
327
326
  ["systemctl", "--user", "disable", SERVICE_NAME],
328
- capture_output=True, timeout=5,
327
+ capture_output=True, timeout=SYSTEMCTL_TIMEOUT,
329
328
  )
330
329
  subprocess.run(
331
330
  ["systemctl", "--user", "stop", SERVICE_NAME],
332
- capture_output=True, timeout=5,
331
+ capture_output=True, timeout=SYSTEMCTL_TIMEOUT,
333
332
  )
334
333
  except (FileNotFoundError, subprocess.TimeoutExpired):
335
334
  pass
@@ -343,7 +342,7 @@ def uninstall_service() -> bool:
343
342
  try:
344
343
  subprocess.run(
345
344
  ["systemctl", "--user", "daemon-reload"],
346
- capture_output=True, timeout=5,
345
+ capture_output=True, timeout=SYSTEMCTL_TIMEOUT,
347
346
  )
348
347
  except (FileNotFoundError, subprocess.TimeoutExpired):
349
348
  pass
@@ -388,7 +387,7 @@ def start_service() -> bool:
388
387
  try:
389
388
  result = subprocess.run(
390
389
  ["systemctl", "--user", "start", SERVICE_NAME],
391
- capture_output=True, timeout=5,
390
+ capture_output=True, timeout=SYSTEMCTL_TIMEOUT,
392
391
  )
393
392
  return result.returncode == 0
394
393
  except (FileNotFoundError, subprocess.TimeoutExpired):
@@ -1 +0,0 @@
1
- __version__ = "1.15.2"
File without changes