Skip to content

Commit f5487c8

Browse files
Fix direct mic speaker labels
Keep DirectMic mapped to self when provider diarization adds speaker indexes, and cover the live and floating transcript labels.
1 parent cb040fc commit f5487c8

8 files changed

Lines changed: 196 additions & 31 deletions

File tree

apps/desktop/src/meeting-float/host.test.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,58 @@ describe("getFloatingTranscriptBubbles", () => {
253253
"segment-7",
254254
]);
255255
});
256+
257+
it("labels diarized direct-mic bubbles as self", () => {
258+
const bubbles = getFloatingTranscriptBubbles([
259+
createSegment({
260+
id: "local-mic",
261+
key: {
262+
channel: "DirectMic",
263+
speaker_index: 2,
264+
speaker_human_id: null,
265+
},
266+
start_ms: 0,
267+
text: "hello",
268+
words: [{ text: "hello" }],
269+
}),
270+
]);
271+
272+
expect(bubbles).toEqual([
273+
{
274+
id: "local-mic",
275+
speakerLabel: "You",
276+
text: "hello",
277+
isSelf: true,
278+
isFinal: true,
279+
},
280+
]);
281+
});
282+
283+
it("does not label assigned direct-mic bubbles as self", () => {
284+
const bubbles = getFloatingTranscriptBubbles([
285+
createSegment({
286+
id: "assigned-mic",
287+
key: {
288+
channel: "DirectMic",
289+
speaker_index: 1,
290+
speaker_human_id: "participant-1",
291+
},
292+
start_ms: 0,
293+
text: "hello",
294+
words: [{ text: "hello" }],
295+
}),
296+
]);
297+
298+
expect(bubbles).toEqual([
299+
{
300+
id: "assigned-mic",
301+
speakerLabel: "Speaker 2",
302+
text: "hello",
303+
isSelf: false,
304+
isFinal: true,
305+
},
306+
]);
307+
});
256308
});
257309

