Skip to main content

transcribe_cpp/
vad.rs

1//! [`VadSession`] — the VAD role (where there is speech), from
2//! [`Model::vad_session`]. Threading and lifetime rules are those of
3//! [`Session`](crate::Session). Each call takes the compute lock for that call
4//! only; `run`, `stream_feed` and `stream_flush` return
5//! [`Error::Busy`](crate::Error) while an ASR stream is active. A VAD stream is
6//! session state and does not take the stream lease.
7
8use std::os::raw::c_void;
9use std::sync::atomic::AtomicBool;
10use std::sync::Arc;
11
12use transcribe_cpp_sys as sys;
13
14use crate::cancel::{abort_trampoline, CancelToken};
15use crate::error::{check, Result};
16use crate::model::{Model, ModelInner};
17use crate::result::Timings;
18use crate::session::clamp_len;
19
20/// Static facts about a VAD model ([`Model::vad_info`]).
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22#[non_exhaustive]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct VadInfo {
25    /// Input PCM rate (16000).
26    pub sample_rate: i32,
27    /// Samples per probability (512 = 32 ms).
28    pub frame_samples: i32,
29}
30
31/// Options for creating a VAD session.
32#[derive(Debug, Clone, Default, PartialEq, Eq)]
33#[cfg_attr(
34    feature = "serde",
35    derive(serde::Serialize, serde::Deserialize),
36    serde(default)
37)]
38pub struct VadSessionOptions {
39    /// CPU threads for ops that run on CPU; 0 = library default.
40    pub n_threads: i32,
41}
42
43/// Probabilities -> speech segments ([`VadSession::run`] only). Fields and
44/// defaults are those of Silero's `get_speech_timestamps`.
45#[derive(Debug, Clone, PartialEq)]
46#[cfg_attr(
47    feature = "serde",
48    derive(serde::Serialize, serde::Deserialize),
49    serde(default)
50)]
51pub struct VadOptions {
52    /// A frame with `p >= threshold` is speech (default 0.5). `[0, 1]`.
53    pub threshold: f64,
54    /// Inside speech, a frame with `p < neg_threshold` is silence. Negative
55    /// (the default, -1) means `max(threshold - 0.15, 0.01)`; otherwise
56    /// `[0, threshold]`.
57    pub neg_threshold: f64,
58    /// Segments of at most this length are dropped (default 250), except
59    /// those split by `max_speech_ms`. Durations must be nonnegative.
60    pub min_speech_ms: i32,
61    /// Silence this long ends a segment (default 100).
62    pub min_silence_ms: i32,
63    /// Padding on both sides of each segment (default 30).
64    pub speech_pad_ms: i32,
65    /// Segments longer than this are split; 0 (the default) = no limit.
66    pub max_speech_ms: i32,
67}
68
69impl Default for VadOptions {
70    fn default() -> Self {
71        VadOptions {
72            threshold: 0.5,
73            neg_threshold: -1.0,
74            min_speech_ms: 250,
75            min_silence_ms: 100,
76            speech_pad_ms: 30,
77            max_speech_ms: 0,
78        }
79    }
80}
81
82/// One speech segment, in samples of the run's input: `[start, end)`.
83#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
84#[non_exhaustive]
85#[cfg_attr(
86    feature = "serde",
87    derive(serde::Serialize, serde::Deserialize),
88    serde(default)
89)]
90pub struct VadSegment {
91    /// First sample of speech.
92    pub start_sample: i64,
93    /// First sample after speech.
94    pub end_sample: i64,
95}
96
97/// The result of one [`VadSession::run`], [`VadSession::stream_feed`] or
98/// [`VadSession::stream_flush`].
99#[derive(Debug, Clone, Default, PartialEq)]
100#[non_exhaustive]
101#[cfg_attr(
102    feature = "serde",
103    derive(serde::Serialize, serde::Deserialize),
104    serde(default)
105)]
106pub struct VadResult {
107    /// Speech probability per frame produced by the call.
108    pub probs: Vec<f32>,
109    /// Stream position of `probs[0]`, in frames: 0 after a run; for a feed
110    /// or flush, the frames the stream had scored before the call.
111    pub first_frame: i64,
112    /// Speech segments, in time order; run only (empty after a feed or flush).
113    pub segments: Vec<VadSegment>,
114}
115
116/// A VAD session.
117pub struct VadSession {
118    ptr: *mut sys::transcribe_vad_session,
119    // Keeps the native model alive and carries the per-model compute lock.
120    model: Arc<ModelInner>,
121    // Keeps the abort callback's userdata alive while installed.
122    cancel: Option<Arc<AtomicBool>>,
123}
124
125impl std::fmt::Debug for VadSession {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        f.debug_struct("VadSession").finish_non_exhaustive()
128    }
129}
130
131// SAFETY: as for `Session` — `&mut self` on the mutating calls keeps use to
132// one thread at a time; deliberately NOT Sync.
133unsafe impl Send for VadSession {}
134
135impl Drop for VadSession {
136    fn drop(&mut self) {
137        unsafe { sys::transcribe_vad_session_free(self.ptr) };
138    }
139}
140
141const BUSY_RUN: &str = "a stream is active on this model; finish or drop it before vad run()";
142const BUSY_FEED: &str =
143    "a stream is active on this model; finish or drop it before vad stream_feed()";
144const BUSY_FLUSH: &str =
145    "a stream is active on this model; finish or drop it before vad stream_flush()";
146
147/// Copy the last call's result out; called under the compute lock.
148fn copy_result(ptr: *mut sys::transcribe_vad_session) -> Result<VadResult> {
149    let mut res: sys::transcribe_vad_result = unsafe { std::mem::zeroed() };
150    unsafe { sys::transcribe_vad_result_init(&mut res) };
151    check(
152        unsafe { sys::transcribe_vad_get_result(ptr, &mut res) },
153        "vad result",
154    )?;
155    let p = unsafe { sys::transcribe_vad_probs(ptr) };
156    let probs = if p.is_null() || res.n_probs <= 0 {
157        Vec::new()
158    } else {
159        unsafe { std::slice::from_raw_parts(p, res.n_probs as usize) }.to_vec()
160    };
161    let segments = (0..res.n_segments)
162        .map(|i| {
163            let mut raw: sys::transcribe_vad_segment = unsafe { std::mem::zeroed() };
164            unsafe { sys::transcribe_vad_segment_init(&mut raw) };
165            let _ = unsafe { sys::transcribe_vad_get_segment(ptr, i, &mut raw) };
166            VadSegment {
167                start_sample: raw.start_sample,
168                end_sample: raw.end_sample,
169            }
170        })
171        .collect();
172    Ok(VadResult {
173        probs,
174        first_frame: res.first_frame,
175        segments,
176    })
177}
178
179impl VadSession {
180    pub(crate) fn new(model: &Model, options: &VadSessionOptions) -> Result<VadSession> {
181        let mut params: sys::transcribe_vad_session_params = unsafe { std::mem::zeroed() };
182        unsafe { sys::transcribe_vad_session_params_init(&mut params) };
183        params.n_threads = options.n_threads;
184
185        let mut out: *mut sys::transcribe_vad_session = std::ptr::null_mut();
186        let status =
187            unsafe { sys::transcribe_vad_session_init(model.inner.ptr, &params, &mut out) };
188        check(status, "vad session init")?;
189        debug_assert!(!out.is_null());
190
191        Ok(VadSession {
192            ptr: out,
193            model: Arc::clone(&model.inner),
194            cancel: None,
195        })
196    }
197
198    /// Install a [`CancelToken`] so an in-flight run, feed or flush can be aborted
199    /// from another thread (the call then returns
200    /// [`Error::Aborted`](crate::Error)). Replaces any previously installed token.
201    pub fn set_cancel_token(&mut self, token: &CancelToken) {
202        let flag = Arc::clone(&token.flag);
203        let userdata = Arc::as_ptr(&flag) as *mut c_void;
204        unsafe {
205            sys::transcribe_vad_set_abort_callback(self.ptr, Some(abort_trampoline), userdata)
206        };
207        self.cancel = Some(flag);
208    }
209
210    /// Remove any installed cancel token.
211    pub fn clear_cancel_token(&mut self) {
212        unsafe { sys::transcribe_vad_set_abort_callback(self.ptr, None, std::ptr::null_mut()) };
213        self.cancel = None;
214    }
215
216    /// Score one clip of 16 kHz mono float32 PCM from a fresh state and
217    /// segment it. Resets any stream in progress.
218    pub fn run(&mut self, pcm: &[f32], options: &VadOptions) -> Result<VadResult> {
219        let n = clamp_len(pcm.len())?;
220        let mut params: sys::transcribe_vad_params = unsafe { std::mem::zeroed() };
221        unsafe { sys::transcribe_vad_params_init(&mut params) };
222        params.threshold = options.threshold;
223        params.neg_threshold = options.neg_threshold;
224        params.min_speech_ms = options.min_speech_ms;
225        params.min_silence_ms = options.min_silence_ms;
226        params.speech_pad_ms = options.speech_pad_ms;
227        params.max_speech_ms = options.max_speech_ms;
228
229        // Results are copied out under the compute lock, like every other
230        // binding, so a concurrent call on the same model cannot interleave.
231        let ptr = self.ptr;
232        self.model
233            .with_compute(Some(BUSY_RUN), |_| -> Result<VadResult> {
234                check(
235                    unsafe { sys::transcribe_vad_run(ptr, pcm.as_ptr(), n, &params) },
236                    "vad run",
237                )?;
238                copy_result(ptr)
239            })?
240    }
241
242    /// Append audio to the stream and return the probabilities of the frames
243    /// it completed; a partial frame waits for the next call. On CPU the
244    /// stream's probabilities equal a [`run`](Self::run) over the same audio.
245    /// [`Error::InvalidArgument`](crate::Error) (e.g. NaN input) leaves the
246    /// stream intact; an abort or processing error resets it to frame 0.
247    pub fn stream_feed(&mut self, pcm: &[f32]) -> Result<VadResult> {
248        let n = clamp_len(pcm.len())?;
249        let ptr = self.ptr;
250        self.model
251            .with_compute(Some(BUSY_FEED), |_| -> Result<VadResult> {
252                check(
253                    unsafe { sys::transcribe_vad_stream_feed(ptr, pcm.as_ptr(), n) },
254                    "vad stream feed",
255                )?;
256                copy_result(ptr)
257            })?
258    }
259
260    /// End the stream: score the buffered partial frame, zero padded (one
261    /// probability, or none), and reset. The next feed starts at frame 0; a
262    /// processing error also resets the stream.
263    pub fn stream_flush(&mut self) -> Result<VadResult> {
264        let ptr = self.ptr;
265        self.model
266            .with_compute(Some(BUSY_FLUSH), |_| -> Result<VadResult> {
267                check(
268                    unsafe { sys::transcribe_vad_stream_flush(ptr) },
269                    "vad stream flush",
270                )?;
271                copy_result(ptr)
272            })?
273    }
274
275    /// Drop the stream without scoring; the next feed starts at frame 0.
276    pub fn stream_reset(&mut self) {
277        let ptr = self.ptr;
278        let _ = self
279            .model
280            .with_compute(None, |_| unsafe { sys::transcribe_vad_stream_reset(ptr) });
281    }
282
283    /// Model load time plus the last call's stage timings (`encode_ms`: front
284    /// end and encoder; `decode_ms`: the recurrent decoder).
285    pub fn timings(&self) -> Timings {
286        let mut raw: sys::transcribe_timings = unsafe { std::mem::zeroed() };
287        unsafe { sys::transcribe_timings_init(&mut raw) };
288        let _ = unsafe { sys::transcribe_vad_get_timings(self.ptr, &mut raw) };
289        Timings::from_raw(&raw)
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use crate::Error;
297    use std::sync::Mutex;
298
299    #[test]
300    fn calls_are_busy_while_a_stream_holds_the_lease() {
301        // Null native handles (both frees are NULL no-ops): the Busy check runs
302        // under the lock before any native call.
303        let mut session = VadSession {
304            ptr: std::ptr::null_mut(),
305            model: Arc::new(ModelInner {
306                ptr: std::ptr::null_mut(),
307                compute_lock: Mutex::new(true),
308            }),
309            cancel: None,
310        };
311        for (r, msg) in [
312            (session.run(&[0.0; 160], &VadOptions::default()), BUSY_RUN),
313            (session.stream_feed(&[0.0; 160]), BUSY_FEED),
314            (session.stream_flush(), BUSY_FLUSH),
315        ] {
316            assert!(matches!(r, Err(Error::Busy(ref m)) if m == msg), "{r:?}");
317        }
318    }
319
320    #[test]
321    fn defaults_match_the_c_init() {
322        let mut p: sys::transcribe_vad_params = unsafe { std::mem::zeroed() };
323        unsafe { sys::transcribe_vad_params_init(&mut p) };
324        let d = VadOptions::default();
325        assert_eq!(
326            (
327                d.threshold,
328                d.neg_threshold,
329                d.min_speech_ms,
330                d.min_silence_ms,
331                d.speech_pad_ms,
332                d.max_speech_ms
333            ),
334            (
335                p.threshold,
336                p.neg_threshold,
337                p.min_speech_ms,
338                p.min_silence_ms,
339                p.speech_pad_ms,
340                p.max_speech_ms
341            )
342        );
343    }
344}