diff --git a/tools/speech-eval/native-validation/README.md b/tools/speech-eval/native-validation/README.md index 5d683c1..7a3db64 100644 --- a/tools/speech-eval/native-validation/README.md +++ b/tools/speech-eval/native-validation/README.md @@ -138,3 +138,17 @@ missing source windows prohibit numeric accepted timing scores. Historical full-source gated DTW **6/8** is context, never an inherited result or an isolated causal comparison with this end-to-end hybrid run. No new timing score exists until this exact mode executes and all checks finish. + +The first attempt at `.searchi/qa/speech-live-timing-v1` failed during source +preflight, before any model load or inference. Its immutable `failure.json` +SHA-256 is `9ba49fd5cdb69e57c267f579818f226949b02eafbc0e592161fad7593f00c6aa`. +The bark recording lasts 2.038594 seconds: its rounded 2039 ms window implies +32624 samples, while aligned decoding correctly retains 32618 real samples. +The six-sample difference is 0.375 ms. The evaluator now accepts the existing +one-millisecond coverage bound (16 samples), without padding, trimming or +changing source bytes, labels, ranking, or DTW points. Its regression test +accepts that real tail and rejects a 17-sample discrepancy and invalid bounds. +Seven Rust tests, strict all-target Clippy and five timing-adapter tests passed. +A retry must use a fresh output directory after the corrected evaluator is +frozen and the CPU inference slot is granted again; the failed attempt remains +preserved and has no quality score. diff --git a/tools/speech-eval/native-validation/src/main.rs b/tools/speech-eval/native-validation/src/main.rs index 370fd05..c13829c 100644 --- a/tools/speech-eval/native-validation/src/main.rs +++ b/tools/speech-eval/native-validation/src/main.rs @@ -60,6 +60,18 @@ fn file_hash(path: &Path) -> Result { } Ok(format!("{:x}", state.finalize())) } +fn validate_sample_coverage(samples: usize, duration_ms: u32) -> Result<()> { + let expected = duration_ms as usize * 16; + ensure!( + samples > 0 + && samples <= 480000 + && expected > 0 + && expected <= 480000 + && samples.abs_diff(expected) <= 16, + "Fixture source clock coverage differs beyond existing one-millisecond bound" + ); + Ok(()) +} fn write_new(path: &Path, value: &Value) -> Result<()> { let mut file = fs::OpenOptions::new() .write(true) @@ -174,16 +186,13 @@ fn run_case( let mut source_windows = Vec::new(); for window in windows(metadata.audio_start_ms, metadata.audio_end_ms, max_windows)? { let pcm = decode_audio_aligned(&assets.tools, &path, window, &limits)?; - ensure!( - pcm.len() == window.duration_ms as usize * 16, - "Fixture source clock coverage differs" - ); + validate_sample_coverage(pcm.len(), window.duration_ms)?; let sha = hash( &pcm.iter() .flat_map(|value| value.to_le_bytes()) .collect::>(), ); - let record = json!({"asset":id,"start_ms":window.start_ms,"duration_ms":window.duration_ms,"samples":pcm.len(),"pcm_sha256":sha}); + let record = json!({"asset":id,"start_ms":window.start_ms,"duration_ms":window.duration_ms,"samples":pcm.len(),"nominal_samples":window.duration_ms as usize*16,"coverage_difference_samples":pcm.len() as i64-i64::from(window.duration_ms)*16,"pcm_sha256":sha}); expected.entry(sha).or_default().push(record.clone()); source_windows.push(record); } @@ -487,6 +496,13 @@ mod tests { assert!(values.iter().all(|window| window.duration_ms <= 30000)); } #[test] + fn fractional_millisecond_tail_preserves_real_samples() { + assert!(validate_sample_coverage(32618, 2039).is_ok()); + assert!(validate_sample_coverage(32607, 2039).is_err()); + assert!(validate_sample_coverage(0, 1).is_err()); + assert!(validate_sample_coverage(480001, 30000).is_err()); + } + #[test] #[ignore = "Requires the five hash-verified real media fixture files; no inference"] fn timing_mode_preflight_checks_all_real_frozen_sources_without_models() -> Result<()> { let root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../fixtures/media");