python-voiceio 1.9.3__tar.gz → 1.11.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 (225) hide show
  1. {python_voiceio-1.9.3/python_voiceio.egg-info → python_voiceio-1.11.0}/PKG-INFO +12 -3
  2. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/README.md +11 -2
  3. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/pyproject.toml +1 -1
  4. {python_voiceio-1.9.3 → python_voiceio-1.11.0/python_voiceio.egg-info}/PKG-INFO +12 -3
  5. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/python_voiceio.egg-info/SOURCES.txt +5 -0
  6. python_voiceio-1.11.0/tests/test_mic_gain.py +85 -0
  7. python_voiceio-1.11.0/tests/test_report.py +314 -0
  8. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_service.py +24 -2
  9. python_voiceio-1.11.0/voiceio/__init__.py +1 -0
  10. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/app.py +34 -20
  11. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/__init__.py +2 -0
  12. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/doctor.py +24 -0
  13. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/uninstall.py +5 -1
  14. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/config.py +5 -1
  15. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/feedback.py +13 -1
  16. python_voiceio-1.11.0/voiceio/mic_gain.py +123 -0
  17. python_voiceio-1.11.0/voiceio/report.py +278 -0
  18. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/service.py +31 -0
  19. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/steps.py +7 -3
  20. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/system.py +7 -0
  21. python_voiceio-1.11.0/voiceio/sounds/alert.wav +0 -0
  22. python_voiceio-1.9.3/voiceio/__init__.py +0 -1
  23. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/LICENSE +0 -0
  24. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
  25. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/python_voiceio.egg-info/entry_points.txt +0 -0
  26. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/python_voiceio.egg-info/requires.txt +0 -0
  27. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/python_voiceio.egg-info/top_level.txt +0 -0
  28. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/setup.cfg +0 -0
  29. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_app_wiring.py +0 -0
  30. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_audio_quality.py +0 -0
  31. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_audio_source.py +0 -0
  32. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_backend_probes.py +0 -0
  33. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_cancel.py +0 -0
  34. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_cli.py +0 -0
  35. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_clipboard_read.py +0 -0
  36. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_clipboard_typer.py +0 -0
  37. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_commands.py +0 -0
  38. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_concurrency_lockdown.py +0 -0
  39. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_config.py +0 -0
  40. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_context_terms.py +0 -0
  41. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_corrections.py +0 -0
  42. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_dead_mic.py +0 -0
  43. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_decoders.py +0 -0
  44. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_engine.py +0 -0
  45. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_evdev_uaccess.py +0 -0
  46. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_fallback.py +0 -0
  47. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_fcitx_bridge.py +0 -0
  48. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_feedback_stream.py +0 -0
  49. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_focus.py +0 -0
  50. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_health.py +0 -0
  51. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_hints.py +0 -0
  52. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_history.py +0 -0
  53. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_hotkey_modes.py +0 -0
  54. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_engine_limits.py +0 -0
  55. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_engine_listener.py +0 -0
  56. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_pending.py +0 -0
  57. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_pids.py +0 -0
  58. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_ping.py +0 -0
  59. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_session.py +0 -0
  60. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_textlimit.py +0 -0
  61. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_ibus_typer.py +0 -0
  62. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_idle_audio.py +0 -0
  63. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_clean.py +0 -0
  64. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_import.py +0 -0
  65. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_language.py +0 -0
  66. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_metrics.py +0 -0
  67. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_pipeline.py +0 -0
  68. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_quiet.py +0 -0
  69. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_resume.py +0 -0
  70. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_review.py +0 -0
  71. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_learn_targets.py +0 -0
  72. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_llm.py +0 -0
  73. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_llm_api.py +0 -0
  74. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_modifier_guard.py +0 -0
  75. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_numbers.py +0 -0
  76. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_paste_last.py +0 -0
  77. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_pipeline.py +0 -0
  78. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_platform.py +0 -0
  79. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_postcorrect.py +0 -0
  80. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_postprocess.py +0 -0
  81. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_prebuffer.py +0 -0
  82. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_presence.py +0 -0
  83. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_prompt.py +0 -0
  84. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_quiet_audio.py +0 -0
  85. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_recorder_integration.py +0 -0
  86. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_retention.py +0 -0
  87. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_robustness.py +0 -0
  88. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_security_hardening.py +0 -0
  89. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_server.py +0 -0
  90. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_setup.py +0 -0
  91. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_streaming.py +0 -0
  92. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_suggest.py +0 -0
  93. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_tokens.py +0 -0
  94. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_transcriber.py +0 -0
  95. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_tts.py +0 -0
  96. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_typer_manager.py +0 -0
  97. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_vad.py +0 -0
  98. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_vocabulary.py +0 -0
  99. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_wordfreq.py +0 -0
  100. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/tests/test_worker_decode.py +0 -0
  101. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/__main__.py +0 -0
  102. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/audio_source.py +0 -0
  103. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/backends.py +0 -0
  104. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/configcmd.py +0 -0
  105. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/corrections.py +0 -0
  106. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/history.py +0 -0
  107. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/learn.py +0 -0
  108. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/models.py +0 -0
  109. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/servicecmd.py +0 -0
  110. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/cli/vocab.py +0 -0
  111. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/clipboard_read.py +0 -0
  112. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/commands.py +0 -0
  113. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/consent.py +0 -0
  114. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/context_terms.py +0 -0
  115. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/__init__.py +0 -0
  116. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/bias.py +0 -0
  117. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/__init__.py +0 -0
  118. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/base.py +0 -0
  119. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/catalog.py +0 -0
  120. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
  121. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
  122. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/lanes.py +0 -0
  123. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/sherpa.py +0 -0
  124. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/decoders/whispercpp.py +0 -0
  125. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/engine.py +0 -0
  126. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/core/pipeline.py +0 -0
  127. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/corrections.py +0 -0
  128. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  129. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/data/__init__.py +0 -0
  130. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/demo.py +0 -0
  131. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/display.py +0 -0
  132. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/focus.py +0 -0
  133. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/health.py +0 -0
  134. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hints.py +0 -0
  135. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/history.py +0 -0
  136. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/__init__.py +0 -0
  137. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/base.py +0 -0
  138. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/chain.py +0 -0
  139. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/evdev.py +0 -0
  140. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/pynput_backend.py +0 -0
  141. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/hotkeys/socket_backend.py +0 -0
  142. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/__init__.py +0 -0
  143. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/engine.py +0 -0
  144. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/install.py +0 -0
  145. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/pending.py +0 -0
  146. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/session.py +0 -0
  147. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ibus/textlimit.py +0 -0
  148. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/idle_audio.py +0 -0
  149. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/__init__.py +0 -0
  150. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/augment.py +0 -0
  151. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/clean.py +0 -0
  152. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/evaluate.py +0 -0
  153. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/importer.py +0 -0
  154. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/metrics.py +0 -0
  155. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/quiet.py +0 -0
  156. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/review.py +0 -0
  157. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/store.py +0 -0
  158. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/teacher.py +0 -0
  159. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/learn/train.py +0 -0
  160. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/llm.py +0 -0
  161. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/llm_api.py +0 -0
  162. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/models/__init__.py +0 -0
  163. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/models/silero_vad.onnx +0 -0
  164. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/numbers.py +0 -0
  165. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/pidlock.py +0 -0
  166. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/platform.py +0 -0
  167. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/postcorrect.py +0 -0
  168. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/postprocess.py +0 -0
  169. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/power.py +0 -0
  170. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/presence.py +0 -0
  171. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/privacy.py +0 -0
  172. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/prompt.py +0 -0
  173. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/quiet_audio.py +0 -0
  174. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/recorder.py +0 -0
  175. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/retention.py +0 -0
  176. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/server/__init__.py +0 -0
  177. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/server/__main__.py +0 -0
  178. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/server/app.py +0 -0
  179. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/server/config.py +0 -0
  180. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/server/runtime.py +0 -0
  181. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/__init__.py +0 -0
  182. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/configfile.py +0 -0
  183. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/extras.py +0 -0
  184. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/hotkey.py +0 -0
  185. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/noninteractive.py +0 -0
  186. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/speech.py +0 -0
  187. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/setup/ui.py +0 -0
  188. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/sounds/__init__.py +0 -0
  189. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/sounds/commit.wav +0 -0
  190. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/sounds/start.wav +0 -0
  191. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/sounds/stop.wav +0 -0
  192. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/streaming.py +0 -0
  193. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/suggest.py +0 -0
  194. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tokens.py +0 -0
  195. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tray/__init__.py +0 -0
  196. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tray/_icons.py +0 -0
  197. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tray/_indicator.py +0 -0
  198. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tray/_pystray.py +0 -0
  199. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/__init__.py +0 -0
  200. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/base.py +0 -0
  201. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/chain.py +0 -0
  202. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/edge_engine.py +0 -0
  203. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/espeak.py +0 -0
  204. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/piper_engine.py +0 -0
  205. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/tts/player.py +0 -0
  206. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/__init__.py +0 -0
  207. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/base.py +0 -0
  208. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/chain.py +0 -0
  209. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/clipboard.py +0 -0
  210. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/collecting.py +0 -0
  211. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/fcitx_bridge.py +0 -0
  212. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/ibus.py +0 -0
  213. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/manager.py +0 -0
  214. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/modifier_guard.py +0 -0
  215. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/paste.py +0 -0
  216. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/pynput_type.py +0 -0
  217. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/wtype.py +0 -0
  218. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/xdotool.py +0 -0
  219. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/typers/ydotool.py +0 -0
  220. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/ui.py +0 -0
  221. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/vad.py +0 -0
  222. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/vocab_stats.py +0 -0
  223. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/vocabulary.py +0 -0
  224. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/watchdog.py +0 -0
  225. {python_voiceio-1.9.3 → python_voiceio-1.11.0}/voiceio/wordfreq.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 1.9.3
