1use kcode_speaker_types::{Key, LabeledSample, ObjectId};
2use std::collections::{BTreeMap, BTreeSet};
3use std::error::Error;
4use std::fmt;
5
6#[derive(Clone, Debug)]
8pub struct Dataset {
9 active: Vec<LabeledSample>,
10 repeatability: Vec<LabeledSample>,
11}
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub struct FoldConfig {
16 pub known_folds: u8,
17}
18
19#[derive(Clone, Debug, PartialEq, Eq)]
21pub struct OpenSetFold {
22 pub pseudo_unknown_speaker: Key,
23 pub train_indices: Vec<usize>,
24 pub known_test_indices: Vec<usize>,
25 pub unknown_test_indices: Vec<usize>,
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
30pub struct RepeatabilityGroup {
31 pub speaker_id: Key,
32 pub clip_object: ObjectId,
33 pub sample_indices: Vec<usize>,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum DatasetError {
39 EmptyActive,
40 MixedActiveCohorts,
41 DuplicateActiveSampleId,
42 DuplicateRepeatabilitySampleId,
43 ConflictingSharedSampleId,
44 DuplicateActiveObservation,
45 InconsistentClipGroup,
46 InvalidRecordingQuality,
47 NoUsableSpeech,
48 InvalidKnownFolds,
49 ImpossibleFold,
50}
51
52impl fmt::Display for DatasetError {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 let message = match self {
55 Self::EmptyActive => "active data is empty",
56 Self::MixedActiveCohorts => "active rows contain multiple cohorts",
57 Self::DuplicateActiveSampleId => "active rows contain a duplicate sample ID",
58 Self::DuplicateRepeatabilitySampleId => {
59 "repeatability rows contain a duplicate sample ID"
60 }
61 Self::ConflictingSharedSampleId => {
62 "a sample ID shared by both views has different rows"
63 }
64 Self::DuplicateActiveObservation => {
65 "active rows contain a duplicate clip and speaker observation"
66 }
67 Self::InconsistentClipGroup => "one active clip appears in multiple leakage groups",
68 Self::InvalidRecordingQuality => "recording quality exceeds 100",
69 Self::NoUsableSpeech => "a row has no usable speech",
70 Self::InvalidKnownFolds => "known_folds must be at least two",
71 Self::ImpossibleFold => "the requested leakage-safe folds cannot be formed",
72 };
73 f.write_str(message)
74 }
75}
76
77impl Error for DatasetError {}
78
79pub fn build(
81 mut active_rows: Vec<LabeledSample>,
82 mut repeatability_rows: Vec<LabeledSample>,
83) -> Result<Dataset, DatasetError> {
84 if active_rows.is_empty() {
85 return Err(DatasetError::EmptyActive);
86 }
87
88 active_rows.sort_by(|a, b| a.sample_id.as_ref().cmp(b.sample_id.as_ref()));
89 repeatability_rows.sort_by(|a, b| a.sample_id.as_ref().cmp(b.sample_id.as_ref()));
90
91 validate_rows(&active_rows)?;
92 validate_rows(&repeatability_rows)?;
93
94 if active_rows
95 .windows(2)
96 .any(|rows| rows[0].sample_id == rows[1].sample_id)
97 {
98 return Err(DatasetError::DuplicateActiveSampleId);
99 }
100 if repeatability_rows
101 .windows(2)
102 .any(|rows| rows[0].sample_id == rows[1].sample_id)
103 {
104 return Err(DatasetError::DuplicateRepeatabilitySampleId);
105 }
106
107 let cohort = active_rows[0].cohort_id.as_ref();
108 if active_rows
109 .iter()
110 .any(|row| row.cohort_id.as_ref() != cohort)
111 {
112 return Err(DatasetError::MixedActiveCohorts);
113 }
114
115 let mut observations = BTreeSet::new();
116 let mut clip_groups: BTreeMap<&str, &str> = BTreeMap::new();
117 for row in &active_rows {
118 if !observations.insert((row.clip_object.as_ref(), row.speaker_id.as_ref())) {
119 return Err(DatasetError::DuplicateActiveObservation);
120 }
121
122 match clip_groups.get(row.clip_object.as_ref()) {
123 Some(group) if *group != row.group_id.as_ref() => {
124 return Err(DatasetError::InconsistentClipGroup);
125 }
126 Some(_) => {}
127 None => {
128 clip_groups.insert(row.clip_object.as_ref(), row.group_id.as_ref());
129 }
130 }
131 }
132
133 let active_by_id: BTreeMap<&str, &LabeledSample> = active_rows
134 .iter()
135 .map(|row| (row.sample_id.as_ref(), row))
136 .collect();
137 for row in &repeatability_rows {
138 if let Some(active) = active_by_id.get(row.sample_id.as_ref())
139 && *active != row
140 {
141 return Err(DatasetError::ConflictingSharedSampleId);
142 }
143 }
144
145 Ok(Dataset {
146 active: active_rows,
147 repeatability: repeatability_rows,
148 })
149}
150
151fn validate_rows(rows: &[LabeledSample]) -> Result<(), DatasetError> {
152 for row in rows {
153 if row.recording_quality > 100 {
154 return Err(DatasetError::InvalidRecordingQuality);
155 }
156 if row.usable_speech_ms == 0 {
157 return Err(DatasetError::NoUsableSpeech);
158 }
159 }
160 Ok(())
161}
162
163pub fn active_rows(dataset: &Dataset) -> &[LabeledSample] {
165 &dataset.active
166}
167
168pub fn repeatability_rows(dataset: &Dataset) -> &[LabeledSample] {
170 &dataset.repeatability
171}
172
173#[derive(Debug)]
174struct Group {
175 id: String,
176 indices: Vec<usize>,
177 speaker_counts: BTreeMap<String, usize>,
178}
179
180fn active_groups(dataset: &Dataset) -> Vec<Group> {
181 let mut indices_by_group: BTreeMap<String, Vec<usize>> = BTreeMap::new();
182 for (index, row) in dataset.active.iter().enumerate() {
183 indices_by_group
184 .entry(row.group_id.as_ref().to_owned())
185 .or_default()
186 .push(index);
187 }
188
189 indices_by_group
190 .into_iter()
191 .map(|(id, indices)| {
192 let mut speaker_counts = BTreeMap::new();
193 for &index in &indices {
194 *speaker_counts
195 .entry(dataset.active[index].speaker_id.as_ref().to_owned())
196 .or_default() += 1;
197 }
198 Group {
199 id,
200 indices,
201 speaker_counts,
202 }
203 })
204 .collect()
205}
206
207pub fn open_set_folds(
209 dataset: &Dataset,
210 config: FoldConfig,
211) -> Result<Vec<OpenSetFold>, DatasetError> {
212 let fold_count = usize::from(config.known_folds);
213 if fold_count < 2 {
214 return Err(DatasetError::InvalidKnownFolds);
215 }
216
217 let groups = active_groups(dataset);
218 let speakers: BTreeMap<String, Key> = dataset
219 .active
220 .iter()
221 .map(|row| (row.speaker_id.as_ref().to_owned(), row.speaker_id.clone()))
222 .collect();
223 let mut output = Vec::with_capacity(speakers.len() * fold_count);
224
225 for (pseudo_name, pseudo_key) in speakers {
226 let (blocked, mut eligible): (Vec<&Group>, Vec<&Group>) = groups
227 .iter()
228 .partition(|group| group.speaker_counts.contains_key(&pseudo_name));
229
230 if blocked.is_empty() || eligible.len() < fold_count {
231 return Err(DatasetError::ImpossibleFold);
232 }
233
234 eligible.sort_by(|a, b| {
235 b.indices
236 .len()
237 .cmp(&a.indices.len())
238 .then_with(|| b.speaker_counts.len().cmp(&a.speaker_counts.len()))
239 .then_with(|| a.id.cmp(&b.id))
240 });
241
242 let mut test_counts = vec![BTreeMap::<String, usize>::new(); fold_count];
243 let mut row_counts = vec![0_usize; fold_count];
244 let mut group_counts = vec![0_usize; fold_count];
245 let mut assignment = BTreeMap::<&str, usize>::new();
246
247 for group in &eligible {
248 let chosen = (0..fold_count)
249 .min_by_key(|&fold| {
250 let speaker_cost: usize = group
251 .speaker_counts
252 .iter()
253 .map(|(speaker, addition)| {
254 let current =
255 test_counts[fold].get(speaker).copied().unwrap_or_default();
256 2 * current * addition + addition * addition
257 })
258 .sum();
259 (speaker_cost, row_counts[fold], group_counts[fold], fold)
260 })
261 .expect("fold count is nonzero");
262
263 assignment.insert(group.id.as_str(), chosen);
264 for (speaker, count) in &group.speaker_counts {
265 *test_counts[chosen].entry(speaker.clone()).or_default() += count;
266 }
267 row_counts[chosen] += group.indices.len();
268 group_counts[chosen] += 1;
269 }
270
271 if group_counts.contains(&0) {
272 return Err(DatasetError::ImpossibleFold);
273 }
274
275 for fold in 0..fold_count {
276 let mut train_indices: Vec<usize> = Vec::new();
277 let mut known_test_indices: Vec<usize> = Vec::new();
278 let mut unknown_test_indices: Vec<usize> = Vec::new();
279
280 for group in &blocked {
281 for &index in &group.indices {
282 if dataset.active[index].speaker_id.as_ref() == pseudo_name {
283 unknown_test_indices.push(index);
284 } else {
285 known_test_indices.push(index);
286 }
287 }
288 }
289
290 for group in &eligible {
291 if assignment[group.id.as_str()] == fold {
292 known_test_indices.extend(&group.indices);
293 } else {
294 train_indices.extend(&group.indices);
295 }
296 }
297
298 train_indices.sort_unstable();
299 known_test_indices.sort_unstable();
300 unknown_test_indices.sort_unstable();
301
302 if train_indices.is_empty()
303 || known_test_indices.is_empty()
304 || unknown_test_indices.is_empty()
305 {
306 return Err(DatasetError::ImpossibleFold);
307 }
308
309 let trained_speakers: BTreeSet<&str> = train_indices
310 .iter()
311 .map(|index: &usize| dataset.active[*index].speaker_id.as_ref())
312 .collect();
313 if known_test_indices.iter().any(|index: &usize| {
314 !trained_speakers.contains(dataset.active[*index].speaker_id.as_ref())
315 }) {
316 return Err(DatasetError::ImpossibleFold);
317 }
318
319 output.push(OpenSetFold {
320 pseudo_unknown_speaker: pseudo_key.clone(),
321 train_indices,
322 known_test_indices,
323 unknown_test_indices,
324 });
325 }
326 }
327
328 Ok(output)
329}
330
331pub fn repeatability_groups(dataset: &Dataset) -> Vec<RepeatabilityGroup> {
333 let mut grouped: BTreeMap<(String, String), Vec<usize>> = BTreeMap::new();
334 for (index, row) in dataset.repeatability.iter().enumerate() {
335 grouped
336 .entry((
337 row.speaker_id.as_ref().to_owned(),
338 row.clip_object.as_ref().to_owned(),
339 ))
340 .or_default()
341 .push(index);
342 }
343
344 grouped
345 .into_values()
346 .filter(|indices| {
347 indices
348 .iter()
349 .map(|&index| dataset.repeatability[index].attempt_id.as_ref())
350 .collect::<BTreeSet<_>>()
351 .len()
352 >= 2
353 })
354 .map(|sample_indices| {
355 let row = &dataset.repeatability[sample_indices[0]];
356 RepeatabilityGroup {
357 speaker_id: row.speaker_id.clone(),
358 clip_object: row.clip_object.clone(),
359 sample_indices,
360 }
361 })
362 .collect()
363}
364
365#[cfg(test)]
366mod tests {
367 use super::*;
368 use kcode_speaker_types::{FEATURE_COUNT, FeatureVector, RecordingKind};
369
370 fn key(value: &str) -> Key {
371 Key::parse(value).unwrap()
372 }
373
374 fn object(value: &str) -> ObjectId {
375 ObjectId::parse(value).unwrap()
376 }
377
378 fn sample_with_attempt(
379 id: &str,
380 attempt: &str,
381 speaker: &str,
382 clip: &str,
383 group: &str,
384 ) -> LabeledSample {
385 LabeledSample {
386 sample_id: key(id),
387 attempt_id: key(attempt),
388 speaker_id: key(speaker),
389 cohort_id: key("cohort"),
390 group_id: key(group),
391 clip_object: object(clip),
392 recording_kind: RecordingKind::Meeting,
393 recording_quality: 100,
394 usable_speech_ms: 900,
395 primary_language: key("en"),
396 features: FeatureVector::new([50; FEATURE_COUNT]).unwrap(),
397 }
398 }
399
400 fn sample(id: &str, speaker: &str, clip: &str, group: &str) -> LabeledSample {
401 sample_with_attempt(id, id, speaker, clip, group)
402 }
403
404 fn fixture_rows() -> Vec<LabeledSample> {
405 vec![
406 sample("a1", "a", "CLIP0001", "a-source"),
407 sample("a2", "a", "CLIP0002", "a-source"),
408 sample("a3", "a", "CLIP0003", "a-three"),
409 sample("a4", "a", "CLIP0004", "meeting"),
410 sample("b1", "b", "CLIP0011", "b-one"),
411 sample("b2", "b", "CLIP0012", "b-two"),
412 sample("b3", "b", "CLIP0013", "b-three"),
413 sample("b4", "b", "CLIP0004", "meeting"),
414 sample("c1", "c", "CLIP0021", "c-source"),
415 sample("c2", "c", "CLIP0022", "c-source"),
416 sample("c3", "c", "CLIP0023", "c-three"),
417 ]
418 }
419
420 fn repeat_rows() -> Vec<LabeledSample> {
421 vec![
422 sample_with_attempt("r3", "try-3", "a", "CLIP0031", "repeat"),
423 sample_with_attempt("r1", "try-1", "a", "CLIP0030", "repeat"),
424 sample_with_attempt("r2", "try-2", "a", "CLIP0030", "repeat"),
425 sample_with_attempt("r4", "try-4", "b", "CLIP0032", "repeat"),
426 sample_with_attempt("r5", "same-try", "c", "CLIP0033", "repeat"),
427 sample_with_attempt("r6", "same-try", "c", "CLIP0033", "repeat"),
428 ]
429 }
430
431 fn side(fold: &OpenSetFold, index: usize) -> u8 {
432 if fold.train_indices.contains(&index) {
433 1
434 } else if fold.known_test_indices.contains(&index)
435 || fold.unknown_test_indices.contains(&index)
436 {
437 2
438 } else {
439 0
440 }
441 }
442
443 #[test]
444 fn groups_never_cross_and_co_speakers_are_test_only() {
445 let dataset = build(fixture_rows(), vec![]).unwrap();
446 let folds = open_set_folds(&dataset, FoldConfig { known_folds: 2 }).unwrap();
447
448 for fold in &folds {
449 let mut group_sides: BTreeMap<&str, u8> = BTreeMap::new();
450 for (index, row) in active_rows(&dataset).iter().enumerate() {
451 let current = side(fold, index);
452 assert_ne!(current, 0);
453 let previous = group_sides.entry(row.group_id.as_ref()).or_insert(current);
454 assert_eq!(*previous, current);
455 }
456 }
457
458 let meeting_b = active_rows(&dataset)
459 .iter()
460 .position(|row| row.sample_id.as_ref() == "b4")
461 .unwrap();
462 for fold in folds
463 .iter()
464 .filter(|fold| fold.pseudo_unknown_speaker.as_ref() == "a")
465 {
466 assert!(fold.known_test_indices.contains(&meeting_b));
467 assert!(!fold.train_indices.contains(&meeting_b));
468 }
469 }
470
471 #[test]
472 fn sibling_duplicate_excerpt_and_overlap_group_stays_whole() {
473 let dataset = build(fixture_rows(), vec![]).unwrap();
474 let folds = open_set_folds(&dataset, FoldConfig { known_folds: 2 }).unwrap();
475 let related: Vec<_> = active_rows(&dataset)
476 .iter()
477 .enumerate()
478 .filter(|(_, row)| row.group_id.as_ref() == "a-source")
479 .map(|(index, _)| index)
480 .collect();
481
482 assert_eq!(related.len(), 2);
483 for fold in folds {
484 assert_eq!(side(&fold, related[0]), side(&fold, related[1]));
485 }
486 }
487
488 #[test]
489 fn permutation_balancing_and_index_views_are_stable() {
490 let active = fixture_rows();
491 let repeatability = repeat_rows();
492 let mut reversed_active = active.clone();
493 let mut reversed_repeatability = repeatability.clone();
494 reversed_active.reverse();
495 reversed_repeatability.reverse();
496
497 let first = build(active, repeatability).unwrap();
498 let second = build(reversed_active, reversed_repeatability).unwrap();
499 assert_eq!(
500 active_rows(&first)
501 .iter()
502 .map(|row| row.sample_id.as_ref())
503 .collect::<Vec<_>>(),
504 active_rows(&second)
505 .iter()
506 .map(|row| row.sample_id.as_ref())
507 .collect::<Vec<_>>()
508 );
509
510 let first_folds = open_set_folds(&first, FoldConfig { known_folds: 2 }).unwrap();
511 assert_eq!(
512 first_folds,
513 open_set_folds(&second, FoldConfig { known_folds: 2 }).unwrap()
514 );
515 assert_eq!(repeatability_groups(&first), repeatability_groups(&second));
516 assert_eq!(first_folds.len(), 6);
517
518 let b_folds: Vec<_> = first_folds
519 .iter()
520 .filter(|fold| fold.pseudo_unknown_speaker.as_ref() == "b")
521 .collect();
522 let known_ids = |fold: &OpenSetFold| {
523 fold.known_test_indices
524 .iter()
525 .map(|&index| active_rows(&first)[index].sample_id.as_ref())
526 .collect::<BTreeSet<_>>()
527 };
528 assert_eq!(
529 known_ids(b_folds[0]),
530 BTreeSet::from(["a1", "a2", "a4", "c3"])
531 );
532 assert_eq!(
533 known_ids(b_folds[1]),
534 BTreeSet::from(["a3", "a4", "c1", "c2"])
535 );
536
537 for fold in first_folds {
538 for index in fold
539 .train_indices
540 .iter()
541 .chain(&fold.known_test_indices)
542 .chain(&fold.unknown_test_indices)
543 {
544 assert!(active_rows(&first).get(*index).is_some());
545 }
546 }
547 for group in repeatability_groups(&first) {
548 for index in group.sample_indices {
549 assert!(repeatability_rows(&first).get(index).is_some());
550 }
551 }
552 }
553
554 #[test]
555 fn exact_repeatability_grouping_requires_distinct_attempts() {
556 let dataset = build(fixture_rows(), repeat_rows()).unwrap();
557 let groups = repeatability_groups(&dataset);
558
559 assert_eq!(groups.len(), 1);
560 assert_eq!(groups[0].speaker_id.as_ref(), "a");
561 assert_eq!(groups[0].clip_object.as_ref(), "CLIP0030");
562 let ids: Vec<_> = groups[0]
563 .sample_indices
564 .iter()
565 .map(|&index| repeatability_rows(&dataset)[index].sample_id.as_ref())
566 .collect();
567 assert_eq!(ids, ["r1", "r2"]);
568 }
569
570 #[test]
571 fn duplicate_and_cross_view_rules_are_exact() {
572 let row = sample("same", "a", "CLIP0040", "g");
573 assert_eq!(
574 build(vec![row.clone(), row.clone()], vec![]).unwrap_err(),
575 DatasetError::DuplicateActiveSampleId
576 );
577 assert_eq!(
578 build(fixture_rows(), vec![row.clone(), row.clone()]).unwrap_err(),
579 DatasetError::DuplicateRepeatabilitySampleId
580 );
581
582 let dataset = build(vec![row.clone()], vec![row.clone()]).unwrap();
583 assert_eq!(active_rows(&dataset)[0], repeatability_rows(&dataset)[0]);
584
585 let mut changed = row.clone();
586 changed.usable_speech_ms = 1;
587 assert_eq!(
588 build(vec![row], vec![changed]).unwrap_err(),
589 DatasetError::ConflictingSharedSampleId
590 );
591 }
592
593 #[test]
594 fn sparse_and_impossible_configs_fail_closed() {
595 let dataset = build(fixture_rows(), vec![]).unwrap();
596 assert_eq!(
597 open_set_folds(&dataset, FoldConfig { known_folds: 1 }).unwrap_err(),
598 DatasetError::InvalidKnownFolds
599 );
600 assert_eq!(
601 open_set_folds(&dataset, FoldConfig { known_folds: 20 }).unwrap_err(),
602 DatasetError::ImpossibleFold
603 );
604
605 let rows = vec![
606 sample("a1", "a", "CLIP0050", "ab"),
607 sample("b1", "b", "CLIP0050", "ab"),
608 sample("b2", "b", "CLIP0051", "b-only"),
609 sample("c1", "c", "CLIP0052", "c-one"),
610 sample("c2", "c", "CLIP0053", "c-two"),
611 ];
612 let sparse = build(rows, vec![]).unwrap();
613 assert_eq!(
614 open_set_folds(&sparse, FoldConfig { known_folds: 2 }).unwrap_err(),
615 DatasetError::ImpossibleFold
616 );
617 }
618
619 #[test]
620 fn active_observation_and_supplied_clip_group_are_validated() {
621 let first = sample("x1", "a", "CLIP0060", "one");
622 let duplicate = sample("x2", "a", "CLIP0060", "one");
623 assert_eq!(
624 build(vec![first.clone(), duplicate], vec![]).unwrap_err(),
625 DatasetError::DuplicateActiveObservation
626 );
627
628 let other_speaker = sample("x3", "b", "CLIP0060", "two");
629 assert_eq!(
630 build(vec![first, other_speaker], vec![]).unwrap_err(),
631 DatasetError::InconsistentClipGroup
632 );
633 }
634
635 #[test]
636 fn cohort_quality_and_usable_speech_are_validated() {
637 let mut mixed = sample("x2", "b", "CLIP0062", "two");
638 mixed.cohort_id = key("other");
639 assert_eq!(
640 build(vec![sample("x1", "a", "CLIP0061", "one"), mixed], vec![]).unwrap_err(),
641 DatasetError::MixedActiveCohorts
642 );
643
644 let mut poor = sample("x3", "a", "CLIP0063", "three");
645 poor.recording_quality = 101;
646 assert_eq!(
647 build(vec![poor], vec![]).unwrap_err(),
648 DatasetError::InvalidRecordingQuality
649 );
650
651 let mut silent = sample("x4", "a", "CLIP0064", "four");
652 silent.usable_speech_ms = 0;
653 assert_eq!(
654 build(vec![silent], vec![]).unwrap_err(),
655 DatasetError::NoUsableSpeech
656 );
657 }
658}