use crate::types::{SpeakerTurn, TimeRange};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy)]
pub struct DerResult {
pub der: f64,
pub miss_rate: f64,
pub false_alarm_rate: f64,
pub confusion_rate: f64,
pub total_speech: f64,
pub total_ref_frames: u64,
pub missed_frames: u64,
pub false_alarm_frames: u64,
pub confusion_frames: u64,
}
impl std::fmt::Display for DerResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"DER={:.1}% (miss={:.1}%, fa={:.1}%, conf={:.1}%, speech={:.1}s)",
self.der * 100.0,
self.miss_rate * 100.0,
self.false_alarm_rate * 100.0,
self.confusion_rate * 100.0,
self.total_speech,
)
}
}
pub fn compute_der(
reference: &[SpeakerTurn],
hypothesis: &[SpeakerTurn],
collar: f64,
) -> DerResult {
der_core(reference, hypothesis, collar, Region::All, None)
}
pub fn compute_der_single_speaker_regions(
reference: &[SpeakerTurn],
hypothesis: &[SpeakerTurn],
collar: f64,
) -> DerResult {
der_core(reference, hypothesis, collar, Region::SingleSpeaker, None)
}
pub fn compute_der_with_uem(
reference: &[SpeakerTurn],
hypothesis: &[SpeakerTurn],
collar: f64,
scored: &[TimeRange],
) -> DerResult {
der_core(reference, hypothesis, collar, Region::All, Some(scored))
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Region {
All,
SingleSpeaker,
Overlap,
}
pub(crate) fn der_core(
reference: &[SpeakerTurn],
hypothesis: &[SpeakerTurn],
collar: f64,
region: Region,
uem: Option<&[TimeRange]>,
) -> DerResult {
if reference.is_empty() {
return DerResult {
der: 0.0,
miss_rate: 0.0,
false_alarm_rate: 0.0,
confusion_rate: 0.0,
total_speech: 0.0,
total_ref_frames: 0,
missed_frames: 0,
false_alarm_frames: 0,
confusion_frames: 0,
};
}
if !collar.is_finite() || collar < 0.0 {
return DerResult {
der: 0.0,
miss_rate: 0.0,
false_alarm_rate: 0.0,
confusion_rate: 0.0,
total_speech: 0.0,
total_ref_frames: 0,
missed_frames: 0,
false_alarm_frames: 0,
confusion_frames: 0,
};
}
let resolution = 0.01; const MAX_FRAMES: usize = 24 * 3600 * 100;
let max_time = reference
.iter()
.chain(hypothesis.iter())
.map(|t| t.time.end)
.fold(0.0f64, f64::max);
if !max_time.is_finite() || max_time < 0.0 {
return DerResult {
der: 0.0,
miss_rate: 0.0,
false_alarm_rate: 0.0,
confusion_rate: 0.0,
total_speech: 0.0,
total_ref_frames: 0,
missed_frames: 0,
false_alarm_frames: 0,
confusion_frames: 0,
};
}
let n_frames = ((max_time / resolution).ceil() as usize + 1).min(MAX_FRAMES);
let mut ignore_mask = build_collar_mask(reference, collar, resolution, n_frames);
let ref_frames = build_speaker_frames(reference, resolution, n_frames);
let hyp_frames = build_speaker_frames(hypothesis, resolution, n_frames);
match region {
Region::All => {}
Region::SingleSpeaker => {
for (i, frame) in ref_frames.iter().enumerate() {
if frame.len() >= 2 {
ignore_mask[i] = true;
}
}
}
Region::Overlap => {
for (i, frame) in ref_frames.iter().enumerate() {
if frame.len() < 2 {
ignore_mask[i] = true;
}
}
}
}
if let Some(scored) = uem {
for (i, slot) in ignore_mask.iter_mut().enumerate() {
if *slot {
continue;
}
let center = (i as f64 + 0.5) * resolution;
let in_scope = scored.iter().any(|r| center >= r.start && center < r.end);
if !in_scope {
*slot = true;
}
}
}
let mapping = optimal_speaker_mapping(&ref_frames, &hyp_frames, &ignore_mask);
let mut total_ref = 0u64;
let mut missed = 0u64;
let mut false_alarm = 0u64;
let mut confusion = 0u64;
for i in 0..n_frames {
if ignore_mask[i] {
continue;
}
let ref_spk = &ref_frames[i];
let hyp_spk = &hyp_frames[i];
let n_ref = ref_spk.len() as u64;
let n_hyp = hyp_spk.len() as u64;
total_ref += n_ref;
let mut n_correct = 0u64;
for h in hyp_spk {
if let Some(&mapped_ref) = mapping.get(h)
&& ref_spk.contains(&mapped_ref)
{
n_correct += 1;
}
}
n_correct = n_correct.min(n_ref);
missed += n_ref.saturating_sub(n_hyp);
false_alarm += n_hyp.saturating_sub(n_ref);
confusion += n_ref.min(n_hyp) - n_correct;
}
let total_ref_f = total_ref as f64;
if total_ref == 0 {
return DerResult {
der: 0.0,
miss_rate: 0.0,
false_alarm_rate: 0.0,
confusion_rate: 0.0,
total_speech: 0.0,
total_ref_frames: 0,
missed_frames: 0,
false_alarm_frames: 0,
confusion_frames: 0,
};
}
let total_speech_secs = total_ref as f64 * resolution;
DerResult {
der: (missed + false_alarm + confusion) as f64 / total_ref_f,
miss_rate: missed as f64 / total_ref_f,
false_alarm_rate: false_alarm as f64 / total_ref_f,
confusion_rate: confusion as f64 / total_ref_f,
total_speech: total_speech_secs,
total_ref_frames: total_ref,
missed_frames: missed,
false_alarm_frames: false_alarm,
confusion_frames: confusion,
}
}
pub(crate) fn build_collar_mask(
reference: &[SpeakerTurn],
collar: f64,
resolution: f64,
n_frames: usize,
) -> Vec<bool> {
let mut mask = vec![false; n_frames];
if collar <= 0.0 {
return mask;
}
for turn in reference {
for boundary in [turn.time.start, turn.time.end] {
let start_frame = ((boundary - collar).max(0.0) / resolution) as usize;
let end_frame = ((boundary + collar) / resolution).ceil() as usize;
for item in mask
.iter_mut()
.take(end_frame.min(n_frames))
.skip(start_frame)
{
*item = true;
}
}
}
mask
}
pub(crate) fn build_speaker_frames(
turns: &[SpeakerTurn],
resolution: f64,
n_frames: usize,
) -> Vec<Vec<u32>> {
let mut frames: Vec<Vec<u32>> = vec![Vec::new(); n_frames];
for turn in turns {
let start_frame = (turn.time.start / resolution) as usize;
let end_frame = (turn.time.end / resolution).ceil() as usize;
for frame in frames
.iter_mut()
.take(end_frame.min(n_frames))
.skip(start_frame)
{
if !frame.contains(&turn.speaker.0) {
frame.push(turn.speaker.0);
}
}
}
frames
}
pub(crate) fn optimal_speaker_mapping(
ref_frames: &[Vec<u32>],
hyp_frames: &[Vec<u32>],
collar_mask: &[bool],
) -> HashMap<u32, u32> {
let mut cooccurrence: HashMap<(u32, u32), u64> = HashMap::new();
for i in 0..ref_frames.len().min(hyp_frames.len()) {
if collar_mask[i] {
continue;
}
for &r in &ref_frames[i] {
for &h in &hyp_frames[i] {
*cooccurrence.entry((h, r)).or_insert(0) += 1;
}
}
}
if cooccurrence.is_empty() {
return HashMap::new();
}
let mut hyp_ids: Vec<u32> = cooccurrence.keys().map(|&(h, _)| h).collect();
hyp_ids.sort_unstable();
hyp_ids.dedup();
let mut ref_ids: Vec<u32> = cooccurrence.keys().map(|&(_, r)| r).collect();
ref_ids.sort_unstable();
ref_ids.dedup();
let n = hyp_ids.len().max(ref_ids.len());
let mut cost = vec![vec![0.0_f32; n]; n];
for (&(h, r), &count) in &cooccurrence {
if let (Ok(i), Ok(j)) = (hyp_ids.binary_search(&h), ref_ids.binary_search(&r)) {
cost[i][j] = -(count as f32);
}
}
let assignment = match crate::hungarian::solve(&cost) {
Some(a) => a,
None => return HashMap::new(),
};
let mut mapping: HashMap<u32, u32> = HashMap::new();
for (row, &col) in assignment.iter().enumerate() {
if let (Some(&h), Some(&r)) = (hyp_ids.get(row), ref_ids.get(col))
&& cooccurrence.get(&(h, r)).copied().unwrap_or(0) > 0
{
mapping.insert(h, r);
}
}
mapping
}
pub fn compute_der_from_rttm(
reference: &[(f64, f64, &str)],
hypothesis: &[SpeakerTurn],
collar: f64,
) -> DerResult {
let mut speaker_map: HashMap<&str, u32> = HashMap::new();
let mut next_id = 1000u32;
let ref_turns: Vec<SpeakerTurn> = reference
.iter()
.map(|&(start, end, speaker)| {
let id = *speaker_map.entry(speaker).or_insert_with(|| {
let id = next_id;
next_id += 1;
id
});
SpeakerTurn {
speaker: crate::types::SpeakerId(id),
time: TimeRange { start, end },
text: None,
stable: true,
}
})
.collect();
compute_der(&ref_turns, hypothesis, collar)
}
pub fn parse_uem(text: &str) -> HashMap<String, Vec<TimeRange>> {
let mut out: HashMap<String, Vec<TimeRange>> = HashMap::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() || line.starts_with(';') || line.starts_with('#') {
continue;
}
let mut it = line.split_whitespace();
let (Some(file), Some(_channel), Some(start), Some(end)) =
(it.next(), it.next(), it.next(), it.next())
else {
continue;
};
let (Ok(start), Ok(end)) = (start.parse::<f64>(), end.parse::<f64>()) else {
continue;
};
if !start.is_finite() || !end.is_finite() || end <= start {
continue;
}
out.entry(file.to_owned())
.or_default()
.push(TimeRange { start, end });
}
out
}