3
+ Version: 1.11.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -87,7 +87,16 @@ pipx install 'python-voiceio[desktop]'
87
87
  voiceio setup # model, hotkey, autostart
88
88
  ```
89
89
 
90
- Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works and `voiceio doctor --fix` repairs what it can. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
90
+ **Or as a Debian/Ubuntu package** (new; bundles everything, nothing to compile):
91
+
92
+ ```bash
93
+ # Debian / Ubuntu: download voiceio_*.deb from the latest release, then
94
+ sudo apt install ./voiceio_*.deb
95
+
96
+ voiceio setup
97
+ ```
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).
91
100
 
92
101
  ## Make it yours
93
102
 
@@ -121,7 +130,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
121
130
  - **Unbounded vocabulary**: a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
122
131
  - **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
123
132
  - **Typing without IBus on GNOME and KDE**: a libei backend.
124
- - **Distro packages**: AUR, .deb and .rpm.
133
+ - **Distro packages**: AUR (packaged, not yet published) and .rpm.
125
134
  - **One model everywhere**: sync your fine-tune across your machines.
126
135
 
127
136
  ## Contributing
@@ -34,7 +34,16 @@ pipx install 'python-voiceio[desktop]'
34
34
  voiceio setup # model, hotkey, autostart
35
35
  ```
