1use super::{AnonymousNumbering, Embedder, Label, Profile, Role, SpeakerError, SpeakerName};
6
7pub const ENROLLED_THRESHOLD: f32 = 0.5;
9pub const OWN_VOICE_THRESHOLD: f32 = 0.35;
13pub const CLUSTER_THRESHOLD: f32 = 0.21;
15const STICKY_MARGIN: f32 = 0.10;
18
19pub struct Diarizer<E> {
21 embedder: E,
22 enrolled: Vec<Profile>,
23 clusters: Vec<Cluster>,
24 numbering: AnonymousNumbering,
25}
26
27struct Cluster {
28 label: Label,
29 sum: Vec<f32>,
30}
31
32impl<E: Embedder> Diarizer<E> {
33 pub fn new(embedder: E, enrolled: Vec<Profile>) -> Self {
35 Self {
36 embedder,
37 enrolled,
38 clusters: Vec::new(),
39 numbering: AnonymousNumbering::default(),
40 }
41 }
42
43 pub fn embed(&mut self, window: &[f32]) -> Result<Option<Vec<f32>>, SpeakerError> {
46 self.embedder.embed(window)
47 }
48
49 pub fn label(&mut self, embedding: &[f32], speaking: Option<&Label>) -> Label {
53 assign(
54 &self.enrolled,
55 &mut self.clusters,
56 &mut self.numbering,
57 embedding,
58 speaking,
59 )
60 }
61
62 pub fn set_enrolled(&mut self, enrolled: Vec<Profile>) {
65 self.enrolled = enrolled;
66 }
67
68 pub fn owners(&self) -> usize {
71 self.enrolled
72 .iter()
73 .filter(|profile| profile.role == Role::Owner)
74 .count()
75 }
76
77 #[cfg(all(feature = "silero", not(target_family = "wasm")))]
78 pub(super) fn matched_profile(&self, embedding: &[f32]) -> Option<&Profile> {
79 enrolled_match(&self.enrolled, embedding).map(|at| &self.enrolled[at])
80 }
81
82 #[cfg(not(target_family = "wasm"))]
86 #[cfg(all(feature = "silero", not(target_family = "wasm")))]
87 pub(super) fn owner_similarity(&self, embedding: &[f32]) -> Option<f32> {
88 closest(
89 self.enrolled
90 .iter()
91 .filter(|profile| profile.role == Role::Owner)
92 .map(|profile| ((), dot(&profile.vector, embedding))),
93 )
94 .map(|(_, similarity)| similarity)
95 }
96
97 pub fn relabel(&self, paragraphs: &[Option<Vec<f32>>]) -> Vec<Option<Label>> {
101 recluster(&self.enrolled, paragraphs)
102 }
103}
104
105pub fn settle<T: Clone>(labels: &mut [Option<T>], opens_utterance: impl Fn(usize) -> bool) {
108 for index in (0..labels.len()).rev() {
109 if labels[index].is_none() && opens_utterance(index) {
110 let next = labels.get(index + 1).cloned().flatten();
111 labels[index] = next;
112 }
113 }
114 for index in 1..labels.len() {
115 if labels[index].is_none() {
116 labels[index] = labels[index - 1].clone();
117 }
118 }
119 let opening = labels.iter().position(Option::is_some);
120 if let Some(first) = opening {
121 let voice = labels[first].clone();
122 labels[..first].fill(voice);
123 }
124}
125
126fn dot(a: &[f32], b: &[f32]) -> f32 {
127 a.iter().zip(b).map(|(x, y)| x * y).sum()
128}
129
130fn assign(
137 enrolled: &[Profile],
138 clusters: &mut Vec<Cluster>,
139 numbering: &mut AnonymousNumbering,
140 embedding: &[f32],
141 speaking: Option<&Label>,
142) -> Label {
143 if let Some(at) = enrolled_match(enrolled, embedding) {
144 return Label::Enrolled(enrolled[at].name.clone());
145 }
146 let similarities: Vec<f32> = clusters
147 .iter()
148 .map(|cluster| dot(&cluster.sum, embedding) / dot(&cluster.sum, &cluster.sum).sqrt())
149 .collect();
150 let best = closest(similarities.iter().copied().enumerate());
151
152 let held = speaking
153 .and_then(|label| clusters.iter().position(|cluster| &cluster.label == label))
154 .filter(|&at| similarities[at] >= CLUSTER_THRESHOLD)
155 .filter(|&at| best.is_none_or(|(_, top)| similarities[at] + STICKY_MARGIN >= top));
156 let taking = held.or_else(|| {
157 best.filter(|(_, similarity)| *similarity >= CLUSTER_THRESHOLD)
158 .map(|(at, _)| at)
159 });
160
161 if let Some(at) = taking {
162 return absorb(&mut clusters[at], embedding).clone();
163 }
164 let label = numbering.issue();
165 clusters.push(Cluster {
166 label: label.clone(),
167 sum: embedding.to_vec(),
168 });
169 label
170}
171
172fn absorb<'a>(cluster: &'a mut Cluster, embedding: &[f32]) -> &'a Label {
173 for (sum, value) in cluster.sum.iter_mut().zip(embedding) {
174 *sum += value;
175 }
176 &cluster.label
177}
178
179fn enrolled_match(enrolled: &[Profile], embedding: &[f32]) -> Option<usize> {
181 let (at, similarity) = closest(
182 enrolled
183 .iter()
184 .map(|profile| dot(&profile.vector, embedding))
185 .enumerate(),
186 )?;
187 let bar = match enrolled[at].role {
188 Role::Owner => OWN_VOICE_THRESHOLD,
189 Role::Guest => ENROLLED_THRESHOLD,
190 };
191 (similarity >= bar).then_some(at)
192}
193
194pub(super) fn name_at_enrolled_threshold<'a>(
198 enrolled: &'a [Profile],
199 embedding: &[f32],
200) -> Option<&'a SpeakerName> {
201 closest(
202 enrolled
203 .iter()
204 .map(|profile| (&profile.name, dot(&profile.vector, embedding))),
205 )
206 .filter(|(_, similarity)| *similarity >= ENROLLED_THRESHOLD)
207 .map(|(name, _)| name)
208}
209
210fn closest<T>(candidates: impl Iterator<Item = (T, f32)>) -> Option<(T, f32)> {
211 candidates.reduce(|best, candidate| {
212 if candidate.1 > best.1 {
213 candidate
214 } else {
215 best
216 }
217 })
218}
219
220fn recluster(enrolled: &[Profile], paragraphs: &[Option<Vec<f32>>]) -> Vec<Option<Label>> {
228 struct Group {
229 sum: Vec<f32>,
230 count: usize,
231 members: Vec<usize>,
232 profile: Option<usize>,
234 }
235 let mut groups: Vec<Group> = paragraphs
236 .iter()
237 .enumerate()
238 .filter_map(|(index, embedding)| {
239 embedding.as_ref().map(|embedding| Group {
240 sum: embedding.clone(),
241 count: 1,
242 members: vec![index],
243 profile: enrolled_match(enrolled, embedding),
244 })
245 })
246 .collect();
247
248 loop {
249 let mut best: Option<(usize, usize, f32)> = None;
250 for a in 0..groups.len() {
251 for b in a + 1..groups.len() {
252 if let (Some(first), Some(second)) = (groups[a].profile, groups[b].profile)
253 && first != second
254 {
255 continue;
256 }
257 let similarity = dot(&groups[a].sum, &groups[b].sum)
258 / (groups[a].count * groups[b].count) as f32;
259 if best.is_none_or(|(_, _, top)| similarity > top) {
260 best = Some((a, b, similarity));
261 }
262 }
263 }
264 let Some((a, b, similarity)) = best else {
265 break;
266 };
267 if similarity < CLUSTER_THRESHOLD {
268 break;
269 }
270 let absorbed = groups.swap_remove(b);
271 let keeper = &mut groups[a];
272 for (sum, value) in keeper.sum.iter_mut().zip(&absorbed.sum) {
273 *sum += value;
274 }
275 keeper.count += absorbed.count;
276 keeper.members.extend(absorbed.members);
277 keeper.profile = keeper.profile.or(absorbed.profile);
278 }
279
280 groups.sort_by_key(|group| group.members.iter().min().copied());
281 let mut labels = vec![None; paragraphs.len()];
282 let mut numbering = AnonymousNumbering::default();
283 for group in &groups {
284 let mean: Vec<f32> = group
285 .sum
286 .iter()
287 .map(|value| value / group.count as f32)
288 .collect();
289 let label = group
290 .profile
291 .or_else(|| enrolled_match(enrolled, &mean))
292 .map(|at| Label::Enrolled(enrolled[at].name.clone()))
293 .unwrap_or_else(|| numbering.issue());
294 for &member in &group.members {
295 labels[member] = Some(label.clone());
296 }
297 }
298 labels
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use crate::speaker::normalized;
305
306 const A: [f32; 4] = [1.0, 0.0, 0.0, 0.0];
307 const B: [f32; 4] = [0.0, 1.0, 0.0, 0.0];
308
309 fn near_a() -> Vec<f32> {
310 normalized(vec![0.9, 0.1, 0.0, 0.0]).expect("nonzero")
311 }
312
313 #[derive(Default)]
315 struct Live {
316 clusters: Vec<Cluster>,
317 numbering: AnonymousNumbering,
318 }
319
320 impl Live {
321 fn assign(&mut self, embedding: &[f32], speaking: Option<&Label>) -> Label {
322 assign(
323 &[],
324 &mut self.clusters,
325 &mut self.numbering,
326 embedding,
327 speaking,
328 )
329 }
330 }
331
332 fn enrolled(name: &str) -> Label {
333 Label::Enrolled(SpeakerName::new(name).expect("a usable name"))
334 }
335
336 #[test]
337 fn assign_joins_close_voices_and_splits_far_ones() {
338 let mut live = Live::default();
339 assert_eq!(live.assign(&A, None), Label::Anonymous(1));
340 assert_eq!(live.assign(&near_a(), None), Label::Anonymous(1));
341 assert_eq!(live.assign(&B, None), Label::Anonymous(2));
342 }
343
344 #[test]
345 fn a_voice_already_talking_keeps_the_label_through_a_close_call() {
346 let mut live = Live::default();
350 assert_eq!(live.assign(&A, None), Label::Anonymous(1));
351 assert_eq!(live.assign(&B, None), Label::Anonymous(2));
352
353 let between = normalized(vec![0.55, 0.45, 0.0, 0.0]).expect("nonzero");
354 assert_eq!(
355 live.assign(&between, Some(&Label::Anonymous(1))),
356 Label::Anonymous(1),
357 "the voice talking holds it"
358 );
359 }
360
361 #[test]
362 fn a_voice_that_is_clearly_someone_else_still_takes_the_label() {
363 let mut live = Live::default();
366 assert_eq!(live.assign(&A, None), Label::Anonymous(1));
367 assert_eq!(live.assign(&B, None), Label::Anonymous(2));
368 assert_eq!(
369 live.assign(&B, Some(&Label::Anonymous(1))),
370 Label::Anonymous(2)
371 );
372 }
373
374 #[test]
375 fn assign_prefers_an_enrolled_name_and_leaves_its_profile_alone() {
376 let profiles = vec![super::super::enrolled_voice(
377 "alex",
378 Role::Owner,
379 A.to_vec(),
380 )];
381 let mut clusters = Vec::new();
382 let mut numbering = AnonymousNumbering::default();
383 assert_eq!(
384 assign(&profiles, &mut clusters, &mut numbering, &near_a(), None),
385 enrolled("alex")
386 );
387 assert!(clusters.is_empty());
388 assert_eq!(profiles[0].vector, A.to_vec());
389 }
390
391 #[test]
392 fn recluster_merges_an_online_split_and_leaves_unembedded_slots_open() {
393 let paragraphs = vec![
394 Some(A.to_vec()),
395 None,
396 Some(near_a()),
397 Some(B.to_vec()),
398 None,
399 ];
400 let labels = recluster(&[], ¶graphs);
401 let expected = [
402 Some(Label::Anonymous(1)),
403 None,
404 Some(Label::Anonymous(1)),
405 Some(Label::Anonymous(2)),
406 None,
407 ];
408 assert_eq!(labels, expected);
409 }
410
411 #[test]
412 fn settle_names_an_opening_nobody_could_embed() {
413 let named = |name: &str| Some(name.to_owned());
414 let mut labels = vec![None, None, named("Speaker 1")];
417 settle(&mut labels, |_| false);
418 let actual: Vec<Option<&str>> = labels.iter().map(Option::as_deref).collect();
419 assert_eq!(
420 actual,
421 [Some("Speaker 1"), Some("Speaker 1"), Some("Speaker 1")]
422 );
423 }
424
425 #[test]
426 fn settle_leaves_a_session_nobody_named_alone() {
427 let mut labels: Vec<Option<Label>> = vec![None, None];
428 settle(&mut labels, |_| false);
429 assert_eq!(labels, [None, None]);
430 }
431
432 #[test]
433 fn settle_sends_an_opener_forward_and_a_tail_back() {
434 let named = |name: &str| Some(name.to_owned());
435 let mut labels = vec![
436 named("Speaker 1"),
437 None, None, named("Speaker 2"),
440 ];
441 settle(&mut labels, |index| index == 2);
442 let actual: Vec<Option<&str>> = labels.iter().map(Option::as_deref).collect();
443 assert_eq!(
444 actual,
445 [
446 Some("Speaker 1"),
447 Some("Speaker 1"),
448 Some("Speaker 2"),
449 Some("Speaker 2"),
450 ]
451 );
452 }
453
454 #[test]
455 fn recluster_gives_a_merged_group_its_enrolled_name() {
456 let profiles = vec![super::super::enrolled_voice(
457 "alex",
458 Role::Owner,
459 A.to_vec(),
460 )];
461 let paragraphs = vec![Some(near_a()), Some(B.to_vec())];
462 let labels = recluster(&profiles, ¶graphs);
463 assert_eq!(labels, [Some(enrolled("alex")), Some(Label::Anonymous(1))]);
464 }
465
466 #[test]
467 fn recluster_keeps_two_enrolled_people_apart_when_their_voices_are_close() {
468 let close_to_a = normalized(vec![0.32, 0.95, 0.0, 0.0]).expect("nonzero");
469 let profiles = vec![
470 super::super::enrolled_voice("alex", Role::Owner, A.to_vec()),
471 super::super::enrolled_voice("blake", Role::Guest, close_to_a.clone()),
472 ];
473 let paragraphs = vec![Some(A.to_vec()), Some(close_to_a)];
474
475 let labels = recluster(&profiles, ¶graphs);
476
477 assert_eq!(labels, [Some(enrolled("alex")), Some(enrolled("blake"))]);
478 }
479
480 #[test]
481 fn an_owner_heard_below_a_guests_bar_keeps_the_name_through_relabel() {
482 let profiles = vec![
483 super::super::enrolled_voice("alex", Role::Owner, A.to_vec()),
484 super::super::enrolled_voice("blake", Role::Guest, B.to_vec()),
485 ];
486 let owner_like = normalized(vec![0.4, 0.0, 0.9165, 0.0]).expect("nonzero");
489 let guest_like = normalized(vec![0.0, 0.4, 0.0, 0.9165]).expect("nonzero");
490 let mut clusters = Vec::new();
491 let mut numbering = AnonymousNumbering::default();
492
493 let live = assign(&profiles, &mut clusters, &mut numbering, &owner_like, None);
494 let relabeled = recluster(&profiles, &[Some(owner_like), Some(guest_like)]);
495
496 assert_eq!(live, enrolled("alex"));
497 assert_eq!(
498 relabeled,
499 [Some(enrolled("alex")), Some(Label::Anonymous(1))]
500 );
501 }
502}