Commit Graph

7 Commits

Author SHA1 Message Date
150059e174 fix(rms_vad): correct types and threshold order in flush idempotency test
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
Two issues in flush_is_idempotent_and_leaves_clean_state from
581a098:

1. silence_close_samples and max_chunk_samples were cast `as u64`
   but with_thresholds takes usize — wouldn't compile.
2. enter_threshold was 0.005 and exit_threshold 0.01, which
   violates the hysteresis invariant (enter must be >= exit) and
   panics in debug_assert at runtime. Swap to 0.01 / 0.005 so the
   test actually runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 19:22:22 +01:00
Claude
581a098508 fix(rms_vad): flush always leaves the chunker in clean state
The earlier audit noted that `flush()` had three exit paths but only two
of them explicitly cleared all state-machine fields:
1. InSpeech with non-empty active_chunk: emit_active_chunk_and_close()
   handled it.
2. InSpeech with empty active_chunk (hit_max-mid-flush): handled inline.
3. Idle (no padded frame, or padded frame closed cleanly): no explicit
   reset — silent_tail_samples / pending_onset_frames / onset_buffer
   could carry stale values from `consume_frame` calls inside the same
   flush.

In the worst case, the first feed of a fresh recording could see leftover
onset bookkeeping and produce a chunk start that doesn't match the new
session's audio. Reusing the same `RmsVadChunker` across stop/start is
the main path that would hit this.

Add a single defence-in-depth reset block at the end of flush — every
exit path lands the chunker in the same fields a fresh chunker has,
except `next_sample_index` (the running total-samples counter, intent-
ionally preserved). Test asserts: a second flush after a full speech →
silence → partial-pending sequence emits zero chunks, and a subsequent
silent feed also emits zero, proving no stale state leaked.

(kon-transcription doesn't build in the audit sandbox because ort-sys's
build script can't reach pyke's CDN; CI cross-platform compiles it.)

https://claude.ai/code/session_0189xUb6ie6t9qHkzatGZ9Rb
2026-04-25 09:37:39 +00:00
fe61661305 chore(lint): clean up clippy warnings across workspace
Auto-applied cargo clippy --fix across 11 files — needless return,
unnecessary cast, map_or simplification, repeat().take() → repeat_n(),
iter().any() → contains(), manual char comparison, lifetime elision,
push_str single-char, reference immediately dereferenced.

Also fixed three lints on file_storage.rs manually: two doc-list-item
overindentations, plus the same needless-return. Baseline main was
not clippy-clean with -D warnings before; after this pass one
needless_range_loop warning remains (live.rs:1089) that clippy's
suggested rewrite would make less readable — left for a dedicated
refactor session.

Build + workspace tests remain green (245 passing, 0 failing, 1
ignored).
2026-04-24 09:43:56 +01:00
9b0067b4c0 Land release blocker fixes and workspace cleanup
Some checks failed
check / cargo check (macos-latest) (push) Has been cancelled
check / cargo check (ubuntu-22.04) (push) Has been cancelled
check / cargo check (windows-latest) (push) Has been cancelled
check / svelte build + lint (push) Has been cancelled
2026-04-23 00:16:09 +01:00
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