python-voiceio 0.9.3__tar.gz → 0.9.4__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 (130) hide show
  1. {python_voiceio-0.9.3/python_voiceio.egg-info → python_voiceio-0.9.4}/PKG-INFO +1 -1
  2. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/pyproject.toml +1 -1
  3. {python_voiceio-0.9.3 → python_voiceio-0.9.4/python_voiceio.egg-info}/PKG-INFO +1 -1
  4. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_robustness.py +25 -7
  5. python_voiceio-0.9.4/voiceio/__init__.py +1 -0
  6. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/recorder.py +32 -10
  7. python_voiceio-0.9.3/voiceio/__init__.py +0 -1
  8. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/LICENSE +0 -0
  9. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/README.md +0 -0
  10. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/python_voiceio.egg-info/SOURCES.txt +0 -0
  11. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/python_voiceio.egg-info/dependency_links.txt +0 -0
  12. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/python_voiceio.egg-info/entry_points.txt +0 -0
  13. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/python_voiceio.egg-info/requires.txt +0 -0
  14. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/python_voiceio.egg-info/top_level.txt +0 -0
  15. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/setup.cfg +0 -0
  16. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_adjudicate.py +0 -0
  17. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_app_wiring.py +0 -0
  18. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_audio_quality.py +0 -0
  19. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_audit.py +0 -0
  20. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_autocorrect.py +0 -0
  21. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_autocorrect_state.py +0 -0
  22. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_backend_probes.py +0 -0
  23. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_cli.py +0 -0
  24. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_clipboard_read.py +0 -0
  25. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_commands.py +0 -0
  26. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_concurrency_lockdown.py +0 -0
  27. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_config.py +0 -0
  28. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_correct_batch.py +0 -0
  29. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_corrections.py +0 -0
  30. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_evaluate.py +0 -0
  31. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_fallback.py +0 -0
  32. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_health.py +0 -0
  33. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_hints.py +0 -0
  34. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_history.py +0 -0
  35. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_ibus_pending.py +0 -0
  36. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_ibus_ping.py +0 -0
  37. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_ibus_typer.py +0 -0
  38. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_llm.py +0 -0
  39. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_llm_api.py +0 -0
  40. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_numbers.py +0 -0
  41. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_platform.py +0 -0
  42. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_postcorrect.py +0 -0
  43. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_postprocess.py +0 -0
  44. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_prebuffer.py +0 -0
  45. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_prompt.py +0 -0
  46. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_recorder_integration.py +0 -0
  47. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_retention.py +0 -0
  48. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_security_hardening.py +0 -0
  49. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_snapshots.py +0 -0
  50. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_streaming.py +0 -0
  51. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_tokens.py +0 -0
  52. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_transcriber.py +0 -0
  53. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_tts.py +0 -0
  54. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_vad.py +0 -0
  55. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_vocabulary.py +0 -0
  56. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_wizard.py +0 -0
  57. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/tests/test_wordfreq.py +0 -0
  58. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/__main__.py +0 -0
  59. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/app.py +0 -0
  60. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/audit.py +0 -0
  61. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/autocorrect.py +0 -0
  62. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/autocorrect_state.py +0 -0
  63. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/backends.py +0 -0
  64. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/cli.py +0 -0
  65. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/clipboard_read.py +0 -0
  66. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/commands.py +0 -0
  67. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/config.py +0 -0
  68. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/consent.py +0 -0
  69. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/corrections.py +0 -0
  70. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/demo.py +0 -0
  71. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/evaluate.py +0 -0
  72. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/feedback.py +0 -0
  73. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/health.py +0 -0
  74. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/hints.py +0 -0
  75. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/history.py +0 -0
  76. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/hotkeys/__init__.py +0 -0
  77. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/hotkeys/base.py +0 -0
  78. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/hotkeys/chain.py +0 -0
  79. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/hotkeys/evdev.py +0 -0
  80. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/hotkeys/pynput_backend.py +0 -0
  81. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/hotkeys/socket_backend.py +0 -0
  82. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/ibus/__init__.py +0 -0
  83. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/ibus/engine.py +0 -0
  84. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/ibus/pending.py +0 -0
  85. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/llm.py +0 -0
  86. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/llm_api.py +0 -0
  87. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/models/__init__.py +0 -0
  88. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/models/silero_vad.onnx +0 -0
  89. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/numbers.py +0 -0
  90. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/pidlock.py +0 -0
  91. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/platform.py +0 -0
  92. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/postcorrect.py +0 -0
  93. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/postprocess.py +0 -0
  94. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/prompt.py +0 -0
  95. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/retention.py +0 -0
  96. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/service.py +0 -0
  97. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/snapshots.py +0 -0
  98. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/sounds/__init__.py +0 -0
  99. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/sounds/commit.wav +0 -0
  100. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/sounds/start.wav +0 -0
  101. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/sounds/stop.wav +0 -0
  102. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/streaming.py +0 -0
  103. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tokens.py +0 -0
  104. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/transcriber.py +0 -0
  105. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tray/__init__.py +0 -0
  106. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tray/_icons.py +0 -0
  107. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tray/_indicator.py +0 -0
  108. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tray/_pystray.py +0 -0
  109. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tts/__init__.py +0 -0
  110. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tts/base.py +0 -0
  111. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tts/chain.py +0 -0
  112. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tts/edge_engine.py +0 -0
  113. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tts/espeak.py +0 -0
  114. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tts/piper_engine.py +0 -0
  115. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/tts/player.py +0 -0
  116. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/typers/__init__.py +0 -0
  117. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/typers/base.py +0 -0
  118. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/typers/chain.py +0 -0
  119. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/typers/clipboard.py +0 -0
  120. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/typers/ibus.py +0 -0
  121. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/typers/pynput_type.py +0 -0
  122. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/typers/wtype.py +0 -0
  123. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/typers/xdotool.py +0 -0
  124. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/typers/ydotool.py +0 -0
  125. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/vad.py +0 -0
  126. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/vocab_stats.py +0 -0
  127. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/vocabulary.py +0 -0
  128. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/wizard.py +0 -0
  129. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/wordfreq.py +0 -0
  130. {python_voiceio-0.9.3 → python_voiceio-0.9.4}/voiceio/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-voiceio
