feat(bridge): gate Whisper behind Silero VAD; harden broadcast auto-start
Address review of the noise/broadcast fixes: - STT now refuses to run Whisper on non-speech. transcribe() runs the Silero VAD (bundled with faster-whisper, no new dep) BEFORE the model, so noise or a brief loud blip with no real speech never reaches STT and can't be hallucinated into a transcript. The no_speech_prob/avg_logprob post-filter stays as a second line of defence (a clap the VAD lets through is still killed by Whisper's own no_speech_prob). VAD is env-tunable (VAD_THRESHOLD, VAD_MIN_SPEECH_MS, VAD_ENABLED) and fail-open so a VAD error never swallows a real utterance. Validated on real audio: synthesised Korean speech passes; silence, a 50ms blip and white noise are rejected. - Broadcast auto-start no longer blocks the voice join and no longer silently swallows failures: wiring is synchronous, the Go-Live start runs in the background with a bounded retry and a loud final-failure log. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -7,15 +7,39 @@ produces no transcript and no reply. Thresholds are config-driven, so the tests
|
||||
pass explicit references rather than hardcoding the production defaults.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
from bridge.stt_filter import (
|
||||
filter_speech_segments,
|
||||
has_speech,
|
||||
is_non_speech,
|
||||
segment_confidence,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_has_speech_rejects_silence():
|
||||
# Pure digital silence is unambiguously non-speech: VAD must skip STT.
|
||||
silence = np.zeros(16000, dtype=np.float32) # 1s @ 16kHz
|
||||
assert has_speech(silence, 16000) is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_has_speech_rejects_a_brief_loud_blip():
|
||||
# A short loud transient (a clap / pop): mostly silence with a 50ms spike,
|
||||
# below the min-speech duration, so no real speech region is found.
|
||||
audio = np.zeros(16000, dtype=np.float32)
|
||||
audio[8000:8800] = 0.9 # ~50ms full-scale burst
|
||||
assert has_speech(audio, 16000, min_speech_duration_ms=200) is False
|
||||
|
||||
|
||||
@pytest.mark.unit
|
||||
def test_has_speech_fails_open_on_empty_when_vad_present_returns_false():
|
||||
# Empty audio has nothing to transcribe; treat as non-speech.
|
||||
assert has_speech(np.zeros(0, dtype=np.float32), 16000) is False
|
||||
|
||||
|
||||
class Seg:
|
||||
"""Minimal stand-in for a faster-whisper segment."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user