diff --git a/crates/transcription/src/streaming/buffer_trim.rs b/crates/transcription/src/streaming/buffer_trim.rs index 82e9f05..e7bda4a 100644 --- a/crates/transcription/src/streaming/buffer_trim.rs +++ b/crates/transcription/src/streaming/buffer_trim.rs @@ -14,8 +14,14 @@ /// 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 <= 0.0 { + if !end_secs.is_finite() || end_secs <= 0.0 { return 0; } (end_secs * sample_rate as f64).round() as u64 @@ -65,6 +71,17 @@ mod tests { 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.