python-voiceio 1.9.2__tar.gz → 1.10.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.2/python_voiceio.egg-info → python_voiceio-1.10.0}/PKG-INFO +2 -2
  2. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/README.md +1 -1
  3. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/pyproject.toml +1 -1
  4. {python_voiceio-1.9.2 → python_voiceio-1.10.0/python_voiceio.egg-info}/PKG-INFO +2 -2
  5. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/python_voiceio.egg-info/SOURCES.txt +7 -0
  6. python_voiceio-1.10.0/tests/test_dead_mic.py +94 -0
  7. python_voiceio-1.10.0/tests/test_learn_quiet.py +247 -0
  8. python_voiceio-1.10.0/tests/test_learn_resume.py +243 -0
  9. python_voiceio-1.10.0/tests/test_mic_gain.py +85 -0
  10. python_voiceio-1.10.0/tests/test_presence.py +99 -0
  11. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_service.py +34 -2
  12. python_voiceio-1.10.0/voiceio/__init__.py +1 -0
  13. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/app.py +64 -17
  14. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/cli/learn.py +127 -35
  15. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/config.py +14 -4
  16. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/feedback.py +13 -1
  17. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/learn/evaluate.py +4 -1
  18. python_voiceio-1.10.0/voiceio/learn/quiet.py +172 -0
  19. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/learn/store.py +9 -3
  20. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/learn/teacher.py +3 -3
  21. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/learn/train.py +219 -44
  22. python_voiceio-1.10.0/voiceio/mic_gain.py +123 -0
  23. python_voiceio-1.10.0/voiceio/presence.py +160 -0
  24. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/recorder.py +38 -0
  25. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/service.py +28 -6
  26. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/setup/steps.py +1 -1
  27. python_voiceio-1.10.0/voiceio/sounds/alert.wav +0 -0
  28. python_voiceio-1.9.2/tests/test_learn_quiet.py +0 -103
  29. python_voiceio-1.9.2/voiceio/__init__.py +0 -1
  30. python_voiceio-1.9.2/voiceio/learn/quiet.py +0 -41
  31. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/LICENSE +0 -0
  32. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
  33. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/python_voiceio.egg-info/entry_points.txt +0 -0
  34. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/python_voiceio.egg-info/requires.txt +0 -0
  35. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/python_voiceio.egg-info/top_level.txt +0 -0
  36. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/setup.cfg +0 -0
  37. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_app_wiring.py +0 -0
  38. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_audio_quality.py +0 -0
  39. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_audio_source.py +0 -0
  40. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_backend_probes.py +0 -0
  41. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_cancel.py +0 -0
  42. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_cli.py +0 -0
  43. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_clipboard_read.py +0 -0
  44. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_clipboard_typer.py +0 -0
  45. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_commands.py +0 -0
  46. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_concurrency_lockdown.py +0 -0
  47. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_config.py +0 -0
  48. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_context_terms.py +0 -0
  49. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_corrections.py +0 -0
  50. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_decoders.py +0 -0
  51. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_engine.py +0 -0
  52. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_evdev_uaccess.py +0 -0
  53. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_fallback.py +0 -0
  54. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_fcitx_bridge.py +0 -0
  55. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_feedback_stream.py +0 -0
  56. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_focus.py +0 -0
  57. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_health.py +0 -0
  58. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_hints.py +0 -0
  59. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_history.py +0 -0
  60. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_hotkey_modes.py +0 -0
  61. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_ibus_engine_limits.py +0 -0
  62. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_ibus_engine_listener.py +0 -0
  63. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_ibus_pending.py +0 -0
  64. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_ibus_pids.py +0 -0
  65. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_ibus_ping.py +0 -0
  66. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_ibus_session.py +0 -0
  67. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_ibus_textlimit.py +0 -0
  68. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_ibus_typer.py +0 -0
  69. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_idle_audio.py +0 -0
  70. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_learn_clean.py +0 -0
  71. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_learn_import.py +0 -0
  72. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_learn_language.py +0 -0
  73. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_learn_metrics.py +0 -0
  74. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_learn_pipeline.py +0 -0
  75. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_learn_review.py +0 -0
  76. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_learn_targets.py +0 -0
  77. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_llm.py +0 -0
  78. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_llm_api.py +0 -0
  79. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_modifier_guard.py +0 -0
  80. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_numbers.py +0 -0
  81. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_paste_last.py +0 -0
  82. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_pipeline.py +0 -0
  83. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_platform.py +0 -0
  84. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_postcorrect.py +0 -0
  85. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_postprocess.py +0 -0
  86. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_prebuffer.py +0 -0
  87. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_prompt.py +0 -0
  88. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_quiet_audio.py +0 -0
  89. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_recorder_integration.py +0 -0
  90. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_retention.py +0 -0
  91. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_robustness.py +0 -0
  92. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_security_hardening.py +0 -0
  93. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_server.py +0 -0
  94. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_setup.py +0 -0
  95. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_streaming.py +0 -0
  96. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_suggest.py +0 -0
  97. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_tokens.py +0 -0
  98. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_transcriber.py +0 -0
  99. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_tts.py +0 -0
  100. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_typer_manager.py +0 -0
  101. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_vad.py +0 -0
  102. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_vocabulary.py +0 -0
  103. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_wordfreq.py +0 -0
  104. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/tests/test_worker_decode.py +0 -0
  105. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/__main__.py +0 -0
  106. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/audio_source.py +0 -0
  107. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/backends.py +0 -0
  108. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/cli/__init__.py +0 -0
  109. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/cli/configcmd.py +0 -0
  110. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/cli/corrections.py +0 -0
  111. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/cli/doctor.py +0 -0
  112. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/cli/history.py +0 -0
  113. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/cli/models.py +0 -0
  114. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/cli/servicecmd.py +0 -0
  115. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/cli/uninstall.py +0 -0
  116. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/cli/vocab.py +0 -0
  117. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/clipboard_read.py +0 -0
  118. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/commands.py +0 -0
  119. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/consent.py +0 -0
  120. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/context_terms.py +0 -0
  121. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/__init__.py +0 -0
  122. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/bias.py +0 -0
  123. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/decoders/__init__.py +0 -0
  124. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/decoders/base.py +0 -0
  125. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/decoders/catalog.py +0 -0
  126. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/decoders/faster_whisper/__init__.py +0 -0
  127. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/decoders/faster_whisper/worker.py +0 -0
  128. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/decoders/lanes.py +0 -0
  129. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/decoders/sherpa.py +0 -0
  130. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/decoders/whispercpp.py +0 -0
  131. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/engine.py +0 -0
  132. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/core/pipeline.py +0 -0
  133. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/corrections.py +0 -0
  134. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/data/60-voiceio-uaccess.rules +0 -0
  135. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/data/__init__.py +0 -0
  136. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/demo.py +0 -0
  137. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/display.py +0 -0
  138. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/focus.py +0 -0
  139. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/health.py +0 -0
  140. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/hints.py +0 -0
  141. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/history.py +0 -0
  142. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/hotkeys/__init__.py +0 -0
  143. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/hotkeys/base.py +0 -0
  144. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/hotkeys/chain.py +0 -0
  145. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/hotkeys/evdev.py +0 -0
  146. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/hotkeys/pynput_backend.py +0 -0
  147. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/hotkeys/socket_backend.py +0 -0
  148. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/ibus/__init__.py +0 -0
  149. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/ibus/engine.py +0 -0
  150. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/ibus/install.py +0 -0
  151. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/ibus/pending.py +0 -0
  152. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/ibus/session.py +0 -0
  153. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/ibus/textlimit.py +0 -0
  154. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/idle_audio.py +0 -0
  155. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/learn/__init__.py +0 -0
  156. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/learn/augment.py +0 -0
  157. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/learn/clean.py +0 -0
  158. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/learn/importer.py +0 -0
  159. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/learn/metrics.py +0 -0
  160. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/learn/review.py +0 -0
  161. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/llm.py +0 -0
  162. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/llm_api.py +0 -0
  163. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/models/__init__.py +0 -0
  164. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/models/silero_vad.onnx +0 -0
  165. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/numbers.py +0 -0
  166. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/pidlock.py +0 -0
  167. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/platform.py +0 -0
  168. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/postcorrect.py +0 -0
  169. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/postprocess.py +0 -0
  170. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/power.py +0 -0
  171. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/privacy.py +0 -0
  172. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/prompt.py +0 -0
  173. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/quiet_audio.py +0 -0
  174. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/retention.py +0 -0
  175. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/server/__init__.py +0 -0
  176. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/server/__main__.py +0 -0
  177. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/server/app.py +0 -0
  178. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/server/config.py +0 -0
  179. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/server/runtime.py +0 -0
  180. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/setup/__init__.py +0 -0
  181. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/setup/configfile.py +0 -0
  182. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/setup/extras.py +0 -0
  183. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/setup/hotkey.py +0 -0
  184. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/setup/noninteractive.py +0 -0
  185. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/setup/speech.py +0 -0
  186. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/setup/system.py +0 -0
  187. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/setup/ui.py +0 -0
  188. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/sounds/__init__.py +0 -0
  189. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/sounds/commit.wav +0 -0
  190. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/sounds/start.wav +0 -0
  191. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/sounds/stop.wav +0 -0
  192. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/streaming.py +0 -0
  193. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/suggest.py +0 -0
  194. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tokens.py +0 -0
  195. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tray/__init__.py +0 -0
  196. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tray/_icons.py +0 -0
  197. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tray/_indicator.py +0 -0
  198. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tray/_pystray.py +0 -0
  199. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tts/__init__.py +0 -0
  200. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tts/base.py +0 -0
  201. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tts/chain.py +0 -0
  202. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tts/edge_engine.py +0 -0
  203. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tts/espeak.py +0 -0
  204. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tts/piper_engine.py +0 -0
  205. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/tts/player.py +0 -0
  206. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/__init__.py +0 -0
  207. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/base.py +0 -0
  208. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/chain.py +0 -0
  209. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/clipboard.py +0 -0
  210. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/collecting.py +0 -0
  211. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/fcitx_bridge.py +0 -0
  212. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/ibus.py +0 -0
  213. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/manager.py +0 -0
  214. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/modifier_guard.py +0 -0
  215. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/paste.py +0 -0
  216. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/pynput_type.py +0 -0
  217. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/wtype.py +0 -0
  218. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/xdotool.py +0 -0
  219. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/typers/ydotool.py +0 -0
  220. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/ui.py +0 -0
  221. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/vad.py +0 -0
  222. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/vocab_stats.py +0 -0
  223. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/vocabulary.py +0 -0
  224. {python_voiceio-1.9.2 → python_voiceio-1.10.0}/voiceio/watchdog.py +0 -0
  225. {python_voiceio-1.9.2 → python_voiceio-1.10.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.2
3
+ Version: 1.10.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -98,7 +98,7 @@ voiceio learn schedule on # learn from your recordings while the mac
98
98
  voiceio learn status # what it learned and how each model scores
99
99
  ```
100
100
 
101
- Scheduled learning runs daily or weekly at a time you pick, only on an idle machine on mains power, with a notification you can stop. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
101
+ Scheduled learning runs daily or weekly at a time you pick, on mains power and only while you're away from the computer: it stops within seconds when you come back and resumes later where it was. You get a notification with a button to stop it. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
102
102
 
103
103
  ## Configure
104
104
 
@@ -45,7 +45,7 @@ voiceio learn schedule on # learn from your recordings while the mac
45
45
  voiceio learn status # what it learned and how each model scores
46
46
  ```
47
47
 
48
- Scheduled learning runs daily or weekly at a time you pick, only on an idle machine on mains power, with a notification you can stop. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
48
+ Scheduled learning runs daily or weekly at a time you pick, on mains power and only while you're away from the computer: it stops within seconds when you come back and resumes later where it was. You get a notification with a button to stop it. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
49
49
 
50
50
  ## Configure
51
51
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "1.9.2"
7
+ version = "1.10.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.2
3
+ Version: 1.10.0
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -98,7 +98,7 @@ voiceio learn schedule on # learn from your recordings while the mac
98
98
  voiceio learn status # what it learned and how each model scores
99
99
  ```
100
100
 
101
- Scheduled learning runs daily or weekly at a time you pick, only on an idle machine on mains power, with a notification you can stop. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
101
+ Scheduled learning runs daily or weekly at a time you pick, on mains power and only while you're away from the computer: it stops within seconds when you come back and resumes later where it was. You get a notification with a button to stop it. It never switches models unless the new one wins, and `voiceio learn rollback` undoes a switch. Details: [docs/learning.md](docs/learning.md).
102
102
 
103
103
  ## Configure
104
104
 
@@ -20,6 +20,7 @@ tests/test_concurrency_lockdown.py
20
20
  tests/test_config.py
21
21
  tests/test_context_terms.py
22
22
  tests/test_corrections.py
23
+ tests/test_dead_mic.py
23
24
  tests/test_decoders.py
24
25
  tests/test_engine.py
25
26
  tests/test_evdev_uaccess.py
@@ -46,10 +47,12 @@ tests/test_learn_language.py
46
47
  tests/test_learn_metrics.py
47
48
  tests/test_learn_pipeline.py
48
49
  tests/test_learn_quiet.py
50
+ tests/test_learn_resume.py
49
51
  tests/test_learn_review.py
50
52
  tests/test_learn_targets.py
51
53
  tests/test_llm.py
52
54
  tests/test_llm_api.py
55
+ tests/test_mic_gain.py
53
56
  tests/test_modifier_guard.py
54
57
  tests/test_numbers.py
55
58
  tests/test_paste_last.py
@@ -58,6 +61,7 @@ tests/test_platform.py
58
61
  tests/test_postcorrect.py
59
62
  tests/test_postprocess.py
60
63
  tests/test_prebuffer.py
64
+ tests/test_presence.py
61
65
  tests/test_prompt.py
62
66
  tests/test_quiet_audio.py
63
67
  tests/test_recorder_integration.py
@@ -98,12 +102,14 @@ voiceio/history.py
98
102
  voiceio/idle_audio.py
99
103
  voiceio/llm.py
100
104
  voiceio/llm_api.py
105
+ voiceio/mic_gain.py
101
106
  voiceio/numbers.py
102
107
  voiceio/pidlock.py
103
108
  voiceio/platform.py
104
109
  voiceio/postcorrect.py
105
110
  voiceio/postprocess.py
106
111
  voiceio/power.py
112
+ voiceio/presence.py
107
113
  voiceio/privacy.py
108
114
  voiceio/prompt.py
109
115
  voiceio/quiet_audio.py
@@ -183,6 +189,7 @@ voiceio/setup/steps.py
183
189
  voiceio/setup/system.py
184
190
  voiceio/setup/ui.py
185
191
  voiceio/sounds/__init__.py
192
+ voiceio/sounds/alert.wav
186
193
  voiceio/sounds/commit.wav
187
194
  voiceio/sounds/start.wav
188
195
  voiceio/sounds/stop.wav
@@ -0,0 +1,94 @@
1
+ """A mic stream that dies mid-recording is reopened, not recorded to the end.
2
+
3
+ The real failure: after a suspend/resume the stream delivered 2 s of audio,
4
+ then 102 s of exact zeros; the whole dictation was lost.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import threading
9
+ from unittest.mock import MagicMock, patch
10
+
11
+ import numpy as np
12
+
13
+ from voiceio.config import AudioConfig
14
+ from voiceio.recorder import AudioRecorder
15
+
16
+
17
+ def _recorder() -> AudioRecorder:
18
+ rec = AudioRecorder(AudioConfig(sample_rate=16000, prebuffer_secs=0.5,
19
+ auto_stop_silence_secs=0, auto_stop_no_speech_secs=0))
20
+ rec._stream = MagicMock()
21
+ return rec
22
+
23
+
24
+ def _feed(rec: AudioRecorder, value: float, secs: float) -> None:
25
+ for _ in range(int(secs * 16000 / 1600)):
26
+ rec._callback(np.full((1600, 1), value, dtype=np.float32), 1600, None, None)
27
+
28
+
29
+ def _armed(rec: AudioRecorder) -> threading.Event:
30
+ fired = threading.Event()
31
+ rec.set_on_dead_stream(fired.set)
32
+ return fired
33
+
34
+
35
+ def test_zeros_after_real_audio_fire_once():
36
+ rec = _recorder()
37
+ rec.start()
38
+ fired = _armed(rec)
39
+ _feed(rec, 0.02, 1.0)
40
+ _feed(rec, 0.0, 1.2)
41
+ assert fired.wait(2)
42
+ fired.clear()
43
+ _feed(rec, 0.0, 1.2) # still reviving: not fired again
44
+ assert not fired.wait(0.2)
45
+
46
+
47
+ def test_a_mic_silent_from_the_start_is_left_to_the_muted_check():
48
+ rec = _recorder()
49
+ rec.start()
50
+ fired = _armed(rec)
51
+ _feed(rec, 0.0, 2.0)
52
+ assert not fired.wait(0.2)
53
+
54
+
55
+ def test_quiet_room_noise_is_not_a_dead_stream():
56
+ rec = _recorder()
57
+ rec.start()
58
+ fired = _armed(rec)
59
+ _feed(rec, 0.02, 0.5)
60
+ _feed(rec, 0.0005, 3.0) # a working mic's silence is never exact zero
61
+ assert not fired.wait(0.2)
62
+
63
+
64
+ def test_revive_reopens_and_allows_at_most_two_attempts():
65
+ rec = _recorder()
66
+ rec.start()
67
+ with patch.object(rec, "open_stream") as opened, patch.object(rec, "close_stream"):
68
+ for expected in (True, True, False):
69
+ fired = _armed(rec)
70
+ _feed(rec, 0.02, 0.2)
71
+ _feed(rec, 0.0, 1.2)
72
+ assert fired.wait(0.5) is expected
73
+ if expected:
74
+ rec.revive_stream()
75
+ assert opened.call_count == 2
76
+ assert rec.is_recording
77
+
78
+
79
+ def test_daemon_revives_and_warns_only_when_still_dead():
80
+ from voiceio.app import VoiceIO, _State
81
+ vio = VoiceIO.__new__(VoiceIO)
82
+ vio._hotkey_lock = threading.Lock()
83
+ vio._state = _State.RECORDING
84
+ vio._recording = 7
85
+ vio.recorder = MagicMock(silent_signal_secs=0.0)
86
+ with patch("voiceio.app.time.sleep"), patch("voiceio.feedback.notify") as notify:
87
+ vio._revive_mic(7)
88
+ vio.recorder.revive_stream.assert_called_once()
89
+ notify.assert_not_called()
90
+ vio.recorder.silent_signal_secs = 1.5
91
+ vio._revive_mic(7)
92
+ notify.assert_called_once()
93
+ vio._revive_mic(8) # a later recording: not ours to touch
94
+ assert vio.recorder.revive_stream.call_count == 2
@@ -0,0 +1,247 @@
1
+ import argparse
2
+
3
+ import pytest
4
+
5
+ from voiceio import power
6
+ from voiceio.learn import quiet
7
+
8
+
9
+ def _supply(root, name, kind, online):
10
+ d = root / name
11
+ d.mkdir()
12
+ (d / "type").write_text(kind + "\n")
13
+ (d / "online").write_text(online + "\n")
14
+
15
+
16
+ def test_on_battery_needs_a_mains_supply_that_is_offline(tmp_path):
17
+ assert not power.on_battery(tmp_path) # desktop: no supplies listed
18
+ _supply(tmp_path, "BAT0", "Battery", "0")
19
+ _supply(tmp_path, "AC", "Mains", "0")
20
+ assert power.on_battery(tmp_path)
21
+ (tmp_path / "AC" / "online").write_text("1\n")
22
+ assert not power.on_battery(tmp_path)
23
+
24
+
25
+ def test_busy_reason_is_load_per_cpu_then_battery(monkeypatch):
26
+ monkeypatch.setattr(quiet.os, "cpu_count", lambda: 8)
27
+ monkeypatch.setattr(quiet, "on_battery", lambda: False)
28
+ monkeypatch.setattr(quiet.os, "getloadavg", lambda: (4.0, 0, 0)) # 0.5 per CPU
29
+ assert "load 0.50 per CPU" in quiet.busy_reason(0.25)
30
+ monkeypatch.setattr(quiet.os, "getloadavg", lambda: (1.0, 0, 0))
31
+ assert quiet.busy_reason(0.25) is None
32
+ monkeypatch.setattr(quiet, "on_battery", lambda: True)
33
+ assert quiet.busy_reason(0.25) == "on battery"
34
+
35
+
36
+ def test_wait_until_quiet_waits_then_gives_up(monkeypatch):
37
+ now = [0.0]
38
+ sleeps = []
39
+ reasons = iter(["busy", "busy", None])
40
+ monkeypatch.setattr(quiet, "busy_reason", lambda *_: next(reasons))
41
+ args = dict(poll=300, sleep=lambda s: (sleeps.append(s), now.__setitem__(0, now[0] + s)),
42
+ clock=lambda: now[0], say=lambda _: None)
43
+ assert quiet.wait_until_quiet(0.25, 3, **args)
44
+ assert sleeps == [300, 300]
45
+
46
+ monkeypatch.setattr(quiet, "busy_reason", lambda *_: "on battery")
47
+ said = []
48
+ now[0] = 0.0
49
+ assert not quiet.wait_until_quiet(0.25, 1, **{**args, "say": said.append})
50
+ assert now[0] <= 3600 and said[-1].startswith("still on battery after 1 h")
51
+
52
+
53
+ def test_busy_reason_waits_for_the_user_to_be_away(monkeypatch):
54
+ from voiceio import presence
55
+ monkeypatch.setattr(quiet, "on_battery", lambda: False)
56
+ monkeypatch.setattr(quiet.os, "getloadavg", lambda: (0.0, 0, 0))
57
+ seen = []
58
+ monkeypatch.setattr(presence, "check",
59
+ lambda away: seen.append(away) or presence.Presence(True, "in use (x)"))
60
+ assert quiet.busy_reason(0.25, away_after=600) == "in use (x)" and seen == [600]
61
+ assert quiet.busy_reason(0.25) is None # unattended `maintain`: no presence check
62
+ monkeypatch.setattr(presence, "check", lambda away: presence.Presence(False, "away"))
63
+ assert quiet.busy_reason(0.25, away_after=600) is None
64
+
65
+
66
+ def test_waiting_logs_each_reason_once_not_every_reading(monkeypatch):
67
+ readings = iter(["in use (last input 3 s ago)", "in use (last input 9 s ago)",
68
+ "on battery", None])
69
+ monkeypatch.setattr(quiet, "busy_reason", lambda *_: next(readings))
70
+ said = []
71
+ assert quiet.wait_until_quiet(0.25, 3, poll=1, sleep=lambda s: None, say=said.append)
72
+ assert said == ["waiting: in use (last input 3 s ago)", "waiting: on battery"]
73
+
74
+
75
+ class _Proc:
76
+ """A worker that runs for `polls` checks, then exits with `code`."""
77
+ def __init__(self, polls=99, code=0):
78
+ self.left, self.code, self.pid, self.stopped = polls, code, 4242, False
79
+
80
+ def poll(self):
81
+ if self.stopped:
82
+ return -15
83
+ if self.left <= 0:
84
+ return self.code
85
+ self.left -= 1
86
+ return None
87
+
88
+ @property
89
+ def returncode(self):
90
+ return self.poll()
91
+
92
+
93
+ def _watch(monkeypatch, tmp_path, procs, reasons, *, quiet_ok=True):
94
+ spawned, said = [], []
95
+ procs = iter(procs)
96
+ reasons = iter(reasons)
97
+
98
+ def spawn(argv, env):
99
+ spawned.append(env)
100
+ return next(procs)
101
+
102
+ def stop(proc):
103
+ proc.stopped = True
104
+ monkeypatch.setattr(quiet, "wait_until_quiet", lambda *a, **k: quiet_ok)
105
+ monkeypatch.setattr(quiet, "stop_reason", lambda away: next(reasons, None))
106
+ code = quiet.run_while_away(["voiceio", "learn", "maintain"], max_load=0.25, away_minutes=10,
107
+ wait_hours=3, threads=3, hold_file=tmp_path / "hold",
108
+ spawn=spawn, stop=stop, sleep=lambda s: None, say=said.append)
109
+ return code, spawned, said
110
+
111
+
112
+ def test_work_stops_when_the_user_returns_and_resumes_later(monkeypatch, tmp_path):
113
+ first, second = _Proc(), _Proc(polls=3, code=0)
114
+ code, spawned, said = _watch(monkeypatch, tmp_path, [first, second],
115
+ [None, None, "in use (last input 0 s ago)"])
116
+ assert code == 0 and first.stopped and not second.stopped
117
+ assert said == ["paused: in use (last input 0 s ago); it resumes where it stopped "
118
+ "once you've been away 10 min"]
119
+ assert spawned[0][quiet.WORKER_ENV] == "1" and spawned[0]["OMP_NUM_THREADS"] == "3"
120
+ assert quiet.RESUMED_ENV not in spawned[0] and spawned[1][quiet.RESUMED_ENV] == "1"
121
+
122
+
123
+ def test_watcher_reports_the_work_exit_status_and_gives_up_quietly(monkeypatch, tmp_path):
124
+ assert _watch(monkeypatch, tmp_path, [_Proc(polls=1, code=3)], [])[0] == 3
125
+ code, spawned, _ = _watch(monkeypatch, tmp_path, [], [], quiet_ok=False)
126
+ assert code == 0 and spawned == [] # never away: nothing started, unit doesn't fail
127
+
128
+
129
+ def test_a_model_switch_in_progress_is_never_cut_off(monkeypatch, tmp_path):
130
+ proc = _Proc(polls=3, code=0)
131
+ reasons = iter(["in use"] * 10)
132
+
133
+ def spawn(argv, env):
134
+ (tmp_path / "hold").touch() # the worker holds it
135
+ return proc
136
+ monkeypatch.setattr(quiet, "wait_until_quiet", lambda *a, **k: True)
137
+ monkeypatch.setattr(quiet, "stop_reason", lambda away: next(reasons))
138
+ assert quiet.run_while_away(["x"], max_load=1, away_minutes=1, wait_hours=1, threads=1,
139
+ hold_file=tmp_path / "hold", spawn=spawn,
140
+ stop=lambda p: pytest.fail("cut off a model switch"),
141
+ sleep=lambda s: None, say=print) == 0
142
+ assert not (tmp_path / "hold").exists()
143
+
144
+
145
+ def test_hold_marks_the_file_only_inside_the_block(monkeypatch, tmp_path):
146
+ flag = tmp_path / "hold"
147
+ monkeypatch.setenv(quiet.HOLD_ENV, str(flag))
148
+ with quiet.hold():
149
+ assert flag.exists()
150
+ assert not flag.exists()
151
+ monkeypatch.delenv(quiet.HOLD_ENV)
152
+ with quiet.hold(): # not under the watcher: a no-op
153
+ pass
154
+
155
+
156
+ def test_stop_ends_the_whole_process_group_at_once():
157
+ """The decoder runs in a subprocess of the worker: it must go too."""
158
+ import os
159
+ import time
160
+ proc = quiet._spawn(["sh", "-c", "sleep 30 & sleep 30; wait"], dict(os.environ))
161
+ time.sleep(0.2)
162
+ t0 = time.monotonic()
163
+ quiet._stop(proc)
164
+ assert time.monotonic() - t0 < 3 and proc.poll() is not None
165
+ with pytest.raises(ProcessLookupError):
166
+ os.killpg(proc.pid, 0) # nothing left in the group
167
+
168
+
169
+ def test_threads_default_to_half_the_cpus(monkeypatch):
170
+ monkeypatch.setattr(quiet.os, "cpu_count", lambda: 16)
171
+ assert quiet.worker_threads(0) == 8 and quiet.worker_threads(5) == 5
172
+
173
+
174
+ def test_maintain_in_the_worker_does_the_work(monkeypatch):
175
+ from voiceio.cli import learn as cli
176
+ monkeypatch.setattr(cli, "_yield_cpu", lambda: None)
177
+ monkeypatch.setenv(quiet.WORKER_ENV, "1")
178
+ monkeypatch.setattr(quiet, "run_while_away", lambda *a, **k: pytest.fail("worker must not watch"))
179
+ ran = []
180
+ monkeypatch.setattr(cli, "_maintain", lambda args, cfg: ran.append(cfg.away_minutes))
181
+ cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1, when_idle=True))
182
+ assert ran == [10.0]
183
+
184
+
185
+ def test_scheduled_maintain_fails_when_its_work_does(monkeypatch):
186
+ from voiceio.cli import learn as cli
187
+ monkeypatch.setattr(cli, "_yield_cpu", lambda: None)
188
+ monkeypatch.delenv(quiet.WORKER_ENV, raising=False)
189
+ monkeypatch.setattr(quiet, "run_while_away", lambda *a, **k: 2)
190
+ with pytest.raises(SystemExit) as e:
191
+ cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1, when_idle=True))
192
+ assert e.value.code == 2
193
+
194
+
195
+ def test_scheduled_maintain_waits_for_a_quiet_machine(monkeypatch):
196
+ from voiceio.cli import learn as cli
197
+ monkeypatch.setattr(cli, "_yield_cpu", lambda: None)
198
+ monkeypatch.setattr(quiet, "wait_until_quiet", lambda *a, **k: False)
199
+ ran = []
200
+ monkeypatch.setattr(cli, "cmd_label", lambda a: ran.append("label"))
201
+ cli.cmd_maintain(argparse.Namespace(min_new=5, base="small", epochs=1, when_idle=True))
202
+ assert ran == [] # busy all night: nothing ran
203
+
204
+
205
+ def test_learning_announcement_offers_stop_and_weekly(monkeypatch, tmp_path):
206
+ from voiceio import feedback
207
+ from voiceio.cli import learn as cli
208
+ sent = []
209
+ monkeypatch.setattr(feedback, "notify_actions", lambda *a: sent.append(a))
210
+ monkeypatch.setattr(cli.store, "load_labels", lambda: {})
211
+ monkeypatch.setattr(cli.store, "clips", lambda: [argparse.Namespace(audio=f"a{i}.wav") for i in range(3)])
212
+ monkeypatch.setattr(cli.store, "latest_dataset", lambda: None)
213
+ cli._announce_learning(min_new=50)
214
+ (title, body, actions, _), = sent
215
+ assert "learning" in title and body.startswith("Labeling 3 new recordings,")
216
+ assert "while you're away" in body and set(actions) == {"stop", "weekly"}
217
+
218
+ sent.clear()
219
+ monkeypatch.setattr(cli.store, "load_labels", lambda: {f"a{i}.wav": 1 for i in range(3)})
220
+ monkeypatch.setattr(cli.store, "latest_dataset", lambda: tmp_path)
221
+ monkeypatch.setattr(cli.store, "dataset_clips", lambda _: {f"a{i}.wav" for i in range(3)})
222
+ cli._announce_learning(min_new=50)
223
+ assert sent == [] # nothing to do: no notification
224
+
225
+
226
+ def test_notify_actions_runs_the_picked_action(monkeypatch):
227
+ import subprocess
228
+
229
+ from voiceio import feedback
230
+ monkeypatch.setattr("sys.platform", "linux")
231
+ monkeypatch.setattr(feedback, "_which", lambda _: "/usr/bin/notify-send")
232
+ monkeypatch.setattr(feedback, "_notify_send_has_actions", lambda: True)
233
+ calls = []
234
+
235
+ def fake_run(args, **kw):
236
+ calls.append(args)
237
+ return subprocess.CompletedProcess(args, 0, stdout="weekly\n")
238
+ monkeypatch.setattr(feedback.subprocess, "run", fake_run)
239
+ picked = []
240
+
241
+ class Now: # run the waiting thread inline
242
+ def __init__(self, target, daemon): self.target = target
243
+ def start(self): self.target()
244
+ monkeypatch.setattr(feedback.threading, "Thread", Now)
245
+ feedback.notify_actions("t", "b", {"stop": "Stop", "weekly": "Weekly"}, picked.append)
246
+ assert picked == ["weekly"]
247
+ assert "--wait" in calls[0] and "--action=stop=Stop" in calls[0]