Core — Speaker Assignment
POST /meetings/:id/speaker-labels, person lookup/create, speaker_label to person_id mapping persisted to transcript_segments and surviving PUT /transcriptions/:id/segments replacement, and SpeakerStateUpdatedEvent to ML.
Core — Speaker Assignment
Owner: Core Engineer Domain: core Complexity: M Prerequisite: Milestone 04 merged
When this slice is complete, a user can map a transcript's raw diariser speaker_label to an existing person or a newly created one via POST /meetings/{id}/speaker-labels. The mapping is persisted on the meeting and re-applied to every segment carrying that label — including segments that arrive later in the live session and segments written by a wholesale batch replacement (PUT /transcriptions/{id}/segments) during finalization. Clearing an assignment (DELETE .../speaker-labels/{label}) removes the mapping from every affected segment. Unassigned labels remain visible with no person_id.
Required Capabilities
-
POST /meetings/{id}/speaker-labels { speaker_label, person_id }or{ speaker_label, person: { display_name } }assigns a label to an existing or newly created person, returning{meeting_id, speaker_label, person_id, display_name, state: "manual", updated_segment_count}. - The assignment requires either
person_idorperson— a barespeaker_labelwith neither is rejected422, as is aperson_idthat does not exist. -
GET /meetings/{id}/speaker-labelslists every assigned{speaker_label, person_id, display_name}mapping for the meeting. -
DELETE /meetings/{id}/speaker-labels/{label}removes the mapping and clearsperson_idfrom every segment carrying that label. - Every existing segment carrying the assigned
speaker_labelis updated with the newperson_idin the same request that creates the assignment. - A segment appended later in the live session, still carrying the assigned label, is written with
person_idalready applied. -
PUT /transcriptions/{id}/segments(the batch/final rebuild replacement) re-applies the meeting's stored label→person mapping to every incoming segment that carries a mapped label. - Assigning or clearing a label broadcasts
EntityChangedEvent { entity: "transcription", action: "updated" }to the meeting's live session. - During a live session, Core pushes the label→person mapping to ML's streaming session (
POST {ml}/streaming/{ml_session_id}/speaker-states). - Segments whose
speaker_labelhas no mapping keepperson_idabsent/null and remain listed.
Dependencies
- Milestone 04 (
live-transcript-to-final-rebuild) must be merged — specifically the live transcript segment stream and thetranscript_segmentsschema. - The
peopleresource (POST /people,GET /people/{id}) must exist for the "create a new person" path.
Domain Notes
The mapping lives on the meeting (speaker_label → person_id), not on individual segments as a one-time stamp — this is what lets it survive a wholesale segment replacement. PUT /transcriptions/{id}/segments re-applies the mapping to whatever it writes rather than the mapping being copied forward segment-by-segment.
Test Cases
Test cases map to tests/bets/meeting-recording/test_milestone_09_speaker_labelling.py. Run via ./dev test bet meeting-recording.
| Test | Location | Assertion |
|---|---|---|
test_transcript_displays_distinct_speaker_labels | test_milestone_09 | Live segment events and GET /transcriptions/{id}/segments both carry distinct diariser speaker_label values ({"A", "B"}) with no person_id yet assigned. |
test_user_can_assign_existing_person_to_speaker | test_milestone_09 | Assigning an existing person to label A updates updated_segment_count for both existing A segments, broadcasts entity.changed(transcription, updated), is reflected in GET /speaker-labels and on every A segment, and a segment appended afterward still carrying label A picks up the mapping automatically. |
test_user_can_create_person_from_speaker_flow | test_milestone_09 | Assigning label B with a new person: {display_name} creates the person (visible via GET /people/{id}), maps it onto every B segment, and a bare speaker_label or a nonexistent person_id is rejected 422. |
test_speaker_assignments_persist_after_refresh | test_milestone_09 | A fresh client sees the same speaker_label → person_id mapping and segment person_id values; DELETE /speaker-labels/A removes the mapping and clears person_id from every affected segment. |
test_assignments_survive_final_transcript_replacement | test_milestone_09 | After stop/finalization grows the segment set beyond the provisional rows, every final segment carrying the assigned label still resolves to the mapped person, unassigned labels stay unassigned, and an explicit PUT /transcriptions/{id}/segments re-run (the same write-back path ML uses) re-applies the mapping to the new segments too. |
test_unassigned_speaker_labels_remain_visible | test_milestone_09 | After finalization, segments whose label was never assigned are still listed with their raw speaker_label and no person_id; GET /speaker-labels only lists the label that was actually assigned. |
Completion Checklist
- Code merged and deployed
- Bet progress tests pass (
./dev test bet meeting-recording) - Permanent service tests implemented per testing strategy
- Code review completed
- API review completed (
POST /meetings/{id}/speaker-labelscontract) - Testing review completed
- System documentation updated (as applicable)
App — Speaker Labelling UI
Speaker label display on transcript segments, inline assignment dropdown (existing person or create new), optimistic assignment, and visually distinct unassigned labels.
ML — Speaker Diarization
speaker_label populated on TranscriptSegmentProducedEvent from AssemblyAI diarization, and SegmentFeaturesProducedEvent for voice embeddings.