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> {
name.parse().ok()
}
pub fn as_str(self) -> &'static str {
match self {
Self::Realtime => "realtime",
Self::Balanced => "balanced",
Self::Accurate => "accurate",
}
}
}
impl std::str::FromStr for LatencyPreset {
type Err = LatencyPresetParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_ascii_lowercase().as_str() {
"realtime" | "real-time" | "low" | "low-latency" => Ok(Self::Realtime),
"balanced" | "default" => Ok(Self::Balanced),
"accurate" | "accuracy" | "high" => Ok(Self::Accurate),
other => Err(LatencyPresetParseError(other.to_owned())),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LatencyPresetParseError(pub String);
impl std::fmt::Display for LatencyPresetParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"unknown latency preset '{}': expected realtime|balanced|accurate",
self.0
)
}
}
impl std::error::Error for LatencyPresetParseError {}
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 from_str_matches_parse_name_with_typed_error() {
use std::str::FromStr;
for (name, preset) in [
("realtime", LatencyPreset::Realtime),
(" low-latency ", LatencyPreset::Realtime),
("Balanced", LatencyPreset::Balanced),
("accuracy", LatencyPreset::Accurate),
] {
assert_eq!(LatencyPreset::from_str(name), Ok(preset));
assert_eq!(name.parse::<LatencyPreset>(), Ok(preset));
}
let err = LatencyPreset::from_str("nope").expect_err("unknown preset");
assert_eq!(err, LatencyPresetParseError("nope".to_owned()));
assert!(err.to_string().contains("nope"));
}
#[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);
}
}