use crate::error::{Error, Result};
use crate::vad::options::{SpeechSegment, VadSegmentOptions};
use crate::vad::segment::segments_from_probabilities;
use boostr::model::audio::vad::SileroVad;
use numr::dtype::DType;
use numr::ops::{ConvOps, TensorOps};
use numr::runtime::{Runtime, RuntimeClient};
pub fn speech_timestamps<R, C>(
vad: &SileroVad<R>,
client: &C,
samples: &[f32],
opts: &VadSegmentOptions,
) -> Result<Vec<SpeechSegment>>
where
R: Runtime<DType = DType>,
C: RuntimeClient<R> + TensorOps<R> + ConvOps<R>,
{
let config = *vad.config();
let window = config.chunk_samples;
if window == 0 {
return Err(Error::InvalidArgument {
arg: "config.chunk_samples",
reason: "must be non-zero".to_string(),
});
}
let mut state = vad.new_state(client.device())?;
let chunks = samples.len().div_ceil(window);
let mut probs = Vec::with_capacity(chunks);
let mut padded = vec![0.0f32; window];
for i in 0..chunks {
let start = i * window;
let end = (start + window).min(samples.len());
let chunk = &samples[start..end];
let prob = if chunk.len() == window {
vad.chunk_probability(client, &mut state, chunk)?
} else {
padded[..chunk.len()].copy_from_slice(chunk);
for sample in &mut padded[chunk.len()..] {
*sample = 0.0;
}
vad.chunk_probability(client, &mut state, &padded)?
};
probs.push(prob);
}
segments_from_probabilities(&probs, samples.len(), config.sample_rate, window, opts)
}
#[cfg(test)]
mod tests {
use super::*;
use boostr::model::audio::vad::{
ENCODER_KERNEL, HIDDEN_SIZE, SileroVad, SileroVadWeights, VadConfig,
};
use numr::runtime::cpu::{CpuClient, CpuDevice, CpuRuntime};
use numr::tensor::Tensor;
fn patterned(shape: &[usize], scale: f32, device: &CpuDevice) -> Tensor<CpuRuntime> {
let n: usize = shape.iter().product();
let data: Vec<f32> = (0..n).map(|i| scale * ((i % 17) as f32 - 8.0)).collect();
Tensor::<CpuRuntime>::from_slice(&data, shape, device).expect("patterned tensor")
}
fn model(device: &CpuDevice) -> SileroVad<CpuRuntime> {
let config = VadConfig::silero_16k();
let bins = config.freq_bins();
let encoder: Vec<_> = config
.encoder_channels()
.iter()
.map(|&(in_c, out_c)| {
(
patterned(&[out_c, in_c, ENCODER_KERNEL], 0.001, device),
patterned(&[out_c], 0.01, device),
)
})
.collect();
let weights = SileroVadWeights {
stft_basis: patterned(&[2 * bins, 1, config.n_fft], 0.002, device),
encoder,
rnn_weight_ih: patterned(&[4 * HIDDEN_SIZE, HIDDEN_SIZE], 0.001, device),
rnn_weight_hh: patterned(&[4 * HIDDEN_SIZE, HIDDEN_SIZE], 0.001, device),
rnn_bias_ih: patterned(&[4 * HIDDEN_SIZE], 0.01, device),
rnn_bias_hh: patterned(&[4 * HIDDEN_SIZE], 0.01, device),
head_weight: patterned(&[1, HIDDEN_SIZE, 1], 0.01, device),
head_bias: patterned(&[1], 0.02, device),
};
SileroVad::new(config, weights).expect("synthetic weights are shape-correct")
}
fn signal(len: usize) -> Vec<f32> {
(0..len).map(|i| (i as f32 * 0.01).sin() * 0.5).collect()
}
#[test]
fn matches_the_segmenter_over_the_zero_padded_chunk_grid() {
let device = CpuDevice::new();
let client = CpuClient::new(device.clone());
let vad = model(&device);
let window = vad.config().chunk_samples;
let samples = signal(3 * window + 100);
let mut padded = samples.clone();
padded.resize(4 * window, 0.0);
let probs = vad.probabilities(&client, &padded).expect("probabilities");
assert_eq!(probs.len(), 4, "the partial chunk must be scored");
let opts = VadSegmentOptions {
threshold: 0.02,
min_speech_duration_ms: 0,
min_silence_duration_ms: 0,
speech_pad_ms: 0,
..VadSegmentOptions::default()
};
let expected = segments_from_probabilities(&probs, samples.len(), 16000, window, &opts)
.expect("segment");
let got = speech_timestamps(&vad, &client, &samples, &opts).expect("timestamps");
assert_eq!(got, expected);
for seg in &got {
assert!(seg.end <= samples.len(), "segment past the signal: {seg:?}");
}
}
#[test]
fn an_empty_signal_yields_no_segments() {
let device = CpuDevice::new();
let client = CpuClient::new(device.clone());
let vad = model(&device);
let got = speech_timestamps(&vad, &client, &[], &VadSegmentOptions::default())
.expect("empty signal");
assert!(got.is_empty());
}
}