Meeting speaker diarization — feature spec & exploration#68
Conversation
Implements the spec from PR sebsto#64: splits the system-audio ("Others") track into per-speaker labels (Speaker 1, Speaker 2, …) using FluidAudio's SortformerDiarizer. The mic track stays "You". - MeetingSpeaker becomes .you / .others(speakerIndex: Int?) with a displayName computed property (0-based storage, +1 only at display). - New MeetingDiarizer actor wraps SortformerDiarizer: warmUp() loads the CoreML model, ingest() feeds chunks, dominantSpeaker(in:) resolves the speaker over a time window. Sortformer slots map to monotonic per-meeting indices. Failures degrade gracefully to plain "Others". - MeetingAudioEngine exposes a single sampleRate constant and emits the system-audio stream as timestamped SystemAudioChunk values. - MeetingStateManager warms up the diarizer on start (when enabled), routes system chunks through ingest + dominantSpeaker, resets on stop. - MeetingTranscriptView uses displayName with a per-speaker color palette. - SettingsStore gains meetingDiarizationEnabled (default false, opt-in); Settings adds a Meeting section toggle. - Cold-start nil speaker renders as "Others" until the diarizer warms up. Tests: MeetingDiarizerTests for the pre-init no-op contract, updated MeetingTranscript/AudioEngine tests for the new APIs. Diarization quality is verified via the manual E2E plan in the spec.
The diarizer warmUp() (model load/download, several seconds) ran before meetingState flipped to .recording, so the button stayed on 'Start Meeting' with no feedback, and the meetingState==.idle guard was still true during the gap — a second click double-triggered startCapture(). - Add an isStarting re-entrancy guard. - Start capture and flip to .recording immediately for instant feedback; warm up the diarizer concurrently inside the recording task group. - Check diarizationActive per-chunk so diarization engages once warmup completes mid-meeting. - MeetingDiarizer now tracks the meeting-relative time of its first ingested chunk and offsets segment times, keeping speaker windows aligned when the diarizer starts after the meeting has begun. - stopMeeting resets the diarizer unconditionally (safe no-op) to avoid a race with the concurrent warmup task.
- Switch Sortformer from the default fastV2_1 config to balancedV2: same ~1s latency but a much larger FIFO (188 vs 40) for better quality, and v2 weights handle overlapping/high-speaker-count audio better than v2.1 (which is documented to degrade and likely caused phantom speakers). - Give the system-audio path its own shorter chunk size (~3s vs the mic's ~5s) so each transcript entry spans less time, reducing how often a speaker change inside one entry gets flattened to a single label.
Local editor config and agent skills should not be committed to the repo.
c206890 to
8bfbf52
Compare
|
Synced now :) |
Fixes issue sebsto#65: without headphones, remote participants' speech plays out of the speakers and leaks back into the microphone, so the same utterance was transcribed twice — once as 'Others' (system audio) and once as 'You' (mic echo). - MeetingTranscript.appendSuppressingEcho() de-duplicates across the two tracks: a 'You' entry that closely matches a recent 'Others' entry is dropped, and an arriving 'Others' entry removes a prior 'You' echo (handles either arrival order). Matching uses normalized text + Levenshtein similarity within an 8s window, skipping short (<3 word) backchannels to avoid dropping genuine speech. - MeetingStateManager routes both transcription paths through record(), which applies suppression when enabled (text is never logged). - SettingsStore gains meetingEchoSuppressionEnabled (default true, opt-out); Settings adds a 'Suppress Microphone Echo' toggle. - Tests: MeetingEchoSuppressionTests for the de-dup contract and SettingsStore default/persistence/restore coverage.
|
@sebsto I might have found a fix for #65 but it is clearly a beta feature for now. Approach: since true audio-level AEC isn't feasible for audio played by another app, this de-duplicates at the transcript layer. A mic ("You") entry is compared against the concatenation of nearby "Others" speech and suppressed when ≥60% of its words are covered (fuzzy per-word matching to absorb ASR drift, e.g. "démonté"/"démonter"). It handles both arrival orders, mic-first entries are pruned once the matching remote lines come in. It's a best-effort heuristic, so I've labeled the Settings toggle "Suppress Microphone Echo (Beta)" (default on, opt-out). Two tunable knobs: Known limitation: in the mic-first case there's a brief visible flicker ("You" line appears, then is replaced by the diarized remote line). Happy to add a short grace delay to remove it if you'd prefer. |
Summary
Branch setup for exploring speaker diarization in the meeting transcriber — separating the "Others" audio stream into individual speakers ("Speaker 1", "Speaker 2", etc.).
Branched from the meeting-transcriber work (#63) with the upstream SPM layout (#62) already merged in, so this stays in sync with
main.Background
The current meeting transcriber does simple binary routing:
This is attribution by input source, not by voice identity. Diarization would identify distinct speakers within the system audio stream.
Proposed approach
MeetingTranscript.Speakerfrom a fixed enum to dynamic speaker identities (e.g..other(id: Int, label: String?))Constraints to respect
Status
🚧 Exploratory / spec stage — opening this against
spec/meeting-speaker-diarizationto align on design before implementation.Open questions