36
36
 
37
- Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works and `voiceio doctor --fix` repairs what it can. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
37
+ **Or as a Debian/Ubuntu package** (new; bundles everything, nothing to compile):
38
+
39
+ ```bash
40
+ # Debian / Ubuntu: download voiceio_*.deb from the latest release, then
41
+ sudo apt install ./voiceio_*.deb
42
+
43
+ voiceio setup
44
+ ```
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).
38
47
 
39
48
  ## Make it yours
40
49
 
@@ -68,7 +77,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
68
77
  - **Unbounded vocabulary**: a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
69
78
  - **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
70
79
  - **Typing without IBus on GNOME and KDE**: a libei backend.
71
- - **Distro packages**: AUR, .deb and .rpm.
80
+ - **Distro packages**: AUR (packaged, not yet published) and .rpm.
72
81
  - **One model everywhere**: sync your fine-tune across your machines.
73
82
 
74
83
  ## Contributing
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "1.9.3"
7
+ version = "1.11.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.9.3
3
+ Version: 1.11.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -87,7 +87,16 @@ pipx install 'python-voiceio[desktop]'
87
87
  voiceio setup # model, hotkey, autostart
88
88
  ```
89
89
 
90
- Then press your hotkey, speak, and press it again (or hold it while you speak). `voiceio doctor` shows what works and `voiceio doctor --fix` repairs what it can. NixOS, other extras and installing from source: [docs/linux.md](docs/linux.md). Agent runbook: [INSTALL.md](INSTALL.md).
90
+ **Or as a Debian/Ubuntu package** (new; bundles everything, nothing to compile):
91
+
92
+ ```bash
93
+ # Debian / Ubuntu: download voiceio_*.deb from the latest release, then
94
+ sudo apt install ./voiceio_*.deb
95
+
96
+ voiceio setup
97
+ ```
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).
91
100
 
92
101
  ## Make it yours
93
102
 
@@ -121,7 +130,7 @@ Everything lives in `~/.config/voiceio/config.toml`; [config.example.toml](confi
121
130
  - **Unbounded vocabulary**: a decoder whose biasing has no token cap (see [model choice](CONTRIBUTING.md#model-choice-why-whisper-and-what-would-change-it)).
122
131
  - **GPU on AMD and Intel**: let setup run a Vulkan whisper.cpp server for you.
123
132
  - **Typing without IBus on GNOME and KDE**: a libei backend.
124
- - **Distro packages**: AUR, .deb and .rpm.
133
+ - **Distro packages**: AUR (packaged, not yet published) and .rpm.
125
134
  - **One model everywhere**: sync your fine-tune across your machines.
126
135
 
127
136
  ## Contributing
@@ -52,6 +52,7 @@ tests/test_learn_review.py
52
52
  tests/test_learn_targets.py
53
53
  tests/test_llm.py
54
54
  tests/test_llm_api.py
55
+ tests/test_mic_gain.py
55
56
  tests/test_modifier_guard.py
56
57
  tests/test_numbers.py
57
58
  tests/test_paste_last.py
@@ -64,6 +65,7 @@ tests/test_presence.py
64
65
  tests/test_prompt.py
65
66
  tests/test_quiet_audio.py
66
67
  tests/test_recorder_integration.py
68
+ tests/test_report.py
67
69
  tests/test_retention.py
68
70
  tests/test_robustness.py
69
71
  tests/test_security_hardening.py
@@ -101,6 +103,7 @@ voiceio/history.py
101
103
  voiceio/idle_audio.py
102
104
  voiceio/llm.py
103
105
  voiceio/llm_api.py
106
+ voiceio/mic_gain.py
104
107
  voiceio/numbers.py
105
108
  voiceio/pidlock.py
106
109
  voiceio/platform.py
@@ -112,6 +115,7 @@ voiceio/privacy.py
112
115
  voiceio/prompt.py
113
116
  voiceio/quiet_audio.py
114
117
  voiceio/recorder.py
118
+ voiceio/report.py
115
119
  voiceio/retention.py
116
120
  voiceio/service.py
117
121
  voiceio/streaming.py
@@ -187,6 +191,7 @@ voiceio/setup/steps.py
187
191
  voiceio/setup/system.py
188
192
  voiceio/setup/ui.py
189
193
  voiceio/sounds/__init__.py
194
+ voiceio/sounds/alert.wav
190
195
  voiceio/sounds/commit.wav
191
196
  voiceio/sounds/start.wav
192
197
  voiceio/sounds/stop.wav
@@ -0,0 +1,85 @@
1
+ """Input gain follows the recordings: down on clipping, up when very quiet,
2
+ never back to a level that clipped."""
3
+ from __future__ import annotations
4
+
5
+ from voiceio import mic_gain
6
+ from voiceio.mic_gain import MicGain
7
+
8
+
9
+ class FakeSource:
10
+ def __init__(self, volume):
11
+ self.volume = volume
12
+ self.sets = []
13
+
14
+ def get(self):
15
+ return self.volume
16
+
17
+ def set(self, v):
18
+ self.sets.append(v)
19
+ self.volume = v
20
+ return True
21
+
22
+
23
+ def _gain(volume=0.5):
24
+ g = MicGain(enabled=False)
25
+ g._source = FakeSource(volume)
26
+ return g
27
+
28
+
29
+ def test_clipping_lowers_by_a_fifth():
30
+ g = _gain(0.5)
31
+ change = g.after_recording(peak=1.0, clip_ratio=0.02, secs=10)
32
+ assert change.reason == "clipping" and g._source.volume == 0.4
33
+
34
+
35
+ def test_never_below_the_floor():
36
+ g = _gain(0.11)
37
+ g.after_recording(1.0, 0.02, 10)
38
+ assert g._source.volume == mic_gain.FLOOR
39
+
40
+
41
+ def test_three_quiet_recordings_raise_but_not_to_a_level_that_clipped():
42
+ g = _gain(0.5)
43
+ g.after_recording(1.0, 0.02, 10) # 0.5 clipped -> 0.4
44
+ for _ in range(2):
45
+ assert g.after_recording(0.05, 0.0, 10) is None
46
+ assert g.after_recording(0.05, 0.0, 10) is None # third quiet one: would reach 0.5
47
+ assert g._source.volume == 0.4 # capped at 0.5 * 0.8: no oscillation
48
+
49
+
50
+ def test_quiet_without_any_clipping_history_raises():
51
+ g = _gain(0.3)
52
+ for _ in range(3):
53
+ change = g.after_recording(0.05, 0.0, 10)
54
+ assert change.new == round(0.3 / mic_gain.STEP, 2)
55
+
56
+
57
+ def test_short_or_dead_recordings_dont_count():
58
+ g = _gain(0.3)
59
+ for _ in range(5):
60
+ assert g.after_recording(0.05, 0.0, 1.0) is None # too short
61
+ assert g.after_recording(0.0, 0.0, 10) is None # muted mic: not a gain problem
62
+ assert g._source.sets == []
63
+
64
+
65
+ def test_normal_level_resets_the_quiet_streak():
66
+ g = _gain(0.3)
67
+ g.after_recording(0.05, 0.0, 10)
68
+ g.after_recording(0.05, 0.0, 10)
69
+ g.after_recording(0.5, 0.0, 10)
70
+ assert g.after_recording(0.05, 0.0, 10) is None
71
+
72
+
73
+ def test_a_chosen_device_is_left_alone():
74
+ assert not MicGain(enabled=True, device="hw:1").active
75
+
76
+
77
+ def test_off_without_volume_tools(monkeypatch):
78
+ # conftest: quiet_audio._which finds nothing in tests
79
+ assert not MicGain(enabled=True).active
80
+
81
+
82
+ def test_failures_never_raise():
83
+ g = _gain(0.5)
84
+ g._source.get = lambda: (_ for _ in ()).throw(RuntimeError("boom"))
85
+ assert g.after_recording(1.0, 0.02, 10) is None
@@ -0,0 +1,314 @@
1
+ """`voiceio doctor --report` and the post-setup feedback line: what goes public."""
2
+ from __future__ import annotations
3
+
4
+ import io
5
+ import json
6
+ import sys
7
+
8
+ import pytest
9
+
10
+ from voiceio import report
11
+ from voiceio.config import Config
12
+ from voiceio.health import BackendStatus, HealthReport
13
+ from voiceio.platform import Platform
14
+
15
+ FAKE_HOME = "/home/zelda-tester"
16
+ FAKE_USER = "zelda-tester"
17
+ FAKE_HOST = "zeldas-thinkpad"
18
+
19
+
20
+ @pytest.fixture
21
+ def identity(monkeypatch):
22
+ """A fake user, home and hostname that must never reach a report."""
23
+ monkeypatch.setenv("HOME", FAKE_HOME)
24
+ monkeypatch.setenv("USER", FAKE_USER)
25
+ monkeypatch.setattr(report.getpass, "getuser", lambda: FAKE_USER)
26
+ monkeypatch.setattr(report.socket, "gethostname", lambda: FAKE_HOST)
27
+ monkeypatch.setattr(report._py_platform, "node", lambda: FAKE_HOST)
28
+ monkeypatch.setattr(report, "_homes", lambda: [FAKE_HOME])
29
+
30
+
31
+ def _health(**kw) -> HealthReport:
32
+ defaults = dict(
33
+ platform=Platform(os="linux", display_server="wayland", desktop="gnome"),
34
+ hotkey_backends=[BackendStatus("evdev", True)],
35
+ typer_backends=[BackendStatus("ibus", True), BackendStatus("ydotool", False, "not installed")],
36
+ audio_ok=True, cli_in_path=True, tray_ok=True,
37
+ )
38
+ defaults.update(kw)
39
+ return HealthReport(**defaults)
40
+
41
+
42
+ def _assert_private(text: str) -> None:
43
+ for secret in (FAKE_HOME, FAKE_USER, FAKE_HOST, "zelda"):
44
+ assert secret not in text.lower(), (secret, text)
45
+
46
+
47
+ # ── scrubbing ──
48
+
49
+ def test_scrub_removes_home_user_host_and_addresses(identity):
50
+ raw = (f"cannot open {FAKE_HOME}/.config/voiceio/x.toml as {FAKE_USER}@{FAKE_HOST}; "
51
+ "also /home/someoneelse/.cache, 192.168.1.40, fe80:0:0:0:1:2:3:4, "
52
+ "aa:bb:cc:dd:ee:ff, /dev/input/by-id/usb-Logitech_K120_SN12345-event-kbd, "
53
+ "ZELDA-TESTER shouted")
54
+ out = report.scrub(raw)
55
+ _assert_private(out)
56
+ for leaked in ("someoneelse", "192.168", "fe80", "aa:bb", "SN12345", "Logitech"):
57
+ assert leaked not in out
58
+ assert "~/.config/voiceio/x.toml" in out
59
+
60
+
61
+ def test_scrub_keeps_versions_and_backend_names(identity):
62
+ out = report.scrub("voiceio 1.10.0.2 · ibus · python 3.12.3")
63
+ assert out == "voiceio 1.10.0.2 · ibus · python 3.12.3"
64
+
65
+
66
+ def test_report_scrubs_check_messages_and_comment(identity):
67
+ health = _health(
68
+ hotkey_backends=[BackendStatus("evdev", False, f"{FAKE_HOME}/dev denied for {FAKE_USER}")],
69
+ audio_ok=False, audio_reason=f"device on {FAKE_HOST} busy",
70
+ warnings=[f"see {FAKE_HOME}/.local/state/voiceio/voiceio.log"],
71
+ )
72
+ text = report.build_report(health, comment=f"broke in {FAKE_HOME}/src, I'm {FAKE_USER}",
73
+ cfg=Config())
74
+ _assert_private(text)
75
+ assert "failed: hotkey evdev: ~/dev denied" in text
76
+ assert "audio: device on <host> busy" in text
77
+ assert "comment: broke in ~/src" in text
78
+
79
+
80
+ def test_report_contains_only_backend_and_model_names(identity):
81
+ cfg = Config()
82
+ cfg.model.name = f"{FAKE_HOME}/models/my-tune"
83
+ cfg.model.vocabulary_file = f"{FAKE_HOME}/vocab.txt"
84
+ cfg.cloud.api_key = "sk-secret"
85
+ cfg.hotkey.key = "ctrl+alt+zelda"
86
+ text = report.build_report(_health(), cfg=cfg)
87
+ _assert_private(text)
88
+ assert "sk-secret" not in text and "vocab" not in text and "ctrl+alt" not in text
89
+ assert "model: custom (local path)" in text
90
+ assert "hotkey evdev" in text and "typer ibus" in text
91
+ assert "fallbacks unavailable: ydotool" in text
92
+ assert "failed: none" in text
93
+ assert text.splitlines()[0].startswith("voiceio ")
94
+
95
+
96
+ def test_report_lists_failures_and_warnings():
97
+ health = _health(typer_backends=[BackendStatus("ibus", False, "daemon down")],
98
+ 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
+ assert "typer none" in text
101
+ assert "typer ibus: daemon down" in text
102
+ assert "missing system deps" in text
103
+ assert "cli not in PATH" in text and "tray: no AppIndicator" in text
104
+ assert "insecure permissions on 2 file(s)" in text
105
+
106
+
107
+ def test_comment_is_one_bounded_line():
108
+ text = report.build_report(_health(), comment="a\nb\tc " + "x" * 1000, cfg=Config())
109
+ line = next(ln for ln in text.splitlines() if ln.startswith("comment:"))
110
+ assert line.startswith("comment: a b c x")
111
+ assert len(line) <= len("comment: ") + report.MAX_COMMENT
112
+
113
+
114
+ # ── posting ──
115
+
116
+ class _Resp(io.BytesIO):
117
+ def __enter__(self):
118
+ return self
119
+
120
+ def __exit__(self, *a):
121
+ return False
122
+
123
+
124
+ def test_post_success_reads_receipt(monkeypatch):
125
+ seen = []
126
+
127
+ def fake(url, timeout):
128
+ seen.append((url, timeout))
129
+ return _Resp(json.dumps({"ok": True, "receipt": {"id": "r123"}}).encode())
130
+
131
+ monkeypatch.setattr(report, "_urlopen", fake)
132
+ assert report.post("hello world") == (True, "r123")
133
+ url, timeout = seen[0]
134
+ assert url.startswith(report.POST_URL + "?")
135
+ assert "text=hello+world" in url and "format=json" in url and "request_id=voiceio-" in url
136
+ assert timeout <= 10
137
+
138
+
139
+ @pytest.mark.parametrize("body", [b'{"ok": false, "error": "rate limited"}', b"[]", b"not json",
140
+ b'{"ok": true}'])
141
+ def test_post_bad_response_is_failure(monkeypatch, body):
142
+ monkeypatch.setattr(report, "_urlopen", lambda url, timeout: _Resp(body))
143
+ ok, why = report.post("x")
144
+ assert not ok and why
145
+
146
+
147
+ def test_post_network_error_never_raises():
148
+ # conftest refuses every connection: this is the offline path.
149
+ ok, why = report.post("x")
150
+ assert not ok and "ConnectionRefused" in why
151
+
152
+
153
+ # ── confirm / --yes / no TTY ──
154
+
155
+ @pytest.fixture
156
+ def posted(monkeypatch):
157
+ calls: list[str] = []
158
+ monkeypatch.setattr(report, "post", lambda text: (calls.append(text), (True, "r1"))[1])
159
+ return calls
160
+
161
+
162
+ def _tty(monkeypatch, tty: bool):
163
+ monkeypatch.setattr(report, "_interactive", lambda: tty)
164
+
165
+
166
+ def test_share_asks_and_posts_on_yes(monkeypatch, capsys, posted):
167
+ _tty(monkeypatch, True)
168
+ monkeypatch.setattr("builtins.input", lambda prompt: "y")
169
+ assert report.share("the report") is True
170
+ out = capsys.readouterr().out
171
+ assert "PUBLICLY" in out and "the report" in out and report.BOARD_URL in out
172
+ assert posted == ["the report"]
173
+
174
+
175
+ @pytest.mark.parametrize("answer", ["", "n", "no", "whatever"])
176
+ def test_share_defaults_to_no(monkeypatch, capsys, posted, answer):
177
+ _tty(monkeypatch, True)
178
+ monkeypatch.setattr("builtins.input", lambda prompt: answer)
179
+ assert report.share("r") is False
180
+ assert posted == []
181
+
182
+
183
+ def test_share_eof_is_no(monkeypatch, posted):
184
+ _tty(monkeypatch, True)
185
+
186
+ def eof(prompt):
187
+ raise EOFError
188
+
189
+ monkeypatch.setattr("builtins.input", eof)
190
+ assert report.share("r") is False and posted == []
191
+
192
+
193
+ def test_share_without_tty_prints_but_never_posts(monkeypatch, capsys, posted):
194
+ _tty(monkeypatch, False)
195
+ monkeypatch.setattr("builtins.input", lambda p: pytest.fail("must not prompt"))
196
+ assert report.share("the report") is False
197
+ out = capsys.readouterr().out
198
+ assert "the report" in out and "--yes" in out
199
+ assert posted == []
200
+
201
+
202
+ def test_share_yes_skips_prompt(monkeypatch, posted):
203
+ _tty(monkeypatch, False)
204
+ monkeypatch.setattr("builtins.input", lambda p: pytest.fail("must not prompt"))
205
+ assert report.share("r", yes=True) is True and posted == ["r"]
206
+
207
+
208
+ def test_share_failure_is_reported_not_raised(monkeypatch, capsys):
209
+ assert report.share("r", yes=True) is False
210
+ assert "Could not confirm the post" in capsys.readouterr().out
211
+
212
+
213
+ # ── CLI wiring ──
214
+
215
+ @pytest.fixture
216
+ def doctor_env(monkeypatch, identity):
217
+ from voiceio import config, health
218
+ from voiceio import platform as plat
219
+ monkeypatch.setattr(health, "check_health", lambda: _health(
220
+ warnings=[f"something at {FAKE_HOME}/.config"]))
221
+ monkeypatch.setattr(health, "format_report", lambda r: "DOCTOR OUTPUT")
222
+ monkeypatch.setattr(health, "usage_findings", lambda: [])
223
+ monkeypatch.setattr(plat, "check_system_deps", lambda: [])
224
+ monkeypatch.setattr(config, "check_permissions", lambda: [])
225
+ monkeypatch.setattr("voiceio.hints.hint", lambda *a, **k: None)
226
+
227
+
228
+ def _doctor(monkeypatch, capsys, *argv):
229
+ from voiceio.cli import main
230
+ monkeypatch.setattr(sys, "argv", ["voiceio", "doctor", *argv])
231
+ with pytest.raises(SystemExit):
232
+ main()
233
+ return capsys.readouterr().out
234
+
235
+
236
+ def test_doctor_report_yes_posts_scrubbed_comment(monkeypatch, capsys, doctor_env, posted):
237
+ out = _doctor(monkeypatch, capsys, "--report", "--yes",
238
+ "--comment", f"worked, {FAKE_USER} here")
239
+ assert len(posted) == 1
240
+ _assert_private(posted[0])
241
+ _assert_private(out)
242
+ assert "comment: worked, <user> here" in posted[0]
243
+ assert report.BOARD_URL in out
244
+
245
+
246
+ def test_doctor_report_without_tty_or_yes_does_not_post(monkeypatch, capsys, doctor_env, posted):
247
+ _tty(monkeypatch, False)
248
+ out = _doctor(monkeypatch, capsys, "--report")
249
+ assert posted == []
250
+ assert "voiceio " in out and "Not posted" in out
251
+
252
+
253
+ def test_doctor_without_report_never_posts(monkeypatch, capsys, doctor_env, posted):
254
+ out = _doctor(monkeypatch, capsys)
255
+ assert posted == [] and "PUBLICLY" not in out
256
+
257
+
258
+ # ── after setup ──
259
+
260
+ def test_after_setup_enter_skips_silently(monkeypatch, capsys, posted):
261
+ _tty(monkeypatch, True)
262
+ monkeypatch.setattr("builtins.input", lambda p: "")
263
+ monkeypatch.setattr(report, "gather", lambda c: pytest.fail("no checks on skip"))
264
+ report.offer_after_setup()
265
+ assert posted == [] and "PUBLICLY" not in capsys.readouterr().out
266
+
267
+
268
+ def test_after_setup_comment_is_shown_confirmed_and_posted(monkeypatch, capsys, posted):
269
+ _tty(monkeypatch, True)
270
+ answers = iter(["went fine", "y"])
271
+ monkeypatch.setattr("builtins.input", lambda p: next(answers))
272
+ monkeypatch.setattr(report, "gather", lambda c: (None, f"voiceio x report\ncomment: {c}"))
273
+ report.offer_after_setup()
274
+ assert posted == ["voiceio x report\ncomment: went fine"]
275
+
276
+
277
+ def test_after_setup_never_raises(monkeypatch, capsys):
278
+ _tty(monkeypatch, True)
279
+ monkeypatch.setattr("builtins.input", lambda p: "it broke")
280
+
281
+ def boom(c):
282
+ raise RuntimeError("probe exploded")
283
+
284
+ monkeypatch.setattr(report, "gather", boom)
285
+ report.offer_after_setup() # must not raise
286
+ assert "Skipped" in capsys.readouterr().out
287
+
288
+
289
+ def test_after_setup_needs_a_terminal(monkeypatch, posted):
290
+ _tty(monkeypatch, False)
291
+ monkeypatch.setattr("builtins.input", lambda p: pytest.fail("must not prompt"))
292
+ report.offer_after_setup()
293
+ assert posted == []
294
+
295
+
296
+ def test_setup_wizard_offers_the_report(monkeypatch):
297
+ from voiceio.cli import main
298
+ calls = []
299
+ monkeypatch.setattr("voiceio.setup.run_wizard", lambda: calls.append("wizard"))
300
+ monkeypatch.setattr(report, "offer_after_setup", lambda: calls.append("offer"))
301
+ monkeypatch.setattr("voiceio.hints.hint", lambda *a, **k: None)
302
+ monkeypatch.setattr(sys, "argv", ["voiceio", "setup"])
303
+ main()
304
+ assert calls == ["wizard", "offer"]
305
+
306
+
307
+ def test_network_guard_blocks_real_hosts():
308
+ import socket
309
+ s = socket.socket()
310
+ try:
311
+ with pytest.raises(ConnectionRefusedError):
312
+ s.connect(("93.184.216.34", 443))
313
+ finally:
314
+ s.close()
@@ -25,6 +25,28 @@ def test_install_symlinks_relinks_and_drops_legacy(monkeypatch, tmp_path):
25
25
  assert not (local_bin / "voiceio-doctor").is_symlink()
26
26
 
27
27
 
28
+ def test_packaged_install_links_nothing_and_serves_its_own_bin(monkeypatch, tmp_path):
29
+ prefix = tmp_path / "usr-lib-voiceio"
30
+ (prefix / "bin").mkdir(parents=True)
31
+ (prefix / "bin" / "voiceio").write_text("#!/bin/sh\n")
32
+ (prefix / service.PACKAGED_MARKER).write_text("deb\n")
33
+ local_bin = tmp_path / "local-bin"
34
+ monkeypatch.setattr(service, "LOCAL_BIN", local_bin)
35
+ monkeypatch.setattr(sys, "prefix", str(prefix))
36
+
37
+ assert service.packaged_by() == "deb"
38
+ assert service.package_remove_cmd() == "sudo apt remove voiceio"
39
+ service.install_symlinks()
40
+ assert not local_bin.exists()
41
+ # The unit's ExecStart is the package's own path, stable across upgrades.
42
+ assert service._find_voiceio_bin() == str((prefix / "bin" / "voiceio").resolve())
43
+
44
+
45
+ def test_unpackaged_install_has_no_package(monkeypatch, tmp_path):
46
+ monkeypatch.setattr(sys, "prefix", str(tmp_path))
47
+ assert service.packaged_by() is None and service.package_remove_cmd() is None
48
+
49
+
28
50
  def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
29
51
  calls = []
30
52
  monkeypatch.setattr(service, "has_systemd", lambda: True)
@@ -44,7 +66,7 @@ def test_learn_timer_runs_maintain_at_idle_priority(monkeypatch, tmp_path):
44
66
  "MemoryHigh=25%", "OOMScoreAdjust=500"):
45
67
  assert f"\n{line}\n" in unit, line
46
68
  timer = service.LEARN_TIMER_PATH.read_text()
47
- assert "Persistent=true" in timer and "OnCalendar=*-*-* 03:30" in timer
69
+ assert "Persistent=true" in timer and "OnCalendar=Sun *-*-* 03:30" in timer
48
70
  assert ("enable", "--now", service.LEARN_TIMER_NAME) in calls
49
71
 
50
72
  service.uninstall_learn_timer()
@@ -109,7 +131,7 @@ def test_learn_schedule_comes_from_config(monkeypatch, tmp_path):
109
131
  monkeypatch.setattr(service, "LEARN_SERVICE_PATH", tmp_path / "s.service")
110
132
  monkeypatch.setattr(service, "LEARN_TIMER_PATH", tmp_path / "s.timer")
111
133
  service.install_learn_timer()
112
- assert "OnCalendar=*-*-* 03:30" in service.LEARN_TIMER_PATH.read_text()
134
+ assert "OnCalendar=Sun *-*-* 03:30" in service.LEARN_TIMER_PATH.read_text() # weekly by default
113
135
 
114
136
  config.set_value("learn", "at", "2:05")
115
137
  config.set_value("learn", "every", "week")
@@ -0,0 +1 @@
1
+ __version__ = "1.11.0"