From b7441b645a4e9488744e5a97fd9c0750aac4806d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 14:40:19 +0000 Subject: [PATCH 1/2] fix: speaker assignment not saving when selected from dropdown Two issues in the Rust transcript rendering pipeline: 1. Auto-generated channel assignments (from participant list) were appended AFTER user assignments, causing HashMap::insert to overwrite user choices with auto-generated ones. Fix: reverse ordering so user assignments are processed last and take priority. 2. Channel-scoped assignments were gated by complete_channels, which only includes RemoteParty for exactly 2-participant meetings. For 3+ participant meetings without speaker diarization, user assignments on RemoteParty were silently ignored. Fix: remove the complete_channels gate from apply_identity_rules and assign_complete_channel_human_id so channel-wide assignments are always applied. Closes #4860 Co-Authored-By: John --- crates/transcript/src/render.rs | 40 ++++++++++++++++++++-- crates/transcript/src/segments/speakers.rs | 4 --- crates/transcript/src/segments/tests.rs | 10 ++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/crates/transcript/src/render.rs b/crates/transcript/src/render.rs index b1b26efa34..6bb3392014 100644 --- a/crates/transcript/src/render.rs +++ b/crates/transcript/src/render.rs @@ -71,11 +71,11 @@ pub fn render_transcript_segments( .map(|started_at| started_at - base_started_at) .unwrap_or(0); - let (words, mut assignments) = + let (words, user_assignments) = offset_transcript_data(transcript.words, transcript.assignments, offset); - let channel_assignments = + let mut assignments = channel_assignments_for_participants(&participant_human_ids, self_human_id.as_deref()); - assignments.extend(channel_assignments); + assignments.extend(user_assignments); let segments = build_segments(&words, &[], &assignments, Some(&segment_options)); all_segments.extend(segments); @@ -464,6 +464,40 @@ mod tests { assert_eq!(segments[1].text, "remote more"); } + #[test] + fn user_assignment_overrides_auto_channel_assignment() { + let segments = render_transcript_segments(RenderTranscriptRequest { + transcripts: vec![RenderTranscriptInput { + started_at: Some(0), + words: vec![ + word("w1", " hello", 0, 100, 0), + word("w2", " remote", 120, 220, 1), + ], + assignments: vec![channel_assignment("override", ChannelProfile::RemoteParty)], + }], + participant_human_ids: vec!["self".to_string(), "auto-remote".to_string()], + self_human_id: Some("self".to_string()), + humans: vec![ + RenderTranscriptHuman { + human_id: "self".to_string(), + name: "Me".to_string(), + }, + RenderTranscriptHuman { + human_id: "auto-remote".to_string(), + name: "Auto".to_string(), + }, + RenderTranscriptHuman { + human_id: "override".to_string(), + name: "Override".to_string(), + }, + ], + }); + + assert_eq!(segments.len(), 2); + assert_eq!(segments[0].speaker_label, "Me"); + assert_eq!(segments[1].speaker_label, "Override"); + } + #[test] fn keeps_missing_started_at_rows_anchored_at_zero() { let segments = render_transcript_segments(RenderTranscriptRequest { diff --git a/crates/transcript/src/segments/speakers.rs b/crates/transcript/src/segments/speakers.rs index 7bb6d7fea9..6c1e44e304 100644 --- a/crates/transcript/src/segments/speakers.rs +++ b/crates/transcript/src/segments/speakers.rs @@ -91,9 +91,6 @@ pub(super) fn assign_complete_channel_human_id(segment: &mut ProtoSegment, state } let channel = segment.key.channel; - if !state.complete_channels.contains(&channel) { - return; - } if let Some(human_id) = state.human_id_by_channel.get(&channel) { segment.key = SegmentKey { @@ -120,7 +117,6 @@ fn apply_identity_rules( } if identity.human_id.is_none() - && state.complete_channels.contains(&word.channel) && let Some(human_id) = state.human_id_by_channel.get(&word.channel) { identity.human_id = Some(human_id.clone()); diff --git a/crates/transcript/src/segments/tests.rs b/crates/transcript/src/segments/tests.rs index aa97062e2d..21c59fdf55 100644 --- a/crates/transcript/src/segments/tests.rs +++ b/crates/transcript/src/segments/tests.rs @@ -405,6 +405,16 @@ fn propagates_remote_party_identity_when_channel_marked_complete() { assert_eq!(result[0].key.speaker_human_id.as_deref(), Some("remote")); } +#[test] +fn applies_channel_assignment_even_without_complete_channel() { + let finals = vec![fw("0", 0, 100, 1), fw("1", 200, 300, 1)]; + let assignments = vec![channel_human("remote", ChannelProfile::RemoteParty)]; + // Default options only mark DirectMic as complete, not RemoteParty + let result = build_segments(&finals, &[], &assignments, None); + assert_eq!(result.len(), 1); + assert_eq!(result[0].key.speaker_human_id.as_deref(), Some("remote")); +} + #[test] fn partial_word_ignores_its_own_runtime_hint_and_keeps_previous_segment_key() { let finals = vec![fw_si("0", 0, 100, 0, 0)]; From 372245a72eead1cf991d87254d625d4557604078 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 01:09:15 +0000 Subject: [PATCH 2/2] refactor: rename assign_complete_channel_human_id to assign_channel_human_id Co-Authored-By: John --- crates/transcript/src/segments/collect.rs | 4 ++-- crates/transcript/src/segments/speakers.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/transcript/src/segments/collect.rs b/crates/transcript/src/segments/collect.rs index deb482f75c..83b5af1b71 100644 --- a/crates/transcript/src/segments/collect.rs +++ b/crates/transcript/src/segments/collect.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use crate::types::{ChannelProfile, Segment, SegmentBuilderOptions, SegmentKey, SegmentWord}; use super::model::{ProtoSegment, ResolvedWordFrame, SpeakerIdentity, SpeakerState}; -use super::speakers::assign_complete_channel_human_id; +use super::speakers::assign_channel_human_id; pub(super) fn collect_segments( frames: Vec, @@ -44,7 +44,7 @@ pub(super) fn propagate_identity(segments: &mut Vec, speaker_state let mut last_kept_idx: Option = None; for read_index in 0..segments.len() { - assign_complete_channel_human_id(&mut segments[read_index], speaker_state); + assign_channel_human_id(&mut segments[read_index], speaker_state); let should_merge = last_kept_key.as_ref().is_some_and(|last_key| { *last_key == segments[read_index].key && segments[read_index].key.has_speaker_identity() diff --git a/crates/transcript/src/segments/speakers.rs b/crates/transcript/src/segments/speakers.rs index 6c1e44e304..e3d7493681 100644 --- a/crates/transcript/src/segments/speakers.rs +++ b/crates/transcript/src/segments/speakers.rs @@ -85,7 +85,7 @@ pub(super) fn resolve_identities( .collect() } -pub(super) fn assign_complete_channel_human_id(segment: &mut ProtoSegment, state: &SpeakerState) { +pub(super) fn assign_channel_human_id(segment: &mut ProtoSegment, state: &SpeakerState) { if segment.key.speaker_human_id.is_some() { return; }