Multi-exemplar voiceprints for speaker matching#1488
Merged
Conversation
A person's clean in-person mic and their compressed remote/Zoom mic produce
systematically different embeddings. Storing ONE running-average voiceprint per
person (the EMA-blended SpeakerProfile.embedding) fuses both into a mediocre
centroid that fits neither condition well, depressing match confidence in both.
Store a small, bounded set of representative embeddings ("exemplars") per person
in a new speaker_exemplars table — the K most distinct-yet-confirmed session
means, maintained by a pure centroid-distance/diversity heuristic
(SpeakerExemplarPolicy). Matching now scores a candidate against the BEST-fitting
representative (max cosine over {average} ∪ exemplars) via
SpeakerVectorMath.bestSimilarity, instead of the single blended average.
Additive and backward-compatible:
- SpeakerProfile.exemplars defaults to [] — a legacy single-average profile has
no exemplar rows, so its match score is byte-identical to before (max over the
single average). The change only ever ADDS candidate vectors, so per-profile
similarity is monotonically non-decreasing.
- The single-average path (EMA blend, merge, provenance re-derivation) is
untouched; exemplars are a rebuildable read-side cache layered on top.
- The 192-d/256-d dimension-isolation guard is preserved in both matchers.
Contamination guard preserved: exemplars are written only on a confident/cautious
match (write-back alpha > 0) — the same gate that protects the average — so an
ambiguous/frozen match can never seed or drift an exemplar. Existing match
semantics (maturity bonus, separation guard, second-best margin) still operate on
the per-profile best similarity.
Structural edits stay consistent: merge drops both profiles' exemplars, average
re-derivation (un-merge / reassign) and profile deletion clear them, so exemplars
never outlive the identity they represented (they re-accumulate from future
confident matches). Invisible to the user per the "names just appear" design —
model/pipeline layer only.
Validated: full Core `swift test` (694 tests, 0 failures) including the in-package
speaker eval harness (SpeakerNamingSimulationRunnerTests — false-merge /
confusion / identity-stability indicators) and dimension-isolation suite; app
build + launch smoke; new focused unit tests for the policy and for both
single-exemplar and multi-exemplar matching.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
07be2f3 to
c2dab86
Compare
This was referenced Jul 7, 2026
r3dbars
added a commit
that referenced
this pull request
Jul 7, 2026
…ansport The negative-exemplar veto (#1487) was "sound but regime-limited" (eval in #1493): after a correction it removed 46% of repeat wrong-matches in-room (AMI) but only 12% cross-condition (VoxCeleb). Root cause: a single rejected in-room sample and a later telephone/VoIP sample of the same rejected impostor sit too far apart in embedding space for the raw cosine to clear the 0.80 veto floor. The positive side already closed this gap with multi-exemplar voiceprints (#1488); the negative side — one rejected embedding — had no analog. Give the negative side the same multi-condition treatment (eval rec #3): compare the candidate against each rejected sample AND against that sample transported along the profile's own observed condition shifts (unit(exemplar) − average). Channel/condition shifts are largely speaker-independent, so a rejected sample shifted by a condition the profile has actually seen approximates the same rejected voice returning in that other condition. Owner-safe by construction: - transport only along +(exemplar − average) — real, forward, profile-observed conditions; bidirectional/synthetic directions were measured to leak owner-collateral and are not used. - a profile with no positive exemplars derives nothing → reduces exactly to the raw maxNegativeSimilarity → single-condition profiles unchanged. - the 0.80 floor and the ≥positiveSimilarity owner gate are untouched; transport only widens which impostor returns are caught, never lowers the floor. Real qmatrix eval (SpeakerExemplarDeltaEvalTests): AMI cross-condition vetoed-among-re-match 0.350 → 0.374 (+7.0% rel) with owner-collateral flat (2.17% → 2.32%, +9 of 5773 checks). VoxCeleb (single-utterance corpus, no condition structure) byte-identical, incl. owner-collateral — no regression. Pushing VoxCeleb's 12% further was investigated and rejected: every mechanism that moves it trades owner-collateral ~1:1, violating the #1493 guardrail. See docs/speaker-eval-negative-veto-cross-condition-2026-07.md. Refs #1493. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Replaces the single running-average voiceprint per person with a small, bounded set of representative exemplars, and matches a candidate against the best-fitting exemplar instead of one blended-across-conditions average.
Why
A person's clean in-person mic and their compressed remote/Zoom mic produce systematically different embeddings. EMA-blending both into one
SpeakerProfile.embeddingyields a mediocre centroid that fits neither condition well, depressing match confidence in both. Storing the K most distinct-yet-confirmed session means lets a returning voice match its actual capture condition.How
SpeakerExemplarPolicy(new, pure/deterministic): maintains ≤maxExemplars(3) exemplars via a centroid-distance/diversity heuristic — a same-condition mean (cosine ≥ 0.80 to a representative) EMA-blends into the nearest exemplar (denoise); a distinct condition earns its own slot; at the cap, the most-redundant exemplar is evicted only when the newcomer is strictly more distinct (diversity never decreases).SpeakerVectorMath.bestSimilarity(candidate:average:exemplars:): max cosine over{average} ∪ exemplars, skipping dimension-mismatched vectors.speaker_exemplarstable (SpeakerExemplarStore): batch-loaded intoSpeakerProfile.exemplarson read; both matchers (matchAgainstProfilessnapshot path +matchSpeakerImpl) now usebestSimilarity.Additive & backward-compatible
SpeakerProfile.exemplarsdefaults to[]→ a legacy single-average profile matches byte-identically (max over the single average). The change only ever adds candidate vectors, so per-profile similarity is monotonically non-decreasing — it cannot lower a true match's score.alpha > 0) — the same gate that protects the average — so an ambiguous/frozen match can never seed or drift an exemplar.Validation
swift test: 694 tests, 0 failures (12 skipped/timing-sensitive), including:SpeakerNamingSimulationRunnerTests— the in-package deterministic speaker eval harness (false-merge / confusion / identity-stability indicators): no regression.SpeakerDBDimensionIsolationTests,SpeakerMatchingServiceTests,SpeakerEmbeddingMatcherTests,SpeakerProfileMergerTests,SpeakerProfileProvenanceTests: pass.SpeakerExemplarPolicyTests(pure policy) +SpeakerMultiExemplarMatchingTests(legacy single-exemplar regression and multi-exemplar benefit through the real DB write→read→match path).bash build.sh --no-open(app build + launch smoke) exit 0; source-list guardrail passes;build-depscompiles Core into both the app and SPM archives.Eval-corpus note
The AMI audio corpus (
data/ami/…, gitignored, multi-GB download) is not present locally, so a numeric before/after DER sweep (scripts/run_speaker_eval.sh) was not runnable in this environment. Validation is against the runnable in-package eval (SpeakerNamingSimulationRunner), which exercises the real clusterer +matchAgainstProfiles+ naming/write path and reports no regression. A full AMI sweep on a machine with the corpus is the recommended follow-up before broad rollout.🤖 Generated with Claude Code