aria_segmentation/pyannote/
mod.rs1use std::path::PathBuf;
24
25use aria_kit::backend::{BackendError, ModelDownload, Unpack};
26use aria_kit::segmentation::{
27 Activity, ChunkShape, Engine, SegmentationError, Segmenter, validate_model_file,
28};
29use ort::session::Session;
30use ort::value::Tensor;
31
32pub const NAME: &str = "pyannote";
33
34pub const MODEL: &[ModelDownload] = &[ModelDownload {
38 file_name: "segmentation-3.0.onnx",
39 url: "https://github.com/k2-fsa/sherpa-onnx/releases/download/speaker-segmentation-models/sherpa-onnx-pyannote-segmentation-3-0.tar.bz2",
40 bytes: 6_958_444,
41 sha256: "24615ee884c897d9d2ba09bb4d30da6bb1b15e685065962db5b02e76e4996488",
42 unpack: Unpack::TarBz2Member("sherpa-onnx-pyannote-segmentation-3-0/model.onnx"),
43}];
44
45const WINDOW_SAMPLES: usize = 160_000;
47const FRAME_SHIFT: usize = 270;
49const FRAME_SIZE: usize = 991;
52const LOCAL_SPEAKERS: usize = 3;
54const AUDIO_INPUT: &str = "x";
55const SCORES_OUTPUT: &str = "y";
56
57const POWERSET: [&[usize]; 7] = [&[], &[0], &[1], &[2], &[0, 1], &[0, 2], &[1, 2]];
60
61pub struct Config {
62 pub model: PathBuf,
63}
64
65pub struct Pyannote {
68 session: Session,
69}
70
71impl Engine for Pyannote {
72 const NAME: &'static str = NAME;
73 const MODEL: &'static [ModelDownload] = MODEL;
74
75 type Config = Config;
76
77 fn check_from(config: &Self::Config) -> Result<PathBuf, SegmentationError> {
78 validate_model_file(config.model.clone(), NAME)
79 }
80
81 fn load_from(config: Self::Config) -> Result<Self, SegmentationError> {
82 let model = Self::check_from(&config)?;
83 let session = Session::builder()
84 .and_then(|mut builder| builder.commit_from_file(&model))
85 .map_err(|source| SegmentationError::Load {
86 backend: NAME,
87 model: model.display().to_string(),
88 source: BackendError::engine(source),
89 })?;
90 let mut engine = Self { session };
91 engine.segment(&vec![0.0; WINDOW_SAMPLES])?;
92 Ok(engine)
93 }
94}
95
96impl Segmenter for Pyannote {
97 fn shape(&self) -> ChunkShape {
98 ChunkShape {
99 samples: WINDOW_SAMPLES,
100 frame_shift: FRAME_SHIFT,
101 frame_size: FRAME_SIZE,
102 speakers: LOCAL_SPEAKERS,
103 }
104 }
105
106 fn segment(&mut self, chunk: &[f32]) -> Result<Vec<Activity>, SegmentationError> {
107 if chunk.len() != WINDOW_SAMPLES {
108 return Err(unusable(format!(
109 "a chunk of {} samples reached the model, which reads {WINDOW_SAMPLES}",
110 chunk.len()
111 )));
112 }
113 let shape = [1_i64, 1, WINDOW_SAMPLES as i64];
114 let input = Tensor::from_array((shape, chunk.to_vec())).map_err(inference)?;
115 let mut outputs = self
116 .session
117 .run(ort::inputs![AUDIO_INPUT => input])
118 .map_err(inference)?;
119 let scores = outputs
120 .remove(SCORES_OUTPUT)
121 .ok_or_else(|| unusable(format!("the model returned no {SCORES_OUTPUT} output")))?;
122 let (shape, values) = scores.try_extract_tensor::<f32>().map_err(inference)?;
123 let classes = *shape.last().unwrap_or(&0) as usize;
124 if classes != POWERSET.len() {
125 return Err(unusable(format!(
126 "the model scores {classes} classes, expected {}",
127 POWERSET.len()
128 )));
129 }
130 Ok(values.chunks_exact(classes).map(loudest).collect())
131 }
132}
133
134fn loudest(frame: &[f32]) -> Activity {
136 let winner = frame
137 .iter()
138 .enumerate()
139 .max_by(|a, b| a.1.total_cmp(b.1))
140 .map_or(0, |(class, _)| class);
141 POWERSET[winner]
142 .iter()
143 .fold(Activity::SILENT, |active, &voice| active.speaking(voice))
144}
145
146fn inference(source: ort::Error) -> SegmentationError {
147 SegmentationError::Inference {
148 backend: NAME,
149 source: BackendError::engine(source),
150 }
151}
152
153fn unusable(reason: String) -> SegmentationError {
154 SegmentationError::Inference {
155 backend: NAME,
156 source: BackendError::Unusable(reason),
157 }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[test]
165 fn the_classes_run_silence_then_singles_then_pairs() {
166 assert_eq!(
167 loudest(&[9.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
168 Activity::SILENT
169 );
170 assert_eq!(
171 loudest(&[0.0, 9.0, 0.0, 0.0, 0.0, 0.0, 0.0]),
172 Activity::SILENT.speaking(0)
173 );
174 assert_eq!(
175 loudest(&[0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 9.0]),
176 Activity::SILENT.speaking(1).speaking(2)
177 );
178 }
179
180 #[test]
181 fn every_class_names_at_most_two_voices() {
182 for class in POWERSET {
183 assert!(class.len() <= 2);
184 assert!(class.iter().all(|&voice| voice < LOCAL_SPEAKERS));
185 }
186 }
187}