3
- Version: 0.9.3
3
+ Version: 0.9.4
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python-voiceio"
7
- version = "0.9.3"
7
+ version = "0.9.4"
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: 0.9.3
3
+ Version: 0.9.4
4
4
  Summary: Voice dictation for Linux. Speak → text, locally, instantly.
5
5
  Author: Hugo Montenegro
6
6
  License-Expression: MIT
@@ -76,27 +76,45 @@ def _make_recorder():
76
76
  class TestAutoStop:
77
77
  """Auto-stop distinguishes 'spoke then paused' from 'nothing ever heard'."""
78
78
 
79
- def _feed(self, rec, secs, *, speech):
80
- """Drive _callback with `secs` of audio; speech=False → silence."""
79
+ def _feed(self, rec, secs, *, speech, amplitude=0.0):
80
+ """Drive _callback with `secs` of audio.
81
+
82
+ speech → what the (mocked) VAD reports (is_speech).
83
+ amplitude → raw sample level; 0.0 = digital silence (muted mic),
84
+ >1e-4 = a real audio signal reaching the mic.
85
+ """
81
86
  rec._vad.is_speech.return_value = speech
82
87
  frames = int(secs * rec.sample_rate)
83
- rec._callback(np.zeros((frames, 1), dtype=np.float32), frames, None, None)
88
+ data = np.full((frames, 1), amplitude, dtype=np.float32)
89
+ rec._callback(data, frames, None, None)
84
90
 
85
- def test_no_speech_fires_no_speech_reason(self):
91
+ def test_muted_mic_fires_no_speech_reason(self):
86
92
  rec = _make_recorder()
87
93
  rec._recording = True
88
94
  fired = []
89
95
  rec.set_on_auto_stop(lambda reason: fired.append(reason))
90
- # 21s of pure silence, speech never heard.
91
- self._feed(rec, 21, speech=False)
96
+ # 21s of digital zeros a muted / dead mic.
97
+ self._feed(rec, 21, speech=False, amplitude=0.0)
92
98
  assert fired == ["no_speech"]
93
99
 
100
+ def test_real_audio_vad_misses_does_not_autostop(self):
101
+ """Regression: real audio the VAD fails to flag as speech must NOT be
102
+ cut off. This is the 0.9.3 bug — a 20s dictation the Silero VAD scored
103
+ as silence auto-stopped mid-sentence."""
104
+ rec = _make_recorder()
105
+ rec._recording = True
106
+ fired = []
107
+ rec.set_on_auto_stop(lambda reason: fired.append(reason))
108
+ # 25s of clearly-present audio, but the VAD never calls it speech.
109
+ self._feed(rec, 25, speech=False, amplitude=0.05)
110
+ assert fired == [] # signal is present → neither auto-stop fires
111
+
94
112
  def test_no_speech_does_not_fire_early(self):
95
113
  rec = _make_recorder()
96
114
  rec._recording = True
97
115
  fired = []
98
116
  rec.set_on_auto_stop(lambda reason: fired.append(reason))
99
- self._feed(rec, 10, speech=False) # below the 20s threshold
117
+ self._feed(rec, 10, speech=False, amplitude=0.0) # below the 20s threshold
100
118
  assert fired == []
101
119
 
102
120
  def test_speech_then_silence_fires_silence_reason(self):
