feat(speaker-id): Gating als bewusster Schalter — Default AUS (fail-open)
Stefans Repro "nichts geht mehr" kam NICHT von den Marker/Guard-Aenderungen, sondern von der Speaker-ID: die Bridge-Logs zeigten fast durchgehend "stt_endpoint mit leerem Text — ignoriert (reason=speaker_mismatch)" — ein aus einem kaputten Enroll (AAC-als-PCM) entstandener Muell-Fingerprint hat Stefans EIGENE Stimme abgelehnt und damit die komplette STT lahmgelegt. Fix: Speaker-ID-Gating ist jetzt ein expliziter Schalter, Default AUS. Bei aus laeuft die Pruefung GAR NICHT (fail-open, alle Stimmen durch) — ein schlechter Enroll kann nie wieder alles abwuergen. Damit ist Stefans Problem schon durch den Voxtral-Rebuild geloest (kein Loeschen noetig, der Check greift einfach nicht). - voxtral + whisper bridge: SPEAKER_ID_ENABLED (Default False, ENV VOICE_ID_ENABLED), _check_speaker faellt bei aus sofort fail-open zurueck; config-Broadcast voiceIdEnabled setzt es zur Laufzeit. - diagnostic: Schalter "Nur meine Stimme" in der Voice-ID-Sektion (Default aus, Hinweis: erst an wenn enrollt), broadcastet + persistiert voiceIdEnabled. Reihenfolge lt. Stefan: erst Konversation sauber, dann Multi-Person/nur-ich. Deploy: docker compose up -d --build voxtral-bridge; aria-diagnostic neu starten. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+19
-1
@@ -788,6 +788,18 @@
|
|||||||
<div id="voice-id-status" style="font-size:13px;color:#E0E0F0;margin-bottom:10px;">
|
<div id="voice-id-status" style="font-size:13px;color:#E0E0F0;margin-bottom:10px;">
|
||||||
Status wird geladen...
|
Status wird geladen...
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
|
||||||
|
<label style="color:#8888AA;font-size:12px;min-width:130px;">Nur meine Stimme:</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:8px;cursor:pointer;flex:1;">
|
||||||
|
<input type="checkbox" id="diag-voice-id-enabled" onchange="sendVoiceConfig()">
|
||||||
|
<span style="color:#E0E0F0;font-size:12px;">Speaker-ID-Prüfung aktiv</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:10px;color:#555570;margin-bottom:12px;">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
|
<div style="display:flex;align-items:center;gap:12px;margin-bottom:8px;">
|
||||||
<label style="color:#8888AA;font-size:12px;min-width:130px;">Match-Threshold:</label>
|
<label style="color:#8888AA;font-size:12px;min-width:130px;">Match-Threshold:</label>
|
||||||
<input type="range" id="diag-voice-id-threshold" min="0.30" max="0.70" step="0.05" value="0.50"
|
<input type="range" id="diag-voice-id-threshold" min="0.30" max="0.70" step="0.05" value="0.50"
|
||||||
@@ -1899,6 +1911,11 @@
|
|||||||
if (slider) slider.value = msg.voiceIdThreshold;
|
if (slider) slider.value = msg.voiceIdThreshold;
|
||||||
if (display) display.textContent = Number(msg.voiceIdThreshold).toFixed(2);
|
if (display) display.textContent = Number(msg.voiceIdThreshold).toFixed(2);
|
||||||
}
|
}
|
||||||
|
// Speaker-ID Gating-Schalter wiederherstellen (Default aus)
|
||||||
|
{
|
||||||
|
const cb = document.getElementById('diag-voice-id-enabled');
|
||||||
|
if (cb) cb.checked = !!msg.voiceIdEnabled;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3600,13 +3617,14 @@
|
|||||||
const huggingfaceToken = document.getElementById('diag-flux-hf-token')?.value;
|
const huggingfaceToken = document.getElementById('diag-flux-hf-token')?.value;
|
||||||
const voiceIdThresholdRaw = document.getElementById('diag-voice-id-threshold')?.value;
|
const voiceIdThresholdRaw = document.getElementById('diag-voice-id-threshold')?.value;
|
||||||
const voiceIdThreshold = voiceIdThresholdRaw ? parseFloat(voiceIdThresholdRaw) : undefined;
|
const voiceIdThreshold = voiceIdThresholdRaw ? parseFloat(voiceIdThresholdRaw) : undefined;
|
||||||
|
const voiceIdEnabled = document.getElementById('diag-voice-id-enabled')?.checked;
|
||||||
send({
|
send({
|
||||||
action: 'send_voice_config',
|
action: 'send_voice_config',
|
||||||
ttsEnabled, xttsVoice, whisperModel,
|
ttsEnabled, xttsVoice, whisperModel,
|
||||||
f5ttsModel, f5ttsCkptFile, f5ttsVocabFile,
|
f5ttsModel, f5ttsCkptFile, f5ttsVocabFile,
|
||||||
f5ttsCfgStrength, f5ttsNfeStep,
|
f5ttsCfgStrength, f5ttsNfeStep,
|
||||||
fluxDefaultModel, fluxKeywordRaw, fluxKeywordSwitch, huggingfaceToken,
|
fluxDefaultModel, fluxKeywordRaw, fluxKeywordSwitch, huggingfaceToken,
|
||||||
voiceIdThreshold,
|
voiceIdThreshold, voiceIdEnabled,
|
||||||
});
|
});
|
||||||
const statusEl = document.getElementById('voice-status');
|
const statusEl = document.getElementById('voice-status');
|
||||||
if (statusEl && xttsVoice) {
|
if (statusEl && xttsVoice) {
|
||||||
|
|||||||
@@ -2681,6 +2681,12 @@ wss.on("connection", (ws) => {
|
|||||||
const t = parseFloat(msg.voiceIdThreshold);
|
const t = parseFloat(msg.voiceIdThreshold);
|
||||||
if (t >= 0.0 && t <= 1.0) voiceConfig.voiceIdThreshold = t;
|
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 {
|
try {
|
||||||
fs.mkdirSync("/shared/config", { recursive: true });
|
fs.mkdirSync("/shared/config", { recursive: true });
|
||||||
fs.writeFileSync("/shared/config/voice_config.json", JSON.stringify(voiceConfig, null, 2));
|
fs.writeFileSync("/shared/config/voice_config.json", JSON.stringify(voiceConfig, null, 2));
|
||||||
|
|||||||
@@ -74,6 +74,18 @@ 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"))
|
||||||
|
|
||||||
|
# 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:
|
def pcm_s16le_to_float32(data: bytes) -> np.ndarray:
|
||||||
if not data:
|
if not data:
|
||||||
@@ -282,6 +294,10 @@ class SessionManager:
|
|||||||
"""Einmalig: erste ~1.5s → Embedding → Vergleich mit Fingerprint.
|
"""Einmalig: erste ~1.5s → Embedding → Vergleich mit Fingerprint.
|
||||||
Ohne Fingerprint fail-open (match=True). Bei Mismatch: Session beenden."""
|
Ohne Fingerprint fail-open (match=True). Bei Mismatch: Session beenden."""
|
||||||
sess.speaker_checked = True
|
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])
|
head = bytes(sess.pcm_buffer[: STREAM_SPEAKER_CHECK_MS * 32])
|
||||||
if len(head) < speaker_id.MIN_SAMPLE_BYTES:
|
if len(head) < speaker_id.MIN_SAMPLE_BYTES:
|
||||||
sess.speaker_match = True
|
sess.speaker_match = True
|
||||||
@@ -535,6 +551,10 @@ async def run_loop(sessions: SessionManager) -> None:
|
|||||||
logger.info("[speaker-id] threshold gesetzt: %.2f", t)
|
logger.info("[speaker-id] threshold gesetzt: %.2f", t)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
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:
|
except Exception as e:
|
||||||
logger.warning("RVS-Verbindung verloren: %s — retry in %ds", e, retry_s)
|
logger.warning("RVS-Verbindung verloren: %s — retry in %ds", e, retry_s)
|
||||||
if use_tls and RVS_TLS_FALLBACK and not tls_fallback_tried:
|
if use_tls and RVS_TLS_FALLBACK and not tls_fallback_tried:
|
||||||
|
|||||||
@@ -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_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_MAX = 0.020 # Obergrenze (lautes Auto: Sprache nie ganz aussperren)
|
||||||
STREAM_VOICE_RMS_THRESHOLD = 0.012 # Legacy-Konstante (nicht mehr im Cut-Pfad genutzt)
|
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,
|
# Rein-semantischer Backstop: wenn die Energie NIE faellt (laute Umgebung,
|
||||||
# z.B. Auto), endpointen wir trotzdem — aber erst nach diesem Faktor x
|
# z.B. Auto), endpointen wir trotzdem — aber erst nach diesem Faktor x
|
||||||
# endpoint_ms, damit normales Sprechen mit Pausen nicht abgeschnitten wird.
|
# 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
|
Ohne Fingerprint → fail-open (match=True). Bei mismatch wird die
|
||||||
Session sofort beendet mit synthetischem stt_endpoint."""
|
Session sofort beendet mit synthetischem stt_endpoint."""
|
||||||
sess.speaker_checked = True
|
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)
|
# 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])
|
head_bytes = bytes(sess.pcm_buffer[: STREAM_SPEAKER_CHECK_MS * 32])
|
||||||
if len(head_bytes) < speaker_id.MIN_SAMPLE_BYTES:
|
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)
|
logger.info("[speaker-id] threshold gesetzt: %.2f", t)
|
||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
pass
|
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:
|
if "whisperDebugLog" in payload:
|
||||||
global _DEBUG_LOG_TO_BRIDGE
|
global _DEBUG_LOG_TO_BRIDGE
|
||||||
old = _DEBUG_LOG_TO_BRIDGE
|
old = _DEBUG_LOG_TO_BRIDGE
|
||||||
|
|||||||
Reference in New Issue
Block a user