258310
describe("getCurrentFloatingBarColorScheme", () => {

apps/desktop/src/meeting-float/host.tsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -573,9 +573,7 @@ export function getFloatingTranscriptBubbles(
573573
id: segment.id,
574574
speakerLabel: getFloatingSpeakerLabel(segment.key),
575575
text,
576-
isSelf:
577-
segment.key.channel === "DirectMic" &&
578-
segment.key.speaker_index == null,
576+
isSelf: isFloatingSelfSpeaker(segment.key),
579577
isFinal: segment.words.every((word) => word.is_final),
580578
};
581579
})
@@ -598,7 +596,7 @@ function getFloatingSegmentText(
598596
function getFloatingSpeakerLabel(
599597
key: ListenerState["liveSegments"][number]["key"],
600598
) {
601-
if (key.channel === "DirectMic" && key.speaker_index == null) {
599+
if (isFloatingSelfSpeaker(key)) {
602600
return "You";
603601
}
604602

@@ -613,6 +611,12 @@ function getFloatingSpeakerLabel(
613611
return "Audio";
614612
}
615613

614+
function isFloatingSelfSpeaker(
615+
key: ListenerState["liveSegments"][number]["key"],
616+
) {
617+
return key.channel === "DirectMic" && key.speaker_human_id == null;
618+
}
619+
616620
export function shouldShowFloatingLiveCaptionToggle({
617621
liveTranscriptionActive,
618622
}: {

apps/desktop/src/stt/live-segment.test.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,25 @@ const ctx: RenderLabelContext = {
1414
};
1515

1616
describe("SegmentKeyUtils", () => {
17-
it("does not treat diarized direct-mic segments as self", () => {
17+
it("treats diarized direct-mic segments as self", () => {
1818
const key: Parameters<typeof SegmentKeyUtils.isKnownSpeaker>[0] = {
1919
channel: "DirectMic",
2020
speaker_index: 2,
2121
speaker_human_id: null,
2222
};
2323

24-
expect(SegmentKeyUtils.isKnownSpeaker(key, ctx)).toBe(false);
25-
expect(SegmentKeyUtils.renderLabel(key, ctx)).toBe("Speaker 3");
24+
expect(SegmentKeyUtils.isKnownSpeaker(key, ctx)).toBe(true);
25+
expect(SegmentKeyUtils.renderLabel(key, ctx)).toBe("Me");
26+
});
27+
28+
it("does not label assigned direct-mic segments as self when the name is unavailable", () => {
29+
const key: Parameters<typeof SegmentKeyUtils.renderLabel>[0] = {
30+
channel: "DirectMic",
31+
speaker_index: 1,
32+
speaker_human_id: "remote",
33+
};
34+
35+
expect(SegmentKeyUtils.renderLabel(key, ctx)).toBe("Speaker 2");
2636
});
2737

2838
it("caps unknown speaker labels when a participant max is provided", () => {

apps/desktop/src/stt/live-segment.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ export const SegmentKeyUtils = {
109109
return true;
110110
}
111111

112-
if (ctx && key.channel === "DirectMic" && key.speaker_index == null) {
112+
if (ctx && key.channel === "DirectMic") {
113113
return Boolean(ctx.getSelfHumanId());
114114
}
115115

@@ -121,14 +121,16 @@ export const SegmentKeyUtils = {
121121
ctx?: RenderLabelContext,
122122
manager?: SpeakerLabelManager,
123123
): string => {
124-
if (ctx && key.speaker_human_id) {
125-
const human = ctx.getHumanName(key.speaker_human_id);
124+
const assignedHumanId = key.speaker_human_id;
125+
126+
if (ctx && assignedHumanId != null) {
127+
const human = ctx.getHumanName(assignedHumanId);
126128
if (human) {
127129
return human;
128130
}
129131
}
130132

131-
if (ctx && key.channel === "DirectMic" && key.speaker_index == null) {
133+
if (ctx && key.channel === "DirectMic" && assignedHumanId == null) {
132134
const selfHumanId = ctx.getSelfHumanId();
133135
if (selfHumanId) {
134136
const selfHuman = ctx.getHumanName(selfHumanId);

crates/transcript/src/label.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ impl SegmentKey {
7373
Some(SpeakerLabelContext {
7474
self_human_id: Some(_),
7575
..
76-
}) if self.channel == ChannelProfile::DirectMic && self.speaker_index.is_none()
76+
}) if self.channel == ChannelProfile::DirectMic
7777
)
7878
}
7979
}
@@ -92,7 +92,6 @@ pub fn render_speaker_label(
9292
}
9393

9494
if key.channel == ChannelProfile::DirectMic
95-
&& key.speaker_index.is_none()
9695
&& let Some(self_human_id) = ctx.self_human_id.as_ref()
9796
{
9897
if let Some(name) = ctx.human_name_by_id.get(self_human_id) {
@@ -190,7 +189,7 @@ mod tests {
190189
}
191190

192191
#[test]
193-
fn does_not_treat_direct_mic_with_provider_speaker_as_self() {
192+
fn treats_direct_mic_with_provider_speaker_as_self() {
194193
let ctx = SpeakerLabelContext {
195194
self_human_id: Some("self".to_string()),
196195
human_name_by_id: HashMap::new(),
@@ -201,7 +200,7 @@ mod tests {
201200
speaker_human_id: None,
202201
};
203202

204-
assert!(!key.is_known_speaker(Some(&ctx)));
205-
assert_eq!(render_speaker_label(&key, Some(&ctx), None), "Speaker 3");
203+
assert!(key.is_known_speaker(Some(&ctx)));
204+
assert_eq!(render_speaker_label(&key, Some(&ctx), None), "You");
206205
}
207206
}

crates/transcript/src/render.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,28 @@ mod tests {
348348
assert_eq!(segments[2].speaker_label, "Speaker 2");
349349
}
350350

351+
#[test]
352+
fn labels_diarized_direct_mic_as_self_without_remote_participant() {
353+
let segments = render_transcript_segments(RenderTranscriptRequest {
354+
transcripts: vec![RenderTranscriptInput {
355+
started_at: Some(0),
356+
words: vec![word_si("w1", " hello", 0, 100, 0, 2)],
357+
assignments: vec![],
358+
}],
359+
participant_human_ids: vec![],
360+
self_human_id: Some("self".to_string()),
361+
humans: vec![RenderTranscriptHuman {
362+
human_id: "self".to_string(),
363+
name: "Me".to_string(),
364+
}],
365+
});
366+
367+
assert_eq!(segments.len(), 1);
368+
assert_eq!(segments[0].speaker_label, "Me");
369+
assert_eq!(segments[0].key.speaker_index, Some(2));
370+
assert_eq!(segments[0].key.speaker_human_id.as_deref(), Some("self"));
371+
}
372+
351373
#[test]
352374
fn normalizes_word_spacing_for_rendered_segments() {
353375
let words = normalize_rendered_segment_words(vec![

crates/transcript/src/segments/tests.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,21 @@ fn propagates_direct_mic_channel_identity_forward() {
433433
assert_eq!(result[2].key.speaker_human_id.as_deref(), Some("carol"));
434434
}
435435

436+
#[test]
437+
fn applies_direct_mic_channel_identity_to_provider_speakers() {
438+
let finals = vec![fw_si("0", 0, 100, 0, 2)];
439+
let assignments = vec![channel_human("self", ChannelProfile::DirectMic)];
440+
let opts = SegmentBuilderOptions {
441+
complete_channels: Some(vec![ChannelProfile::DirectMic]),
442+
..Default::default()
443+
};
444+
445+
let result = build_segments(&finals, &[], &assignments, Some(&opts));
446+
447+
assert_eq!(result.len(), 1);
448+
assert_eq!(result[0].key, key_speaker_human(0, 2, "self"));
449+
}
450+
436451
#[test]
437452
fn propagates_remote_party_identity_when_channel_marked_complete() {
438453
let finals = vec![fw("0", 0, 100, 1), fw("1", 200, 300, 1)];

crates/transcript/src/types/speaker.rs

Lines changed: 76 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,26 +32,23 @@ pub fn channel_assignments_for_participants(
3232
_ => return vec![],
3333
};
3434

35-
let remote_id = unique_other_participant(participant_human_ids, self_id);
36-
let remote_id = match remote_id {
37-
Some(id) => id,
38-
None => return vec![],
39-
};
40-
41-
vec![
42-
IdentityAssignment {
43-
human_id: self_id.to_string(),
44-
scope: IdentityScope::Channel {
45-
channel: ChannelProfile::DirectMic,
46-
},
35+
let mut assignments = vec![IdentityAssignment {
36+
human_id: self_id.to_string(),
37+
scope: IdentityScope::Channel {
38+
channel: ChannelProfile::DirectMic,
4739
},
48-
IdentityAssignment {
40+
}];
41+
42+
if let Some(remote_id) = unique_other_participant(participant_human_ids, self_id) {
43+
assignments.push(IdentityAssignment {
4944
human_id: remote_id.to_string(),
5045
scope: IdentityScope::Channel {
5146
channel: ChannelProfile::RemoteParty,
5247
},
53-
},
54-
]
48+
});
49+
}
50+
51+
assignments
5552
}
5653

5754
pub fn segment_options_for_participants(
@@ -97,3 +94,67 @@ fn unique_other_participant<'a>(
9794
None
9895
}
9996
}
97+
98+
#[cfg(test)]
99+
mod tests {
100+
use super::*;
101+
102+
#[test]
103+
fn assigns_self_to_direct_mic_without_unique_remote() {
104+
let assignments = channel_assignments_for_participants(&[], Some("self"));
105+
106+
assert_eq!(
107+
assignments,
108+
vec![IdentityAssignment {
109+
human_id: "self".to_string(),
110+
scope: IdentityScope::Channel {
111+
channel: ChannelProfile::DirectMic,
112+
},
113+
}]
114+
);
115+
}
116+
117+
#[test]
118+
fn assigns_unique_remote_to_remote_party() {
119+
let assignments = channel_assignments_for_participants(
120+
&["self".to_string(), "remote".to_string()],
121+
Some("self"),
122+
);
123+
124+
assert_eq!(
125+
assignments,
126+
vec![
127+
IdentityAssignment {
128+
human_id: "self".to_string(),
129+
scope: IdentityScope::Channel {
130+
channel: ChannelProfile::DirectMic,
131+
},
132+
},
133+
IdentityAssignment {
134+
human_id: "remote".to_string(),
135+
scope: IdentityScope::Channel {
136+
channel: ChannelProfile::RemoteParty,
137+
},
138+
},
139+
]
140+
);
141+
}
142+
143+
#[test]
144+
fn skips_remote_assignment_when_remote_is_ambiguous() {
145+
let assignments = channel_assignments_for_participants(
146+
&["remote-a".to_string(), "remote-b".to_string()],
147+
Some("self"),
148+
);
149+
150+
assert_eq!(
151+
assignments,
152+
vec![IdentityAssignment {
153+
human_id: "self".to_string(),
154+
scope: IdentityScope::Channel {
155+
channel: ChannelProfile::DirectMic,
156+
},
157+
}]
158+
);
159+
}
160+
}

0 commit comments

Comments
 (0)