@@ -0,0 +1 @@
1
+ __version__ = "0.9.4"
@@ -15,6 +15,11 @@ if TYPE_CHECKING:
15
15
 
16
16
  log = logging.getLogger(__name__)
17
17
 
18
+ # Peak amplitude below this is treated as no signal at all (a muted mic emits
19
+ # exact zeros; a working mic's "silence" still noises above ~1e-4). Matches the
20
+ # floor in has_signal(). Used only to detect a dead mic, never speech vs pause.
21
+ _SIGNAL_FLOOR = 1e-4
22
+
18
23
 
19
24
  class RingBuffer:
20
25
  """Fixed-size ring buffer for float32 audio samples."""
@@ -111,6 +116,10 @@ class AudioRecorder:
111
116
  self._no_speech_secs = cfg.auto_stop_no_speech_secs
112
117
  self._sustained_silence = 0.0
113
118
  self._heard_speech = False
119
+ # Raw-signal (amplitude) tracking for the muted-mic auto-stop, kept
120
+ # separate from the VAD-based _heard_speech / _sustained_silence above.
121
+ self._heard_signal = False
122
+ self._silent_signal_secs = 0.0
114
123
  # Called with a reason: "silence" (spoke then paused) or "no_speech"
115
124
  # (nothing ever heard — a muted mic or a forgotten hotkey).
116
125
  self._on_auto_stop: Callable[[str], None] | None = None
@@ -221,6 +230,8 @@ class AudioRecorder:
221
230
  self._silent_chunks = 0.0
222
231
  self._sustained_silence = 0.0
223
232
  self._heard_speech = False
233
+ self._heard_signal = False
234
+ self._silent_signal_secs = 0.0
224
235
  self._pause_fired_at = 0
225
236
  self._meter_peak = 0.0
226
237
  self._meter_clipped = 0
@@ -301,8 +312,19 @@ class AudioRecorder:
301
312
 
302
313
  # Level metering
303
314
  abs_chunk = np.abs(chunk.ravel())
304
- self._meter_peak = max(self._meter_peak, float(abs_chunk.max(initial=0.0)))
315
+ chunk_peak = float(abs_chunk.max(initial=0.0))
316
+ self._meter_peak = max(self._meter_peak, chunk_peak)
305
317
  self._meter_samples += len(abs_chunk)
318
+
319
+ # Raw-signal presence, independent of the VAD. A muted mic delivers
320
+ # digital zeros; real audio (even speech the VAD fails to classify)
321
+ # sits well above the floor. This — NOT the VAD — gates the muted-mic
322
+ # auto-stop, so a recording is never cut while real audio is coming in.
323
+ if chunk_peak > _SIGNAL_FLOOR:
324
+ self._heard_signal = True
325
+ self._silent_signal_secs = 0.0
326
+ else:
327
+ self._silent_signal_secs += frames / self.sample_rate
306
328
  pinned = abs_chunk >= 0.99
307
329
  if np.count_nonzero(pinned) >= 4:
308
330
  runs = np.convolve(pinned.astype(np.int8), np.ones(4, dtype=np.int8), "valid")
@@ -345,18 +367,18 @@ class AudioRecorder:
345
367
  log.info("Auto-stopping after %.0fs of silence", self._auto_stop_secs)
346
368
  cb("silence")
347
369
 
348
- # Safety net: nothing heard at all for a long time. Because speech
349
- # resets _sustained_silence AND is gated out by not-_heard_speech, this
350
- # measures continuous silence from the very start of the recording a
351
- # muted mic (all zeros) or a hotkey pressed by accident. Distinct reason
352
- # so the app can surface it instead of failing silently.
370
+ # Safety net for a dead mic: NO raw audio signal at all for a long
371
+ # time the mic is muted / unplugged / delivering zeros, never merely
372
+ # that the VAD didn't flag speech (that would cut real dictation off
373
+ # mid-sentence). _heard_signal latches on the first non-zero chunk, so
374
+ # this only fires for a mic that was silent from the very start.
353
375
  elif (self._on_auto_stop is not None
354
376
  and self._no_speech_secs > 0
355
- and not self._heard_speech
356
- and self._sustained_silence >= self._no_speech_secs):
377
+ and not self._heard_signal
378
+ and self._silent_signal_secs >= self._no_speech_secs):
357
379
  cb = self._on_auto_stop
358
380
  self._on_auto_stop = None
359
- self._sustained_silence = 0.0
360
- log.info("Auto-stopping: no speech heard in %.0fs (mic muted?)",
381
+ self._silent_signal_secs = 0.0
382
+ log.info("Auto-stopping: no mic signal in %.0fs (muted?)",
361
383
  self._no_speech_secs)
362
384
  cb("no_speech")
@@ -1 +0,0 @@
1
- __version__ = "0.9.3"
File without changes
File without changes
File without changes