Files
Lumotia/crates/transcription/src/streaming/buffer_trim.rs
Jake 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

208 lines
8.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Buffer-trim helpers for streaming transcription.
//!
//! Brief item #25: replace the current `OVERLAP_SAMPLES`-based drain
//! in `src-tauri/src/commands/live.rs` with a trim tied to the last
//! commit point emitted by the `CommitPolicy`. This keeps the capture
//! buffer bounded regardless of wall-clock session length (ufal #120 /
//! #102) by guaranteeing that any sample already committed to the
//! transcript is never kept in the working buffer.
//!
//! The helpers here are pure — they don't know about the live session
//! loop. Integration into `live.rs` ships as a follow-up after the
//! LocalAgreement wiring (#24) is dogfooded.
/// Absolute sample index at the end of the given session-relative
/// seconds mark, rounded to the nearest sample. `end_secs` typically
/// comes from `LocalAgreement::last_committed_end_secs()`.
///
/// Guards against non-finite inputs: NaN and ±infinity both return 0
/// ("nothing committed yet"). Without this, Rust's saturating
/// float-to-int cast turns `f64::INFINITY` into `u64::MAX`, which
/// would park the capture buffer origin at an index beyond any
/// reachable sample and trim the entire buffer forever.
pub fn sample_index_for_seconds(end_secs: f64, sample_rate: u32) -> u64 {
if !end_secs.is_finite() || end_secs <= 0.0 {
return 0;
}
(end_secs * sample_rate as f64).round() as u64
}
/// Drain the prefix of `buffer` whose absolute sample indices fall
/// below `commit_sample_index`. `buffer_start_sample` is the absolute
/// index of `buffer[0]` before the trim.
///
/// Returns the new `buffer_start_sample`. If the commit point is
/// before or equal to `buffer_start_sample`, nothing is drained.
/// If the commit point is beyond the current end of the buffer, the
/// whole buffer is drained and the new start is set to the commit
/// index — the buffer is still empty, but its absolute-index origin
/// moves forward so subsequent samples are positioned correctly.
pub fn trim_buffer_to_commit_point(
buffer: &mut Vec<f32>,
buffer_start_sample: u64,
commit_sample_index: u64,
) -> u64 {
if commit_sample_index <= buffer_start_sample {
return buffer_start_sample;
}
let drain_count = (commit_sample_index - buffer_start_sample) as usize;
if drain_count >= buffer.len() {
buffer.clear();
return commit_sample_index;
}
buffer.drain(..drain_count);
buffer_start_sample + drain_count as u64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sample_index_for_seconds_zero_is_zero() {
assert_eq!(sample_index_for_seconds(0.0, 16_000), 0);
}
#[test]
fn sample_index_for_seconds_negative_is_zero() {
// Defensive: end_secs should never be negative, but if it is
// (clock skew in a future f64 source) treat as "nothing
// committed yet" rather than wrapping to a huge u64.
assert_eq!(sample_index_for_seconds(-1.0, 16_000), 0);
}
#[test]
fn sample_index_for_seconds_rejects_nan_and_infinity() {
// Defensive against non-finite inputs: without the is_finite()
// check, Rust's saturating float-to-int cast makes +infinity
// become u64::MAX, which would park the buffer origin beyond
// reach and trim the whole buffer forever.
assert_eq!(sample_index_for_seconds(f64::NAN, 16_000), 0);
assert_eq!(sample_index_for_seconds(f64::INFINITY, 16_000), 0);
assert_eq!(sample_index_for_seconds(f64::NEG_INFINITY, 16_000), 0);
}
#[test]
fn sample_index_for_seconds_rounds_nearest() {
// 0.5 s at 16 kHz = 8000 samples exactly.
assert_eq!(sample_index_for_seconds(0.5, 16_000), 8_000);
// Round-nearest: 0.50003 s × 16 kHz = 8000.48 → 8000.
assert_eq!(sample_index_for_seconds(0.50003, 16_000), 8_000);
// 0.5001 s × 16 kHz = 8001.6 → 8002.
assert_eq!(sample_index_for_seconds(0.5001, 16_000), 8_002);
}
#[test]
fn trim_does_nothing_when_commit_is_before_buffer_start() {
let mut buf = vec![1.0, 2.0, 3.0];
let new_start = trim_buffer_to_commit_point(&mut buf, 1000, 500);
assert_eq!(new_start, 1000);
assert_eq!(buf, vec![1.0, 2.0, 3.0]);
}
#[test]
fn trim_does_nothing_when_commit_equals_buffer_start() {
let mut buf = vec![1.0, 2.0, 3.0];
let new_start = trim_buffer_to_commit_point(&mut buf, 1000, 1000);
assert_eq!(new_start, 1000);
assert_eq!(buf, vec![1.0, 2.0, 3.0]);
}
#[test]
fn trim_drains_prefix_when_commit_is_inside_buffer() {
let mut buf = vec![1.0, 2.0, 3.0, 4.0, 5.0];
// buffer starts at absolute index 100, commit is at 102.
// Drain 2 samples; remaining buffer starts at 102.
let new_start = trim_buffer_to_commit_point(&mut buf, 100, 102);
assert_eq!(new_start, 102);
assert_eq!(buf, vec![3.0, 4.0, 5.0]);
}
#[test]
fn trim_clears_buffer_when_commit_is_at_buffer_end() {
let mut buf = vec![1.0, 2.0, 3.0];
// buffer is [100, 103). commit at 103 means every sample is
// committed — drain all, start moves forward.
let new_start = trim_buffer_to_commit_point(&mut buf, 100, 103);
assert_eq!(new_start, 103);
assert!(buf.is_empty());
}
#[test]
fn trim_clears_buffer_when_commit_is_past_buffer_end() {
let mut buf = vec![1.0, 2.0, 3.0];
// Commit well beyond the buffer — this happens in rare edge
// cases where the committer's notion of time outstrips the
// current buffer (e.g. after a reset). Defensive: drain and
// park the origin at the commit point.
let new_start = trim_buffer_to_commit_point(&mut buf, 100, 200);
assert_eq!(new_start, 200);
assert!(buf.is_empty());
}
#[test]
fn trim_bounds_buffer_over_long_session() {
// Simulate a committer that keeps up with capture: each cycle
// feeds 16_000 samples and commits all but a 200-sample
// tentative tail. Over 100 cycles the buffer must stay near
// that tentative envelope — not accumulate 100 × 16_000 samples
// as it would without the commit-point trim.
//
// The tentative tail stacks by 200 per cycle because each new
// push extends the buffer BEFORE the trim runs against the
// previous cycle's commit point, so the expected bound is
// (tentative_per_cycle + new_push_minus_commit), not just
// tentative_per_cycle.
let mut buf: Vec<f32> = Vec::new();
let mut start: u64 = 0;
let mut total_pushed: u64 = 0;
let tentative_per_cycle: u64 = 200;
for _ in 0..100 {
buf.extend(std::iter::repeat_n(0.25_f32, 16_000));
total_pushed += 16_000;
let commit_point = total_pushed - tentative_per_cycle;
start = trim_buffer_to_commit_point(&mut buf, start, commit_point);
}
assert!(
buf.len() as u64 <= 2 * tentative_per_cycle,
"buffer outgrew the commit-bounded envelope: len = {} (bound {})",
buf.len(),
2 * tentative_per_cycle
);
}
#[test]
fn integrates_with_local_agreement_last_committed_end_secs() {
use super::super::commit_policy::{LocalAgreement, Token};
let mut la = LocalAgreement::new(2);
let _ = la.push(vec![Token {
text: "hello".into(),
start_secs: 0.0,
end_secs: 0.5,
}]);
let _ = la.push(vec![
Token {
text: "hello".into(),
start_secs: 0.0,
end_secs: 0.5,
},
Token {
text: "world".into(),
start_secs: 0.5,
end_secs: 1.0,
},
]);
// "hello" is committed, ending at 0.5 s.
let commit_idx = sample_index_for_seconds(la.last_committed_end_secs(), 16_000);
assert_eq!(commit_idx, 8_000);
// Simulate a capture buffer that has received 1.2 s of audio
// starting at t=0.
let mut buf: Vec<f32> = std::iter::repeat_n(0.1_f32, 19_200).collect();
let new_start = trim_buffer_to_commit_point(&mut buf, 0, commit_idx);
assert_eq!(new_start, 8_000);
assert_eq!(buf.len(), 19_200 - 8_000);
}
}