Commit Graph

3 Commits

Author SHA1 Message Date
0ea230fef4 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.
2026-04-22 09:03:50 +01:00
ed90de3c93 fix(A.3 #21): preserve audio contiguity across max_chunk splits in RmsVadChunker
Review feedback (CRITICAL): when a chunk hit max_chunk_samples during
continuous speech, emit_active_chunk reset state to Idle. The next
1-2 loud frames of post-split continued speech went into onset_buffer
and were silently cleared if silence arrived before the 3-frame onset
threshold — 50-100ms of user audio lost at every max-chunk boundary
in long-continuous-speech scenarios.

Splits emit_active_chunk into two variants:

- emit_active_chunk_and_close: the existing behaviour. Used for
  end-of-utterance closes and end-of-session flush. Truncates trailing
  silence, resets state to Idle.
- emit_active_chunk_continue: mid-utterance split on max_chunk. Stays
  in State::InSpeech, clears active_chunk for continued accumulation,
  advances active_chunk_start by the emitted length so the next
  chunk's start_sample is contiguous with this one's end. No
  silence-trim (by definition still in speech — end-of-utterance
  takes priority).

Adds max_chunk_split_preserves_audio_contiguity test: feeds 17 frames
of continuous speech into a chunker with a 4-frame cap, asserts
(a) chunk[i+1].start_sample == chunk[i].start_sample + chunk[i].samples.len()
across every pair, and (b) the final emitted region reaches the end
of the fed speech with no sample loss.
2026-04-22 08:01:14 +01:00
05eea41649 feat(A.3 #21): VadChunker trait + RMS backend (Silero deferred)
New crates/transcription/src/streaming/ module with:

- VadChunker trait: Send-bound, object-safe, push/flush/reset/
  next_sample_index. Same surface a future Silero backend will
  present, so live.rs wiring does not change when Silero drops in.
- VadChunk type: (start_sample: u64, samples: Vec<f32>) for
  commit-policy sample-offset bookkeeping in #24.
- RmsVadChunker: fallback backend the plan permits while the
  ort 2.0.0-rc.10 vs rc.12 ecosystem conflict blocks silero-vad-rust
  / voice_activity_detector. Tuned to match the existing
  evaluate_speech_gate behaviour (enter 0.003, exit 0.0014, 3-frame
  onset, 500 ms silence close, 2 s max chunk).

Key behavioural properties, each backed by a test:
- pure silence emits nothing
- samples between exit and enter thresholds never trigger onset
- a single loud frame does not start a chunk (sustained speech only)
- sustained speech followed by silence emits exactly one chunk
- hysteresis: a dip between enter and exit does not split a chunk
- max_chunk_samples caps continuous speech (Whisper never fed > 2 s)
- flush surfaces in-flight speech
- flush on an idle chunker emits nothing
- reset restores a clean state
- emitted chunk start_sample includes the onset buffer (Whisper sees
  the speech attack, not post-onset audio)

Open items tracked as follow-ups:
1. Silero backend via direct ort rc.12 bridge (Handy-style). Blocked
   on either ecosystem ort alignment or dedicated bridge session.
2. Integration into src-tauri/src/commands/live.rs. Deferred so
   threshold tuning can be validated against real microphone
   captures rather than synthetic constant-signal fixtures.
2026-04-22 07:47:30 +01:00