Skip to content

Multi-exemplar voiceprints for speaker matching#1488

Merged
r3dbars merged 1 commit into
mainfrom
feat/multi-exemplar-voiceprints
Jul 7, 2026
Merged

Multi-exemplar voiceprints for speaker matching#1488
r3dbars merged 1 commit into
mainfrom
feat/multi-exemplar-voiceprints

Conversation

@r3dbars

@r3dbars r3dbars commented Jul 7, 2026

Copy link
Copy Markdown
Owner

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.embedding yields 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_exemplars table (SpeakerExemplarStore): batch-loaded into SpeakerProfile.exemplars on read; both matchers (matchAgainstProfiles snapshot path + matchSpeakerImpl) now use bestSimilarity.

Additive & backward-compatible

  • SpeakerProfile.exemplars defaults 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.
  • Single-average path (EMA blend, merge, provenance re-derivation) untouched; exemplars are a rebuildable read-side cache on top.
  • 192-d/256-d dimension-isolation guard 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/ambiguity 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; they re-accumulate from future confident matches.
  • Invisible to the user per the "names just appear" design — model/pipeline layer only, no UI surface.

Validation

  • Full Core 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.
  • New focused tests: 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-deps compiles 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

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>
@r3dbars r3dbars force-pushed the feat/multi-exemplar-voiceprints branch from 07be2f3 to c2dab86 Compare July 7, 2026 15:42
@r3dbars r3dbars merged commit aa74fed into main Jul 7, 2026
6 checks passed
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant