129 lines
5.6 KiB
Rust
129 lines
5.6 KiB
Rust
//! 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,
|
|
/// Drop an utterance shorter than this before transcription
|
|
/// (`VAD_MIN_UTTERANCE_MS`, default 300). `0` disables the check. Filters
|
|
/// the short VAD false-triggers (breaths, clicks, echo).
|
|
pub min_utterance_ms: u32,
|
|
/// Drop an utterance whose average speech probability is below this before
|
|
/// transcription (`VAD_MIN_AVG_PROB`, default 0.25). `0.0` disables the
|
|
/// check. Filters the soft/noisy segments that the STT would otherwise
|
|
/// turn into hallucinated text. The average is taken over the **voiced
|
|
/// windows only** (probability ≥ `neg_threshold`), so the trailing
|
|
/// hangover silence (`min_silence_ms`) does not dilute short utterances
|
|
/// like "uno" / "due". A genuine utterance still fluctuates around the
|
|
/// speech threshold on its onset/release, so keep this well below
|
|
/// `threshold` (default 0.5).
|
|
pub min_avg_prob: f32,
|
|
/// 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: 700,
|
|
speech_pad_ms: 30,
|
|
barge_in_min_speech_ms: 150,
|
|
min_utterance_ms: 300,
|
|
min_avg_prob: 0.25,
|
|
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);
|
|
s.min_utterance_ms = env_u32("VAD_MIN_UTTERANCE_MS", s.min_utterance_ms);
|
|
s.min_avg_prob = env_f32("VAD_MIN_AVG_PROB", s.min_avg_prob);
|
|
|
|
// 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
|
|
}
|