use std::path::PathBuf;
use aria_kit::backend::{BackendError, ModelDownload, Unpack};
use aria_kit::segmentation::{
Activity, ChunkShape, Engine, SegmentationError, Segmenter, validate_model_file,
};
use ort::session::Session;
use ort::value::Tensor;
pub const NAME: &str = "pyannote";
pub const MODEL: &[ModelDownload] = &[ModelDownload {
file_name: "segmentation-3.0.onnx",
url: "https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-segmentation-models/sherpa-onnx-pyannote-segmentation-3-0.tar.bz2",
bytes: 6_958_444,
sha256: "24615ee884c897d9d2ba09bb4d30da6bb1b15e685065962db5b02e76e4996488",
unpack: Unpack::TarBz2Member("sherpa-onnx-pyannote-segmentation-3-0/model.onnx"),
}];
const WINDOW_SAMPLES: usize = 160_000;
const FRAME_SHIFT: usize = 270;
const FRAME_SIZE: usize = 991;
const LOCAL_SPEAKERS: usize = 3;
const AUDIO_INPUT: &str = "x";
const SCORES_OUTPUT: &str = "y";
const POWERSET: [&[usize]; 7] = [&[], &[0], &[1], &[2], &[0, 1], &[0, 2], &[1, 2]];
pub struct Config {
pub model: PathBuf,
}
pub struct Pyannote {
session: Session,
}
impl Engine for Pyannote {
const NAME: &'static str = NAME;
const MODEL: &'static [ModelDownload] = MODEL;
type Config = Config;
fn check_from(config: &Self::Config) -> Result<PathBuf, SegmentationError> {
validate_model_file(config.model.clone(), NAME)
}
fn load_from(config: Self::Config) -> Result<Self, SegmentationError> {
let model = Self::check_from(&config)?;
let session = Session::builder()
.and_then(|mut builder| builder.commit_from_file(&model))
.map_err(|source| SegmentationError::Load {
backend: NAME,
model: model.display().to_string(),
source: BackendError::engine(source),
})?;
let mut engine = Self { session };
engine.segment(&vec![0.0; WINDOW_SAMPLES])?;
Ok(engine)
}
}
impl Segmenter for Pyannote {
fn shape(&self) -> ChunkShape {
ChunkShape {
samples: WINDOW_SAMPLES,
frame_shift: FRAME_SHIFT,
frame_size: FRAME_SIZE,
speakers: LOCAL_SPEAKERS,
}
}
fn segment(&mut self, chunk: &[f32]) -> Result<Vec<Activity>, SegmentationError> {
if chunk.len() != WINDOW_SAMPLES {
return Err(unusable(format!(
"a chunk of {} samples reached the model, which reads {WINDOW_SAMPLES}",
chunk.len()
)));
}
let shape = [1_i64, 1, WINDOW_SAMPLES as i64];
let input = Tensor::from_array((shape, chunk.to_vec())).map_err(inference)?;
let mut outputs = self
.session
.run(ort::inputs![AUDIO_INPUT => input])
.map_err(inference)?;
let scores = outputs
.remove(SCORES_OUTPUT)
.ok_or_else(|| unusable(format!("the model returned no {SCORES_OUTPUT} output")))?;
let (shape, values) = scores.try_extract_tensor::<f32>().map_err(inference)?;
let classes = *shape.last().unwrap_or(&0) as usize;
if classes != POWERSET.len() {
return Err(unusable(format!(
"the model scores {classes} classes, expected {}",
POWERSET.len()
)));
}
Ok(values.chunks_exact(classes).map(loudest).collect())
}
}
fn loudest(frame: &[f32]) -> Activity {
let winner = frame
.iter()
.enumerate()
.max_by(|a, b| a.1.total_cmp(b.1))
.map_or(0, |(class, _)| class);
POWERSET[winner]
.iter()
.fold(Activity::SILENT, |active, &voice| active.speaking(voice))
}
fn inference(source: ort::Error) -> SegmentationError {
SegmentationError::Inference {
backend: NAME,
source: BackendError::engine(source),
}
}
fn unusable(reason: String) -> SegmentationError {
SegmentationError::Inference {
backend: NAME,
source: BackendError::Unusable(reason),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_classes_run_silence_then_singles_then_pairs() {
assert_eq!(
loudest(&[9.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
Activity::SILENT
);
assert_eq!(
loudest(&[0.0, 9.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
Activity::SILENT.speaking(0)
);
assert_eq!(
loudest(&[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 9.0]),
Activity::SILENT.speaking(1).speaking(2)
);
}
#[test]
fn every_class_names_at_most_two_voices() {
for class in POWERSET {
assert!(class.len() <= 2);
assert!(class.iter().all(|&voice| voice < LOCAL_SPEAKERS));
}
}
}