WordloopWordloop
WorkMeeting RecordingTechnical Design DocMilestones09 Speaker Labelling

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_id or person — a bare speaker_label with neither is rejected 422, as is a person_id that does not exist.
  • GET /meetings/{id}/speaker-labels lists every assigned {speaker_label, person_id, display_name} mapping for the meeting.
  • DELETE /meetings/{id}/speaker-labels/{label} removes the mapping and clears person_id from every segment carrying that label.
  • Every existing segment carrying the assigned speaker_label is updated with the new person_id in the same request that creates the assignment.
  • A segment appended later in the live session, still carrying the assigned label, is written with person_id already 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_label has no mapping keep person_id absent/null and remain listed.

Dependencies

  • Milestone 04 (live-transcript-to-final-rebuild) must be merged — specifically the live transcript segment stream and the transcript_segments schema.
  • The people resource (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.

TestLocationAssertion
test_transcript_displays_distinct_speaker_labelstest_milestone_09Live 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_speakertest_milestone_09Assigning 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_flowtest_milestone_09Assigning 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_refreshtest_milestone_09A 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_replacementtest_milestone_09After 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_visibletest_milestone_09After 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-labels contract)
  • Testing review completed
  • System documentation updated (as applicable)

On this page