aria_kit/voice_activity/
endpointer.rs1use crate::audio::{SilenceDuration, canonical_samples};
2
3use super::{SPEECH_THRESHOLD, VoiceActivityDetector, VoiceActivityError};
4
5const SPEECH_END_THRESHOLD: f32 = 0.35;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum Boundary {
14 Start(u64),
16 End(u64),
19}
20
21pub struct Endpointer<V> {
53 vad: V,
54 trailing_silence_samples: u64,
55 frame: Vec<i16>,
56 scored_samples: u64,
57 speech_samples: u64,
58 speaking: bool,
59 silence_since: Option<u64>,
60}
61
62impl<V: VoiceActivityDetector> Endpointer<V> {
63 pub fn new(mut vad: V, trailing_silence: SilenceDuration) -> Self {
65 const { assert!(V::FRAME_SAMPLES > 0) };
67 vad.reset();
68 Self {
69 vad,
70 trailing_silence_samples: canonical_samples(trailing_silence.get()),
71 frame: Vec::with_capacity(V::FRAME_SAMPLES),
72 scored_samples: 0,
73 speech_samples: 0,
74 speaking: false,
75 silence_since: None,
76 }
77 }
78
79 #[cfg(all(feature = "silero", not(target_family = "wasm")))]
82 pub(crate) fn silence_since(&self) -> Option<u64> {
83 self.silence_since
84 }
85
86 #[cfg(all(feature = "silero", not(target_family = "wasm")))]
87 pub(crate) fn speech_samples(&self) -> u64 {
88 self.speech_samples
89 }
90
91 pub fn push(&mut self, mut samples: &[i16]) -> Result<Vec<Boundary>, VoiceActivityError> {
93 let mut boundaries = Vec::new();
94 while !samples.is_empty() {
95 let copied = (V::FRAME_SAMPLES - self.frame.len()).min(samples.len());
96 self.frame.extend_from_slice(&samples[..copied]);
97 samples = &samples[copied..];
98 if self.frame.len() == V::FRAME_SAMPLES {
99 boundaries.extend(self.score_frame()?);
100 self.frame.clear();
101 }
102 }
103 Ok(boundaries)
104 }
105
106 fn score_frame(&mut self) -> Result<Option<Boundary>, VoiceActivityError> {
107 let probability = self.vad.probability(&self.frame)?;
108 let frame_start = self.scored_samples;
111 self.scored_samples += V::FRAME_SAMPLES as u64;
112 if probability >= SPEECH_THRESHOLD {
113 self.speech_samples += V::FRAME_SAMPLES as u64;
114 }
115
116 if !self.speaking {
117 self.speaking = probability >= SPEECH_THRESHOLD;
118 return Ok(self.speaking.then_some(Boundary::Start(frame_start)));
119 }
120 if probability >= SPEECH_THRESHOLD {
121 self.silence_since = None;
122 } else if probability <= SPEECH_END_THRESHOLD && self.silence_since.is_none() {
123 self.silence_since = Some(frame_start);
124 }
125 let silence = self
126 .silence_since
127 .map_or(0, |since| self.scored_samples - since);
128 if silence < self.trailing_silence_samples {
129 return Ok(None);
130 }
131 self.speaking = false;
132 self.silence_since = None;
133 Ok(Some(Boundary::End(self.scored_samples)))
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use std::time::Duration;
140
141 use super::*;
142
143 struct Scripted {
144 scores: Vec<f32>,
145 calls: usize,
146 resets: usize,
147 }
148
149 impl VoiceActivityDetector for Scripted {
150 const FRAME_SAMPLES: usize = 160;
151
152 fn reset(&mut self) {
153 self.resets += 1;
154 }
155
156 fn probability(&mut self, _samples: &[i16]) -> Result<f32, VoiceActivityError> {
157 let score = self.scores[self.calls];
158 self.calls += 1;
159 Ok(score)
160 }
161 }
162
163 fn scripted(scores: &[f32]) -> Scripted {
164 Scripted {
165 scores: scores.to_vec(),
166 calls: 0,
167 resets: 0,
168 }
169 }
170
171 const FRAME: u64 = Scripted::FRAME_SAMPLES as u64;
172
173 fn ten_frames_of_silence() -> SilenceDuration {
174 SilenceDuration::new(Duration::from_millis(100)).expect("the duration is in range")
175 }
176
177 fn boundaries(scores: &[f32], batch: usize) -> Vec<Boundary> {
178 let mut endpointer = Endpointer::new(scripted(scores), ten_frames_of_silence());
179 let samples = vec![0; scores.len() * Scripted::FRAME_SAMPLES];
180 let mut found = Vec::new();
181 for piece in samples.chunks(batch) {
182 found.extend(endpointer.push(piece).expect("the scripted VAD scores"));
183 }
184 found
185 }
186
187 #[test]
188 fn consecutive_utterances_each_get_a_start_and_an_end() {
189 let mut scores = vec![0.0, 0.9, 0.9];
190 scores.extend([0.0; 10]);
191 scores.extend([0.9, 0.0]);
192 let ten_frames = 10 * FRAME;
193
194 for batch in [1, 100, 160, 1_024] {
195 assert_eq!(
196 boundaries(&scores, batch),
197 [
198 Boundary::Start(FRAME),
199 Boundary::End(3 * FRAME + ten_frames),
200 Boundary::Start(13 * FRAME),
201 ],
202 "batches of {batch} samples"
203 );
204 }
205 }
206
207 #[test]
208 fn a_score_between_the_thresholds_does_not_end_the_utterance() {
209 let mut scores = vec![0.9];
210 scores.extend([0.4; 20]);
211
212 assert_eq!(boundaries(&scores, 160), [Boundary::Start(0)]);
213 }
214
215 #[test]
216 fn a_new_endpointer_resets_its_detector() {
217 let mut vad = scripted(&[]);
218
219 Endpointer::new(&mut vad, ten_frames_of_silence());
220
221 assert_eq!(vad.resets, 1);
222 }
223}