Speaker labeling overhaul: user-paced naming, split voices, speaking-time metric#33
Merged
Merged
Conversation
…aker list Naming flow (was: auto-open + 120s auto-save countdown): - Remove the countdown and auto-open entirely — naming is now user-initiated via the menu bar row, orange badge, or a new Speakers-tab banner button. - Closing the Name Speakers window keeps candidates pending instead of force-skipping them; Skip All is the only path that persists placeholders. - onNamingRequired appends candidate batches (a second meeting no longer clobbers a pending batch) and onPipelineIdle preserves the .userAction badge while candidates are pending. Unplayable speakers (dead play buttons): - Candidates with no extractable clip never reach the naming prompt and no profile is created for them (placeholders are excluded from matching, so a clipless placeholder could never be identified by voice or by ear). - skipNaming no longer persists placeholders whose clips failed to persist. - SpeakerStore.pruneUnidentifiablePlaceholders() removes legacy clipless placeholder profiles at launch. - Roster-auto-named profiles now get voice clips extracted directly into speaker_clips/ instead of shipping without audio. Speakers table: - "Time in Meetings" column was missing its sort key path — now sortable. Split voices (merged diarization clusters): - Pipeline computes a per-clip embedding for every extracted sample from the diarizer's chunk embeddings (VAD-mapped overlap, aggregated via SpeakerEmbeddingAggregator), carried on NamingCandidate.clipEmbeddings. - The naming window offers "Split voices" on multi-sample candidates: AppModel.splitCandidate replaces the candidate with one sub-candidate per clip, each with its clip-local embedding (never the polluted centroid). Matching/database hygiene: - Naming a candidate with an existing profile's name merges embedding, clips and stats into that profile instead of creating a duplicate (SpeakerMatcher.addEmbedding, extracted from updateDatabase). - Removed the N:N roster ordered auto-assignment — pairing sorted names to cluster order mislabeled speakers; multiple unknowns become suggestions. - Removed dead code: namingDismissTask, showNamingPrompt, NamingCandidateRow. Tests: SpeakerStore prune, SpeakerMatcher.addEmbedding cap/replace/ignore. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUDNuK27wfK8FgajWw5wt1
…rovements Speaking Time (new metric; Time in Meetings retained): - SpeakerProfile.totalSpeakingTime accumulates each speaker's summed transcript-segment durations, computed from pre-merge segments so merged blocks don't overcount silence between sentences. Flows through UnmatchedSpeaker -> NamingCandidate -> profile create/merge paths, and SpeakerStore.updateStats. New sortable "Speaking Time" table column; both duration columns share a durationText formatter (2h 05m / 14m / 38s). - Old speakers.json files decode with speaking time defaulting to 0. Pending naming candidates survive restart: - NamingCandidate is Codable; NamingCandidateStore persists the pending list (naming_candidates.json) on every change via didSet and restores it at bootstrap, pruning clips whose files are gone and dropping candidates left clipless. Pending clips are excluded from the 48h stale-recordings sweep. startWatching/stopWatching no longer clobber the restored .userAction badge. Voice match strictness is user-tunable: - AppSettings.speakerMatchThreshold (default 0.30, 0.15-0.45) feeds SpeakerMatcher.matchSpeakers(matchThreshold:); new Advanced -> Voice Matching slider card (Stricter <-> Looser, reset to default). Honest roster suggestions: - Candidates carry the full unmatched-roster list (suggestedNames) rendered as tappable chips that fill the name field, replacing the arbitrary index pairing; pre-fill only when exactly one name is unmatched. Split parts inherit the parent's suggestions. N-way merge: - Merge Selected accepts 2+ profiles; survivor picked by SpeakerMatcher.mergePrimary (named > placeholder, then meetings, then oldest). One consent dialog covers all named profiles being folded in. - SpeakerStore.merge caps merged embeddings (5) and clips (5, overflow files deleted) and sums speaking time. Cleanup: the 5-field unmatched-speaker tuple threaded through TranscriptDocument/AudioClipExtractor is now the UnmatchedSpeaker struct. Tests: updateStats speaking time, merge caps, NamingCandidateStore round-trip + clip pruning, custom match threshold, mergePrimary ordering. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NUDNuK27wfK8FgajWw5wt1
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.
Summary
Test plan
swift buildswift run HeardTestsGenerated by Claude Code