use anyhow::{Context, Result};
use std::path::{Path, PathBuf};
use crate::models::ModelRegistry;
use crate::onnx::ExecutionProvider;
use crate::pipeline_v2::{ClustererKind, Pipeline, PipelineConfig};
use crate::rttm::{RttmSegment, group_by_file, parse_rttm_file, to_speaker_turns};
use crate::types::{ClusterConfig, DiarizationConfig, SpeakerTurn};
use crate::{FbankOnnxExtractor, SileroVad};
pub fn parse_clusterer_kind(name: &str, threshold: f32) -> Result<ClustererKind> {
match name {
"ahc" => Ok(ClustererKind::Ahc { threshold }),
"vbx" => Ok(ClustererKind::Vbx),
other => anyhow::bail!("unknown --clusterer '{other}' (expected 'ahc' or 'vbx')"),
}
}
pub fn parse_execution_provider(s: &str) -> Result<ExecutionProvider> {
Ok(match s {
"auto" => ExecutionProvider::auto(),
"cpu" => ExecutionProvider::Cpu,
"coreml" => ExecutionProvider::CoreMl,
"nnapi" => ExecutionProvider::Nnapi,
"cuda" => ExecutionProvider::Cuda,
"xnnpack" => ExecutionProvider::XnnPack,
other => anyhow::bail!(
"unknown --execution-provider '{other}' (expected auto|cpu|coreml|nnapi|cuda|xnnpack)"
),
})
}
pub fn max_speakers_u8(n: usize) -> Result<u8> {
u8::try_from(n)
.ok()
.filter(|&v| v > 0)
.ok_or_else(|| anyhow::anyhow!("max_speakers must be in 1..=255, got {n}"))
}
pub fn legacy_diarization_config(threshold: f32) -> DiarizationConfig {
DiarizationConfig {
cluster: ClusterConfig {
threshold,
..Default::default()
},
..DiarizationConfig::default()
}
}
pub struct LegacyStack {
pub extractor: FbankOnnxExtractor,
pub vad: SileroVad,
}
pub fn load_legacy_stack(
embedder_path: &Path,
embedding_dim: usize,
embedder_ep: ExecutionProvider,
vad_path: &Path,
vad_frame_size: usize,
) -> Result<LegacyStack> {
let extractor = FbankOnnxExtractor::new(embedder_path, embedding_dim, 1, embedder_ep)
.context("load embedder")?;
let vad = SileroVad::new(vad_path, vad_frame_size).context("load vad")?;
Ok(LegacyStack { extractor, vad })
}
pub fn build_v2_pipeline(config: PipelineConfig, registry: ModelRegistry) -> Result<Pipeline> {
let vbx = matches!(config.clusterer, ClustererKind::Vbx);
Pipeline::builder()
.config(config)
.with_models_from(registry)
.build()
.with_context(|| {
if vbx {
"build pipeline v2 (clusterer=vbx): set vbx_plda_dir / POLYVOICE_VBX_PLDA_DIR \
(CLI flag: --vbx-plda-dir), allow registry PLDA download, or select the ahc \
clusterer (CLI: --clusterer ahc)"
.to_string()
} else {
"build pipeline v2".to_string()
}
})
}
pub fn list_wavs(dataset: &Path, max_files: Option<usize>) -> Result<Vec<PathBuf>> {
let audio_dir = dataset.join("audio");
let mut wavs: Vec<PathBuf> = std::fs::read_dir(&audio_dir)
.with_context(|| format!("read_dir {}", audio_dir.display()))?
.filter_map(|e| e.ok())
.filter(|e| e.path().extension().is_some_and(|x| x == "wav"))
.map(|e| e.path())
.collect();
wavs.sort();
if let Some(n) = max_files {
wavs.truncate(n);
}
Ok(wavs)
}
pub fn load_rttm_segments(rttm_dir: &Path, stem: &str) -> Result<Vec<RttmSegment>> {
let rttm = rttm_dir.join(format!("{stem}.rttm"));
let raw = parse_rttm_file(&rttm).with_context(|| format!("parse {}", rttm.display()))?;
let grouped = group_by_file(&raw);
let segs: Vec<RttmSegment> = grouped
.get(stem)
.or_else(|| stem.split('.').next().and_then(|s| grouped.get(s)))
.map(|v| v.iter().map(|s| (*s).clone()).collect())
.unwrap_or_default();
Ok(segs)
}
pub fn load_ref_turns(rttm_dir: &Path, stem: &str) -> Result<Vec<SpeakerTurn>> {
let (turns, _) = to_speaker_turns(&load_rttm_segments(rttm_dir, stem)?);
Ok(turns)
}
#[allow(clippy::unwrap_used)]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn clusterer_kind_parses_known_names() {
assert!(matches!(
parse_clusterer_kind("vbx", 0.7).unwrap(),
ClustererKind::Vbx
));
match parse_clusterer_kind("ahc", 0.7).unwrap() {
ClustererKind::Ahc { threshold } => assert_eq!(threshold, 0.7),
other => panic!("expected Ahc, got {other:?}"),
}
assert!(parse_clusterer_kind("nope", 0.7).is_err());
}
#[test]
fn execution_provider_parses_known_names() {
for name in ["auto", "cpu", "coreml", "nnapi", "cuda", "xnnpack"] {
assert!(parse_execution_provider(name).is_ok(), "{name}");
}
assert!(parse_execution_provider("tpu").is_err());
}
#[test]
fn max_speakers_u8_accepts_valid_range() {
assert_eq!(max_speakers_u8(1).unwrap(), 1);
assert_eq!(max_speakers_u8(255).unwrap(), 255);
}
#[test]
fn max_speakers_u8_rejects_out_of_range() {
assert!(max_speakers_u8(0).is_err());
assert!(max_speakers_u8(256).is_err());
}
#[test]
fn rttm_segments_fall_back_to_ami_style_prefix() {
let dir = tempfile::tempdir().unwrap();
let rttm_dir = dir.path();
std::fs::write(
rttm_dir.join("plain.rttm"),
"SPEAKER plain 1 0.0 1.0 <NA> <NA> B <NA> <NA>\n",
)
.unwrap();
let exact = load_rttm_segments(rttm_dir, "plain").unwrap();
assert_eq!(exact.len(), 1);
assert_eq!(exact[0].speaker, "B");
std::fs::write(
rttm_dir.join("EN2002a.Mix-Headset.rttm"),
"SPEAKER EN2002a 1 0.0 1.0 <NA> <NA> A <NA> <NA>\n",
)
.unwrap();
let fallback = load_rttm_segments(rttm_dir, "EN2002a.Mix-Headset").unwrap();
assert_eq!(fallback.len(), 1);
assert_eq!(fallback[0].speaker, "A");
}
#[test]
fn list_wavs_sorts_and_truncates() {
let dir = tempfile::tempdir().unwrap();
let audio = dir.path().join("audio");
std::fs::create_dir(&audio).unwrap();
for name in ["b.wav", "a.wav", "c.txt"] {
std::fs::write(audio.join(name), []).unwrap();
}
let all = list_wavs(dir.path(), None).unwrap();
assert_eq!(all.len(), 2);
assert!(all[0].ends_with("a.wav"));
let one = list_wavs(dir.path(), Some(1)).unwrap();
assert_eq!(one.len(), 1);
}
}