use burn::{
config::Config,
module::Module,
nn::{
Linear,
LinearConfig,
LinearLayout,
PaddingConfig1d,
conv::{
Conv1d,
Conv1dConfig,
},
},
prelude::{
Backend,
Tensor,
s,
},
tensor::{
activation::{
relu,
sigmoid,
},
ops::PadMode,
},
};
use crate::{
blocks::conv::{
ConvBlock1dConfig,
ConvSeq1d,
ConvSeq1dConfig,
ConvSeq1dMeta,
},
burner::module::ModuleInit,
errors::{
BunsenError,
BunsenResult,
},
};
const HIDDEN: usize = 128;
pub trait SileroVadMeta {
fn sample_rate(&self) -> usize;
fn input_pad(&self) -> usize;
fn n_freq(&self) -> usize;
fn hidden_size(&self) -> usize;
fn gate_size(&self) -> usize {
4 * self.hidden_size()
}
}
#[derive(Config, Debug)]
pub struct SileroVadConfig {
pub sample_rate: usize,
pub input_pad: usize,
pub stft: Conv1dConfig,
pub encoder: ConvSeq1dConfig,
pub hidden_gate: LinearConfig,
pub input_gate: LinearConfig,
pub decoder: Conv1dConfig,
}
impl SileroVadMeta for SileroVadConfig {
fn sample_rate(&self) -> usize {
self.sample_rate
}
fn input_pad(&self) -> usize {
self.input_pad
}
fn n_freq(&self) -> usize {
self.stft.channels_out / 2
}
fn hidden_size(&self) -> usize {
self.encoder.out_channels()
}
}
impl SileroVadConfig {
pub fn standard_16khz() -> Self {
Self::standard(16000, 64, 256, 128, 129)
}
pub fn standard_8khz() -> Self {
Self::standard(8000, 32, 128, 64, 65)
}
fn standard(
sample_rate: usize,
input_pad: usize,
stft_kernel: usize,
stft_stride: usize,
n_freq: usize,
) -> Self {
Self {
sample_rate,
input_pad,
stft: Conv1dConfig::new(1, 2 * n_freq, stft_kernel)
.with_stride(stft_stride)
.with_padding(PaddingConfig1d::Valid)
.with_bias(false),
encoder: encoder_config(n_freq),
hidden_gate: lstm_gate_config(HIDDEN),
input_gate: lstm_gate_config(HIDDEN),
decoder: Conv1dConfig::new(HIDDEN, 1, 1)
.with_padding(PaddingConfig1d::Valid)
.with_bias(true),
}
}
pub fn validate(&self) -> BunsenResult<()> {
self.encoder.validate()?;
let hidden = self.hidden_size();
if self.encoder.in_channels() != self.n_freq() {
return Err(BunsenError::Invalid(format!(
"SileroVad encoder in_channels ({}) != n_freq ({})",
self.encoder.in_channels(),
self.n_freq(),
)));
}
if self.hidden_gate.d_input != hidden || self.input_gate.d_input != hidden {
return Err(BunsenError::Invalid(format!(
"SileroVad gate inputs ({}, {}) must both equal hidden ({hidden})",
self.hidden_gate.d_input, self.input_gate.d_input,
)));
}
if self.hidden_gate.d_output != self.gate_size()
|| self.input_gate.d_output != self.gate_size()
{
return Err(BunsenError::Invalid(format!(
"SileroVad gate outputs ({}, {}) must both equal gate_size ({})",
self.hidden_gate.d_output,
self.input_gate.d_output,
self.gate_size(),
)));
}
if self.decoder.channels_in != hidden || self.decoder.channels_out != 1 {
return Err(BunsenError::Invalid(format!(
"SileroVad decoder must map hidden ({hidden}) -> 1, got {} -> {}",
self.decoder.channels_in, self.decoder.channels_out,
)));
}
Ok(())
}
}
impl<B: Backend> ModuleInit<B, SileroVad<B>> for SileroVadConfig {
fn try_init(
&self,
device: &B::Device,
) -> BunsenResult<SileroVad<B>> {
self.validate()?;
Ok(SileroVad {
sample_rate: self.sample_rate,
input_pad: self.input_pad,
stft: self.stft.init(device),
encoder: self.encoder.try_init(device)?,
hidden_gate: self.hidden_gate.init(device),
input_gate: self.input_gate.init(device),
decoder: self.decoder.init(device),
})
}
}
#[derive(Module, Debug)]
pub struct SileroVad<B: Backend> {
sample_rate: usize,
input_pad: usize,
pub stft: Conv1d<B>,
pub encoder: ConvSeq1d<B>,
pub hidden_gate: Linear<B>,
pub input_gate: Linear<B>,
pub decoder: Conv1d<B>,
}
impl<B: Backend> SileroVadMeta for SileroVad<B> {
fn sample_rate(&self) -> usize {
self.sample_rate
}
fn input_pad(&self) -> usize {
self.input_pad
}
fn n_freq(&self) -> usize {
self.stft.weight.dims()[0] / 2
}
fn hidden_size(&self) -> usize {
self.encoder.out_channels()
}
}
impl<B: Backend> SileroVad<B> {
pub fn init_state(
&self,
batch: usize,
device: &B::Device,
) -> Tensor<B, 3> {
Tensor::zeros([2, batch, self.hidden_size()], device)
}
pub fn frame_features(
&self,
input: Tensor<B, 2>,
) -> Tensor<B, 2> {
let x = input.pad([(0, 0), (0, self.input_pad)], PadMode::Reflect);
let x: Tensor<B, 3> = x.unsqueeze_dim::<3>(1);
let x = self.stft.forward(x);
let f = x.dims()[1] / 2;
let real = x.clone().slice_dim(1, ..f);
let imag = x.slice_dim(1, f..);
let mag = (real.powi_scalar(2) + imag.powi_scalar(2)).sqrt();
let encoded = self.encoder.forward(mag);
encoded.slice_dim(2, 0).squeeze_dim::<2>(2)
}
pub fn lstm_step(
&self,
feature: Tensor<B, 2>,
hidden: Tensor<B, 2>,
cell: Tensor<B, 2>,
) -> (Tensor<B, 2>, Tensor<B, 2>) {
let gates = self.input_gate.forward(feature) + self.hidden_gate.forward(hidden);
let [g_i, g_f, g_g, g_o] = gates.chunk(4, 1).try_into().unwrap();
let i = sigmoid(g_i);
let forget = sigmoid(g_f);
let g = g_g.tanh();
let o = sigmoid(g_o);
let new_cell = forget * cell + i * g;
let new_hidden = o * new_cell.clone().tanh();
(new_hidden, new_cell)
}
pub fn output_head(
&self,
hidden: Tensor<B, 2>,
) -> Tensor<B, 2> {
let x: Tensor<B, 3> = hidden.unsqueeze_dim::<3>(2);
let x = relu(x);
let x = self.decoder.forward(x);
let x = sigmoid(x);
x.squeeze_dim::<2>(2)
}
fn unpack_state(state: Tensor<B, 3>) -> (Tensor<B, 2>, Tensor<B, 2>) {
let hidden = state.clone().slice_dim(0, 0).squeeze_dim::<2>(0);
let cell = state.slice_dim(0, 1).squeeze_dim::<2>(0);
(hidden, cell)
}
fn pack_state(
hidden: Tensor<B, 2>,
cell: Tensor<B, 2>,
) -> Tensor<B, 3> {
Tensor::stack(vec![hidden, cell], 0)
}
pub fn forward(
&self,
input: Tensor<B, 2>,
state: Tensor<B, 3>,
) -> (Tensor<B, 2>, Tensor<B, 3>) {
let feature = self.frame_features(input);
let (hidden, cell) = Self::unpack_state(state);
let (hidden, cell) = self.lstm_step(feature, hidden, cell);
let prob = self.output_head(hidden.clone());
(prob, Self::pack_state(hidden, cell))
}
pub fn forward_sequence(
&self,
input: Tensor<B, 2>,
state: Tensor<B, 3>,
) -> (Tensor<B, 2>, Tensor<B, 3>) {
let features = self.frame_features(input);
let steps = features.dims()[0];
let (mut hidden, mut cell) = Self::unpack_state(state);
let mut hidden_steps = Vec::with_capacity(steps);
for step in 0..steps {
let feature = features.clone().slice(s![step..step + 1, ..]);
let (new_hidden, new_cell) = self.lstm_step(feature, hidden, cell);
hidden = new_hidden.clone();
cell = new_cell;
hidden_steps.push(new_hidden);
}
let all_hidden = Tensor::cat(hidden_steps, 0);
let probs = self.output_head(all_hidden);
(probs, Self::pack_state(hidden, cell))
}
}
fn encoder_config(n_freq: usize) -> ConvSeq1dConfig {
let block = |in_channels: usize, out_channels: usize, stride: usize| {
ConvBlock1dConfig::new(
Conv1dConfig::new(in_channels, out_channels, 3)
.with_stride(stride)
.with_padding(PaddingConfig1d::Explicit(1, 1))
.with_bias(true),
)
};
ConvSeq1dConfig::new(vec![
block(n_freq, HIDDEN, 1),
block(HIDDEN, 64, 2),
block(64, 64, 2),
block(64, HIDDEN, 1),
])
}
fn lstm_gate_config(hidden: usize) -> LinearConfig {
LinearConfig::new(hidden, 4 * hidden)
.with_bias(true)
.with_layout(LinearLayout::Col)
}
#[cfg(test)]
mod tests {
use burn::tensor::{
Distribution,
Tolerance,
};
use super::*;
use crate::support::testing::CpuBackend;
type B = CpuBackend;
fn chunk_samples(sample_rate: usize) -> usize {
match sample_rate {
16000 => 512,
8000 => 256,
other => panic!("no test chunk for {other}"),
}
}
#[test]
fn test_config_meta() {
let cfg16 = SileroVadConfig::standard_16khz();
assert_eq!(cfg16.sample_rate(), 16000);
assert_eq!(cfg16.input_pad(), 64);
assert_eq!(cfg16.n_freq(), 129);
assert_eq!(cfg16.hidden_size(), 128);
assert_eq!(cfg16.gate_size(), 512);
assert_eq!(cfg16.encoder.in_channels(), cfg16.n_freq());
cfg16.validate().unwrap();
let cfg8 = SileroVadConfig::standard_8khz();
assert_eq!(cfg8.sample_rate(), 8000);
assert_eq!(cfg8.input_pad(), 32);
assert_eq!(cfg8.n_freq(), 65);
assert_eq!(cfg8.hidden_size(), 128);
assert_eq!(cfg8.encoder.in_channels(), cfg8.n_freq());
cfg8.validate().unwrap();
}
#[test]
fn test_validate_rejects_mismatch() {
let bad = SileroVadConfig {
encoder: encoder_config(64),
..SileroVadConfig::standard_16khz()
};
assert!(matches!(bad.validate(), Err(BunsenError::Invalid(_))));
}
#[test]
fn test_config_meta_matches_module() {
let device = Default::default();
for (cfg, n_freq) in [
(SileroVadConfig::standard_16khz(), 129),
(SileroVadConfig::standard_8khz(), 65),
] {
let model: SileroVad<B> = cfg.init(&device);
assert_eq!(model.sample_rate(), cfg.sample_rate());
assert_eq!(model.hidden_size(), cfg.hidden_size());
assert_eq!(model.n_freq(), n_freq);
assert_eq!(model.input_pad(), cfg.input_pad());
}
}
#[test]
fn test_forward_shapes_and_range() {
let device = Default::default();
for cfg in [
SileroVadConfig::standard_16khz(),
SileroVadConfig::standard_8khz(),
] {
let model: SileroVad<B> = cfg.init(&device);
let batch = 3;
let input = Tensor::<B, 2>::random(
[batch, chunk_samples(model.sample_rate())],
Distribution::Default,
&device,
);
let state = model.init_state(batch, &device);
let (prob, next_state) = model.forward(input, state);
assert_eq!(prob.dims(), [batch, 1]);
assert_eq!(next_state.dims(), [2, batch, 128]);
let probs: Vec<f32> = prob.into_data().to_vec().unwrap();
assert!(probs.iter().all(|&p| (0.0..=1.0).contains(&p)));
}
}
#[test]
fn test_forward_sequence_shapes() {
let device = Default::default();
for cfg in [
SileroVadConfig::standard_16khz(),
SileroVadConfig::standard_8khz(),
] {
let model: SileroVad<B> = cfg.init(&device);
let steps = 5;
let input = Tensor::<B, 2>::random(
[steps, chunk_samples(model.sample_rate())],
Distribution::Default,
&device,
);
let state = model.init_state(1, &device);
let (probs, next_state) = model.forward_sequence(input, state);
assert_eq!(probs.dims(), [steps, 1]);
assert_eq!(next_state.dims(), [2, 1, 128]);
}
}
#[test]
fn test_sequence_matches_stepwise() {
let device = Default::default();
let model: SileroVad<B> = SileroVadConfig::standard_16khz().init(&device);
let steps = 4;
let input = Tensor::<B, 2>::random(
[steps, chunk_samples(model.sample_rate())],
Distribution::Default,
&device,
);
let (seq_probs, seq_state) =
model.forward_sequence(input.clone(), model.init_state(1, &device));
let mut state = model.init_state(1, &device);
let mut step_probs = Vec::with_capacity(steps);
for step in 0..steps {
let chunk = input.clone().slice(s![step..step + 1, ..]);
let (prob, next_state) = model.forward(chunk, state);
state = next_state;
step_probs.push(prob);
}
let step_probs = Tensor::cat(step_probs, 0);
let tol = Tolerance::<f32>::default();
seq_probs
.into_data()
.assert_approx_eq::<f32>(&step_probs.into_data(), tol);
seq_state
.into_data()
.assert_approx_eq::<f32>(&state.into_data(), tol);
}
}