python-voiceio 1.16.1__tar.gz → 1.17.1__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 (236) hide show
  1. {python_voiceio-1.16.1/python_voiceio.egg-info → python_voiceio-1.17.1}/PKG-INFO +2 -2
  2. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/README.md +1 -1
  3. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/pyproject.toml +1 -1
  4. {python_voiceio-1.16.1 → python_voiceio-1.17.1/python_voiceio.egg-info}/PKG-INFO +2 -2
  5. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/python_voiceio.egg-info/SOURCES.txt +2 -0
  6. python_voiceio-1.17.1/tests/test_errorlog.py +136 -0
  7. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_security_hardening.py +88 -0
  8. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_setup.py +105 -0
  9. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_tts.py +51 -0
  10. python_voiceio-1.17.1/voiceio/__init__.py +1 -0
  11. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/app.py +6 -3
  12. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/__init__.py +8 -5
  13. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/doctor.py +66 -16
  14. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/learn.py +2 -1
  15. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/uninstall.py +6 -1
  16. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/config.py +40 -3
  17. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/faster_whisper/__init__.py +2 -2
  18. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/corrections.py +3 -2
  19. python_voiceio-1.17.1/voiceio/errorlog.py +178 -0
  20. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/health.py +25 -11
  21. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hints.py +2 -1
  22. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/history.py +1 -2
  23. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/install.py +2 -1
  24. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/augment.py +2 -2
  25. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/evaluate.py +2 -2
  26. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/importer.py +1 -2
  27. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/store.py +3 -7
  28. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/llm.py +6 -4
  29. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/platform.py +65 -0
  30. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/report.py +5 -0
  31. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/retention.py +2 -4
  32. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/configfile.py +2 -2
  33. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/noninteractive.py +6 -2
  34. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/steps.py +15 -1
  35. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/system.py +7 -2
  36. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tray/__init__.py +1 -0
  37. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tray/_indicator.py +4 -0
  38. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tray/_pystray.py +1 -0
  39. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/chain.py +3 -1
  40. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/vocab_stats.py +2 -1
  41. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/vocabulary.py +5 -1
  42. python_voiceio-1.16.1/voiceio/__init__.py +0 -1
  43. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/LICENSE +0 -0
  44. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/python_voiceio.egg-info/dependency_links.txt +0 -0
  45. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/python_voiceio.egg-info/entry_points.txt +0 -0
  46. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/python_voiceio.egg-info/requires.txt +0 -0
  47. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/python_voiceio.egg-info/top_level.txt +0 -0
  48. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/setup.cfg +0 -0
  49. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_announce.py +0 -0
  50. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_app_wiring.py +0 -0
  51. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_audio_quality.py +0 -0
  52. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_audio_source.py +0 -0
  53. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_backend_probes.py +0 -0
  54. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_cancel.py +0 -0
  55. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_cli.py +0 -0
  56. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_clipboard_read.py +0 -0
  57. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_clipboard_typer.py +0 -0
  58. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_commands.py +0 -0
  59. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_concurrency_lockdown.py +0 -0
  60. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_config.py +0 -0
  61. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_context_terms.py +0 -0
  62. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_corrections.py +0 -0
  63. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_dead_mic.py +0 -0
  64. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_decoders.py +0 -0
  65. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_engine.py +0 -0
  66. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_evdev_uaccess.py +0 -0
  67. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_fallback.py +0 -0
  68. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_fcitx_bridge.py +0 -0
  69. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_feedback_stream.py +0 -0
  70. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_focus.py +0 -0
  71. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_gpu_fallback.py +0 -0
  72. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_health.py +0 -0
  73. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_hints.py +0 -0
  74. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_history.py +0 -0
  75. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_hotkey_modes.py +0 -0
  76. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_engine_limits.py +0 -0
  77. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_engine_listener.py +0 -0
  78. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_pending.py +0 -0
  79. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_pids.py +0 -0
  80. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_ping.py +0 -0
  81. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_session.py +0 -0
  82. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_textlimit.py +0 -0
  83. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_ibus_typer.py +0 -0
  84. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_idle_audio.py +0 -0
  85. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_clean.py +0 -0
  86. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_import.py +0 -0
  87. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_language.py +0 -0
  88. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_metrics.py +0 -0
  89. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_pipeline.py +0 -0
  90. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_quiet.py +0 -0
  91. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_resume.py +0 -0
  92. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_review.py +0 -0
  93. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_learn_targets.py +0 -0
  94. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_llm.py +0 -0
  95. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_llm_api.py +0 -0
  96. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_mic_gain.py +0 -0
  97. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_modifier_guard.py +0 -0
  98. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_numbers.py +0 -0
  99. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_paste_last.py +0 -0
  100. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_pipeline.py +0 -0
  101. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_platform.py +0 -0
  102. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_postcorrect.py +0 -0
  103. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_postprocess.py +0 -0
  104. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_prebuffer.py +0 -0
  105. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_presence.py +0 -0
  106. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_prompt.py +0 -0
  107. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_quiet_audio.py +0 -0
  108. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_recorder_integration.py +0 -0
  109. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_report.py +0 -0
  110. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_retention.py +0 -0
  111. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_robustness.py +0 -0
  112. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_server.py +0 -0
  113. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_service.py +0 -0
  114. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_sherpa.py +0 -0
  115. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_spool.py +0 -0
  116. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_streaming.py +0 -0
  117. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_suggest.py +0 -0
  118. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_tokens.py +0 -0
  119. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_transcriber.py +0 -0
  120. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_tray.py +0 -0
  121. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_typer_manager.py +0 -0
  122. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_vad.py +0 -0
  123. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_vocabulary.py +0 -0
  124. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_wordfreq.py +0 -0
  125. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/tests/test_worker_decode.py +0 -0
  126. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/__main__.py +0 -0
  127. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/announce.py +0 -0
  128. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/audio_source.py +0 -0
  129. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/backends.py +0 -0
  130. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/configcmd.py +0 -0
  131. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/corrections.py +0 -0
  132. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/history.py +0 -0
  133. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/models.py +0 -0
  134. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/servicecmd.py +0 -0
  135. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/cli/vocab.py +0 -0
  136. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/clipboard_read.py +0 -0
  137. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/commands.py +0 -0
  138. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/consent.py +0 -0
  139. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/context_terms.py +0 -0
  140. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/__init__.py +0 -0
  141. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/bias.py +0 -0
  142. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/__init__.py +0 -0
  143. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/base.py +0 -0
  144. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/catalog.py +0 -0
  145. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
  146. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/fetch.py +0 -0
  147. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/lanes.py +0 -0
  148. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/sherpa.py +0 -0
  149. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/decoders/whispercpp.py +0 -0
  150. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/engine.py +0 -0
  151. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/core/pipeline.py +0 -0
  152. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  153. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/data/__init__.py +0 -0
  154. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/demo.py +0 -0
  155. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/display.py +0 -0
  156. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/feedback.py +0 -0
  157. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/focus.py +0 -0
  158. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/__init__.py +0 -0
  159. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/base.py +0 -0
  160. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/chain.py +0 -0
  161. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/evdev.py +0 -0
  162. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/pynput_backend.py +0 -0
  163. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/hotkeys/socket_backend.py +0 -0
  164. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/__init__.py +0 -0
  165. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/engine.py +0 -0
  166. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/pending.py +0 -0
  167. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/session.py +0 -0
  168. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ibus/textlimit.py +0 -0
  169. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/idle_audio.py +0 -0
  170. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/__init__.py +0 -0
  171. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/clean.py +0 -0
  172. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/metrics.py +0 -0
  173. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/quiet.py +0 -0
  174. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/review.py +0 -0
  175. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/teacher.py +0 -0
  176. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/learn/train.py +0 -0
  177. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/llm_api.py +0 -0
  178. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/mic_gain.py +0 -0
  179. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/models/__init__.py +0 -0
  180. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/models/silero_vad.onnx +0 -0
  181. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/numbers.py +0 -0
  182. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/pidlock.py +0 -0
  183. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/postcorrect.py +0 -0
  184. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/postprocess.py +0 -0
  185. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/power.py +0 -0
  186. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/presence.py +0 -0
  187. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/privacy.py +0 -0
  188. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/prompt.py +0 -0
  189. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/quiet_audio.py +0 -0
  190. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/recorder.py +0 -0
  191. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/server/__init__.py +0 -0
  192. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/server/__main__.py +0 -0
  193. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/server/app.py +0 -0
  194. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/server/config.py +0 -0
  195. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/server/runtime.py +0 -0
  196. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/service.py +0 -0
  197. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/__init__.py +0 -0
  198. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/extras.py +0 -0
  199. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/hotkey.py +0 -0
  200. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/speech.py +0 -0
  201. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/setup/ui.py +0 -0
  202. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/sounds/__init__.py +0 -0
  203. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/sounds/alert.wav +0 -0
  204. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/sounds/commit.wav +0 -0
  205. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/sounds/start.wav +0 -0
  206. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/sounds/stop.wav +0 -0
  207. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/spool.py +0 -0
  208. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/streaming.py +0 -0
  209. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/suggest.py +0 -0
  210. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tokens.py +0 -0
  211. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tray/_icons.py +0 -0
  212. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/__init__.py +0 -0
  213. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/base.py +0 -0
  214. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/edge_engine.py +0 -0
  215. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/espeak.py +0 -0
  216. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/piper_engine.py +0 -0
  217. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/player.py +0 -0
  218. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/tts/supertonic_engine.py +0 -0
  219. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/__init__.py +0 -0
  220. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/base.py +0 -0
  221. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/chain.py +0 -0
  222. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/clipboard.py +0 -0
  223. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/collecting.py +0 -0
  224. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/fcitx_bridge.py +0 -0
  225. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/ibus.py +0 -0
  226. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/manager.py +0 -0
  227. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/modifier_guard.py +0 -0
  228. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/paste.py +0 -0
  229. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/pynput_type.py +0 -0
  230. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/wtype.py +0 -0
  231. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/xdotool.py +0 -0
  232. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/typers/ydotool.py +0 -0
  233. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/ui.py +0 -0
  234. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/vad.py +0 -0
  235. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/watchdog.py +0 -0
  236. {python_voiceio-1.16.1 → python_voiceio-1.17.1}/voiceio/wordfreq.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 1.16.1
