02 · Cocktail Filter — ignore other voices in the room¶
Implementation plan. Spec:
design/specs/cocktail-filter.md. Roadmap:ROADMAP.md§3.2. Build tier A — P1 (personal-VAD gate) buildable now; P2 (suppression) gated on an open model.
Goal¶
When someone else is talking nearby, keep only the enrolled user's speech out of the transcript. Off by default; fully local; reuses the §2.1 voiceprint.
Why this split¶
The production solution (VoiceFilter-Lite, 2.2 MB streaming, −25% WER multi-talker) is Google-internal; the open third-party impl has no official pretrained weights and isn't pip-installable. So full suppression (masking the interferer) needs a model we don't yet have. The personal-VAD gate (drop frames that aren't the target speaker) is buildable now from a 130K-param speaker-conditioned classifier and the existing VAD, and handles the common "one other voice" case.
Module layout¶
src/yazses/audio/
personal_vad.py # P1: speaker-conditioned per-frame target/non-target gate
src/yazses/separate/ # P2 (later)
voicefilter.py # P2: target-speaker suppression backend (Protocol + Null)
factory.py # build_separator(cfg) -> backend | None
voiceprint/ (§2.1) for the d-vector, audio/vad_calibrated.py (gate it slots beside), audio/recorder.py buffer. Config ([cocktail], off by default)¶
[cocktail]
enabled = false
mode = "gate" # gate (P1) | suppress (P2)
target_threshold = 0.6 # per-frame target-speaker score to keep a frame
min_voiceprint = true # require an enrolled voiceprint (else dormant + warn)
P1 — personal-VAD gate (buildable now)¶
- Requires a voiceprint (§2.1).
personal_vad.gate(audio, embedding, cfg) -> audio: frame the buffer, score each frame's similarity to the target d-vector (a tiny speaker-conditioned classifier or cosine-on-embeddings baseline), zero/drop frames belowtarget_threshold, return the gated buffer. - Wire into
core/daemon.py::_on_hold_endbefore STT (after VAD), when[cocktail] enabled and mode == "gate"and a voiceprint exists. If no voiceprint → dormant + one-line warn (don't gate blindly). - Metadata-only logging: % frames dropped (no transcript). TDD (in-env): synthetic numpy fixtures — single-talker (target) passes through; two-talker mix (target + interferer embeddings) drops interferer-dominant frames; no-voiceprint → pass-through + warn; all-non-target → empty → existing "silent" discard. Fake embedder injected. Needs hardware: real two-talker audio for the WER gate.
P2 — suppression (gated on an open model)¶
separate/backend behind a Protocol (Separator.process(audio, embedding) -> audio),build_separatorreturnsNone/Nullper the dormancy contract. Ships only when a permissively-licensed, CPU-real-time target-speaker model exists (re-evaluate the VoiceFilter-Lite / 3S-TSE landscape). Newcocktailextra (onnxruntime) at that point.- Eval gate: WER improvement on a multi-talker set vs the P1 gate, no clean-speech harm.
Privacy¶
Voiceprint is biometric → encrypted corpus only (ADR-012). Audio gated/suppressed in-RAM; nothing stored or sent.
Verification map¶
- In-env (CI): the gate math + daemon wiring with synthetic mixes and a fake embedder.
- User machine: real multi-talker WER delta (the LOFA — ship the gate if it helps and doesn't harm single-talker).
- Gated on a model: P2 suppression (no open CPU-RT weights today).
Live finding (2026-06-19) — P1 gate false-rejects the user's own voice ⚠️¶
Tested on real hardware: the personal-VAD gate broke dictation — it dropped ~90% of the user's own speech (a 5 s utterance → one 0.5 s window survived, or all audio gated out). ECAPA embeddings are unreliable on 0.5 s windows: the same speaker scores low cosine vs the enrolled voiceprint at that granularity, so any target_threshold strict enough to reject other voices also rejects most of the user's own.
Decision: default [cocktail] enabled = false; do NOT ship the 0.5 s-window gate. Revisit only with (a) 1–1.5 s windows + a much lower threshold tuned live, (b) open-set cohort scoring rather than a fixed threshold, or © a real target-speaker model (VoiceFilter-Lite class — no open CPU-RT weights today). Voice-focus is not production-ready offline as built. See [[project_cocktail_filter_lesson]].