use crate::types::{ClusterConfig, DiarizationConfig, WindowConfig};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
#[non_exhaustive]
pub enum LatencyPreset {
Realtime,
#[default]
Balanced,
Accurate,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct StreamingParams {
pub window_secs: f32,
pub hop_secs: f32,
pub right_context_secs: f32,
pub speaker_cache_cap: usize,
pub min_hits_to_stable: usize,
pub prefer_current_margin: f32,
pub match_threshold: f32,
}
impl LatencyPreset {
pub fn params(self) -> StreamingParams {
match self {
Self::Realtime => StreamingParams {
window_secs: 1.0,
hop_secs: 0.5,
right_context_secs: 0.0,
speaker_cache_cap: 16,
min_hits_to_stable: 2,
prefer_current_margin: 0.05,
match_threshold: 0.45,
},
Self::Balanced => StreamingParams {
window_secs: 1.5,
hop_secs: 0.75,
right_context_secs: 0.0,
speaker_cache_cap: 32,
min_hits_to_stable: 3,
prefer_current_margin: 0.08,
match_threshold: 0.45,
},
Self::Accurate => StreamingParams {
window_secs: 2.0,
hop_secs: 1.0,
right_context_secs: 0.25,
speaker_cache_cap: 64,
min_hits_to_stable: 4,
prefer_current_margin: 0.10,
match_threshold: 0.45,
},
}
}
pub fn input_buffer_latency_secs(self, sample_rate: u32, vad_frame_samples: usize) -> f32 {
let p = self.params();
let vad_frame_secs = vad_frame_samples as f32 / sample_rate as f32;
p.window_secs + p.right_context_secs + vad_frame_secs
}
pub fn apply(self, config: &mut DiarizationConfig) {
let p = self.params();
config.window = WindowConfig {
window_secs: p.window_secs,
hop_secs: p.hop_secs,
sample_rate: config.window.sample_rate,
};
config.cluster = ClusterConfig {
threshold: p.match_threshold,
max_speakers: p.speaker_cache_cap,
..config.cluster
};
}
pub fn parse_name(name: &str) -> Option<Self> {
match name.trim().to_ascii_lowercase().as_str() {
"realtime" | "real-time" | "low" | "low-latency" => Some(Self::Realtime),
"balanced" | "default" => Some(Self::Balanced),
"accurate" | "accuracy" | "high" => Some(Self::Accurate),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Realtime => "realtime",
Self::Balanced => "balanced",
Self::Accurate => "accurate",
}
}
}
impl StreamingParams {
pub fn from_preset(preset: LatencyPreset) -> Self {
preset.params()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn balanced_matches_default_window_geometry() {
let p = LatencyPreset::Balanced.params();
let d = DiarizationConfig::default();
assert!((p.window_secs - d.window.window_secs).abs() < 1e-6);
assert!((p.hop_secs - d.window.hop_secs).abs() < 1e-6);
assert!((p.match_threshold - d.cluster.threshold).abs() < 1e-6);
}
#[test]
fn apply_mutates_config() {
let mut cfg = DiarizationConfig::default();
LatencyPreset::Realtime.apply(&mut cfg);
assert!((cfg.window.window_secs - 1.0).abs() < 1e-6);
assert_eq!(cfg.cluster.max_speakers, 16);
}
#[test]
fn parse_names() {
assert_eq!(
LatencyPreset::parse_name("realtime"),
Some(LatencyPreset::Realtime)
);
assert_eq!(
LatencyPreset::parse_name("Balanced"),
Some(LatencyPreset::Balanced)
);
assert_eq!(
LatencyPreset::parse_name("accurate"),
Some(LatencyPreset::Accurate)
);
assert_eq!(LatencyPreset::parse_name("nope"), None);
}
#[test]
fn latency_budget_includes_vad_frame() {
let lat = LatencyPreset::Realtime.input_buffer_latency_secs(16000, 512);
assert!((lat - 1.032).abs() < 1e-3);
let bal = LatencyPreset::Balanced.input_buffer_latency_secs(16000, 512);
assert!((bal - 1.532).abs() < 1e-3);
let acc = LatencyPreset::Accurate.input_buffer_latency_secs(16000, 512);
assert!((acc - 2.282).abs() < 1e-3);
}
}