From 7b7214967194ef6b08f67d81eeee9424fecc7a70 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Sun, 16 Aug 2026 11:19:06 +0200 Subject: [PATCH] fix(voxtral): Halluzinations-Filter gegen Phantom-Text aus Stille MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Punkt 3: 2. Netz nach der Transkription. Im borderline-Band (wenig echte Stimme) werden leere/Artefakt-Transkripte verworfen statt als Phantom ans Brain zu gehen ('Die Stadt hat eine Fläche von 1,5 km²' aus Fast-Stille). Bekannte Voxtral-Silence-Artefakte (Untertitel-Credits, Geo-/Städte-Fakten) per Regex, gegated auf voiced_frames — echte Geo-FRAGEN (normale Energie) gehen durch. Co-Authored-By: Claude Opus 4.8 --- xtts/voxtral/bridge.py | 45 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/xtts/voxtral/bridge.py b/xtts/voxtral/bridge.py index b8e2837..f9f6b3f 100644 --- a/xtts/voxtral/bridge.py +++ b/xtts/voxtral/bridge.py @@ -29,6 +29,7 @@ import base64 import json import logging import os +import re import tempfile import time from dataclasses import dataclass, field @@ -74,6 +75,25 @@ STREAM_VOICE_RMS_MAX = 0.020 # (Voxtral halluziniert aus Fast-Nichts sonst einen Fuellsatz). 2 ≈ 400ms. STREAM_MIN_VOICED_FRAMES = int(os.getenv("STREAM_MIN_VOICED_FRAMES", "2")) +# Halluzinations-Filter (2. Netz NACH der Transkription). Der voiced_frames-Guard +# oben faengt die reine Stille; hier kommt das "borderline"-Band dazu: wenn wenig +# echte Stimme da war UND das Transkript ein bekanntes Voxtral-Silence-Artefakt +# ist (Untertitel-Credits, Staedte-/Geo-Fakten "Flaeche von X km2"), ist es fast +# sicher ein Phantom aus Fast-Nichts → verwerfen. Gegated auf wenig voiced_frames, +# damit eine ECHTE Geografie-Frage (die hat normale Stimm-Energie) durchgeht. +STREAM_HALLUC_GUARD_FRAMES = int(os.getenv("STREAM_HALLUC_GUARD_FRAMES", + str(STREAM_MIN_VOICED_FRAMES * 4))) # ~1.6s +_HALLUCINATION_RE = re.compile( + r"untertitel" + r"|amara\.org" + r"|vielen\s+dank\s+f[uü]r'?s?\s+(zuschauen|zusehen|zuh[oö]ren)" + r"|bis\s+zum\s+n[aä]chsten\s+mal" + r"|abonnier" + r"|fl[aä]che\s+von\s+[\d.,]+\s*(km|quadratkilometer)" + r"|[\d.,]+\s*(km²|quadratkilometern?|einwohnern?)\b", + re.IGNORECASE, +) + # Speaker-ID Gating global an/aus. DEFAULT AUS (fail-open) — die "nur meine Stimme"- # Pruefung ist ein BEWUSSTER Schalter, kein Automatismus: ein einziger schlechter # Enroll darf nie die ganze STT lahmlegen (genau das ist passiert). Wird per config- @@ -449,6 +469,31 @@ class SessionManager: duration_s = audio.size / 16000.0 logger.info("Stream %s: FINAL (reason=%s, %.1fs, %dms): %r", sess.request_id[:8], reason, duration_s, stt_ms, final_text[:120]) + + # Halluzinations-Filter (2. Netz): leeres/Artefakt-Transkript im borderline- + # Band → als no-speech verwerfen statt ein Phantom ("Die Stadt hat eine + # Flaeche von 1,5 km2") ans Brain zu schicken. Manueller Stop (stream_end) + # ist ausgenommen (bewusst gesprochen, auch kurze Woerter zaehlen). + _clean = final_text.strip(" .,!?…-\t\n\r") + _borderline = sess.voiced_frames < STREAM_HALLUC_GUARD_FRAMES + _is_phantom = (not _clean) or (_borderline and bool(_HALLUCINATION_RE.search(final_text))) + if reason != "stream_end" and _is_phantom: + logger.info("Stream %s: Halluzination verworfen (voiced_frames=%d<%d, %.1fs, text=%r)", + sess.request_id[:8], sess.voiced_frames, STREAM_HALLUC_GUARD_FRAMES, + duration_s, final_text[:80]) + if self._ws is not None: + nospeech = {"requestId": sess.request_id, + "audioRequestId": sess.audio_request_id, + "text": "", "reason": f"hallucination:{reason}", + "durationS": 0.0, "sttMs": stt_ms} + await _send(self._ws, "stt_endpoint", nospeech) + await _send(self._ws, "stt_stream_done", { + "requestId": sess.request_id, + "audioRequestId": sess.audio_request_id, + "text": "", "reason": f"hallucination:{reason}"}) + self.drop(sess.request_id) + return + if self._ws is not None: payload = { "requestId": sess.request_id,