aria_kit/voice_activity/
silero.rs1use std::ops::Range;
5
6use ::silero::{SampleRate, Session, StreamState};
7
8use super::{SPEECH_THRESHOLD, VoiceActivityDetector, VoiceActivityError};
9use crate::audio::f32_from_pcm16;
10
11const FRAME_SAMPLES: usize = 512;
12const TIMELINE_THRESHOLD: f32 = 0.1;
18const EDGE_PAD_SAMPLES: usize = 1_600;
22
23pub struct SileroVad {
24 session: Session,
25 stream: StreamState,
26 frame: [f32; FRAME_SAMPLES],
27}
28
29impl SileroVad {
30 pub fn load() -> Result<Self, VoiceActivityError> {
32 let session =
33 Session::bundled().map_err(|source| VoiceActivityError::LoadModel(Box::new(source)))?;
34 let mut vad = Self {
35 session,
36 stream: StreamState::new(SampleRate::Rate16k),
37 frame: [0.0; FRAME_SAMPLES],
38 };
39 vad.scores(&[0.0; FRAME_SAMPLES])?;
40 vad.stream.reset();
41 Ok(vad)
42 }
43
44 pub fn speech_regions(
48 &mut self,
49 window: &[f32],
50 ) -> Result<Vec<Range<usize>>, VoiceActivityError> {
51 let scores = self.scores(window)?;
52 Ok(regions(&scores, window))
53 }
54
55 pub fn speech(&mut self, window: &[f32]) -> Result<Speech, VoiceActivityError> {
60 let scores = self.scores(window)?;
61 Ok(Speech {
62 regions: regions(&scores, window),
63 samples: runs_at(&scores, SPEECH_THRESHOLD)
64 .into_iter()
65 .flat_map(|run| window[run].iter().copied())
66 .collect(),
67 })
68 }
69
70 fn scores(&mut self, window: &[f32]) -> Result<Vec<(Range<usize>, f32)>, VoiceActivityError> {
74 let mut scores = Vec::with_capacity(window.len().div_ceil(FRAME_SAMPLES));
75 for (index, frame) in window.chunks(FRAME_SAMPLES).enumerate() {
76 let at = index * FRAME_SAMPLES;
77 self.frame[..frame.len()].copy_from_slice(frame);
78 self.frame[frame.len()..].fill(0.0);
79 let probability = self
80 .session
81 .infer_chunk(&mut self.stream, &self.frame)
82 .map_err(|source| VoiceActivityError::Inference(Box::new(source)))?;
83 scores.push((at..at + frame.len(), probability));
84 }
85 Ok(scores)
86 }
87}
88
89pub struct Speech {
91 pub regions: Vec<Range<usize>>,
92 pub samples: Vec<f32>,
93}
94
95fn regions(scores: &[(Range<usize>, f32)], window: &[f32]) -> Vec<Range<usize>> {
96 widen(&runs_at(scores, TIMELINE_THRESHOLD), window.len())
97}
98
99fn runs_at(scores: &[(Range<usize>, f32)], threshold: f32) -> Vec<Range<usize>> {
102 let mut runs: Vec<Range<usize>> = Vec::new();
103 for (frame, probability) in scores {
104 if *probability < threshold {
105 continue;
106 }
107 match runs.last_mut() {
108 Some(open) if open.end == frame.start => open.end = frame.end,
109 _ => runs.push(frame.clone()),
110 }
111 }
112 runs
113}
114
115impl VoiceActivityDetector for SileroVad {
116 const FRAME_SAMPLES: usize = FRAME_SAMPLES;
117
118 fn reset(&mut self) {
119 self.stream.reset();
120 }
121
122 fn probability(&mut self, samples: &[i16]) -> Result<f32, VoiceActivityError> {
123 if samples.len() != FRAME_SAMPLES {
124 return Err(VoiceActivityError::InvalidFrameLength {
125 actual_samples: samples.len(),
126 expected_samples: FRAME_SAMPLES,
127 });
128 }
129 for (destination, &sample) in self.frame.iter_mut().zip(samples) {
130 *destination = f32_from_pcm16(sample);
131 }
132 let probability = self
133 .session
134 .infer_chunk(&mut self.stream, &self.frame)
135 .map_err(|source| VoiceActivityError::Inference(Box::new(source)))?;
136 if !probability.is_finite() || !(0.0..=1.0).contains(&probability) {
137 return Err(VoiceActivityError::InvalidProbability(probability));
138 }
139 Ok(probability)
140 }
141}
142
143fn widen(runs: &[Range<usize>], window_samples: usize) -> Vec<Range<usize>> {
148 runs.iter()
149 .enumerate()
150 .map(|(index, run)| {
151 let behind = match index.checked_sub(1).map(|prior| &runs[prior]) {
152 Some(prior) => (run.start - prior.end) / 2,
153 None => run.start,
154 };
155 let ahead = match runs.get(index + 1) {
156 Some(next) => (next.start - run.end) / 2,
157 None => window_samples - run.end,
158 };
159 (run.start - behind.min(EDGE_PAD_SAMPLES))..(run.end + ahead.min(EDGE_PAD_SAMPLES))
160 })
161 .collect()
162}
163
164#[cfg(test)]
165mod widen_tests {
166 use super::*;
167
168 fn runs<const N: usize>(bounds: [(usize, usize); N]) -> Vec<Range<usize>> {
169 bounds.into_iter().map(|(start, end)| start..end).collect()
170 }
171
172 #[test]
173 fn a_lone_run_grows_by_the_full_pad() {
174 let widened = widen(&runs([(10_000, 20_000)]), 40_000);
175 let expected = runs([(10_000 - EDGE_PAD_SAMPLES, 20_000 + EDGE_PAD_SAMPLES)]);
176 assert_eq!(widened, expected);
177 }
178
179 #[test]
180 fn two_close_runs_split_the_silence_between_them_and_stay_apart() {
181 let widened = widen(&runs([(0, 1_000), (1_400, 2_000)]), 10_000);
182 assert_eq!(widened[0].end, 1_200);
183 assert_eq!(widened[1].start, 1_200);
184 }
185
186 #[test]
187 fn a_run_never_grows_past_the_window() {
188 assert_eq!(widen(&runs([(0, 1_000)]), 1_200), runs([(0, 1_200)]));
189 }
190}