python-voiceio 0.9.2__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.
- {python_voiceio-0.9.2.dist-info → python_voiceio-0.9.4.dist-info}/METADATA +1 -1
- {python_voiceio-0.9.2.dist-info → python_voiceio-0.9.4.dist-info}/RECORD +10 -10
- voiceio/__init__.py +1 -1
- voiceio/app.py +22 -2
- voiceio/config.py +5 -0
- voiceio/recorder.py +45 -4
- {python_voiceio-0.9.2.dist-info → python_voiceio-0.9.4.dist-info}/WHEEL +0 -0
- {python_voiceio-0.9.2.dist-info → python_voiceio-0.9.4.dist-info}/entry_points.txt +0 -0
- {python_voiceio-0.9.2.dist-info → python_voiceio-0.9.4.dist-info}/licenses/LICENSE +0 -0
- {python_voiceio-0.9.2.dist-info → python_voiceio-0.9.4.dist-info}/top_level.txt +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
python_voiceio-0.9.
|
|
2
|
-
voiceio/__init__.py,sha256=
|
|
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
|
-
voiceio/app.py,sha256=
|
|
4
|
+
voiceio/app.py,sha256=8xS_MySy6cvpxXDcd0-6YwQK6KW_iT8Vdf6FqzYUXpI,74743
|
|
5
5
|
voiceio/audit.py,sha256=p4F2BBX4wEC9eS8MSplzfXL52XC2dyIKbgrRvR-WbTU,15802
|
|
6
6
|
voiceio/autocorrect.py,sha256=jGsglEYwAHWk2uWedeEZzyAMW2LWO_8nQGZgrWYy4Dc,26552
|
|
7
7
|
voiceio/autocorrect_state.py,sha256=BNzdHP5slcOV_-OU4MlaKE7xgVJzrPncsYFK30JWTs8,6858
|
|
@@ -9,7 +9,7 @@ voiceio/backends.py,sha256=-rs1YhPblub4NjFb6qCD7Ih9FqBRXtPuvjVv9bQoQ1w,368
|
|
|
9
9
|
voiceio/cli.py,sha256=QE8N7N3d1O7r9-u_jbYMAH9gw0ILX7dSxDZZYj83lIs,78969
|
|
10
10
|
voiceio/clipboard_read.py,sha256=lz75CkRGh7pyvO6xvoUmxJDl-Gwie1xlXQHK70PO84I,4261
|
|
11
11
|
voiceio/commands.py,sha256=Vhtn8s5G5OcCbRN44YZ-_14Fa85ISXl_3Ix-KqSHs2A,4694
|
|
12
|
-
voiceio/config.py,sha256=
|
|
12
|
+
voiceio/config.py,sha256=IxXfs780V4eMhbfoyHZAUER7cmjg950Lp1wsvm9aEIU,15563
|
|
13
13
|
voiceio/consent.py,sha256=jtdp1TkTbicUSTCDmV4qo4SdbS4kQpYzy1Kjc_7pJVw,1883
|
|
14
14
|
voiceio/corrections.py,sha256=7moKhkVvo5CZU1mZobV2-MajkcHiwliKBY1dSBFh0w0,7445
|
|
15
15
|
voiceio/demo.py,sha256=QRNJuNObXCsjwGD-Cz3dh81cfb21xiuSeVyyU720U9Q,8673
|
|
@@ -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=
|
|
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.
|
|
78
|
-
python_voiceio-0.9.
|
|
79
|
-
python_voiceio-0.9.
|
|
80
|
-
python_voiceio-0.9.
|
|
81
|
-
python_voiceio-0.9.
|
|
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.
|
|
1
|
+
__version__ = "0.9.4"
|
voiceio/app.py
CHANGED
|
@@ -283,8 +283,28 @@ class VoiceIO:
|
|
|
283
283
|
self._last_hotkey = now
|
|
284
284
|
self._toggle()
|
|
285
285
|
|
|
286
|
-
def _on_auto_stop(self) -> None:
|
|
287
|
-
"""Called from audio thread when
|
|
286
|
+
def _on_auto_stop(self, reason: str = "silence") -> None:
|
|
287
|
+
"""Called from the audio thread when the recorder auto-stops.
|
|
288
|
+
|
|
289
|
+
reason="silence": spoke, then went quiet — a normal end of dictation.
|
|
290
|
+
reason="no_speech": nothing was heard the whole time — a muted mic or a
|
|
291
|
+
hotkey pressed by accident. Surface it so it isn't a silent failure
|
|
292
|
+
(today's Discord-mute confusion: recording ran, nothing was captured,
|
|
293
|
+
and there was no sign why).
|
|
294
|
+
"""
|
|
295
|
+
if reason == "no_speech":
|
|
296
|
+
from voiceio import feedback
|
|
297
|
+
if not self.recorder.has_signal():
|
|
298
|
+
feedback.notify(
|
|
299
|
+
"VoiceIO: microphone is muted",
|
|
300
|
+
"Stopped — no audio is reaching voiceio. Check your mic's "
|
|
301
|
+
"mute button and that the right input device is selected.",
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
feedback.notify(
|
|
305
|
+
"VoiceIO: stopped — no speech detected",
|
|
306
|
+
"Recording auto-stopped after silence.",
|
|
307
|
+
)
|
|
288
308
|
threading.Thread(target=self._request_stop, daemon=True).start()
|
|
289
309
|
|
|
290
310
|
def _request_stop(self) -> None:
|
voiceio/config.py
CHANGED
|
@@ -71,6 +71,11 @@ class AudioConfig:
|
|
|
71
71
|
silence_threshold: float = 0.01
|
|
72
72
|
silence_duration: float = 0.6
|
|
73
73
|
auto_stop_silence_secs: float = 5.0
|
|
74
|
+
# Safety net for a muted mic or a forgotten hotkey: if NO speech is heard
|
|
75
|
+
# for this long from the start, stop and notify. The 5s auto-stop above
|
|
76
|
+
# only fires once speech has been heard, so it can't catch a dead mic.
|
|
77
|
+
# 0 disables.
|
|
78
|
+
auto_stop_no_speech_secs: float = 20.0
|
|
74
79
|
vad_backend: str = "silero" # "silero" | "rms"
|
|
75
80
|
vad_threshold: float = 0.5 # Silero speech probability threshold
|
|
76
81
|
|
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."""
|
|
@@ -108,9 +113,16 @@ class AudioRecorder:
|
|
|
108
113
|
|
|
109
114
|
# Auto-stop on sustained silence
|
|
110
115
|
self._auto_stop_secs = cfg.auto_stop_silence_secs
|
|
116
|
+
self._no_speech_secs = cfg.auto_stop_no_speech_secs
|
|
111
117
|
self._sustained_silence = 0.0
|
|
112
118
|
self._heard_speech = False
|
|
113
|
-
|
|
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
|
|
123
|
+
# Called with a reason: "silence" (spoke then paused) or "no_speech"
|
|
124
|
+
# (nothing ever heard — a muted mic or a forgotten hotkey).
|
|
125
|
+
self._on_auto_stop: Callable[[str], None] | None = None
|
|
114
126
|
|
|
115
127
|
# Heartbeat: updated by _callback, checked by health watchdog
|
|
116
128
|
self._last_callback_time: float = 0.0
|
|
@@ -218,6 +230,8 @@ class AudioRecorder:
|
|
|
218
230
|
self._silent_chunks = 0.0
|
|
219
231
|
self._sustained_silence = 0.0
|
|
220
232
|
self._heard_speech = False
|
|
233
|
+
self._heard_signal = False
|
|
234
|
+
self._silent_signal_secs = 0.0
|
|
221
235
|
self._pause_fired_at = 0
|
|
222
236
|
self._meter_peak = 0.0
|
|
223
237
|
self._meter_clipped = 0
|
|
@@ -260,7 +274,7 @@ class AudioRecorder:
|
|
|
260
274
|
"""Set/clear the speech pause callback (used by streaming session)."""
|
|
261
275
|
self._on_speech_pause = callback
|
|
262
276
|
|
|
263
|
-
def set_on_auto_stop(self, callback: Callable[[], None] | None) -> None:
|
|
277
|
+
def set_on_auto_stop(self, callback: Callable[[str], None] | None) -> None:
|
|
264
278
|
"""Set/clear the auto-stop callback (fires after sustained silence)."""
|
|
265
279
|
self._on_auto_stop = callback
|
|
266
280
|
|
|
@@ -298,8 +312,19 @@ class AudioRecorder:
|
|
|
298
312
|
|
|
299
313
|
# Level metering
|
|
300
314
|
abs_chunk = np.abs(chunk.ravel())
|
|
301
|
-
|
|
315
|
+
chunk_peak = float(abs_chunk.max(initial=0.0))
|
|
316
|
+
self._meter_peak = max(self._meter_peak, chunk_peak)
|
|
302
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
|
|
303
328
|
pinned = abs_chunk >= 0.99
|
|
304
329
|
if np.count_nonzero(pinned) >= 4:
|
|
305
330
|
runs = np.convolve(pinned.astype(np.int8), np.ones(4, dtype=np.int8), "valid")
|
|
@@ -340,4 +365,20 @@ class AudioRecorder:
|
|
|
340
365
|
self._on_auto_stop = None
|
|
341
366
|
self._sustained_silence = 0.0
|
|
342
367
|
log.info("Auto-stopping after %.0fs of silence", self._auto_stop_secs)
|
|
343
|
-
cb()
|
|
368
|
+
cb("silence")
|
|
369
|
+
|
|
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.
|
|
375
|
+
elif (self._on_auto_stop is not None
|
|
376
|
+
and self._no_speech_secs > 0
|
|
377
|
+
and not self._heard_signal
|
|
378
|
+
and self._silent_signal_secs >= self._no_speech_secs):
|
|
379
|
+
cb = self._on_auto_stop
|
|
380
|
+
self._on_auto_stop = None
|
|
381
|
+
self._silent_signal_secs = 0.0
|
|
382
|
+
log.info("Auto-stopping: no mic signal in %.0fs (muted?)",
|
|
383
|
+
self._no_speech_secs)
|
|
384
|
+
cb("no_speech")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|