fix(voxtral): Halluzinations-Filter gegen Phantom-Text aus Stille

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 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 11:19:06 +02:00
co-authored by Claude Opus 4.8
parent e9439dbccb
commit 7b72149671
+45
View File
@@ -29,6 +29,7 @@ import base64
import json import json
import logging import logging
import os import os
import re
import tempfile import tempfile
import time import time
from dataclasses import dataclass, field 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. # (Voxtral halluziniert aus Fast-Nichts sonst einen Fuellsatz). 2 ≈ 400ms.
STREAM_MIN_VOICED_FRAMES = int(os.getenv("STREAM_MIN_VOICED_FRAMES", "2")) 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"- # Speaker-ID Gating global an/aus. DEFAULT AUS (fail-open) — die "nur meine Stimme"-
# Pruefung ist ein BEWUSSTER Schalter, kein Automatismus: ein einziger schlechter # Pruefung ist ein BEWUSSTER Schalter, kein Automatismus: ein einziger schlechter
# Enroll darf nie die ganze STT lahmlegen (genau das ist passiert). Wird per config- # 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 duration_s = audio.size / 16000.0
logger.info("Stream %s: FINAL (reason=%s, %.1fs, %dms): %r", logger.info("Stream %s: FINAL (reason=%s, %.1fs, %dms): %r",
sess.request_id[:8], reason, duration_s, stt_ms, final_text[:120]) 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: if self._ws is not None:
payload = { payload = {
"requestId": sess.request_id, "requestId": sess.request_id,