fix(voxtral): "Sprache erkannt"-Ping — No-Speech-Watchdog schneidet nicht mehr ab

Im Ohr-Modus (Wake/Passiv/Barge) beendete die App die Aufnahme am Konversations-
fenster (~conv window), obwohl Stefan noch redete — nur dort, nicht beim normalen
Button. Ursache: der App-No-Speech-Watchdog cancelt, solange KEIN stt_partial kam
(streamGotPartial=false). Voxtral schickt aber seit dem Cutoff-Fix keine Live-
Partials mehr → streamGotPartial blieb immer false → Timer feuerte mitten im Satz.
Der normale Button nutzt noSpeechTimeoutMs=0 (Watchdog aus) → dort nie aufgefallen.

Fix: Voxtral-Bridge feuert EIN leeres stt_partial beim ersten Voice-Frame (nach
der Speaker-ID-Pruefung, fremde Stimmen signalisieren also nicht). Die App setzt
darauf streamGotPartial=true und loescht den Watchdog — bestehender Handler, keine
App-Aenderung. Der akustische Endpoint (Stille >= endpointMs) bleibt der einzige
Turn-Ende-Ausloeser.

Deploy: docker compose up -d --build voxtral-bridge (keine neue APK noetig).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 14:15:21 +02:00
co-authored by Claude Opus 4.8
parent 517c993ac8
commit 9e78d75149
+19
View File
@@ -168,6 +168,11 @@ class StreamSession:
noise_floor: float = 0.0 noise_floor: float = 0.0
closed: bool = False closed: bool = False
endpoint_sent: bool = False endpoint_sent: bool = False
# Einmaliges "Sprache erkannt"-Signal an die App gesendet? Voxtral schickt
# keine Live-Partials, aber der App-No-Speech-Watchdog wartet auf ein
# stt_partial, um "der User redet" zu erkennen — sonst cancelt er mitten im
# Satz. Wir feuern EIN leeres stt_partial beim ersten Voice-Frame.
speech_signaled: bool = False
# Speaker-ID Gating (einmalig auf die ersten ~1.5s der Aufnahme) # Speaker-ID Gating (einmalig auf die ersten ~1.5s der Aufnahme)
speaker_checked: bool = False speaker_checked: bool = False
speaker_match: Optional[bool] = None speaker_match: Optional[bool] = None
@@ -361,6 +366,20 @@ class SessionManager:
rms = self._tail_rms(sess) rms = self._tail_rms(sess)
if rms >= self._voice_threshold(sess): if rms >= self._voice_threshold(sess):
sess.last_voice_at = now sess.last_voice_at = now
# Einmalig der App melden, dass Sprache begonnen hat. Ohne Live-Partials
# wuerde ihr No-Speech-Watchdog die Aufnahme sonst am Konversations-
# fenster canceln, obwohl der User noch redet (Stefans Reproduktion:
# "beendet nach ~4s, obwohl ich weiterrede — nur im Ohr-Modus"). Ein
# leeres stt_partial reicht: die App setzt streamGotPartial=true und
# loescht den Watchdog. Nach der Speaker-ID-Pruefung (oben) → fremde
# Stimmen signalisieren NICHT.
if not sess.speech_signaled and self._ws is not None:
sess.speech_signaled = True
await _send(self._ws, "stt_partial", {
"requestId": sess.request_id,
"audioRequestId": sess.audio_request_id,
"text": "",
})
else: else:
self._update_noise_floor(sess, rms) self._update_noise_floor(sess, rms)
# Endpoint: hat der User schon gesprochen UND ist es seit endpoint_ms still? # Endpoint: hat der User schon gesprochen UND ist es seit endpoint_ms still?