feat: save key_frame.jpg and key_face.jpg for each face trace during TKG build
- Extract full frame at best confidence frame using ffmpeg
- Crop face region using average bbox coordinates
- Save to .faces/{file_uuid}/{trace_id}/key_frame.jpg and key_face.jpg
- TKG node properties store full file paths for UI display
- Supports future extension for cut representative frames
This commit is contained in:
@@ -1218,6 +1218,69 @@ async fn build_face_track_nodes_from_qdrant(
|
||||
"status": "pending",
|
||||
});
|
||||
|
||||
// Save key frame and key face JPGs
|
||||
let output_dir = crate::core::config::OUTPUT_DIR.as_str();
|
||||
let face_dir = std::path::PathBuf::from(output_dir)
|
||||
.join(".faces").join(file_uuid).join(&external_id);
|
||||
let _ = std::fs::create_dir_all(&face_dir);
|
||||
|
||||
let key_frame_path = face_dir.join("key_frame.jpg");
|
||||
let key_face_path = face_dir.join("key_face.jpg");
|
||||
|
||||
// Get video path from database
|
||||
let video_path: Option<String> = sqlx::query_scalar(&format!(
|
||||
"SELECT file_path FROM {} WHERE file_uuid = $1",
|
||||
crate::core::db::schema::table_name("videos")
|
||||
))
|
||||
.bind(file_uuid)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
// Extract key frame using ffmpeg
|
||||
if !key_frame_path.exists() {
|
||||
if let Some(ref vp) = video_path {
|
||||
let timestamp = agg.best_frame as f64 / 30.0;
|
||||
let _ = std::process::Command::new("ffmpeg")
|
||||
.args(["-y", "-v", "quiet", "-ss", ×tamp.to_string(), "-i", vp])
|
||||
.args(["-frames:v", "1", "-q:v", "2"])
|
||||
.arg(key_frame_path.to_str().unwrap_or(""))
|
||||
.output();
|
||||
}
|
||||
}
|
||||
|
||||
// Crop key face from key frame using bbox
|
||||
if !key_face_path.exists() && key_frame_path.exists() {
|
||||
let bbox = &props["avg_bbox"];
|
||||
let x = bbox["x"].as_i64().unwrap_or(0) as u32;
|
||||
let y = bbox["y"].as_i64().unwrap_or(0) as u32;
|
||||
let w = bbox["width"].as_i64().unwrap_or(100) as u32;
|
||||
let h = bbox["height"].as_i64().unwrap_or(100) as u32;
|
||||
let _ = std::process::Command::new("ffmpeg")
|
||||
.args(["-y", "-v", "quiet", "-i", key_frame_path.to_str().unwrap_or("")])
|
||||
.args(["-vf", &format!("crop={}:{}:{}:{}", w, h, x, y)])
|
||||
.args(["-q:v", "2"])
|
||||
.arg(key_face_path.to_str().unwrap_or(""))
|
||||
.output();
|
||||
}
|
||||
|
||||
// Update props with actual file paths
|
||||
let final_props = serde_json::json!({
|
||||
"trace_id": tid,
|
||||
"frame_count": agg.frame_count,
|
||||
"start_frame": agg.start_f,
|
||||
"end_frame": agg.end_f,
|
||||
"key_frame": if key_frame_path.exists() { key_frame_path.to_string_lossy().to_string() } else { serde_json::Value::Null.to_string() },
|
||||
"key_face": if key_face_path.exists() { key_face_path.to_string_lossy().to_string() } else { serde_json::Value::Null.to_string() },
|
||||
"avg_bbox": props["avg_bbox"].clone(),
|
||||
"avg_yaw": props["avg_yaw"].clone(),
|
||||
"avg_pitch": props["avg_pitch"].clone(),
|
||||
"avg_roll": props["avg_roll"].clone(),
|
||||
"pose_count": pose_count,
|
||||
"status": "pending",
|
||||
});
|
||||
|
||||
sqlx::query(&format!(
|
||||
r#"
|
||||
INSERT INTO {} (node_type, external_id, file_uuid, label, properties)
|
||||
@@ -1232,7 +1295,7 @@ async fn build_face_track_nodes_from_qdrant(
|
||||
.bind(&external_id)
|
||||
.bind(file_uuid)
|
||||
.bind(&label)
|
||||
.bind(serde_json::to_string(&props)?)
|
||||
.bind(serde_json::to_string(&final_props)?)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user