//! Streaming primitives for live capture: VAD-gated chunking, //! agreement-based commit policy, and bounded buffer management. //! //! These types are tested at the unit level. Integration into //! `src-tauri/src/commands/live.rs` lands in follow-up commits so //! threshold tuning can be validated against real microphone captures //! rather than synthetic fixtures (brief items #21, #24, #25). pub mod buffer_trim; pub mod commit_policy; pub mod rms_vad; pub use buffer_trim::{sample_index_for_seconds, trim_buffer_to_commit_point}; pub use commit_policy::{CommitDecision, CommitPolicy, LocalAgreement, Token}; pub use rms_vad::RmsVadChunker; /// A span of audio the VAD considers worth transcribing. `start_sample` /// is an absolute index into the stream the `VadChunker` has been fed /// since its last `reset`; `samples` is f32 PCM at the chunker's /// configured sample rate. #[derive(Debug, Clone)] pub struct VadChunk { pub start_sample: u64, pub samples: Vec, } /// A streaming VAD-gated chunker. /// /// Implementations accumulate incoming samples, decide whether the /// current segment is speech using a score + hysteresis (brief item /// #21), and emit `VadChunk`s when a speech region ends — or when an /// in-progress speech region exceeds the configured max length so /// Whisper is not fed a 30-second monolith. /// /// `push` returns any chunks ready to dispatch; typical usage is /// `for chunk in chunker.push(&samples) { dispatch(chunk); }` inside /// the capture loop. /// /// `flush` is called at end-of-session to emit any in-flight speech as /// a final chunk (even if silence hasn't closed it). /// /// `Send` because a chunker is owned by the live-session worker thread /// and moved into `spawn_blocking`. pub trait VadChunker: Send { /// Feed new samples. Returns any chunks the chunker has decided to /// emit as a result. An empty Vec means "still gathering". fn push(&mut self, samples: &[f32]) -> Vec; /// End-of-session: emit any in-progress speech as chunks even /// though silence has not closed them. Returns an empty Vec if /// there is nothing buffered (or only sub-threshold samples). /// /// Returns `Vec` rather than `Option` because /// the zero-padded final frame can legitimately trigger both a /// mid-flush emission (end-of-utterance or `max_chunk_samples`) /// AND a closing emission if the backend stays in-speech after /// the mid-flush cut. The previous `Option` signature silently /// dropped the mid-flush chunk. fn flush(&mut self) -> Vec; /// Drop accumulated state. Used between sessions on the same /// chunker instance (or after a context-window reset from the /// repetition detector — brief item #26). fn reset(&mut self); /// Absolute sample index of the next sample that will be fed via /// `push`. Exposed so the commit policy (#24) can compute sample /// offsets for its agreement window. fn next_sample_index(&self) -> u64; } #[cfg(test)] mod tests { use super::*; #[test] fn vad_chunker_trait_is_object_safe() { // Compile-time witness: keep the trait dyn-compatible so the // live-session worker can hold `Box` and swap // between RMS and Silero backends at runtime. let _: Option> = None; } }