Skip to content

fix(speaker): average-based auto-accept margin — restore 0-false-auto without losing the multi-exemplar recall win#1496

Merged
r3dbars merged 1 commit into
mainfrom
fix/exemplar-margin-decouple
Jul 7, 2026
Merged

fix(speaker): average-based auto-accept margin — restore 0-false-auto without losing the multi-exemplar recall win#1496
r3dbars merged 1 commit into
mainfrom
fix/exemplar-margin-decouple

Conversation

@r3dbars

@r3dbars r3dbars commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Why

main (release 1.1.49 prep) has a release-critical regression: the multi-exemplar voiceprints feature (#1488) broke the certified "0 false auto-accept" guarantee on degraded audio. The eval that landed in #1493 (docs/speaker-eval-exemplar-delta-2026-07.md + SpeakerExemplarDeltaEvalTests) measured it on real AMI cross-condition embeddings:

  • Multi-exemplar is a real win: genuine recall +0.109 (0.549 → 0.659) on degraded returning-speaker audio.
  • But it introduced 12 silent mislabels (false-auto 0.0% → 1.6%) where the average-only matcher had 0 — distinct speakers auto-named because ONE lucky exemplar cleared both the 0.92 bar and the 0.12 best-vs-second margin.

The fix (eval recommendation #2)

Decouple the auto-accept margin from the best-exemplar score:

  • KEEP best-of-exemplars for match selection — this is where the recall win lives, untouched.
  • CHANGE only the auto-accept margin: compute best-vs-second against each profile's blended average representative, not its best exemplar.

A genuine owner is close to the profile on both its average and its best exemplar, so the margin still clears. A lucky-exemplar impostor is far from the average, so its average-based margin collapses and the auto-accept is withheld (routed to suggest/ask instead of silently naming). The 0.92 bar still uses the exemplar score, so silent auto-recognition of genuine returning voices is preserved.

Code

  • SpeakerNamingPolicy.shouldAutoAccept / initialMapping gain an optional marginSimilarities: (best, secondBest) (average-based). nil ⇒ legacy behavior; single-average profiles are unaffected (average == best exemplar).
  • SpeakerMatchingService.matchAgainstProfiles computes the winner's average cosine and the best-other average cosine into SnapshotMatchResult, threaded through the pipeline tuple → ChannelSpeakerContextSpeakerClassificationKnowledge to the auto-accept sites (system + mic) and the simulation runner. The exemplar secondBestSimilarity is retained for write-back blend gating.

Verification — re-ran the #1493 harness on the same qmatrix corpus

SpeakerExemplarDeltaEvalTests against the real AMI + VoxCeleb per-quality fingerprint caches (WITH arm = multi-exemplar). Raw output: docs/speaker-eval-exemplar-delta-2026-07.after-fix.result.json.

AMI, degraded slice (741 trials, 147 profiles):

metric before (best-exemplar margin) after (average margin) Δ
false-auto (silent mislabels) 12 (1.6%) 2 (0.27%) −10
genuine recall @ floor 0.70 0.659 0.659 0.000
correct silent auto-recognition 0.097 0.070 −0.027
mean genuine similarity 0.858 0.858 0.000

Recall is fully retained (unchanged by construction — the fix touches neither match selection nor the match floor). WITHOUT (legacy) arm and the VoxCeleb slice are unchanged (0 false-auto both arms). The harness now reports both falseAutoLegacyMarginCount (12) and falseAutoCount (2) per run, so the A/B is reproducible in one artifact.

Residual 2 — characterized, not lucky-exemplar artifacts

The two remaining false-autos are genuinely confusable on the blended average (avgBest 0.82 / 0.70, avg-margin 0.172 / 0.137), not the one-lucky-exemplar failure this fix targets. Probed tighteners on the same slice each removed at most 1 of the 2 while costing 2.5–15× as many genuine auto-recognitions:

lever false-auto genuine autos kept
average margin (shipped) 2 52
+ margin 0.12→0.15 1 47 (−5)
+ exemplar-vs-avg gap ≤ 0.15 1 25 (−27)
+ avgBest ≥ 0.80 1 23 (−29)

So the clean average-margin decouple is the knee of the curve: it removes the entire lucky-exemplar class (10/10) at zero recall cost, and stops before trading the genuine experience away for two irreducibly-confusable cases. 0.27% remains a worst-case degraded-slice upper bound, not a production rate.

Also added

SpeakerAutoAcceptMarginTests — corpus-free, in-tree proof (runs in CI): constructs the lucky-exemplar impostor fixture and asserts the average-based margin withholds it while a genuine owner still auto-accepts, both at the policy gate and through the full matchAgainstProfiles path.

Tests run locally

  • swift test (Core) — 716 pass, 0 failures (13 corpus-gated skips)
  • run-tests.sh (fast suite, compiles app-layer sources) — 12745 pass, 0 failures
  • SpeakerExemplarDeltaEvalTests against the real qmatrix corpus — 12 → 2 false-auto, recall held
  • SpeakerAutoAcceptMarginTests — pass
  • build.sh --no-open (full app build + launch smoke + performance budget) — OK
  • e2e smoke — OK; source-list contract, duplicate-declaration check, agent-preflight — OK
  • Tools package tests — pass
  • Integration smoke: blocked locally only by a missing ESpeakNG.framework (TTS dep not in the local deps copy; unrelated to this change — it fails at dylib load before any test runs). Covered by CI, which builds full deps.

Refs #1493.

🤖 Generated with Claude Code

…re 0-false-auto

Multi-exemplar voiceprints (#1488) lifted genuine recall on degraded audio
(+0.109) but broke the certified "0 false-auto" guarantee: the eval in #1493
(docs/speaker-eval-exemplar-delta-2026-07.md) measured 12 silent mislabels on
the degraded AMI slice where the average-only matcher had 0. Distinct speakers
were auto-named because ONE lucky exemplar cleared both the 0.92 bar and the
0.12 best-vs-second margin.

Fix (the eval's recommendation #2): keep best-of-exemplars for match SELECTION
(where the recall win lives) but compute the auto-accept MARGIN against each
profile's blended AVERAGE representative, not its best exemplar. A genuine owner
is close on both its average and its best exemplar so the margin still clears;
a lucky-exemplar impostor is far from the average so its average-based margin
collapses and the auto-accept is withheld (routed to suggest/ask).

- SpeakerNamingPolicy.shouldAutoAccept/initialMapping gain an optional
  marginSimilarities: (best, secondBest) — the 0.92 bar still uses the exemplar
  score; only the margin uses the average. nil ⇒ legacy behavior (single-average
  profiles are unaffected: average == best exemplar).
- SpeakerMatchingService.matchAgainstProfiles computes the winner's average
  cosine and the best-other average cosine into SnapshotMatchResult, threaded
  through the tuple → ChannelSpeakerContext → SpeakerClassificationKnowledge to
  the auto-accept call sites (system + mic) and the simulation runner.
- SpeakerExemplarDeltaEvalTests exercises the new gate and reports both the
  pre-fix (best-exemplar) and post-fix (average) false-auto counts per run.
- SpeakerAutoAcceptMarginTests: corpus-free proof (policy gate + full
  matchAgainstProfiles path) that the lucky-exemplar impostor is now withheld
  while a genuine owner still auto-accepts.

Re-run of the #1493 harness on the same qmatrix corpus (AMI degraded, WITH arm):
false-auto 12 → 2, genuine recall 0.659 unchanged, mean genuine sim 0.858
unchanged. The residual 2 are genuinely confusable on the blended average
(avgBest 0.70/0.82, avg-margin 0.14/0.17), not lucky-exemplar artifacts;
tightening levers remove at most one while costing 2.5–15x as many genuine
auto-recognitions, so the clean average-margin decouple is the knee of the
curve. Full Core suite (716) + fast suite (12745) green.

Refs #1493.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@r3dbars r3dbars merged commit 84cc69e into main Jul 7, 2026
6 checks passed
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