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