feat: trace quality agent selection report, identity clustering runner_v2 DB write, age/gender CoreML selection, updated experiment config UUID
This commit is contained in:
138
src/core/frame_cache.rs
Normal file
138
src/core/frame_cache.rs
Normal file
@@ -0,0 +1,138 @@
|
||||
use anyhow::{Context, Result};
|
||||
use std::path::{Path, PathBuf};
|
||||
use tracing::info;
|
||||
|
||||
/// A single extracted frame with metadata
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CachedFrame {
|
||||
pub path: PathBuf,
|
||||
pub frame_number: u64,
|
||||
pub timestamp_secs: f64,
|
||||
}
|
||||
|
||||
/// Manages shared frame extraction for concurrent processors
|
||||
pub struct FrameManager {
|
||||
pub dir: PathBuf,
|
||||
pub frames: Vec<CachedFrame>,
|
||||
pub fps: f64,
|
||||
pub total_frames: u64,
|
||||
pub duration_secs: f64,
|
||||
}
|
||||
|
||||
impl FrameManager {
|
||||
/// Extract frames from video at `sample_interval` into a temp directory.
|
||||
pub async fn extract(
|
||||
video_path: &str,
|
||||
sample_interval: u32,
|
||||
fps: f64,
|
||||
total_frames: u64,
|
||||
) -> Result<Self> {
|
||||
let dir = std::env::temp_dir().join(format!("frames_{}", uuid_from_path(video_path)));
|
||||
let _ = std::fs::create_dir_all(&dir);
|
||||
|
||||
let pattern = dir.join("frame_%05d.jpg").to_string_lossy().to_string();
|
||||
let video_path = video_path.to_owned();
|
||||
|
||||
info!(
|
||||
"[FrameCache] Extracting frames (interval={}) to {:?}",
|
||||
sample_interval, dir
|
||||
);
|
||||
|
||||
let output = tokio::process::Command::new("ffmpeg")
|
||||
.args([
|
||||
"-y",
|
||||
"-v",
|
||||
"quiet",
|
||||
"-i",
|
||||
&video_path,
|
||||
"-vf",
|
||||
&format!("select=not(mod(n\\,{})),scale=320:-2", sample_interval),
|
||||
"-vsync",
|
||||
"vfr",
|
||||
"-q:v",
|
||||
"15",
|
||||
&pattern,
|
||||
])
|
||||
.output()
|
||||
.await
|
||||
.context("Frame extraction via ffmpeg failed")?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
anyhow::bail!("ffmpeg frame extraction failed: {}", stderr);
|
||||
}
|
||||
|
||||
// Read extracted frames
|
||||
let mut frames: Vec<CachedFrame> = Vec::new();
|
||||
let mut entries: Vec<_> = std::fs::read_dir(&dir)?
|
||||
.filter_map(|e| e.ok())
|
||||
.filter(|e| e.path().extension().map_or(false, |ext| ext == "jpg"))
|
||||
.collect();
|
||||
entries.sort_by_key(|e| e.file_name());
|
||||
|
||||
for entry in &entries {
|
||||
let fname = entry.file_name();
|
||||
let fname_str = fname.to_string_lossy();
|
||||
if let Some(num_str) = fname_str
|
||||
.strip_prefix("frame_")
|
||||
.and_then(|s| s.strip_suffix(".jpg"))
|
||||
{
|
||||
if let Ok(frame_num) = num_str.parse::<u64>() {
|
||||
let timestamp = frame_num as f64 / fps;
|
||||
frames.push(CachedFrame {
|
||||
path: entry.path(),
|
||||
frame_number: frame_num,
|
||||
timestamp_secs: timestamp,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let duration_secs = if fps > 0.0 {
|
||||
total_frames as f64 / fps
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
info!(
|
||||
"[FrameCache] Extracted {} frames to {:?}",
|
||||
frames.len(),
|
||||
dir
|
||||
);
|
||||
|
||||
Ok(FrameManager {
|
||||
dir,
|
||||
frames,
|
||||
fps,
|
||||
total_frames,
|
||||
duration_secs,
|
||||
})
|
||||
}
|
||||
|
||||
/// Clean up the extracted frame files
|
||||
pub fn cleanup(&self) {
|
||||
let _ = std::fs::remove_dir_all(&self.dir);
|
||||
info!("[FrameCache] Cleaned up {:?}", self.dir);
|
||||
}
|
||||
|
||||
/// Get a frame by index
|
||||
pub fn get_frame(&self, index: usize) -> Option<&CachedFrame> {
|
||||
self.frames.get(index)
|
||||
}
|
||||
|
||||
/// Number of extracted frames
|
||||
pub fn len(&self) -> usize {
|
||||
self.frames.len()
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.frames.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
fn uuid_from_path(path: &str) -> String {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
path.hash(&mut hasher);
|
||||
format!("{:x}", hasher.finish())
|
||||
}
|
||||
Reference in New Issue
Block a user