//! Timeline contract uses actual CPU FFmpeg, without speech/model inference. use anyhow::{Context, Result}; use searchi_media::{decode_audio, decode_audio_aligned, probe, AudioWindow}; use searchi_service::{FfmpegTools, MediaLimits}; use std::{path::Path, process::Command}; fn fixture(ffmpeg: &Path, args: &[&str], output: &Path) -> Result<()> { let result = Command::new(ffmpeg) .args([ "-nostdin", "-hide_banner", "-loglevel", "error", "-threads", "1", ]) .args(args) .arg(output) .output()?; anyhow::ensure!( result.status.success(), "Owned CPU media fixture failed: {}", String::from_utf8_lossy(&result.stderr) ); Ok(()) } #[test] #[ignore = "Requires verified CPU FFmpeg/FFprobe helpers; no model inference"] fn positioned_audio_decodes_at_its_actual_file_timeline_origin() -> Result<()> { assert_eq!(std::env::var("SEARCHI_FORCE_CPU")?, "1"); let tools = FfmpegTools { ffmpeg: std::env::var_os("SEARCHI_TEST_FFMPEG") .context("Set SEARCHI_TEST_FFMPEG")? .into(), ffprobe: std::env::var_os("SEARCHI_TEST_FFPROBE") .context("Set SEARCHI_TEST_FFPROBE")? .into(), }; let directory = tempfile::tempdir()?; let ordinary = directory.path().join("ordinary.wav"); let positioned = directory.path().join("positioned.mkv"); fixture( &tools.ffmpeg, &[ "-f", "lavfi", "-i", "sine=frequency=440:sample_rate=16000:duration=2", "-c:a", "pcm_s16le", ], &ordinary, )?; fixture( &tools.ffmpeg, &[ "-f", "lavfi", "-i", "color=size=16x16:rate=1:duration=4", "-itsoffset", "2", "-f", "lavfi", "-i", "sine=frequency=440:sample_rate=16000:duration=2", "-map", "0:v:0", "-map", "1:a:0", "-c:v", "ffv1", "-c:a", "pcm_s16le", "-threads", "1", ], &positioned, )?; let limits = MediaLimits::default(); let metadata = probe(&tools, &positioned, &limits)?; assert_eq!(metadata.start_ms, 0); assert_eq!(metadata.audio_start_ms, 2000); assert_eq!(metadata.audio_end_ms, 4000); let baseline = decode_audio( &tools, &ordinary, AudioWindow { start_ms: 0, duration_ms: 2000, }, &limits, )?; let actual = decode_audio( &tools, &positioned, AudioWindow { start_ms: metadata.audio_start_ms, duration_ms: metadata.audio_end_ms - metadata.audio_start_ms, }, &limits, )?; assert_eq!(baseline.len(), 32000); let aligned = decode_audio_aligned( &tools, &positioned, AudioWindow { start_ms: 2000, duration_ms: 2000, }, &limits, )?; assert_eq!(aligned, baseline); let whole = decode_audio_aligned( &tools, &positioned, AudioWindow { start_ms: 0, duration_ms: 4000, }, &limits, )?; assert_eq!(whole.len(), 64000); assert!(whole[..32000].iter().all(|sample| *sample == 0.)); assert_eq!(&whole[32000..], baseline.as_slice()); assert_eq!( actual, baseline, "First decoded sample must represent the requested actual audio origin" ); Ok(()) } #[test] #[ignore = "Requires verified CPU FFmpeg/FFprobe helpers; no model inference"] fn aligned_decoder_preserves_internal_gap_seek_and_tail_source_clock() -> Result<()> { assert_eq!(std::env::var("SEARCHI_FORCE_CPU")?, "1"); let tools = FfmpegTools { ffmpeg: std::env::var_os("SEARCHI_TEST_FFMPEG") .context("Set SEARCHI_TEST_FFMPEG")? .into(), ffprobe: std::env::var_os("SEARCHI_TEST_FFPROBE") .context("Set SEARCHI_TEST_FFPROBE")? .into(), }; let directory = tempfile::tempdir()?; let ordinary = directory.path().join("ramp.wav"); let gapped = directory.path().join("gapped.mka"); fixture( &tools.ffmpeg, &[ "-f", "lavfi", "-i", "aevalsrc=0.1+0.1*t:s=16000:d=3", "-c:a", "pcm_s16le", ], &ordinary, )?; // Three exact one-second frames carry PTS 0,2,3; the source clock has a one-second gap. fixture( &tools.ffmpeg, &[ "-f", "lavfi", "-i", "aevalsrc=0.1+0.1*t:s=16000:d=3", "-af", "asetnsamples=n=16000:p=0,asetpts=PTS+gte(T\\,1)/TB", "-c:a", "pcm_s16le", ], &gapped, )?; let limits = MediaLimits::default(); let metadata = probe(&tools, &gapped, &limits)?; assert_eq!(metadata.start_ms, 0); assert_eq!(metadata.audio_start_ms, 0); assert_eq!(metadata.audio_end_ms, 4000); let source = decode_audio( &tools, &ordinary, AudioWindow { start_ms: 0, duration_ms: 3000, }, &limits, )?; assert_eq!(source.len(), 48000); let mut expected = source[..16000].to_vec(); expected.extend(vec![0.; 16000]); expected.extend_from_slice(&source[16000..]); let whole = decode_audio_aligned( &tools, &gapped, AudioWindow { start_ms: 0, duration_ms: 4000, }, &limits, )?; assert_eq!(whole, expected); let seek = decode_audio_aligned( &tools, &gapped, AudioWindow { start_ms: 1500, duration_ms: 1500, }, &limits, )?; assert_eq!(seek, &expected[24000..48000]); let tail = decode_audio_aligned( &tools, &gapped, AudioWindow { start_ms: 3500, duration_ms: 500, }, &limits, )?; assert_eq!(tail, &expected[56000..64000]); Ok(()) }