Skip to main content

voxora_vad/
lib.rs

1//! Energy-based voice activity detection (VAD) for the voxora
2//! workspace.
3//!
4//! `voxora-vad` ships a tiny, deterministic, CPU-only VAD
5//! implementation that downstream crates (batch ASR front-ends,
6//! live-transcription UIs, future speaker-diarization pre-filters)
7//! can pull in without taking on a heavyweight ML dependency.
8//! The detector slides a window over a stream of mono PCM samples,
9//! computes the RMS energy in each frame, and emits
10//! [`VadSegment`]s whenever the speech/silence state machine
11//! transitions past the configured debounce windows.
12//!
13//! # When to use this crate
14//!
15//! | Use case | Notes |
16//! |---|---|
17//! | **Trimming silence before a batch ASR pass** | Pass the audio through [`EnergyVad`] first, then send only the speech runs to the engine. Caveat: trimming changes sample alignment, so re-stamp timestamps against the trimmed audio before showing UI timings. |
18//! | **Live-transcription UIs** | Poll [`VadSegmenter::next_segment`] on each chunk of microphone PCM and only wake the decoder when `is_speech == true`. The ML-based Silero VAD via onnxruntime is a planned follow-up but is **not** part of this crate. |
19//! | **Pre-filtering before diarization** | Speaker diarization ("who spoke when") composes on top of VAD — the diarizer only sees the speech runs, not the gaps. Diarization itself is tracked separately (see `docs/ROADMAP.md` Phase 7). |
20//!
21//! The [`fixtures`] module ships the canonical 16 kHz mono
22//! PCM fixtures (`SILENCE_1S`, `sine_440hz_500ms`) used by
23//! the example and the integration tests. They are kept
24//! byte-identical to `voxora_testkit::audio::*` so a
25//! downstream consumer can swap the source trivially; the
26//! local copy is here because `voxora-testkit` is
27//! `publish = false` and cargo publish cannot resolve
28//! workspace-only crates as dev-dependencies.
29//!
30//! Integration with `voxora_traits::StreamingAsrEngine` is
31//! intentionally **not** included in this crate: streaming
32//! engines are still gated upstream on
33//! whisper-rs / candle incremental-decoding APIs (issues
34//! #50/#51). When the first streaming engine lands, a future
35//! patch will add a `StreamingVadSegmenter` extension that
36//! drives the decoder incrementally.
37//!
38//! # Example
39//!
40//! ```no_run
41//! use voxora_vad::{EnergyVad, VadSegmenter, VadSegment};
42//!
43//! let mut vad = EnergyVad::new();
44//!
45//! // Feed a chunk of PCM. Returns a segment iff this call
46//! // closed a speech/silence run; `None` while the detector is
47//! // still inside an open run.
48//! let samples: Vec<f32> = vec![0.0; 16_000];
49//! let seg: Option<VadSegment> = vad.next_segment(&samples);
50//! assert!(seg.is_none()); // pure silence → no transition
51//!
52//! // At end-of-stream, drain any trailing open run.
53//! let trailing: Option<VadSegment> = vad.flush();
54//! ```
55//!
56//! # Crate conventions
57//!
58//! - **Zero non-workspace runtime deps** — the detector is pure
59//!   `f32` arithmetic over a sliding window, so the crate stays
60//!   hermetic on a fresh `cargo build`. Matches the
61//!   "tiny pure-Rust" stance of [`voxora-config`].
62//! - **Deterministic, no `#[ignore]` tests** — the crate has no
63//!   model/audio fixtures, so every test runs unconditionally
64//!   and the CI test job stays fast.
65//! - **ASR-specific** — not a generic signal-processing library.
66//!   VAD here is a building block for the voxora
67//!   speech-recognition stack only.
68//!
69//! [`voxora-config`]: https://docs.rs/voxora-config
70
71#![forbid(unsafe_code)]
72#![warn(missing_docs)]
73
74mod energy;
75mod error;
76pub mod fixtures;
77mod traits;
78
79pub use energy::{EnergyVad, EnergyVadBuilder, EnergyVadConfig};
80pub use error::VadConfigError;
81pub use traits::{VadSegment, VadSegmenter};
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn public_surface_round_trips() {
89        // Compile-time check that the re-exports stay in sync
90        // with the trait surface — mirrors the round-trip smoke
91        // test in `voxora-traits/src/lib.rs`.
92        let mut vad = EnergyVad::new();
93        let _: Option<VadSegment> = vad.next_segment(&[]);
94        vad.reset();
95        let _: Option<VadSegment> = vad.flush();
96        // The trait itself is reachable behind a dyn pointer.
97        let _: Box<dyn VadSegmenter> = Box::new(EnergyVad::new());
98    }
99}