fix(bridge): gate STT on real speech so noise doesn't trigger replies

The bridge transcribe path joined every Whisper segment unconditionally, so a
brief loud sound or background noise that momentarily opened the mic gate (no
real speech) still produced a transcript, and Whisper's noise hallucinations
("감사합니다", "MBC 뉴스", ...) made the bot reply to nothing.

Add bridge/stt_filter.py mirroring the desktop listener's _filter_noisy_segments
policy: a hard no_speech_prob cutoff (whisper_no_speech_threshold) plus an
avg_logprob confidence floor (whisper_min_confidence), both config-driven. Apply
it in transcribe() so only segments that look like human speech survive; a
noise-only turn yields an empty transcript and the existing empty-transcript
guard drops it with no reply. Add unit tests for the gate.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
javis-bot
2026-06-13 14:45:22 +09:00
parent 568a1ae50b
commit 39c7a22a12
3 changed files with 196 additions and 1 deletions

View File

@@ -51,8 +51,10 @@ from flask import Flask, request, jsonify, Response, stream_with_context
try: # package-relative when imported as ``bridge.server``
from bridge.text_utils import split_sentences
from bridge.stt_filter import filter_speech_segments
except ImportError: # script-relative when run as ``bridge/server.py``
from text_utils import split_sentences
from stt_filter import filter_speech_segments
app = Flask(__name__)
@@ -183,7 +185,19 @@ def transcribe(wav_bytes: bytes) -> dict:
x_new = np.linspace(0.0, 1.0, num=n_out, endpoint=False)
audio = np.interp(x_new, x_old, audio).astype(np.float32)
segments, info = _whisper.transcribe(audio, beam_size=1)
text = "".join(seg.text for seg in segments).strip()
# Speech gate: drop non-speech / hallucinated segments so a brief loud sound
# or background noise (mic blip with no real speech) does not become a
# transcript and make the bot reply to nothing. Mirrors the desktop
# listener's policy, driven by the same config thresholds.
no_speech_threshold = getattr(_cfg, "whisper_no_speech_threshold", 0.5)
min_confidence = getattr(_cfg, "whisper_min_confidence", 0.3)
kept = filter_speech_segments(
segments,
no_speech_threshold=no_speech_threshold,
min_confidence=min_confidence,
log=lambda m: print(f"[bridge] {m}", flush=True),
)
text = "".join(seg.text for seg in kept).strip()
return {"text": text, "language": getattr(info, "language", None)}