fix(voxtral): Speech-Endpoint gegen laute Umgebungsmusik

Problem (Stefan): laeuft Musik, endet eine Aufnahme nie von selbst — die Energie
bleibt oben, der RMS-Stille-Endpoint feuert nicht (last_voice_at immer frisch).
Erst Stop druecken oder Musik leiser (dann echte Stille) beendet.

Fix: waehrend lauter Phasen (rms ueber Schwelle) periodisch (~700ms, gedrosselt)
mit Silero pruefen, ob im letzten endpoint_ms-Fenster ueberhaupt Sprache ist.
Nur Musik/Stille → Turn beenden (finalize endpoint). Real gesprochene Turns
haben Sprache im Fenster → laufen normal weiter; eine kurze Denk-Pause auch,
solange innerhalb der Toleranz noch Sprache im Fenster liegt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 21:57:06 +02:00
co-authored by Claude Opus 4.8
parent 626281dead
commit a3d7933949
+26
View File
@@ -126,6 +126,11 @@ SILERO_VAD_ENABLED = os.getenv("SILERO_VAD_ENABLED", "true").lower() in ("1", "t
SILERO_VAD_THRESHOLD = float(os.getenv("SILERO_VAD_THRESHOLD", "0.5")) SILERO_VAD_THRESHOLD = float(os.getenv("SILERO_VAD_THRESHOLD", "0.5"))
SILERO_MIN_SPEECH_MS = int(os.getenv("SILERO_MIN_SPEECH_MS", "150")) SILERO_MIN_SPEECH_MS = int(os.getenv("SILERO_MIN_SPEECH_MS", "150"))
SILERO_PAD_MS = int(os.getenv("SILERO_PAD_MS", "200")) SILERO_PAD_MS = int(os.getenv("SILERO_PAD_MS", "200"))
# Speech-Endpoint gegen laute Umgebungsmusik: der RMS-Stille-Endpoint feuert bei
# durchgehender Musik NIE (Energie bleibt oben). Deshalb waehrend lauter Phasen
# periodisch (alle X ms) mit Silero pruefen, ob im letzten endpoint_ms-Fenster
# ueberhaupt noch Sprache ist — wenn nicht (nur Musik/Stille), Turn beenden.
STREAM_SPEECH_ENDPOINT_CHECK_MS = int(os.getenv("STREAM_SPEECH_ENDPOINT_CHECK_MS", "700"))
_vad_state = {"model": None, "get_ts": None, "failed": False} _vad_state = {"model": None, "get_ts": None, "failed": False}
@@ -281,6 +286,7 @@ class StreamSession:
last_growth_at: float = 0.0 last_growth_at: float = 0.0
last_transcribe_at: float = 0.0 last_transcribe_at: float = 0.0
last_voice_at: float = 0.0 last_voice_at: float = 0.0
last_speech_check_at: float = 0.0 # Drossel fuer den Silero-Speech-Endpoint
noise_floor: float = 0.0 noise_floor: float = 0.0
closed: bool = False closed: bool = False
endpoint_sent: bool = False endpoint_sent: bool = False
@@ -506,6 +512,26 @@ class SessionManager:
"audioRequestId": sess.audio_request_id, "audioRequestId": sess.audio_request_id,
"text": "", "text": "",
}) })
# Speech-Endpoint gegen laute Umgebungsmusik: es ist gerade laut (rms
# ueber Schwelle) — aber ist es Sprache oder Musik? Der RMS-Endpoint
# unten wuerde bei Musik NIE feuern (last_voice_at bleibt frisch).
# Deshalb gedrosselt mit Silero das letzte endpoint_ms-Fenster pruefen:
# KEINE Sprache drin (nur Musik) → Turn ist zu Ende. Real gesprochene
# Turns haben Sprache im Fenster → laufen weiter.
if (self._buffer_ms(sess) >= sess.endpoint_ms
and (now - sess.last_speech_check_at) * 1000.0 >= STREAM_SPEECH_ENDPOINT_CHECK_MS):
sess.last_speech_check_at = now
try:
tail_bytes = int(sess.endpoint_ms / 1000.0 * sess.sample_rate) * 2
tail = pcm_s16le_to_float32(bytes(sess.pcm_buffer[-tail_bytes:]))
segs = _speech_segments(tail)
if segs is not None and len(segs) == 0:
logger.info("Stream %s: Speech-Endpoint (keine Sprache im letzten %dms — Musik/Stille) → finalize",
sess.request_id[:8], sess.endpoint_ms)
await self._finalize(sess, "endpoint")
return
except Exception:
logger.exception("Speech-Endpoint-Check fehlgeschlagen — ignoriert")
else: else:
self._update_noise_floor(sess, rms) self._update_noise_floor(sess, rms)
# No-Speech-Timeout: wurde die GANZE Zeit KEINE Stimme erkannt # No-Speech-Timeout: wurde die GANZE Zeit KEINE Stimme erkannt