use crate::segmentation::decoder::{
MAX_LOCAL_SPEAKERS, NUM_POWERSET_CLASSES, PowersetClass, PowersetDecoder,
};
use crate::vad::hysteresis::{HysteresisGate, RegionEvent, RegionTracker, TailPolicy};
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct BinarizationConfig {
pub onset: f32,
pub offset: f32,
pub min_duration_on: f32,
pub min_duration_off: f32,
}
impl Default for BinarizationConfig {
fn default() -> Self {
Self {
onset: 0.5,
offset: 0.5,
min_duration_on: 0.0,
min_duration_off: 0.0,
}
}
}
#[allow(clippy::needless_range_loop)]
pub fn binarize_frames(
avg_probs: &[[f32; NUM_POWERSET_CLASSES]],
has_data: &[bool],
stride: f32,
cfg: &BinarizationConfig,
) -> (Vec<Option<PowersetClass>>, Vec<f32>) {
let n = avg_probs.len();
let mut speaker_probs = vec![[0.0_f32; MAX_LOCAL_SPEAKERS]; n];
for g in 0..n {
if !has_data[g] {
continue;
}
for c in 0..NUM_POWERSET_CLASSES {
if let Some(class) = PowersetDecoder::class_for_index(c) {
for s in class.speakers() {
speaker_probs[g][s as usize] += avg_probs[g][c];
}
}
}
}
let min_on = (cfg.min_duration_on / stride).round() as usize;
let min_off = (cfg.min_duration_off / stride).round() as usize;
let mut active = vec![[false; MAX_LOCAL_SPEAKERS]; n];
for s in 0..MAX_LOCAL_SPEAKERS {
let mut gate = HysteresisGate::new(cfg.onset, cfg.offset);
let mut tracker = RegionTracker::new(min_off, min_on, TailPolicy::Trim);
for g in 0..n {
if !has_data[g] {
gate.reset();
let event = tracker.reset();
mark_region(&mut active, s, &tracker, event);
continue;
}
let on = gate.update(speaker_probs[g][s]);
let event = tracker.advance(on, g);
mark_region(&mut active, s, &tracker, event);
}
let event = tracker.flush(n);
mark_region(&mut active, s, &tracker, event);
}
let mut classes = Vec::with_capacity(n);
let mut confidences = Vec::with_capacity(n);
for g in 0..n {
if !has_data[g] {
classes.push(None);
confidences.push(0.0);
continue;
}
let mut on: Vec<u8> = (0..MAX_LOCAL_SPEAKERS as u8)
.filter(|&s| active[g][s as usize])
.collect();
if on.len() > 2 {
on.sort_by(|a, b| {
speaker_probs[g][*b as usize].total_cmp(&speaker_probs[g][*a as usize])
});
on.truncate(2);
on.sort_unstable();
}
debug_assert!(
matches!(on.as_slice(), [] | [_] | [_, _]),
"at most two speakers survive the top-2 truncation"
);
let class = PowersetClass::from_speakers(&on);
classes.push(class);
let conf = if on.is_empty() {
avg_probs[g][0]
} else {
on.iter()
.map(|s| speaker_probs[g][*s as usize])
.sum::<f32>()
/ on.len() as f32
};
confidences.push(conf.clamp(0.0, 1.0));
}
(classes, confidences)
}
fn mark_region(
active: &mut [[bool; MAX_LOCAL_SPEAKERS]],
speaker: usize,
tracker: &RegionTracker,
event: Option<RegionEvent>,
) {
if let Some(RegionEvent::End {
start_frame,
end_frame,
}) = event
&& tracker.keeps(start_frame, end_frame)
{
for frame in active.iter_mut().take(end_frame).skip(start_frame) {
frame[speaker] = true;
}
}
}