python-voiceio 0.9.3__py3-none-any.whl → 0.9.4__py3-none-any.whl

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.
@@ -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
@@ -1,5 +1,5 @@
1
- python_voiceio-0.9.3.dist-info/licenses/LICENSE,sha256=Gz61o8jFTAvZUZyB3nWDB3DQQVuipjfPkVu9W8hBHM0,1072
2
- voiceio/__init__.py,sha256=xKd3pzbczuMsdB08eLAOqZDUd_q1IRxwZ_ccAFL4c4A,22
1
+ python_voiceio-0.9.4.dist-info/licenses/LICENSE,sha256=Gz61o8jFTAvZUZyB3nWDB3DQQVuipjfPkVu9W8hBHM0,1072
2
+ voiceio/__init__.py,sha256=e56AvHfJCtG2ZwwINqsxINVbehWdKxMYgIDbjd7P-II,22
3
3
  voiceio/__main__.py,sha256=xT5QCGGreYMisHO7Lh_Y-xAQ2TOzG2D7npKknGNcWSY,53
4
4
  voiceio/app.py,sha256=8xS_MySy6cvpxXDcd0-6YwQK6KW_iT8Vdf6FqzYUXpI,74743
5
5
  voiceio/audit.py,sha256=p4F2BBX4wEC9eS8MSplzfXL52XC2dyIKbgrRvR-WbTU,15802
@@ -26,7 +26,7 @@ voiceio/platform.py,sha256=f_8MmC5q1sCEuEPyKjRqlVvV9FExuGmwn_aJXhkpiEk,11809
26
26
  voiceio/postcorrect.py,sha256=iyO7MPIR4a4XIASEGOs9Y3iUXVTfwtVkhVOWj2vY1lk,11201
27
27
  voiceio/postprocess.py,sha256=9grkq3I6XgiQV6ZQbIw2_fOUe57tzC7GBPvLXqhhdTs,2979
28
28
  voiceio/prompt.py,sha256=9iza8KoQxvO_U9NPMJ8Ep7j-zkIs9nW8N5sacCRpsEk,2904
29
- voiceio/recorder.py,sha256=x3WG-3GXvqHb0yagXXVeDgjD5PnG6xv6MyRHMTnB68w,13885
29
+ voiceio/recorder.py,sha256=csqm4Q_GuQyDxhJAVDCL6TQd_nna77sfWNiwqF6TsQk,15003
30
30
  voiceio/retention.py,sha256=i_VWPCyykT4kQ1uKilTplpiIrACTYljn09BU1vL71A0,6019
31
31
  voiceio/service.py,sha256=1YhnDPHnu0Aw66uwsp05TWfs18bf3io_h6r8eNla7nA,12525
32
32
  voiceio/snapshots.py,sha256=bm-lzIeYucDUDlPBZiFiWd-jyB2I6Ft1jRANZCO6go4,2351
@@ -74,8 +74,8 @@ voiceio/typers/pynput_type.py,sha256=DTtkT59M-EKOAlTNkvcZrRvf_Fq52OpI8S5t_8S-QAY
74
74
  voiceio/typers/wtype.py,sha256=d1wG-HZdYDQxXIysV_Xvk9HixDpaJgl3VEoKbP5vhIs,1786
75
75
  voiceio/typers/xdotool.py,sha256=dh1zhPqUT8ihJahJ7ZKm6PtfYY087UAzADx664DvQOM,1356
76
76
  voiceio/typers/ydotool.py,sha256=dt0W9ot__W8LQG2so8vZLGIw9leK2Q6g3i-Voo0LXE4,5327
77
- python_voiceio-0.9.3.dist-info/METADATA,sha256=h-f14EX3kSMP26A2Y12MJbprM_03l0TkRSm0wIYWrao,15877
78
- python_voiceio-0.9.3.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
79
- python_voiceio-0.9.3.dist-info/entry_points.txt,sha256=U64fA65zxzyLoC8bgbn2ztQVWHLsc0o0H0qCk1J9DMc,218
80
- python_voiceio-0.9.3.dist-info/top_level.txt,sha256=piwtn309lD6uexQyXdZ-efAVBJF9y6Wfr48Z-8zkNhg,8
81
- python_voiceio-0.9.3.dist-info/RECORD,,
77
+ python_voiceio-0.9.4.dist-info/METADATA,sha256=jwrZH_VW6__XC_VKHFMHr052Iadetbx3V5Ma26UYZNg,15877
78
+ python_voiceio-0.9.4.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
79
+ python_voiceio-0.9.4.dist-info/entry_points.txt,sha256=U64fA65zxzyLoC8bgbn2ztQVWHLsc0o0H0qCk1J9DMc,218
80
+ python_voiceio-0.9.4.dist-info/top_level.txt,sha256=piwtn309lD6uexQyXdZ-efAVBJF9y6Wfr48Z-8zkNhg,8
81
+ python_voiceio-0.9.4.dist-info/RECORD,,
voiceio/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.9.3"
1
+ __version__ = "0.9.4"
voiceio/recorder.py CHANGED
@@ -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")