This commit is contained in:
valenti
2026-10-16 11:36:02 +01:00
commit 236fd509ca
59 changed files with 18858 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
//! Voice Activity Detection (Silero VAD v5 via ONNX Runtime).
//!
//! The detector runs on the 16 kHz mono stream produced by the session layer
//! (the same stream fed to the STT engine) and emits three kinds of events:
//!
//! - [`VadEvent::SpeechOnset`] — fast onset, used to interrupt the agent
//! (barge-in) as soon as the user starts talking.
//! - [`VadEvent::SpeechStart`] — confirmed utterance start.
//! - [`VadEvent::SpeechEnd`] — utterance finished, carrying the captured audio
//! (with a pre-roll pad) that is handed to the STT engine.
//!
//! Every behavioural knob is configured from the environment (see
//! [`VadSettings`]), so the feature can be tuned or disabled without a rebuild.
pub mod silero;
pub use silero::{SileroVad, VadDetector, VadEvent};
use crate::config::{env_bool, env_f32, env_u32};
use std::path::PathBuf;
/// Runtime configuration for the VAD subsystem, read from `.env`.
#[derive(Debug, Clone)]
pub struct VadSettings {
/// Master switch (`VAD_ENABLED`). When false the legacy RMS segmentation
/// inside the STT worker is used instead.
pub enabled: bool,
/// Path to `silero_vad.onnx` (`VAD_MODEL_PATH`).
pub model_path: PathBuf,
/// Speech probability above which a window counts as voice
/// (`VAD_THRESHOLD`, default 0.5).
pub threshold: f32,
/// Probability below which a window counts as silence
/// (`VAD_NEG_THRESHOLD`). Must be lower than `threshold`; the gap is the
/// hysteresis that prevents rapid onset/offset flapping.
pub neg_threshold: f32,
/// Speech required to confirm an utterance (`VAD_MIN_SPEECH_MS`).
pub min_speech_ms: u32,
/// Silence required to close an utterance — the EOU window
/// (`VAD_MIN_SILENCE_MS`).
pub min_silence_ms: u32,
/// Audio kept before the detected onset so the first phoneme is not
/// clipped (`VAD_SPEECH_PAD_MS`).
pub speech_pad_ms: u32,
/// Speech required to fire the barge-in onset (`VAD_BARGE_IN_MIN_SPEECH_MS`).
pub barge_in_min_speech_ms: u32,
/// Feed the STT engine with VAD-segmented utterances instead of letting it
/// run its own RMS segmentation (`VAD_SEGMENT_STT`).
pub segment_stt: bool,
/// Interrupt the agent when the user starts speaking over it
/// (`VAD_BARGE_IN`).
pub barge_in: bool,
/// Log the speech probability of every analysis window (`VAD_DEBUG`).
pub debug: bool,
}
impl Default for VadSettings {
fn default() -> Self {
Self {
enabled: true,
model_path: PathBuf::from("models/silero_vad.onnx"),
threshold: 0.5,
neg_threshold: 0.35,
min_speech_ms: 250,
min_silence_ms: 300,
speech_pad_ms: 30,
barge_in_min_speech_ms: 150,
segment_stt: true,
barge_in: true,
debug: false,
}
}
}
impl VadSettings {
/// Build the settings from environment variables, falling back to defaults.
pub fn from_env() -> Self {
let mut s = Self::default();
if let Ok(p) = std::env::var("VAD_MODEL_PATH") {
if !p.is_empty() {
s.model_path = PathBuf::from(p);
}
}
s.enabled = env_bool("VAD_ENABLED", s.enabled);
s.segment_stt = env_bool("VAD_SEGMENT_STT", s.segment_stt);
s.barge_in = env_bool("VAD_BARGE_IN", s.barge_in);
s.debug = env_bool("VAD_DEBUG", s.debug);
s.threshold = env_f32("VAD_THRESHOLD", s.threshold);
s.neg_threshold = env_f32("VAD_NEG_THRESHOLD", s.neg_threshold);
s.min_speech_ms = env_u32("VAD_MIN_SPEECH_MS", s.min_speech_ms);
s.min_silence_ms = env_u32("VAD_MIN_SILENCE_MS", s.min_silence_ms);
s.speech_pad_ms = env_u32("VAD_SPEECH_PAD_MS", s.speech_pad_ms);
s.barge_in_min_speech_ms = env_u32("VAD_BARGE_IN_MIN_SPEECH_MS", s.barge_in_min_speech_ms);
// Hysteresis must stay below the speech threshold, and the fast
// barge-in onset can never be slower than the utterance onset.
s.neg_threshold = s.neg_threshold.min(s.threshold);
s.barge_in_min_speech_ms = s.barge_in_min_speech_ms.min(s.min_speech_ms);
s
}
}
/// Samples retained ahead of a detected onset (pre-roll + triggering speech).
pub(crate) fn preroll_samples(settings: &VadSettings) -> usize {
let ms = settings
.speech_pad_ms
.saturating_add(settings.min_speech_ms)
.saturating_add(silero::WINDOW_MS);
(crate::config::STT_SAMPLE_RATE as u64 * ms as u64 / 1000) as usize
}