fix(cr-2026-04-22 C2): VadChunker::flush returns Vec<VadChunk> and preserves mid-flush emissions

CRITICAL from the 2026-04-22 code review: RmsVadChunker::flush() was
calling consume_frame() on a zero-padded final frame via `let _ = ...`,
discarding any VadChunk the call emitted. If the padded frame triggered
end-of-utterance (silent tail + padding zeros push past
silence_close_samples) or max_chunk_samples (buffered speech + padding
push past the cap), the emitted chunk was lost; the outer state check
then either returned None or an empty closing chunk.

Changes the VadChunker trait flush signature from Option<VadChunk> to
Vec<VadChunk> so both the mid-flush emission (from consume_frame) and
the closing emission (from emit_active_chunk_and_close) can be
surfaced. Updates RmsVadChunker::flush to collect from both sites
and skip a zero-length closing emit when the hit_max continue variant
already cleared active_chunk.

Two regression tests land alongside:
- flush_preserves_hit_max_chunk_from_padded_final_frame: tight
  max_chunk, sub-frame speech tail; pre-fix dropped the chunk, post-fix
  emitted samples cover the full active-speech region.
- flush_preserves_end_of_utterance_chunk_from_padded_final_frame:
  silent tail near silence_close; padded zero frame closes the
  utterance inside consume_frame; pre-fix returned None.

No production callers yet — the VadChunker wiring in live.rs is a
deferred item from A.3. API change is clean within the repo.
This commit is contained in:
2026-04-22 09:03:50 +01:00
parent e54f0404ce
commit 0ea230fef4
2 changed files with 139 additions and 24 deletions

View File

@@ -46,10 +46,17 @@ pub trait VadChunker: Send {
/// emit as a result. An empty Vec means "still gathering".
fn push(&mut self, samples: &[f32]) -> Vec<VadChunk>;
/// End-of-session: emit any in-progress speech as a chunk even
/// though silence has not closed it. Returns `None` if there is
/// nothing buffered (or only sub-threshold samples).
fn flush(&mut self) -> Option<VadChunk>;
/// 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<VadChunk>` rather than `Option<VadChunk>` 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<VadChunk>;
/// Drop accumulated state. Used between sessions on the same
/// chunker instance (or after a context-window reset from the