Skip to content

Speaker labeling overhaul: user-paced naming, split voices, speaking-time metric#33

Merged
execsumo merged 2 commits into
mainfrom
claude/speaker-labeling-review-tgiikq
Jul 7, 2026
Merged

Speaker labeling overhaul: user-paced naming, split voices, speaking-time metric#33
execsumo merged 2 commits into
mainfrom
claude/speaker-labeling-review-tgiikq

Conversation

@execsumo

@execsumo execsumo commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

  • Overhaul speaker labeling: user-paced naming, split voices, clean speaker list
  • Add speaking-time metric and remaining speaker-labeling improvements

Test plan

  • swift build
  • swift run HeardTests

Generated by Claude Code

claude added 2 commits July 6, 2026 19:20
…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
@execsumo
execsumo merged commit b531663 into main Jul 7, 2026
4 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.

2 participants