python-voiceio 0.7.1__tar.gz → 0.8.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.
- {python_voiceio-0.7.1/python_voiceio.egg-info → python_voiceio-0.8.0}/PKG-INFO +1 -1
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/pyproject.toml +1 -1
- {python_voiceio-0.7.1 → python_voiceio-0.8.0/python_voiceio.egg-info}/PKG-INFO +1 -1
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_streaming.py +29 -21
- python_voiceio-0.8.0/voiceio/__init__.py +1 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/config.py +6 -4
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/streaming.py +78 -31
- python_voiceio-0.7.1/voiceio/__init__.py +0 -1
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/LICENSE +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/README.md +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/python_voiceio.egg-info/SOURCES.txt +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/python_voiceio.egg-info/dependency_links.txt +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/python_voiceio.egg-info/entry_points.txt +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/python_voiceio.egg-info/requires.txt +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/python_voiceio.egg-info/top_level.txt +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/setup.cfg +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_adjudicate.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_app_wiring.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_audio_quality.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_audit.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_autocorrect.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_autocorrect_state.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_backend_probes.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_cli.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_clipboard_read.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_commands.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_concurrency_lockdown.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_config.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_correct_batch.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_corrections.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_fallback.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_health.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_hints.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_history.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_ibus_pending.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_ibus_typer.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_llm.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_llm_api.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_numbers.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_platform.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_postcorrect.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_postprocess.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_prebuffer.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_prompt.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_recorder_integration.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_retention.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_robustness.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_security_hardening.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_snapshots.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_transcriber.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_tts.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_vad.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_vocabulary.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_wizard.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/tests/test_wordfreq.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/__main__.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/app.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/audit.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/autocorrect.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/autocorrect_state.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/backends.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/cli.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/clipboard_read.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/commands.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/consent.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/corrections.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/demo.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/feedback.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/health.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/hints.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/history.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/hotkeys/__init__.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/hotkeys/base.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/hotkeys/chain.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/hotkeys/evdev.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/hotkeys/pynput_backend.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/hotkeys/socket_backend.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/ibus/__init__.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/ibus/engine.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/ibus/pending.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/llm.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/llm_api.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/models/__init__.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/models/silero_vad.onnx +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/numbers.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/pidlock.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/platform.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/postcorrect.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/postprocess.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/prompt.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/recorder.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/retention.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/service.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/snapshots.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/sounds/__init__.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/sounds/commit.wav +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/sounds/start.wav +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/sounds/stop.wav +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/transcriber.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tray/__init__.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tray/_icons.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tray/_indicator.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tray/_pystray.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tts/__init__.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tts/base.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tts/chain.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tts/edge_engine.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tts/espeak.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tts/piper_engine.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/tts/player.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/typers/__init__.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/typers/base.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/typers/chain.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/typers/clipboard.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/typers/ibus.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/typers/pynput_type.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/typers/wtype.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/typers/xdotool.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/typers/ydotool.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/vad.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/vocabulary.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/wizard.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/wordfreq.py +0 -0
- {python_voiceio-0.7.1 → python_voiceio-0.8.0}/voiceio/worker.py +0 -0
|
@@ -678,15 +678,21 @@ class TestSegmentFreeze:
|
|
|
678
678
|
)
|
|
679
679
|
return session, transcriber, recorder
|
|
680
680
|
|
|
681
|
-
def
|
|
681
|
+
def test_freeze_cuts_at_interior_pause(self):
|
|
682
682
|
s, tr, rec = self._make_session(freeze_secs=2.0)
|
|
683
|
-
|
|
683
|
+
# 2.5s speech, 0.5s pause, 2s speech: cut must land in the pause
|
|
684
|
+
rec.get_audio_so_far.return_value = np.concatenate(
|
|
685
|
+
[_loud(2.5), _silent(0.5), _loud(2.0)])
|
|
684
686
|
tr.transcribe.return_value = "hello there friend"
|
|
685
687
|
s._transcribe_and_apply()
|
|
686
688
|
assert s._frozen_raw == "hello there friend"
|
|
687
|
-
|
|
688
|
-
|
|
689
|
+
# Boundary inside the pause (2.5s..3.0s), NOT at the end of audio
|
|
690
|
+
assert SR * 2.5 <= s._frozen_samples <= SR * 3.0
|
|
691
|
+
# Freeze pass gets beam search and decodes ONLY the chunk
|
|
689
692
|
assert tr.transcribe.call_args.kwargs["final"] is True
|
|
693
|
+
assert len(tr.transcribe.call_args.args[0]) == s._frozen_samples
|
|
694
|
+
# Display untouched: interim text already covers this audio
|
|
695
|
+
s._typer.type_text.assert_not_called()
|
|
690
696
|
|
|
691
697
|
def test_no_freeze_when_tail_is_speech(self):
|
|
692
698
|
s, tr, rec = self._make_session(freeze_secs=2.0)
|
|
@@ -785,16 +791,15 @@ class TestDecodeTrace:
|
|
|
785
791
|
freeze_secs=2.0,
|
|
786
792
|
)
|
|
787
793
|
transcriber.transcribe.return_value = "hello there friend"
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
s._transcribe_and_apply() # freeze pass
|
|
794
|
+
recorder.get_audio_so_far.return_value = _silent(3)
|
|
795
|
+
s._transcribe_and_apply() # freeze pass (interior cut in silence)
|
|
791
796
|
s._final_audio = np.concatenate([_silent(3), _loud(1)])
|
|
792
797
|
transcriber.transcribe.return_value = "bye"
|
|
793
798
|
s._transcribe_and_apply(min_seconds=0.5, final=True)
|
|
794
799
|
kinds = [p["kind"] for p in s.trace]
|
|
795
800
|
assert kinds == ["freeze", "final"]
|
|
796
801
|
assert s.trace[0]["text"] == "hello there friend"
|
|
797
|
-
assert s.trace[1]["tail_secs"]
|
|
802
|
+
assert s.trace[1]["tail_secs"] > 0
|
|
798
803
|
assert all("secs" in p for p in s.trace)
|
|
799
804
|
|
|
800
805
|
|
|
@@ -856,31 +861,34 @@ class TestDecodeFailureSafety:
|
|
|
856
861
|
s._typer.delete_chars.assert_not_called()
|
|
857
862
|
assert s._typed_text == "hello there friend how are"
|
|
858
863
|
|
|
859
|
-
def
|
|
860
|
-
from voiceio.streaming import
|
|
864
|
+
def test_quiet_mic_speech_finds_no_cut(self):
|
|
865
|
+
from voiceio.streaming import _find_freeze_cut
|
|
861
866
|
# Continuous speech on a low-gain mic: quiet in absolute terms but
|
|
862
|
-
# uniform —
|
|
867
|
+
# uniform — no window is quiet relative to the tail, so no cut.
|
|
863
868
|
quiet_speech = np.full(SR * 3, 0.005, dtype=np.float32)
|
|
864
|
-
assert
|
|
865
|
-
# Same quiet mic with a real
|
|
869
|
+
assert _find_freeze_cut(quiet_speech, SR, 2.0) is None
|
|
870
|
+
# Same quiet mic with a real pause after the threshold IS cuttable.
|
|
866
871
|
with_pause = np.concatenate([
|
|
867
|
-
np.full(int(SR * 2.
|
|
872
|
+
np.full(int(SR * 2.2), 0.005, dtype=np.float32), _silent(0.5),
|
|
873
|
+
np.full(int(SR * 0.3), 0.005, dtype=np.float32),
|
|
868
874
|
])
|
|
869
|
-
|
|
875
|
+
cut = _find_freeze_cut(with_pause, SR, 2.0)
|
|
876
|
+
assert cut is not None and SR * 2.2 <= cut <= SR * 2.8
|
|
870
877
|
|
|
871
878
|
|
|
872
879
|
class TestFreezeCalibration:
|
|
873
|
-
def
|
|
880
|
+
def test_hot_noise_floor_pause_is_cuttable(self):
|
|
874
881
|
"""Real-mic calibration: noise floor ~0.3x of tail RMS IS a pause."""
|
|
875
|
-
from voiceio.streaming import
|
|
882
|
+
from voiceio.streaming import _find_freeze_cut
|
|
876
883
|
speech = np.full(int(SR * 2.5), 0.12, dtype=np.float32)
|
|
877
884
|
pause = np.full(int(SR * 0.5), 0.03, dtype=np.float32) # noise floor
|
|
878
|
-
|
|
885
|
+
cut = _find_freeze_cut(np.concatenate([speech, pause]), SR, 2.0)
|
|
886
|
+
assert cut is not None and cut >= SR * 2.3
|
|
879
887
|
|
|
880
|
-
def
|
|
881
|
-
from voiceio.streaming import
|
|
888
|
+
def test_speech_level_tail_has_no_cut(self):
|
|
889
|
+
from voiceio.streaming import _find_freeze_cut
|
|
882
890
|
tail = np.full(SR * 3, 0.12, dtype=np.float32)
|
|
883
|
-
assert
|
|
891
|
+
assert _find_freeze_cut(tail, SR, 2.0) is None
|
|
884
892
|
|
|
885
893
|
|
|
886
894
|
class TestDecodeBackpressure:
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.8.0"
|
|
@@ -85,10 +85,12 @@ class OutputConfig:
|
|
|
85
85
|
number_conversion: bool = True
|
|
86
86
|
voice_input_prefix: str = "" # e.g. "[voice]" — empty disables
|
|
87
87
|
# Incremental finalization: once the un-finalized audio tail grows past
|
|
88
|
-
# this many seconds
|
|
89
|
-
# recording, so interim passes and the
|
|
90
|
-
# the short remaining tail.
|
|
91
|
-
|
|
88
|
+
# this many seconds, it is cut at the nearest interior speech pause,
|
|
89
|
+
# beam-decoded and frozen during recording, so interim passes and the
|
|
90
|
+
# stop-time final decode only cover the short remaining tail. Smaller
|
|
91
|
+
# chunks decode faster (less worker blocking) at slightly more boundary
|
|
92
|
+
# risk. 0 disables (full re-decode at stop).
|
|
93
|
+
streaming_freeze_secs: float = 15.0
|
|
92
94
|
# Mirror transcribed text to the system clipboard so it can be pasted:
|
|
93
95
|
# "off" — never
|
|
94
96
|
# "final" — the corrected final text, once ready
|
|
@@ -25,8 +25,7 @@ if TYPE_CHECKING:
|
|
|
25
25
|
log = logging.getLogger(__name__)
|
|
26
26
|
DELETE_SETTLE_SECS = 0.05 # delay between delete and type for ydotool reliability
|
|
27
27
|
|
|
28
|
-
# Incremental finalization: a freeze cut is only safe at a speech pause
|
|
29
|
-
# require the tail's trailing window to be quiet before cutting there.
|
|
28
|
+
# Incremental finalization: a freeze cut is only safe at a speech pause.
|
|
30
29
|
_FREEZE_SILENCE_WINDOW_SECS = 0.3
|
|
31
30
|
# Quiet is judged RELATIVE to the tail's own level: mic gains vary wildly
|
|
32
31
|
# and the decode path normalizes audio anyway, so an absolute threshold
|
|
@@ -37,6 +36,10 @@ _FREEZE_SILENCE_WINDOW_SECS = 0.3
|
|
|
37
36
|
# both with margin; 0.15 never fired on a mic with a normal noise floor.
|
|
38
37
|
_FREEZE_SILENCE_RATIO = 0.6
|
|
39
38
|
_FREEZE_SILENCE_FLOOR = 1e-4 # digital silence is always quiet
|
|
39
|
+
# How far past freeze_secs to look for a pause. Real speech has a usable
|
|
40
|
+
# pause every ~5s (measured: longest pause-free stretch 5.6s in a 3min
|
|
41
|
+
# "continuous" note), so 20s of search span practically always finds one.
|
|
42
|
+
_FREEZE_SEARCH_SECS = 20.0
|
|
40
43
|
# Whisper conditions on ~224 prompt tokens; more frozen context is wasted.
|
|
41
44
|
_FREEZE_CONTEXT_CHARS = 400
|
|
42
45
|
|
|
@@ -45,14 +48,31 @@ def _rms(samples: np.ndarray) -> float:
|
|
|
45
48
|
return float(np.sqrt(np.mean(np.square(samples, dtype=np.float64))))
|
|
46
49
|
|
|
47
50
|
|
|
48
|
-
def
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
threshold
|
|
55
|
-
|
|
51
|
+
def _find_freeze_cut(
|
|
52
|
+
tail: np.ndarray, sample_rate: int, min_secs: float,
|
|
53
|
+
) -> int | None:
|
|
54
|
+
"""Sample index of the best freeze cut in `tail`, or None.
|
|
55
|
+
|
|
56
|
+
Searches tail[min_secs : min_secs+_FREEZE_SEARCH_SECS] for the quietest
|
|
57
|
+
300ms window below the relative silence threshold and cuts at that
|
|
58
|
+
window's end. Searching the interior (rather than requiring the pass to
|
|
59
|
+
land on a trailing pause) is what makes freezing actually fire on real
|
|
60
|
+
speech: a pass's trailing 300ms is almost never a pause, but the
|
|
61
|
+
interior of a 15-35s span almost always contains one.
|
|
62
|
+
"""
|
|
63
|
+
win = int(_FREEZE_SILENCE_WINDOW_SECS * sample_rate)
|
|
64
|
+
start = int(min_secs * sample_rate)
|
|
65
|
+
end = min(len(tail), int((min_secs + _FREEZE_SEARCH_SECS) * sample_rate))
|
|
66
|
+
if end - start < win:
|
|
67
|
+
return None
|
|
68
|
+
threshold = max(_FREEZE_SILENCE_RATIO * _rms(tail[:end]), _FREEZE_SILENCE_FLOOR)
|
|
69
|
+
best_cut, best_rms = None, threshold
|
|
70
|
+
for i in range(start, end - win + 1, win // 2):
|
|
71
|
+
window_rms = _rms(tail[i:i + win])
|
|
72
|
+
if window_rms < best_rms:
|
|
73
|
+
best_rms = window_rms
|
|
74
|
+
best_cut = i + win
|
|
75
|
+
return best_cut
|
|
56
76
|
|
|
57
77
|
|
|
58
78
|
def _common_prefix_len(a: str, b: str) -> int:
|
|
@@ -277,6 +297,40 @@ class StreamingSession:
|
|
|
277
297
|
return None
|
|
278
298
|
return self._frozen_raw[-_FREEZE_CONTEXT_CHARS:]
|
|
279
299
|
|
|
300
|
+
def _freeze_chunk(self, audio: np.ndarray, chunk: np.ndarray) -> None:
|
|
301
|
+
"""Beam-decode `chunk` (the tail up to a pause cut) and freeze it.
|
|
302
|
+
|
|
303
|
+
On decode failure the boundary is left untouched — the audio will be
|
|
304
|
+
decoded again by a later pass (never treat lost audio as silence).
|
|
305
|
+
"""
|
|
306
|
+
t0 = time.monotonic()
|
|
307
|
+
try:
|
|
308
|
+
chunk_text = self._transcriber.transcribe(
|
|
309
|
+
chunk, final=True, context=self._frozen_context(),
|
|
310
|
+
)
|
|
311
|
+
except Exception:
|
|
312
|
+
log.exception("Freeze decode failed (boundary unchanged)")
|
|
313
|
+
return
|
|
314
|
+
t_decode = time.monotonic() - t0
|
|
315
|
+
self._frozen_raw = _join_text(self._frozen_raw, chunk_text)
|
|
316
|
+
self._frozen_samples += len(chunk)
|
|
317
|
+
self._frozen_segments.extend(
|
|
318
|
+
list(getattr(self._transcriber, "last_segments", [])))
|
|
319
|
+
self._last_decode_end = len(audio)
|
|
320
|
+
self._last_decode_secs = t_decode
|
|
321
|
+
self.trace.append({
|
|
322
|
+
"t": round(time.monotonic() - self._t0, 2),
|
|
323
|
+
"kind": "freeze",
|
|
324
|
+
"frozen_secs": round(self._frozen_samples / self._sample_rate, 2),
|
|
325
|
+
"tail_secs": round(len(chunk) / self._sample_rate, 2),
|
|
326
|
+
"secs": round(t_decode, 3),
|
|
327
|
+
"text": chunk_text,
|
|
328
|
+
})
|
|
329
|
+
log.debug(
|
|
330
|
+
"Froze %.1fs of audio in %.1fs (frozen text now %d chars)",
|
|
331
|
+
len(chunk) / self._sample_rate, t_decode, len(self._frozen_raw),
|
|
332
|
+
)
|
|
333
|
+
|
|
280
334
|
def _transcribe_and_apply(
|
|
281
335
|
self, min_seconds: float = 1.0, final: bool = False,
|
|
282
336
|
) -> None:
|
|
@@ -307,25 +361,26 @@ class StreamingSession:
|
|
|
307
361
|
if not final and grown_secs < max(min_seconds, self._last_decode_secs):
|
|
308
362
|
return # backpressure: don't decode faster than we can keep up
|
|
309
363
|
|
|
310
|
-
# Freeze
|
|
311
|
-
#
|
|
312
|
-
#
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
364
|
+
# Freeze: once the tail is long enough, cut it at an interior speech
|
|
365
|
+
# pause and beam-decode ONLY that chunk (its text is never decoded
|
|
366
|
+
# again). The freeze pass does not touch the display — the interim
|
|
367
|
+
# text already covers this audio; the next interim refreshes it.
|
|
368
|
+
if (not final and self._freeze_secs > 0
|
|
369
|
+
and len(tail) >= self._freeze_secs * self._sample_rate):
|
|
370
|
+
cut = _find_freeze_cut(tail, self._sample_rate, self._freeze_secs)
|
|
371
|
+
if cut is not None:
|
|
372
|
+
self._freeze_chunk(audio, tail[:cut])
|
|
373
|
+
return
|
|
319
374
|
|
|
320
375
|
tail_text = ""
|
|
321
376
|
tail_segments: list[dict] = []
|
|
322
377
|
t0 = time.monotonic()
|
|
323
378
|
# The final pass decodes ANY remaining audio (a clipped last word
|
|
324
|
-
# must not be dropped); interim
|
|
379
|
+
# must not be dropped); interim passes skip sub-0.3s tails.
|
|
325
380
|
if len(tail) > (0 if final else self._sample_rate * 0.3):
|
|
326
381
|
try:
|
|
327
382
|
tail_text = self._transcriber.transcribe(
|
|
328
|
-
tail, final=final
|
|
383
|
+
tail, final=final, context=self._frozen_context(),
|
|
329
384
|
)
|
|
330
385
|
except Exception:
|
|
331
386
|
# Decode FAILED (timeout/crash) — this is not silence. Never
|
|
@@ -350,22 +405,14 @@ class StreamingSession:
|
|
|
350
405
|
|
|
351
406
|
self.trace.append({
|
|
352
407
|
"t": round(time.monotonic() - self._t0, 2),
|
|
353
|
-
"kind": "final" if final else
|
|
408
|
+
"kind": "final" if final else "interim",
|
|
354
409
|
"frozen_secs": round(self._frozen_samples / self._sample_rate, 2),
|
|
355
410
|
"tail_secs": round(len(tail) / self._sample_rate, 2),
|
|
356
411
|
"secs": round(t_transcribe, 3),
|
|
357
412
|
"text": tail_text,
|
|
358
413
|
})
|
|
359
414
|
|
|
360
|
-
|
|
361
|
-
self._frozen_raw = _join_text(self._frozen_raw, tail_text)
|
|
362
|
-
self._frozen_samples = len(audio)
|
|
363
|
-
self._frozen_segments.extend(tail_segments)
|
|
364
|
-
log.debug(
|
|
365
|
-
"Froze %.1fs of audio (frozen text now %d chars)",
|
|
366
|
-
len(tail) / self._sample_rate, len(self._frozen_raw),
|
|
367
|
-
)
|
|
368
|
-
raw = self._frozen_raw if freeze else _join_text(self._frozen_raw, tail_text)
|
|
415
|
+
raw = _join_text(self._frozen_raw, tail_text)
|
|
369
416
|
|
|
370
417
|
# A slow interim decode can complete AFTER the user pressed stop
|
|
371
418
|
# (observed: 29s under CPU contention). Applying it would overwrite
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.7.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|