From 36f04f83ffb9b3dd6c095f7e946e2e13e37ef5c7 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sat, 15 Aug 2026 12:02:44 +0200 Subject: [PATCH] =?UTF-8?q?fix(voxtral):=20willkuerliche=20Abbrueche=20?= =?UTF-8?q?=E2=80=94=20semantischen=20Endpoint=20+=20Live-Partials=20raus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ursache: Voxtral-3B transkribiert den ganzen wachsenden Buffer (~5-6s bei langen Aufnahmen). Diese Partial-Latenz war groesser als der semantische Endpoint-Timeout (4.8s) → 'Text waechst nicht mehr' feuerte faelschlich → Abbruch nach 20-40s. Fix: keine Live-Partials mehr, kein semantischer Endpoint — Turn-Ende rein akustisch (Stille-VAD), transkribiert wird nur EINMAL im _finalize. max_new_tokens 512->4096 (512 schnitt lange Diktate ab). Co-Authored-By: Claude Opus 4.8 --- xtts/voxtral/bridge.py | 42 ++++++++++++------------------------------ 1 file changed, 12 insertions(+), 30 deletions(-) diff --git a/xtts/voxtral/bridge.py b/xtts/voxtral/bridge.py index 809db5b..c6d5195 100644 --- a/xtts/voxtral/bridge.py +++ b/xtts/voxtral/bridge.py @@ -121,7 +121,9 @@ class VoxtralRunner: ) inputs = inputs.to(VOXTRAL_DEVICE, dtype=torch.bfloat16) with torch.no_grad(): - outputs = model.generate(**inputs, max_new_tokens=512) + # hoch genug fuer lange Diktate (stoppt eh am EOS); 512 hat + # mehrminutige Aufnahmen abgeschnitten. + outputs = model.generate(**inputs, max_new_tokens=4096) trimmed = outputs[:, inputs.input_ids.shape[1]:] text = proc.batch_decode(trimmed, skip_special_tokens=True) return (text[0] if text else "").strip() @@ -284,40 +286,20 @@ class SessionManager: return if self._buffer_ms(sess) < STREAM_MIN_AUDIO_MS: return - # adaptive akustische Sprach-Aktivitaet + # Adaptive akustische Sprach-Aktivitaet (M0.1). KEINE Live-Partials mehr: + # Voxtral-3B transkribiert den ganzen WACHSENDEN Buffer und braucht dafuer + # bei langen Aufnahmen 5-6 s — zu langsam fuer Live-Text, UND diese Latenz + # hat den semantischen Endpoint faelschlich ausgeloest (Partial-Latenz > + # Timeout → willkuerliche Abbrueche nach 20-40 s). Deshalb: Turn-Ende rein + # AKUSTISCH, transkribiert wird nur EINMAL im _finalize. rms = self._tail_rms(sess) if rms >= self._voice_threshold(sess): sess.last_voice_at = now else: self._update_noise_floor(sess, rms) - # Endpoint-Entscheidung, sobald Text erkannt wurde - if sess.last_growth_at > 0.0: - ac_sil = (now - sess.last_voice_at) * 1000.0 if sess.last_voice_at > 0 else 0.0 - se_sil = (now - sess.last_growth_at) * 1000.0 - ac_done = sess.last_voice_at > 0 and ac_sil >= sess.endpoint_ms - se_done = se_sil >= sess.endpoint_ms * STREAM_SEMANTIC_BACKUP_FACTOR - if ac_done or se_done: - await self._finalize(sess, "endpoint" if ac_done else "endpoint_semantic") - return - # Partial-Transkription (throttled) - if (now - sess.last_transcribe_at) * 1000.0 < STREAM_TRANSCRIBE_INTERVAL_MS: - return - sess.last_transcribe_at = now - audio = pcm_s16le_to_float32(bytes(sess.pcm_buffer)) - try: - text = (await self.runner.transcribe(audio, sess.language)).strip() - except Exception: - logger.exception("Stream %s: Partial-Transcribe crashed", sess.request_id[:8]) - return - if text and text != sess.last_partial: - sess.last_partial = text - sess.last_growth_at = now - if self._ws is not None: - await _send(self._ws, "stt_partial", { - "requestId": sess.request_id, - "audioRequestId": sess.audio_request_id, - "text": text, - }) + # Endpoint: hat der User schon gesprochen UND ist es seit endpoint_ms still? + if sess.last_voice_at > 0 and (now - sess.last_voice_at) * 1000.0 >= sess.endpoint_ms: + await self._finalize(sess, "endpoint") async def _finalize(self, sess: StreamSession, reason: str) -> None: if sess.endpoint_sent: