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:
javis-bot
2026-06-13 14:53:54 +09:00
parent 39c7a22a12
commit 6d72e10f9c
5 changed files with 194 additions and 48 deletions

View File

@@ -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."""