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