use std::{collections::VecDeque, path::Path};
use crate::{
detector::Postprocessor,
error::Result,
event::{FrameResult, SpeechSegment},
features::{FeatureExtractor, NUM_MEL_BINS},
inference::OrtRunner,
options::VadOptions,
};
#[cfg(feature = "bundled")]
#[cfg_attr(docsrs, doc(cfg(feature = "bundled")))]
pub const BUNDLED_MODEL: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/models/fireredvad_stream_vad_with_cache.onnx"
));
#[cfg(feature = "bundled")]
#[cfg_attr(docsrs, doc(cfg(feature = "bundled")))]
pub const BUNDLED_CMVN: &[u8] =
include_bytes!(concat!(env!("CARGO_MANIFEST_DIR"), "/models/cmvn.ark"));
pub struct Vad {
runner: OrtRunner,
features: FeatureExtractor,
detector: Postprocessor,
pending_segments: VecDeque<SpeechSegment>,
feature_scratch: Vec<f32>,
recent_frames: Vec<FrameResult>,
finished: bool,
}
impl Vad {
#[cfg(feature = "bundled")]
#[cfg_attr(docsrs, doc(cfg(feature = "bundled")))]
pub fn bundled() -> Result<Self> {
Self::bundled_with(VadOptions::default())
}
#[cfg(feature = "bundled")]
#[cfg_attr(docsrs, doc(cfg(feature = "bundled")))]
pub fn bundled_with(options: VadOptions) -> Result<Self> {
Self::from_memory_with_cmvn(BUNDLED_MODEL, BUNDLED_CMVN, options)
}
#[cfg(feature = "bundled")]
#[cfg_attr(docsrs, doc(cfg(feature = "bundled")))]
pub fn from_memory(model: &[u8]) -> Result<Self> {
Self::from_memory_with(model, VadOptions::default())
}
#[cfg(feature = "bundled")]
#[cfg_attr(docsrs, doc(cfg(feature = "bundled")))]
pub fn from_memory_with(model: &[u8], options: VadOptions) -> Result<Self> {
Self::from_memory_with_cmvn(model, BUNDLED_CMVN, options)
}
#[cfg(feature = "bundled")]
#[cfg_attr(docsrs, doc(cfg(feature = "bundled")))]
pub fn from_file(model: impl AsRef<Path>) -> Result<Self> {
Self::from_file_with(model, VadOptions::default())
}
#[cfg(feature = "bundled")]
#[cfg_attr(docsrs, doc(cfg(feature = "bundled")))]
pub fn from_file_with(model: impl AsRef<Path>, options: VadOptions) -> Result<Self> {
let runner = OrtRunner::from_file(model, options.session_options())?;
Self::wrap(runner, BUNDLED_CMVN, options)
}
pub fn from_memory_with_cmvn(model: &[u8], cmvn: &[u8], options: VadOptions) -> Result<Self> {
let runner = OrtRunner::from_memory(model, options.session_options())?;
Self::wrap(runner, cmvn, options)
}
pub fn from_file_with_cmvn(
model: impl AsRef<Path>,
cmvn: impl AsRef<Path>,
options: VadOptions,
) -> Result<Self> {
let runner = OrtRunner::from_file(model, options.session_options())?;
let cmvn_bytes =
std::fs::read(cmvn.as_ref()).map_err(|source| crate::error::Error::LoadCmvn {
path: cmvn.as_ref().to_path_buf(),
source,
})?;
Self::wrap(runner, &cmvn_bytes, options)
}
pub fn from_ort_session(
session: ort::session::Session,
cmvn: &[u8],
options: VadOptions,
) -> Result<Self> {
let runner = OrtRunner::from_ort_session(session);
Self::wrap(runner, cmvn, options)
}
fn wrap(runner: OrtRunner, cmvn: &[u8], options: VadOptions) -> Result<Self> {
let features = FeatureExtractor::new(cmvn)?;
let detector = Postprocessor::new(options.clone());
Ok(Self {
runner,
features,
detector,
pending_segments: VecDeque::new(),
feature_scratch: vec![0.0; NUM_MEL_BINS],
recent_frames: Vec::new(),
finished: false,
})
}
pub fn push_samples(&mut self, pcm: &[f32]) -> Result<Option<SpeechSegment>> {
let Self {
runner,
features,
detector,
pending_segments,
feature_scratch,
recent_frames,
..
} = self;
if !pcm.is_empty() {
recent_frames.clear();
features.push_pcm(pcm);
while features.has_full_window() {
features.extract_one(feature_scratch);
runner.push_feature(feature_scratch);
}
if runner.pending_feature_frames() > 0 {
let probs = runner.infer()?;
for &prob in probs {
let (frame_result, segment) = detector.push_probability(prob);
recent_frames.push(frame_result);
if let Some(s) = segment {
pending_segments.push_back(s);
}
}
}
}
Ok(pending_segments.pop_front())
}
pub fn finish(&mut self) -> Result<Option<SpeechSegment>> {
self.finished = true;
if let Some(segment) = self.detector.finish_active() {
self.pending_segments.push_back(segment);
}
Ok(self.pending_segments.pop_front())
}
pub fn reset(&mut self) {
self.runner.reset();
self.features.reset();
self.detector.reset();
self.pending_segments.clear();
self.recent_frames.clear();
self.finished = false;
}
pub const fn options(&self) -> &VadOptions {
self.detector_options()
}
const fn detector_options(&self) -> &VadOptions {
self.detector.options_const()
}
pub fn set_options(&mut self, options: VadOptions) {
self.detector.set_options(options);
}
pub const fn frame_count(&self) -> u64 {
self.detector.frame_count()
}
pub fn pending_samples(&self) -> usize {
self.features.pending_samples()
}
pub fn is_active(&self) -> bool {
self.detector.is_active()
}
pub const fn is_finished(&self) -> bool {
self.finished
}
pub fn pending_segments(&self) -> usize {
self.pending_segments.len()
}
pub fn recent_frames(&self) -> &[FrameResult] {
&self.recent_frames
}
#[cfg(feature = "_debug-features")]
#[doc(hidden)]
pub fn _debug_extract_mel_features(&mut self, pcm: &[f32]) -> Vec<f32> {
self.features.reset();
self.features.push_pcm(pcm);
let mut out = Vec::new();
while self.features.has_full_window() {
let mut frame = vec![0.0; NUM_MEL_BINS];
self.features.extract_one(&mut frame);
out.extend_from_slice(&frame);
}
out
}
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(feature = "bundled")]
#[test]
fn bundled_constructs_with_defaults() {
let _ = Vad::bundled().expect("bundled constructs");
}
#[cfg(feature = "bundled")]
#[test]
fn one_second_of_silence_emits_no_segment() {
let mut vad = Vad::bundled().expect("bundled constructs");
vad.push_samples(&vec![0.0; 16_000]).expect("push silence");
let mut segments = 0usize;
while vad.push_samples(&[]).expect("drain").is_some() {
segments += 1;
}
assert_eq!(segments, 0);
assert!(!vad.is_active());
}
#[cfg(feature = "bundled")]
#[test]
fn reset_clears_segment_queue_and_frame_counter() {
let mut vad = Vad::bundled().expect("bundled");
vad.push_samples(&vec![0.0; 1_600]).expect("push 100ms");
vad.reset();
assert_eq!(vad.frame_count(), 0);
assert_eq!(vad.pending_segments(), 0);
assert_eq!(vad.pending_samples(), 0);
assert!(!vad.is_finished());
}
#[cfg(feature = "bundled")]
#[test]
fn finish_marks_finished_and_flushes_no_segment_when_idle() {
let mut vad = Vad::bundled().expect("bundled");
let result = vad.finish().expect("finish");
assert!(vad.is_finished());
assert!(result.is_none());
let mut segments = 0usize;
while vad.push_samples(&[]).expect("drain").is_some() {
segments += 1;
}
assert_eq!(segments, 0);
}
#[cfg(feature = "bundled")]
#[test]
fn recent_frames_captures_one_frameresult_per_10ms_frame() {
let mut vad = Vad::bundled().expect("bundled");
vad.push_samples(&vec![0.0; 1040]).expect("push samples");
let frames = vad.recent_frames();
assert_eq!(frames.len(), 5);
for (i, frame) in frames.iter().enumerate() {
assert_eq!(frame.frame_index(), i as u64);
assert!(frame.raw_prob() >= 0.0 && frame.raw_prob() <= 1.0);
assert!(frame.smoothed_prob() >= 0.0 && frame.smoothed_prob() <= 1.0);
}
}
#[cfg(feature = "bundled")]
#[test]
fn recent_frames_is_cleared_at_each_non_empty_push() {
let mut vad = Vad::bundled().expect("bundled");
vad.push_samples(&vec![0.0; 1040]).expect("push 1");
let count_after_first = vad.recent_frames().len();
assert!(count_after_first > 0);
vad.push_samples(&vec![0.0; 320]).expect("push 2"); assert_eq!(vad.recent_frames().len(), 2);
}
#[cfg(feature = "bundled")]
#[test]
fn recent_frames_is_empty_after_reset_and_after_drain_calls() {
let mut vad = Vad::bundled().expect("bundled");
vad.push_samples(&vec![0.0; 1040]).expect("push");
assert!(!vad.recent_frames().is_empty());
vad.reset();
assert!(vad.recent_frames().is_empty());
vad.push_samples(&vec![0.0; 1040]).expect("push");
assert!(!vad.recent_frames().is_empty());
let _ = vad.push_samples(&[]).expect("drain"); assert!(
!vad.recent_frames().is_empty(),
"drain calls preserve recent_frames"
);
}
}