Speaker lab: try Nemotron speaker detection side by side (after 1.1.62) - #1789
Draft
claude[bot] wants to merge 34 commits into
Draft
claude[bot] wants to merge 34 commits into
claude[bot] wants to merge 34 commits into
Conversation
0.17.0 adds Nemotron 3 Diarization, which the speaker lab needs. The bump itself should change nothing users see, so: - build-deps: tools 6.2 manifest so FluidAudio can opt out of its default NemoTextProcessing trait (a prebuilt Rust static lib we never call and would not archive). swiftLanguageModes [.v5] keeps in-tree targets on the language mode they built under before. - DiarizationService: clusteringThreshold is now a Euclidean cut distance, so the tuned 0.6 cosine becomes sqrt(0.8); constrainedAssignment pinned off to match 0.15.x assignment. - Parakeet (app + CLI): keep the 0.15.x long-form chunking (melChunkContext on, no seam-gap repair). - Integration fake FluidAudio grows the ASRConfig shape. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
DiarizationBackendPreferences reads TRANSCRIPTED_DIARIZATION_BACKEND, then the diarization-backend-preference default, then falls back to pyannote. No Settings UI. Not wired into meetings yet; that lands with the Core Nemotron backend. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
… ci] DiarizationBackend (.pyannote default, .nemotron experimental) picks the model. The pyannote path is unchanged. The Nemotron path loads a FluidAudio Nemotron3 preset (fast128 by default, TRANSCRIPTED_NEMOTRON_PRESET for the lab), runs it on a private serial queue, turns frame probabilities into exclusive speaker turns with the pure NemotronTurnBuilder, and embeds each turn with the injected embedder or a new FluidWeSpeakerSegmentEmbedder, since Nemotron emits no voiceprints. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
NemotronTurnBuilder: exclusivity, argmax on overlap, ties, gap bridging, min-duration drop and blip rejoin, first-appearance remapping, empty and malformed input. DiarizationService: pyannote default, backend round-trip, not-loaded error, thresholds, shared reembed path. Nemotron preset override and the WeSpeaker fallback embedder's pure helpers. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
MeetingSessionController passes the chosen backend to DiarizationService. With Nemotron and no ERes2Net, voiceprints come from FluidAudio's online WeSpeaker model, so they go to their own speakers_wespeaker-fluid-online database (meetings and Settings > People agree) until the lab proves they match the offline pipeline's vectors. Default pyannote is unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
… thresholds and fingerprint-update knobs [skip ci] dump takes --backend pyannote|nemotron and --embedder native|eres2net (--eres2net-model for a path-based model) and records backend, embedder, dimension, wall time, and audio length. Old dumps still decode. replay takes --thresholds auto|weSpeaker|eRes2Net, --match adaptive, --same-voice, --dedup, and the write-back EMA knobs. Defaults reproduce the old behavior for WeSpeaker dumps. Per meeting it now reports raw cluster count and whether each cluster matched an existing profile. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
… [skip ci] speaker_eval_common.py now holds RTTM parsing, overlap helpers, the identity metrics (fragmentation, false merge, re-ID curve), and a pure Python DER/JER that matches pyannote.metrics to float precision (collar convention, UEM, overlap, Hungarian mapping). It is about 40x faster, which matters once the lab scores many replays. score_speaker_eval.py uses it and no longer needs pyannote installed. Its markdown and JSON output are unchanged on a synthetic fixture. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
…ng-speaker recognition [skip ci] score_speaker_lab.py scores a lab run directory into scores.json and REPORT.md: per variant, raw diarizer DER/JER and count error, pipeline DER after clustering and DB matching, and the recognition scoreboard (recognized / wrong person / asked again / undetected, plus new people false-matched to a known profile). It picks each variant's best knob setting and shows which knob values moved recognition. Own-calls mode reports behavior without ground truth, agreement against a baseline variant, and writes a self-contained timeline.html. Also has helper subcommands the driver uses: grid, dump-ok, and own-calls-list. Unit tests run on Linux with synthetic fixtures. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
…ingle trial [skip ci] run_speaker_lab.sh dumps each meeting once per variant (backend x embedder x Nemotron preset) into its own cache, replays every knob setting, and scores it all into reports/speaker-lab/<stamp>/. Every knob is a flag with an env twin, the run is non-interactive, fails loud, and prints the scores.json path as its last stdout line. --single runs one variant at one setting for an outer optimizer. --own-calls runs the same variants on saved meetings (call track read in place) and writes a timeline page. Tests drive the script end to end against a fake harness, so the orchestration is covered on Linux too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
…s [skip ci] 16 scenario series (8 Edinburgh, 4 Idiap, 4 TNO), 4 sessions each with the same 4 people, for the speaker lab's cross-call recognition test. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
…[skip ci] README covers what the lab measures, how to run it on AMI and on your own saved calls, every knob, how an optimizer drives --single trials, the scores.json schema, and how to add a diarizer or embedder. The test matrix now runs the lab's syntax checks and unit tests when the lab files change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
….17 [skip ci] Review of #1789 found the bump was not as behavior-neutral as claimed: - FluidAudio 0.17 pins speaker-diarization-coreml to one commit and deletes any cache without a matching revision marker. Every 0.15.x cache, and the offline-diarizer-models copy bundled in the app, has none, so the first meeting would delete inside the signed bundle (or fail offline). FluidAudioCompatibility.keepUnpinnedDiarizerCaches() resolves that repo at main, as 0.15.x did; DiarizationService, FluidWeSpeakerSegmentEmbedder and the CLI call it before loading. - The tuned pyannote config moves to FluidAudioCompatibility.tunedOfflineDiarizerConfig() and a test pins the distance threshold, constrainedAssignment=false and every tuned value. - CLI diarize/batch start from the 0.15.x default (cosine 0.6, no constrained assignment), and a config file's clusteringThreshold stays a cosine similarity, converted for 0.17. - Parakeet comments now say only the two new defaults are pinned: 0.17 also changed >15 s chunk merging with no switch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
… [skip ci] - THIRD_PARTY_LICENSES: FluidAudio 0.17's Japanese G2P (Misaki ports, UniDic) and Spanish/French lexicon notices; NeMo text processing is not linked. - Parakeet comment: 0.17 retries blank v3 decodes up to five more times, so near-silent dictation can take longer and return text. - build-deps: FluidAudio's resource bundle never ships, so LuxTts G2p must stay unused. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
…and valueless flags [skip ci] A dump with an unknown TRANSCRIPTED_NEMOTRON_PRESET recorded the typo while Core's runner silently ran fast128, so the cached dump lied about its variant. Validate with FluidAudio's Nemotron3Config.preset(named:) and record 'default' for unset. --write-path-fixes now rejects anything but on|off (a typo used to run the legacy path), and a flag with no value is an error instead of the default. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
…the online WeSpeaker embedder [skip ci] Diarizes one file with today's pipeline, re-embeds the same segments with FluidWeSpeakerSegmentEmbedder (what Nemotron falls back to), and reports per-segment, within-model, cross-model and per-cluster cosine stats against the WeSpeaker match floor, plus a looksInterchangeable heuristic. Answers whether Nemotron voiceprints could share speakers.sqlite instead of a separate DB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
Add DiarizationService.resolvedNemotronPresetName(environment:), a public wrapper over the runner's internal resolver, so the harness records 'fast128' instead of 'default' when TRANSCRIPTED_NEMOTRON_PRESET is unset, and refuses a name Core would silently replace. The lab scorer treats unset, 'default' (older dumps) and 'fast128' as one variant so existing caches stay reusable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
scripts/hillclimb/benches/speaker_lab.py speaks the hill-climb request/result protocol (#1791) and drives run_speaker_lab.sh --single once per trial over the requested AMI series, then splits scores.json + recognition-events.json back into per-series items: recognition_rate, recognized, asked_again, pipeline/raw DER, speaker-count error, objective, plus int gates wrong_person and new_person_false_match. Missing/incomplete series and series without returning speakers are item errors, never zeros. Driver env twins are scrubbed so shell exports can't leak into a trial, and scores.json must echo every knob that was set. app_revision hashes the harness binary, the requested RTTM/audio, and the lab scripts. Imports hc_benches when the climber is on the branch, else uses a local copy of the protocol validator, so it works before and after the merge. Also: config/hillclimb/suites/speaker-lab-ami.json (the 16 download_ami.sh lab series, 12 dev / 4 holdout stratified by site) and speaker_lab.README.md with the benches/knobs/objectives JSON for #1791. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
Changes to the adapter, its tests/README/suite, or the lab driver/scorer it runs now select py_compile + speaker_lab.py --self-test. Kept as its own rule above the SpeakerEvalHarness block so it doesn't collide with #1791's scripts/hillclimb/** rule. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
…eports it [skip ci] run_speaker_lab.sh --embedding-parity (EMBEDDING_PARITY=1) runs the harness's embedding-parity per meeting (RTTM labels on corpora, pyannote clusters on own calls), cached in data/eval/<corpus>/parity/. The scorer pools the reports via their 200-bin histograms into an additive embeddingParity block in scores.json and an 'Embedding parity' section in REPORT.md, with the same interchangeability rule as the harness. schemaVersion stays 1; runs without the flag are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
A cache written by a pinned FluidAudio 0.17 build carries a .fluidaudio-revision marker naming a commit. The app resolves the diarizer repo at main, so a shipped marker naming anything else would make it delete files inside its own signed bundle. build-beta drops the marker after copying the cache. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
…kip ci] Rebased onto e3a559f. The dump now records the resolved Nemotron preset and the scorer treats unset/default/fast128 as one variant, so the knob echo check normalizes presets the same way. fast128 is still not passed because the driver names variants after the preset string (passing it would fork the dump cache). EMBEDDING_PARITY joins the scrubbed env twins; --embedding-parity is a diagnostic and never passed per trial. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
… ci] Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
claude Bot
pushed a commit
that referenced
this pull request
Sep 24, 2026
…, models-dir note [skip ci] Review S1/M5/M7 on #1791: the clustering knob's notes say it's in cosine units and point at the merge note for #1789's FluidAudio 0.17 distance change; recommendation.json records the revision its source line numbers came from; the guide explains the shared FluidAudio model cache. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8VU2yBBnE8eH5f18mubhS
9 tasks
…ip ci] The 0.17 bump can't merge until WER is no worse and blank-audio dictation stops aren't slower (PR #909 retries blank V3 decodes up to 5 times). scripts/stt_fluidaudio_ab.sh builds transcripted-cli twice in git worktrees under ~/stt-fluidaudio-ab: the baseline from origin/main (pre-bump source, since 0.15.4 can't compile the new ASRConfig call) with FLUID_AUDIO_VERSION 0.15.4, the candidate from HEAD with 0.17.0. Builds are reused while their inputs are unchanged. It refuses a baseline ref that already uses the 0.17 API and checks the version SwiftPM actually resolved. scripts/stt_fluidaudio_ab.py then measures, reusing the STT shootout's lecture download, caption parsing and WER scorer (read from git when the shootout isn't in the checkout): - WER: first 20 min of the lecture plus 10 caption-aligned ~45 s pieces - stop time: silence 1/3/8 s, near-silence, low noise, room rumble, a 0.5 s noise burst (gated), plus `say` clips (reported), ABBA rounds, median of CLI processingSeconds Gate flags: --max-wer-delta-pp 0.5, --max-time-ratio 1.25, --max-time-delta-ms 300. Writes result.json + report.md; last stdout line is the result.json path. scripts/test_stt_fluidaudio_ab.py covers the math, fixtures and a fake-CLI end-to-end run, and drives the bash wrapper against a throwaway repo with stubbed Mac tools. Runs on Linux. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
claude Bot
pushed a commit
that referenced
this pull request
Sep 24, 2026
Pasted from #1789's speaker_lab.README.md: the speaker-lab command bench, 6 new knobs (diarization backend, Nemotron preset, match mode and floor, replay dedup, write-path fixes), speaker-lab-recognition, and an identical copy of its suite. The adapter lives on #1789, so trials are item errors until it merges. The objective reports BLOCKED: 4 holdout series, need 8. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8VU2yBBnE8eH5f18mubhS
…t groups [skip ci] The hill-climb lab needs >= 10 dev and >= 8 holdout independent units before an objective can confirm anything; 16 series (12/4) left speaker-lab-recognition BLOCKED. Adds ES2004 ES2011 ES2012 ES2013 IS1006 IS1007 TS3007 TS3008 (all four sessions have pyannote RTTMs, and no participant appears in two series). Holdout re-picked per site by lowest unit_hash under a new salt (speaker-lab-ami-v2): 4 ES, 2 IS, 2 TS. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
claude Bot
pushed a commit
that referenced
this pull request
Sep 24, 2026
… ci] Byte-for-byte copy of config/hillclimb/suites/speaker-lab-ami.json from #1789 at ffe5871 (salt speaker-lab-ami-v2, 16 dev / 8 holdout series), so speaker-lab-recognition is no longer BLOCKED. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M8VU2yBBnE8eH5f18mubhS
Resolves the Core and SpeakerEvalHarness CLAUDE.md file maps: keeps main's per-file map and adds this branch's diarization backend, FluidAudio compatibility and speaker lab entries. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
…[skip ci] main's --models-dir guard (keeps FluidAudio from deleting a Parakeet Ultra install that fails to load) used DownloadUtils.enforceOffline, which 0.16 renamed to ModelHub.offlineMode. Same semantics: loadModels rethrows instead of purging and re-downloading. Also corrects the local-install layout comment: v3 file names are unchanged through 0.17.0. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
build-deps.sh: keep FluidAudio 0.17.0 with traits: [] and take main's removal of mlx-swift-lm. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ
7 tasks
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.
Requested by Justin · project thread
Before: meetings split speakers with one model (pyannote through FluidAudio 0.15.4). There was no way to try another speaker model, or to measure how often a returning person gets recognized on their next call.
After: FluidAudio is on 0.17.0, with the diarizer behavior and model caches kept as they were. NVIDIA's Nemotron 3 speaker model can be swapped in behind a hidden switch that's off by default. A speaker lab scores every combination on the same meetings: who spoke when, speaker counts, and cross-call recognition (recognized, wrong person, asked to name again).
Draft. CI is green on macOS with FluidAudio 0.17.0 (main merged in, including the MLX removal). It can't merge until the Mac runs below pass.
Why
FluidVoice shipped local meeting recording with speaker labels on Nemotron 3. Before switching anything, we should know whether it beats our diarizer, and which knobs raise cross-call recognition.
Product Impact
meetings, anddictationthrough the FluidAudio bumpmeeting reliabilityWhat changed
traits: [], so the new Rust text-normalization library isn't linked.swiftLanguageModes: [.v5].FluidAudioCompatibility(Core) keeps what shipped:main. 0.17 pins it to a commit and deletes markerless caches, which would include the copy bundled in the app.build-beta.shdeletes any.fluidaudio-revisionmarker from the bundled diarizer models, so a pinned-build cache can't ship a marker into the app.diarize/batchkeep the 0.15.x defaults, and config files keep cosine thresholds. The CLI's offline guard for local model folders (Parakeet Ultra) moves fromDownloadUtils.enforceOffline(removed in 0.16) toModelHub.offlineMode.scripts/stt_fluidaudio_ab.shpasses on a Mac (see below).DiarizationBackend,NemotronDiarizationRunner, pureNemotronTurnBuilder→ non-overlapping turns).speakers_wespeaker-fluid-online.sqliteuntil parity with the saved people is measured.TRANSCRIPTED_DIARIZATION_BACKEND=nemotronor thediarization-backend-preferencedefault. No UI, and it's read at launch.scripts/run_speaker_lab.sh,score_speaker_lab.py, harnessdump --backend/--embedder, replay knobs for thresholds and fingerprint updates).--singlewritesscores.jsonfor the hill-climbing lab.-wal/-shmside files.--embedding-parity(online vs offline WeSpeaker, with an "interchangeable or not" verdict in the report).scripts/hillclimb/benches/speaker_lab.py; registry snippets in its README). Merge steps with Hill-climb lab: tune app settings against scored, held-out tests #1791 are written down; whichever PR lands second follows them.scripts/stt_fluidaudio_ab.sh): builds the CLI onorigin/mainwith 0.15.4 and on this branch with 0.17.0, then compares Parakeet V3 on about 27 min of captioned lecture (WER) and on generated silence/noise clips (how fast a blank result comes back, which is what decides how fast dictation stops). Gate: WER no more than +0.5 pp, and silence/noise at most 1.25x and +300 ms slower. Writesresult.json+report.md.How I checked it
DownloadUtilsone above was the only break. 0.17 pins only the diarizer repo, so existing Parakeet caches and the Ultra folder aren't deleted.scripts/dev/linux-checks.sh(52/52), which now also runstest_score_speaker_lab.py,test_speaker_lab.pyandtest_stt_fluidaudio_ab.pycheck-source-pins.py --changed-only,agent-preflightRisk Review
Notes
traits: []; it isn't linked.🤖 Generated with Claude Code
https://claude.ai/code/session_01SvYQfvK9JYWs3DB2fNkVNQ