3
+ Version: 1.17.1
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -96,7 +96,7 @@ sudo apt install ./voiceio_*.deb
96
96
  voiceio setup
97
97
  ```
98
98
 
99
- Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
99
+ Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). Doctor offers one when a check fails, and so does the tray's "Report a problem...". A report names the last error only by its type and `file:line`, never its message. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
100
100
 
101
101
  ## Make it yours
102
102
 
@@ -43,7 +43,7 @@ sudo apt install ./voiceio_*.deb
43
43
  voiceio setup
44
44
  ```
45
45
 
46
- Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
46
+ Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). Doctor offers one when a check fails, and so does the tray's "Report a problem...". A report names the last error only by its type and `file:line`, never its message. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
47
47
 
48
48
  ## Make it yours
49
49
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "1.16.1"
7
+ version = "1.17.1"
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.16.1
3
+ Version: 1.17.1
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -96,7 +96,7 @@ sudo apt install ./voiceio_*.deb
96
96
  voiceio setup
97
97
  ```
98
98
 
99
- Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
99
+ Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works, `voiceio doctor --fix` repairs what it can, and `voiceio doctor --report` shares an anonymous install report on the [feedback board](https://voiceio.dev/#feedback) (it shows you the text and asks first). Doctor offers one when a check fails, and so does the tray's "Report a problem...". A report names the last error only by its type and `file:line`, never its message. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
100
100
 
101
101
  ## Make it yours
102
102
 
@@ -24,6 +24,7 @@ tests/test_corrections.py
24
24
  tests/test_dead_mic.py
25
25
  tests/test_decoders.py
26
26
  tests/test_engine.py
27
+ tests/test_errorlog.py
27
28
  tests/test_evdev_uaccess.py
28
29
  tests/test_fallback.py
29
30
  tests/test_fcitx_bridge.py
@@ -101,6 +102,7 @@ voiceio/context_terms.py
101
102
  voiceio/corrections.py
102
103
  voiceio/demo.py
103
104
  voiceio/display.py
105
+ voiceio/errorlog.py
104
106
  voiceio/feedback.py
105
107
  voiceio/focus.py
106
108
  voiceio/health.py
@@ -0,0 +1,136 @@
1
+ """The last error in a report, and the service's repeated-error nudge."""
2
+ from __future__ import annotations
3
+
4
+ import datetime as dt
5
+ import logging
6
+
7
+ from voiceio import errorlog
8
+
9
+ NOW = dt.datetime(2026, 11, 5, 15, 0, 0)
10
+ SECRET = "please wire the money to my sister on Friday"
11
+ PKG = errorlog._PKG
12
+
13
+
14
+ def _log(tmp_path, text):
15
+ path = tmp_path / "voiceio.log"
16
+ path.write_text(text)
17
+ return path
18
+
19
+
20
+ SERVICE_LOG = f"""\
21
+ 2026-11-05 12:58:10,961 voiceio INFO Recording started
22
+ 2026-11-05 12:58:11,000 voiceio.streaming ERROR Streaming failed on: {SECRET}
23
+ Traceback (most recent call last):
24
+ File "{PKG}/core/decoders/sherpa.py", line 280, in decode
25
+ stream = self._rec.create_stream(hotwords)
26
+ AttributeError: 'NoneType' object has no attribute 'create_stream'
27
+
28
+ The above exception was the direct cause of the following exception:
29
+
30
+ Traceback (most recent call last):
31
+ File "{PKG}/streaming.py", line 488, in _transcribe_and_apply
32
+ result = self._decoder.decode(tail)
33
+ File "/usr/lib/python3/dist-packages/numpy/core.py", line 9, in f
34
+ pass
35
+ voiceio.core.decoders.base.DecodeError: decode failed: {SECRET}
36
+ Word
37
+ {SECRET}
38
+ 2026-11-05 12:58:12,000 voiceio INFO voiceio stopped
39
+ """
40
+
41
+
42
+ def test_last_error_names_only_type_frame_and_age(tmp_path):
43
+ got = errorlog.last_error(_log(tmp_path, SERVICE_LOG), tmp_path / "none", now=NOW)
44
+ assert got == "DecodeError at voiceio/streaming.py:488 (2 h ago)"
45
+
46
+
47
+ def test_dictated_text_in_a_log_never_reaches_the_report(tmp_path):
48
+ from voiceio.health import BackendStatus, HealthReport
49
+ from voiceio.platform import Platform
50
+ from voiceio.report import build_report
51
+ from voiceio.config import Config
52
+
53
+ err = errorlog.last_error(_log(tmp_path, SERVICE_LOG), tmp_path / "none", now=NOW)
54
+ health = HealthReport(
55
+ platform=Platform(os="linux", display_server="wayland", desktop="gnome"),
56
+ hotkey_backends=[BackendStatus("evdev", True)],
57
+ typer_backends=[BackendStatus("ibus", True)], audio_ok=True)
58
+ text = build_report(health, last_error=err, cfg=Config())
59
+ assert "last error: DecodeError at voiceio/streaming.py:488" in text
60
+ for word in ("money", "sister", "Friday", "Word", "NoneType", "create_stream"):
61
+ assert word not in text
62
+
63
+
64
+ def test_an_error_without_traceback_names_its_logger(tmp_path):
65
+ log = f"2026-11-05 14:30:00,000 voiceio.recorder ERROR mic gone: {SECRET}\n"
66
+ got = errorlog.last_error(_log(tmp_path, log), tmp_path / "none", now=NOW)
67
+ assert got == "logged by voiceio.recorder (30 min ago)"
68
+
69
+
70
+ def test_errors_older_than_a_week_are_left_out(tmp_path):
71
+ old = SERVICE_LOG.replace("2026-11-05 12:58:11", "2026-10-20 12:58:11")
72
+ assert errorlog.last_error(_log(tmp_path, old), tmp_path / "none", now=NOW) == ""
73
+
74
+
75
+ def test_crash_log_keeps_the_frame_not_the_argv(tmp_path):
76
+ crash = tmp_path / "crash.log"
77
+ crash.write_text(f"""
78
+ ===== 2026-11-04T14:00:00.123456 =====
79
+ argv: ['voiceio', 'say', '{SECRET}']
80
+ Traceback (most recent call last):
81
+ File "/home/someone/src/voiceio/voiceio/cli/__init__.py", line 205, in _entry_point
82
+ main()
83
+ File "/home/someone/.venv/lib/python3.13/site-packages/numpy/__init__.py", line 1, in <module>
84
+ x
85
+ ImportError: cannot import {SECRET}
86
+ """)
87
+ got = errorlog.last_error(tmp_path / "none", crash, now=NOW)
88
+ assert got == "ImportError at voiceio/cli/__init__.py:205 (1 d ago)"
89
+
90
+
91
+ def test_the_newest_of_service_and_crash_errors_wins(tmp_path):
92
+ crash = tmp_path / "crash.log"
93
+ crash.write_text("===== 2026-11-05T14:59:00 =====\nargv: []\n"
94
+ "Traceback (most recent call last):\n"
95
+ f' File "{PKG}/app.py", line 7, in run\n x\nKeyError: 1\n')
96
+ got = errorlog.last_error(_log(tmp_path, SERVICE_LOG), crash, now=NOW)
97
+ assert got == "KeyError at voiceio/app.py:7 (1 min ago)"
98
+
99
+
100
+ def _record(msg, *args):
101
+ return logging.LogRecord("voiceio.streaming", logging.ERROR, __file__, 1, msg, args, None)
102
+
103
+
104
+ def test_the_same_error_three_times_notifies_once_a_day(tmp_path):
105
+ sent = []
106
+ h = errorlog.RepeatedErrorNotifier(tmp_path / "state.json",
107
+ notify=lambda title, body: sent.append(body))
108
+ for n in range(2):
109
+ h.emit(_record("decode failed: %s", f"words {n}"))
110
+ h.emit(_record("something else"))
111
+ assert sent == []
112
+ h.emit(_record("decode failed: %s", "more words"))
113
+ assert sent == ["Run 'voiceio doctor --report' to tell us."]
114
+ # A restarted daemon reads the same state: no second nudge today.
115
+ h2 = errorlog.RepeatedErrorNotifier(tmp_path / "state.json",
116
+ notify=lambda title, body: sent.append(body))
117
+ for _ in range(3):
118
+ h2.emit(_record("decode failed: %s", "x"))
119
+ assert len(sent) == 1
120
+
121
+
122
+ def test_doctor_offers_a_report_only_after_a_failure_in_a_terminal(monkeypatch):
123
+ import argparse
124
+
125
+ from voiceio import report
126
+ from voiceio.cli.doctor import _offer_report
127
+
128
+ asked = []
129
+ monkeypatch.setattr(report, "_ask", lambda prompt: asked.append(prompt) or "y")
130
+ monkeypatch.setattr(report, "_interactive", lambda: True)
131
+ args = argparse.Namespace(yes=False)
132
+ assert not _offer_report(args, healthy=True)
133
+ assert not _offer_report(argparse.Namespace(yes=True), healthy=False)
134
+ assert _offer_report(args, healthy=False) and "Something isn't working" in asked[0]
135
+ monkeypatch.setattr(report, "_interactive", lambda: False)
136
+ assert not _offer_report(args, healthy=False)
@@ -85,6 +85,94 @@ def test_check_permissions_flags_loose_and_clears_after_harden(tmp_path, monkeyp
85
85
  assert config.check_permissions() == []
86
86
 
87
87
 
88
+ @pytest.fixture
89
+ def umask_002():
90
+ """The user-private-group umask of Debian/Fedora desktops: plain mkdir
91
+ gives 0775, which is how the reported ~/.local/state/voiceio got there."""
92
+ import os
93
+ old = os.umask(0o002)
94
+ try:
95
+ yield
96
+ finally:
97
+ os.umask(old)
98
+
99
+
100
+ @posix_only
101
+ def test_private_dir_creates_0700_with_missing_parents(tmp_path, umask_002):
102
+ (tmp_path / "home").mkdir(mode=0o755)
103
+ target = tmp_path / "home" / ".local" / "state" / "voiceio"
104
+ assert config.private_dir(target) == target
105
+ assert _mode(target) == 0o700
106
+ assert _mode(target.parent) == 0o700 # XDG: created dirs are 0700
107
+ assert _mode(tmp_path / "home") == 0o755 # existing parents untouched
108
+
109
+
110
+ @posix_only
111
+ def test_private_dir_tightens_existing_lax_dir(tmp_path):
112
+ d = tmp_path / "voiceio"
113
+ d.mkdir()
114
+ d.chmod(0o775)
115
+ config.private_dir(d)
116
+ assert _mode(d) == 0o700
117
+
118
+
119
+ @posix_only
120
+ def test_crash_log_dir_is_private(tmp_path, monkeypatch, umask_002, capsys):
121
+ from voiceio import cli
122
+ log_dir = tmp_path / "state" / "voiceio"
123
+ monkeypatch.setattr(config, "LOG_DIR", log_dir)
124
+
125
+ def boom():
126
+ raise RuntimeError("crash before logging")
127
+
128
+ monkeypatch.setattr(cli, "main", boom)
129
+ with pytest.raises(SystemExit):
130
+ cli._entry_point()
131
+ assert (log_dir / "crash.log").exists()
132
+ assert _mode(log_dir) == 0o700
133
+ assert _mode(log_dir.parent) == 0o700
134
+
135
+
136
+ @posix_only
137
+ def test_error_notifier_state_dir_is_private(tmp_path, umask_002):
138
+ import logging
139
+ from voiceio.errorlog import RepeatedErrorNotifier
140
+ state = tmp_path / "state" / "voiceio" / "error-notify.json"
141
+ handler = RepeatedErrorNotifier(state_path=state, notify=lambda *a, **k: None)
142
+ handler.emit(logging.LogRecord("voiceio.x", logging.ERROR, __file__, 1, "boom", (), None))
143
+ assert state.exists()
144
+ assert _mode(state.parent) == 0o700
145
+
146
+
147
+ @posix_only
148
+ def test_secure_write_missing_parents_are_private(tmp_path, umask_002):
149
+ target = tmp_path / "config" / "voiceio" / "config.toml"
150
+ config.secure_write(target, "x = 1\n")
151
+ assert _mode(target.parent) == 0o700
152
+ assert _mode(target.parent.parent) == 0o700
153
+
154
+
155
+ @posix_only
156
+ def test_label_store_dirs_are_private(tmp_path, monkeypatch, umask_002):
157
+ from voiceio.learn import store
158
+ monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "share"))
159
+ store.add_label("a.wav", [], source="test")
160
+ assert _mode(store.learn_dir()) == 0o700
161
+ assert _mode(config.data_dir()) == 0o700
162
+
163
+
164
+ @posix_only
165
+ def test_check_permissions_flags_lax_learn_dir(tmp_path, monkeypatch):
166
+ monkeypatch.setenv("XDG_DATA_HOME", str(tmp_path / "share"))
167
+ learn = config.data_dir() / "learn"
168
+ learn.mkdir(parents=True)
169
+ learn.chmod(0o775)
170
+ assert learn in {p for p, _, _ in config.check_permissions()}
171
+ config.harden_permissions()
172
+ assert _mode(learn) == 0o700
173
+ assert learn not in {p for p, _, _ in config.check_permissions()}
174
+
175
+
88
176
  # ── udev rule (replaces sudo chmod 0666 /dev/uinput) ─────────────────────
89
177
 
90
178
  def test_uinput_udev_rule_content():
@@ -280,3 +280,108 @@ def test_setup_offers_nightly_learning_once(monkeypatch):
280
280
  steps._offer_nightly_learning(state)
281
281
  steps._offer_nightly_learning(state) # already on: not asked again
282
282
  assert installed == [1] and len(asked) == 1
283
+
284
+
285
+ # ── root commands with no terminal (agent, launcher, GUI) ────────────
286
+
287
+ def _no_terminal(monkeypatch, *, sudo_cached=False, pkexec=True, display=True):
288
+ monkeypatch.setattr(plat, "_has_tty", lambda: False)
289
+ monkeypatch.setattr(plat, "_sudo_without_password", lambda: sudo_cached)
290
+ monkeypatch.setattr(plat.shutil, "which",
291
+ lambda name: "/usr/bin/pkexec" if pkexec and name == "pkexec" else None)
292
+ monkeypatch.delenv("DISPLAY", raising=False)
293
+ if display:
294
+ monkeypatch.setenv("WAYLAND_DISPLAY", "wayland-0")
295
+ else:
296
+ monkeypatch.delenv("WAYLAND_DISPLAY", raising=False)
297
+
298
+
299
+ def test_privileged_argv_unchanged_with_a_terminal(monkeypatch):
300
+ monkeypatch.setattr(plat, "_has_tty", lambda: True)
301
+ argv = ["sudo", "apt", "install", "ibus"]
302
+ assert plat.privileged_argv(argv) == argv
303
+
304
+
305
+ def test_privileged_argv_leaves_unprivileged_commands_alone(monkeypatch):
306
+ _no_terminal(monkeypatch, pkexec=False)
307
+ assert plat.privileged_argv(["brew", "install", "x"]) == ["brew", "install", "x"]
308
+
309
+
310
+ def test_privileged_argv_uses_pkexec_without_a_terminal(monkeypatch):
311
+ _no_terminal(monkeypatch)
312
+ assert plat.privileged_argv(["sudo", "apt", "install", "ibus", "gcc"]) == [
313
+ "pkexec", "apt", "install", "-y", "ibus", "gcc"]
314
+ assert plat.privileged_argv(["sudo", "pacman", "-S", "ibus"]) == [
315
+ "pkexec", "pacman", "-S", "--noconfirm", "ibus"]
316
+
317
+
318
+ def test_privileged_argv_runs_shell_scripts_with_inner_sudo_under_pkexec(monkeypatch):
319
+ _no_terminal(monkeypatch)
320
+ script = ["sh", "-c", "printf x | sudo tee /etc/udev/rules.d/r > /dev/null"]
321
+ assert plat.privileged_argv(script) == ["pkexec", *script]
322
+
323
+
324
+ def test_privileged_argv_prefers_cached_sudo(monkeypatch):
325
+ _no_terminal(monkeypatch, sudo_cached=True)
326
+ assert plat.privileged_argv(["sudo", "apt", "install", "x"]) == [
327
+ "sudo", "-n", "apt", "install", "-y", "x"]
328
+
329
+
330
+ @pytest.mark.parametrize("pkexec,display", [(False, True), (True, False)])
331
+ def test_privileged_argv_gives_up_without_pkexec_or_desktop(monkeypatch, pkexec, display):
332
+ _no_terminal(monkeypatch, pkexec=pkexec, display=display)
333
+ assert plat.privileged_argv(["sudo", "apt", "install", "x"]) is None
334
+
335
+
336
+ def test_noninteractive_install_deps_without_terminal_prints_command(cfg_paths, monkeypatch, capsys):
337
+ _patch_noninteractive(monkeypatch)
338
+ monkeypatch.setattr("voiceio.platform.check_system_deps", lambda: ["portaudio"])
339
+ monkeypatch.setattr(plat, "system_deps_install_cmd", lambda keys: "sudo apt install libportaudio2")
340
+ monkeypatch.setattr(plat, "privileged_argv", lambda argv: None)
341
+ ran = []
342
+ monkeypatch.setattr(noninteractive.subprocess, "run", lambda *a, **k: ran.append(a))
343
+ code = noninteractive.run_setup_noninteractive({}, install_deps=True)
344
+ assert code == 4
345
+ assert ran == []
346
+ assert "sudo apt install libportaudio2" in capsys.readouterr().out
347
+
348
+
349
+ def test_noninteractive_install_deps_runs_pkexec_argv(cfg_paths, monkeypatch):
350
+ _patch_noninteractive(monkeypatch)
351
+ state = {"missing": ["portaudio"]}
352
+ monkeypatch.setattr("voiceio.platform.check_system_deps", lambda: state["missing"])
353
+ monkeypatch.setattr(plat, "system_deps_install_cmd", lambda keys: "sudo apt install libportaudio2")
354
+ _no_terminal(monkeypatch)
355
+ ran = []
356
+
357
+ class _Done:
358
+ returncode = 0
359
+
360
+ def fake_run(argv, **kw):
361
+ ran.append(argv)
362
+ state["missing"] = []
363
+ return _Done()
364
+
365
+ monkeypatch.setattr(noninteractive.subprocess, "run", fake_run)
366
+ assert noninteractive.run_setup_noninteractive({}, install_deps=True) == 0
367
+ assert ran == [["pkexec", "apt", "install", "-y", "libportaudio2"]]
368
+
369
+
370
+ def test_doctor_privileged_fix_asks_first_and_never_runs_when_declined(monkeypatch):
371
+ from voiceio.cli import doctor
372
+ ran = []
373
+ monkeypatch.setattr("subprocess.run", lambda *a, **k: ran.append(a))
374
+ monkeypatch.setattr(doctor, "_confirm_privileged", lambda d, c: False)
375
+ assert doctor._run_privileged("Fix x", ["sudo", "true"], 5) is None
376
+ assert ran == []
377
+
378
+
379
+ def test_doctor_privileged_fix_without_terminal_prints_command(monkeypatch, capsys):
380
+ from voiceio.cli import doctor
381
+ ran = []
382
+ monkeypatch.setattr("subprocess.run", lambda *a, **k: ran.append(a))
383
+ monkeypatch.setattr(doctor, "_confirm_privileged", lambda d, c: True)
384
+ monkeypatch.setattr(plat, "privileged_argv", lambda argv: None)
385
+ assert doctor._run_privileged("Fix x", ["sudo", "udevadm", "trigger"], 5) is False
386
+ assert ran == []
387
+ assert "sudo udevadm trigger" in capsys.readouterr().out
@@ -499,3 +499,54 @@ def test_chain_passes_language_to_supertonic():
499
499
  from voiceio.tts import chain
500
500
  engine = chain._create("supertonic", TTSConfig(engine="supertonic", language="de"))
501
501
  assert engine.name == "supertonic" and engine._language == "de"
502
+
503
+
504
+ # ── no engine installed: the default config stays quiet ─────────────
505
+
506
+ def _no_engines(cfg):
507
+ names = ["piper", "supertonic", "edge-tts", "espeak"] if cfg.engine == "auto" else [cfg.engine]
508
+ return [(n, ProbeResult(ok=False, reason=f"{n} not installed")) for n in names]
509
+
510
+
511
+ def test_doctor_tts_auto_without_engine_is_quiet_off():
512
+ from voiceio import health
513
+ with patch("voiceio.tts.probe_all", side_effect=_no_engines):
514
+ status = health._tts_status(TTSConfig())
515
+ assert status.startswith("off")
516
+ assert "enabled but" not in status
517
+ assert "⚠" not in health._feature_line("Text-to-speech", status)
518
+
519
+
520
+ def test_doctor_tts_named_engine_missing_still_warns():
521
+ from voiceio import health
522
+ with patch("voiceio.tts.probe_all", side_effect=_no_engines):
523
+ status = health._tts_status(TTSConfig(engine="piper"))
524
+ assert status.startswith("enabled but piper unavailable")
525
+
526
+
527
+ def test_doctor_tts_on_when_engine_present():
528
+ from voiceio import health
529
+ with patch("voiceio.tts.probe_all",
530
+ return_value=[("piper", ProbeResult(ok=False, reason="x")),
531
+ ("espeak", ProbeResult(ok=True))]):
532
+ assert health._tts_status(TTSConfig()) == "on (espeak)"
533
+
534
+
535
+ def test_doctor_tts_disabled_is_off():
536
+ from voiceio import health
537
+ assert health._tts_status(TTSConfig(enabled=False)) == "off"
538
+
539
+
540
+ def test_auto_select_without_engine_logs_no_warning(caplog):
541
+ import logging
542
+ from voiceio.tts.chain import select, _create
543
+
544
+ def none_ok(name, cfg):
545
+ engine = _create(name, cfg)
546
+ engine.probe = lambda: ProbeResult(ok=False, reason="not installed")
547
+ return engine
548
+
549
+ with patch("voiceio.tts.chain._create", side_effect=none_ok), \
550
+ caplog.at_level(logging.DEBUG, logger="voiceio.tts.chain"):
551
+ assert select(TTSConfig()) is None
552
+ assert not [r for r in caplog.records if r.levelno >= logging.WARNING]
@@ -0,0 +1 @@
1
+ __version__ = "1.17.1"
@@ -202,8 +202,11 @@ class VoiceIO:
202
202
  # Create a second hotkey backend for TTS
203
203
  self._tts_hotkey = hotkey_chain.select(platform, cfg.hotkey.backend)
204
204
  log.info("TTS: engine=%s, hotkey=%s", self._tts_engine.name, cfg.tts.hotkey)
205
+ elif cfg.tts.engine == "auto":
206
+ # The default config: read aloud once an engine is installed.
207
+ log.info("TTS: no engine installed, read-aloud hotkey off")
205
208
  else:
206
- log.warning("TTS enabled but no engine available")
209
+ log.warning("TTS: engine '%s' is configured but unavailable", cfg.tts.engine)
207
210
 
208
211
  # Optional key that cancels a dictation (read only, never grabbed: the
209
212
  # key also reaches the focused app). Needs a backend that sees keys.
@@ -1007,11 +1010,11 @@ class VoiceIO:
1007
1010
  self._cancel_hotkey.start(self.cfg.hotkey.cancel_key, self.cancel)
1008
1011
 
1009
1012
  def run(self) -> None:
1010
- from voiceio.config import PID_PATH, LOG_DIR
1013
+ from voiceio.config import PID_PATH, LOG_DIR, private_dir
1011
1014
 
1012
1015
  self._typers.redetect_at_start()
1013
1016
 
1014
- LOG_DIR.mkdir(parents=True, exist_ok=True)
1017
+ private_dir(LOG_DIR)
1015
1018
  self._pid_fd = open(PID_PATH, "a") # not "w": the running daemon's PID stays
1016
1019
  try:
1017
1020
  from voiceio.pidlock import lock_pid_file
@@ -112,9 +112,9 @@ def _cmd_run(args: argparse.Namespace) -> None:
112
112
  console.setLevel(getattr(logging, cfg.daemon.log_level))
113
113
 
114
114
  # File: always log DEBUG to rotating file
115
- from voiceio.config import LOG_DIR, LOG_PATH
115
+ from voiceio.config import LOG_DIR, LOG_PATH, private_dir
116
116
  from logging.handlers import RotatingFileHandler
117
- LOG_DIR.mkdir(parents=True, exist_ok=True)
117
+ private_dir(LOG_DIR)
118
118
  file_handler = RotatingFileHandler(
119
119
  str(LOG_PATH), maxBytes=2_000_000, backupCount=2,
120
120
  )
@@ -125,6 +125,8 @@ def _cmd_run(args: argparse.Namespace) -> None:
125
125
 
126
126
  logging.basicConfig(level=logging.DEBUG, handlers=[console, file_handler])
127
127
  logging.getLogger("voiceio").setLevel(getattr(logging, cfg.daemon.log_level))
128
+ from voiceio.errorlog import RepeatedErrorNotifier
129
+ logging.getLogger("voiceio").addHandler(RepeatedErrorNotifier())
128
130
 
129
131
  log = logging.getLogger("voiceio")
130
132
  from voiceio import __version__, platform as plat
@@ -210,12 +212,13 @@ def _entry_point() -> None:
210
212
  import traceback
211
213
  tb = traceback.format_exc()
212
214
  try:
213
- from voiceio.config import LOG_DIR
214
- LOG_DIR.mkdir(parents=True, exist_ok=True)
215
+ from voiceio.config import LOG_DIR, private_dir
216
+ private_dir(LOG_DIR)
215
217
  with open(LOG_DIR / "crash.log", "a", encoding="utf-8") as f:
216
218
  f.write(f"\n===== {datetime.datetime.now().isoformat()} =====\n"
217
219
  f"argv: {sys.argv}\n{tb}")
218
- print(f"\n[voiceio crashed — traceback in {LOG_DIR / 'crash.log'}]", file=sys.stderr)
220
+ print(f"\n[voiceio crashed — traceback in {LOG_DIR / 'crash.log'}]\n"
221
+ "Report it: voiceio doctor --report", file=sys.stderr)
219
222
  except Exception: # noqa: BLE001 — last resort
220
223
  pass
221
224
  print(tb, file=sys.stderr)