diff --git a/diagnostic/index.html b/diagnostic/index.html index 26d1652..d16468f 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -788,6 +788,18 @@
Status wird geladen...
+
+ + +
+
+ AUS (Default) = alle Stimmen kommen durch (fail-open). AN = nur der enrollte + Sprecher wird ans Brain geleitet, fremde Stimmen werden verworfen. Erst + einschalten wenn ein Fingerprint eingelernt ist — sonst hört ARIA niemanden. +
{ const t = parseFloat(msg.voiceIdThreshold); if (t >= 0.0 && t <= 1.0) voiceConfig.voiceIdThreshold = t; } + // Speaker-ID Gating an/aus ("nur meine Stimme"). Default aus (fail-open) — + // bewusster Schalter. voxtral/whisper-bridge lesen voiceIdEnabled aus dem + // config-Broadcast; aus = gar keine Pruefung. + if (msg.voiceIdEnabled !== undefined) { + voiceConfig.voiceIdEnabled = !!msg.voiceIdEnabled; + } try { fs.mkdirSync("/shared/config", { recursive: true }); fs.writeFileSync("/shared/config/voice_config.json", JSON.stringify(voiceConfig, null, 2)); diff --git a/xtts/voxtral/bridge.py b/xtts/voxtral/bridge.py index 9937835..b8e2837 100644 --- a/xtts/voxtral/bridge.py +++ b/xtts/voxtral/bridge.py @@ -74,6 +74,18 @@ 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")) +# 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- +# Broadcast (voiceIdEnabled, aus dem Diagnostic) zur Laufzeit gesetzt. Kann per ENV +# vorbelegt werden. +SPEAKER_ID_ENABLED = os.getenv("VOICE_ID_ENABLED", "false").lower() in ("1", "true", "yes") + + +def _set_speaker_id_enabled(val: bool) -> None: + global SPEAKER_ID_ENABLED + SPEAKER_ID_ENABLED = bool(val) + def pcm_s16le_to_float32(data: bytes) -> np.ndarray: if not data: @@ -282,6 +294,10 @@ class SessionManager: """Einmalig: erste ~1.5s → Embedding → Vergleich mit Fingerprint. Ohne Fingerprint fail-open (match=True). Bei Mismatch: Session beenden.""" sess.speaker_checked = True + # Schalter aus (Default) → gar keine Pruefung, alles durchlassen. + if not SPEAKER_ID_ENABLED: + sess.speaker_match = True + return head = bytes(sess.pcm_buffer[: STREAM_SPEAKER_CHECK_MS * 32]) if len(head) < speaker_id.MIN_SAMPLE_BYTES: sess.speaker_match = True @@ -535,6 +551,10 @@ async def run_loop(sessions: SessionManager) -> None: logger.info("[speaker-id] threshold gesetzt: %.2f", t) except (TypeError, ValueError): pass + if "voiceIdEnabled" in payload: + _set_speaker_id_enabled(payload.get("voiceIdEnabled")) + logger.info("[speaker-id] Gating %s (voiceIdEnabled)", + "AN" if SPEAKER_ID_ENABLED else "AUS") except Exception as e: logger.warning("RVS-Verbindung verloren: %s — retry in %ds", e, retry_s) if use_tls and RVS_TLS_FALLBACK and not tls_fallback_tried: diff --git a/xtts/whisper/bridge.py b/xtts/whisper/bridge.py index f859e19..0cf6e98 100644 --- a/xtts/whisper/bridge.py +++ b/xtts/whisper/bridge.py @@ -86,6 +86,16 @@ STREAM_VOICE_FACTOR = 2.5 # Sprache = noise_floor * Faktor STREAM_VOICE_RMS_MIN = 0.005 # Untergrenze (stiller Raum: nicht auf 0 kollabieren) STREAM_VOICE_RMS_MAX = 0.020 # Obergrenze (lautes Auto: Sprache nie ganz aussperren) STREAM_VOICE_RMS_THRESHOLD = 0.012 # Legacy-Konstante (nicht mehr im Cut-Pfad genutzt) + +# Speaker-ID Gating global an/aus. DEFAULT AUS (fail-open) — bewusster Schalter +# ("nur meine Stimme"), kein Automatismus: ein schlechter Enroll darf nie die STT +# lahmlegen. Wird per config-Broadcast (voiceIdEnabled) zur Laufzeit gesetzt. +SPEAKER_ID_ENABLED = os.getenv("VOICE_ID_ENABLED", "false").lower() in ("1", "true", "yes") + + +def _set_speaker_id_enabled(val: bool) -> None: + global SPEAKER_ID_ENABLED + SPEAKER_ID_ENABLED = bool(val) # Rein-semantischer Backstop: wenn die Energie NIE faellt (laute Umgebung, # z.B. Auto), endpointen wir trotzdem — aber erst nach diesem Faktor x # endpoint_ms, damit normales Sprechen mit Pausen nicht abgeschnitten wird. @@ -467,6 +477,11 @@ class SessionManager: Ohne Fingerprint → fail-open (match=True). Bei mismatch wird die Session sofort beendet mit synthetischem stt_endpoint.""" sess.speaker_checked = True + # Schalter aus (Default) → gar keine Pruefung, alles durchlassen. + if not SPEAKER_ID_ENABLED: + sess.speaker_match = True + sess.speaker_similarity = 0.0 + return # Erste ~1.5s aus dem Buffer entnehmen (16kHz * 2 byte/sample = 32 bytes/ms) head_bytes = bytes(sess.pcm_buffer[: STREAM_SPEAKER_CHECK_MS * 32]) if len(head_bytes) < speaker_id.MIN_SAMPLE_BYTES: @@ -975,6 +990,10 @@ async def run_loop(runner: WhisperRunner, sessions: SessionManager) -> None: logger.info("[speaker-id] threshold gesetzt: %.2f", t) except (TypeError, ValueError): pass + if "voiceIdEnabled" in payload: + _set_speaker_id_enabled(payload.get("voiceIdEnabled")) + logger.info("[speaker-id] Gating %s (voiceIdEnabled)", + "AN" if SPEAKER_ID_ENABLED else "AUS") if "whisperDebugLog" in payload: global _DEBUG_LOG_TO_BRIDGE old = _DEBUG_LOG_TO_BRIDGE