#[cfg(feature = "vad")]
use std::path::PathBuf;
use crate::error::DecibriError;
#[derive(Debug, Clone)]
pub struct VadConfig {
pub model_path: PathBuf,
pub sample_rate: u32,
pub threshold: f32,
}
impl Default for VadConfig {
fn default() -> Self {
Self {
model_path: PathBuf::from("silero_vad.onnx"),
sample_rate: 16000,
threshold: 0.5,
}
}
}
#[derive(Debug, Clone)]
pub struct VadResult {
pub probability: f32,
pub is_speech: bool,
}
#[cfg(feature = "vad")]
pub struct SileroVad {
session: ort::session::Session,
state: Vec<f32>,
sample_rate: u32,
threshold: f32,
accumulator: Vec<f32>,
window_size: usize,
}
const STATE_SIZE: usize = 256;
#[cfg(feature = "vad")]
impl SileroVad {
pub fn new(config: VadConfig) -> Result<Self, DecibriError> {
let window_size = match config.sample_rate {
16000 => 512,
8000 => 256,
_ => {
return Err(DecibriError::Other(
"Silero VAD only supports sample rates 8000 and 16000".to_string(),
))
}
};
if config.threshold < 0.0 || config.threshold > 1.0 {
return Err(DecibriError::Other(
"VAD threshold must be between 0.0 and 1.0".to_string(),
));
}
let session = ort::session::Session::builder()
.map_err(|e| DecibriError::Other(format!("Failed to create ort session builder: {e}")))?
.with_intra_threads(1)
.map_err(|e| DecibriError::Other(format!("Failed to set ort threads: {e}")))?
.commit_from_file(&config.model_path)
.map_err(|e| {
DecibriError::Other(format!(
"Failed to load Silero VAD model from {}: {e}",
config.model_path.display()
))
})?;
Ok(Self {
session,
state: vec![0.0f32; STATE_SIZE],
sample_rate: config.sample_rate,
threshold: config.threshold,
accumulator: Vec::new(),
window_size,
})
}
pub fn process(&mut self, samples: &[f32]) -> Result<VadResult, DecibriError> {
self.accumulator.extend_from_slice(samples);
let mut max_probability: f32 = 0.0;
let mut windows_processed = 0;
while self.accumulator.len() >= self.window_size {
let window: Vec<f32> = self.accumulator.drain(..self.window_size).collect();
let probability = self.infer_window(&window)?;
max_probability = max_probability.max(probability);
windows_processed += 1;
}
if windows_processed == 0 {
return Ok(VadResult {
probability: 0.0,
is_speech: false,
});
}
Ok(VadResult {
probability: max_probability,
is_speech: max_probability >= self.threshold,
})
}
pub fn reset(&mut self) {
self.state.fill(0.0);
self.accumulator.clear();
}
fn infer_window(&mut self, window: &[f32]) -> Result<f32, DecibriError> {
let input_tensor =
ort::value::Tensor::from_array(([1i64, self.window_size as i64], window.to_vec()))
.map_err(|e| DecibriError::Other(format!("Failed to create input tensor: {e}")))?;
let state_tensor =
ort::value::Tensor::from_array(([2i64, 1i64, 128i64], self.state.clone()))
.map_err(|e| DecibriError::Other(format!("Failed to create state tensor: {e}")))?;
let sr_tensor = ort::value::Tensor::from_array(([1i64], vec![self.sample_rate as i64]))
.map_err(|e| DecibriError::Other(format!("Failed to create sr tensor: {e}")))?;
let input_values = ort::inputs![
"input" => input_tensor,
"state" => state_tensor,
"sr" => sr_tensor,
];
let outputs = self
.session
.run(input_values)
.map_err(|e| DecibriError::Other(format!("Silero VAD inference failed: {e}")))?;
let prob_tensor = outputs["output"]
.try_extract_tensor::<f32>()
.map_err(|e| DecibriError::Other(format!("Failed to extract output tensor: {e}")))?;
let probability = prob_tensor.1[0];
let state_tensor = outputs["stateN"]
.try_extract_tensor::<f32>()
.map_err(|e| DecibriError::Other(format!("Failed to extract state tensor: {e}")))?;
self.state.copy_from_slice(state_tensor.1);
Ok(probability)
}
}
#[cfg(all(test, feature = "vad"))]
mod tests {
use super::*;
use std::path::Path;
fn model_path() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
Path::new(manifest_dir)
.join("..")
.join("..")
.join("models")
.join("silero_vad.onnx")
}
fn default_config() -> VadConfig {
VadConfig {
model_path: model_path(),
sample_rate: 16000,
threshold: 0.5,
}
}
#[test]
fn test_vad_config_validation() {
let bad_rate = VadConfig {
sample_rate: 44100,
..default_config()
};
assert!(SileroVad::new(bad_rate).is_err());
}
#[test]
fn test_vad_loads_model() {
let vad = SileroVad::new(default_config());
assert!(vad.is_ok(), "Model should load: {:?}", vad.err());
}
#[test]
fn test_vad_silence() {
let mut vad = SileroVad::new(default_config()).unwrap();
let silence = vec![0.0f32; 512];
let result = vad.process(&silence).unwrap();
assert!(
result.probability < 0.5,
"Silence probability should be low, got {}",
result.probability
);
}
#[test]
fn test_vad_state_persistence() {
let mut vad = SileroVad::new(default_config()).unwrap();
let initial_state = vad.state.clone();
let samples = vec![0.0f32; 512];
vad.process(&samples).unwrap();
assert_ne!(
vad.state, initial_state,
"State should change after inference"
);
}
#[test]
fn test_vad_accumulator_windows() {
let mut vad = SileroVad::new(default_config()).unwrap();
let samples = vec![0.0f32; 1600];
let result = vad.process(&samples).unwrap();
assert!(result.probability >= 0.0);
assert_eq!(vad.accumulator.len(), 64);
}
#[test]
fn test_vad_accumulator_carry() {
let mut vad = SileroVad::new(default_config()).unwrap();
let chunk1 = vec![0.0f32; 1600];
vad.process(&chunk1).unwrap();
assert_eq!(vad.accumulator.len(), 64);
let chunk2 = vec![0.0f32; 1600];
vad.process(&chunk2).unwrap();
assert_eq!(vad.accumulator.len(), 128);
}
#[test]
fn test_vad_small_chunk() {
let mut vad = SileroVad::new(default_config()).unwrap();
let samples = vec![0.0f32; 100];
let result = vad.process(&samples).unwrap();
assert_eq!(result.probability, 0.0);
assert_eq!(vad.accumulator.len(), 100);
}
#[test]
fn test_vad_reset() {
let mut vad = SileroVad::new(default_config()).unwrap();
let samples = vec![0.0f32; 512];
vad.process(&samples).unwrap();
vad.accumulator.extend_from_slice(&[0.0; 100]);
vad.reset();
assert!(vad.state.iter().all(|&v| v == 0.0), "State should be zeros");
assert!(vad.accumulator.is_empty(), "Accumulator should be empty");
}
}