+ 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