From 8f877b474fe4ae180c55ad48e1a3c857a4035166 Mon Sep 17 00:00:00 2001 From: Accusys Date: Thu, 14 May 2026 02:41:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20trace=20video=20normal/debug=20mode=20?= =?UTF-8?q?=E2=80=94=20normal=3Draw,=20debug=3Dbbox+frame+identity+cut?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/media_api.rs | 124 +++++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 59 deletions(-) diff --git a/src/api/media_api.rs b/src/api/media_api.rs index 480336c..5799206 100644 --- a/src/api/media_api.rs +++ b/src/api/media_api.rs @@ -261,26 +261,18 @@ async fn trace_video( ) -> Result { use axum::http::header; + let mode = params.get("mode").map(|s| s.as_str()).unwrap_or("debug"); + let videos_table = schema::table_name("videos"); - let row: Option<(String,)> = sqlx::query_as(&format!( - "SELECT file_path FROM {} WHERE file_uuid = $1", + let row: Option<(String, f64, i32, i32)> = sqlx::query_as(&format!( + "SELECT file_path, COALESCE(fps, 24.0), COALESCE(width, 0), COALESCE(height, 0) FROM {} WHERE file_uuid = $1", videos_table )) .bind(&file_uuid) .fetch_optional(state.db.pool()) .await .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; - let (video_path,) = row.ok_or(StatusCode::NOT_FOUND)?; - - let fps: f64 = sqlx::query_scalar(&format!( - "SELECT COALESCE(fps, 24.0) FROM {} WHERE file_uuid = $1", - videos_table - )) - .bind(&file_uuid) - .fetch_optional(state.db.pool()) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? - .unwrap_or(24.0); + let (video_path, fps, _width, _height) = row.ok_or(StatusCode::NOT_FOUND)?; // Get all detections for this trace_id let face_table = schema::table_name("face_detections"); @@ -305,45 +297,82 @@ async fn trace_video( .unwrap_or(2.0); let duration = (last_frame - first_frame) as f64 / fps + padding * 2.0; let seek = (start_sec - padding).max(0.0); + + // === NORMAL MODE: raw video without overlays === + if mode == "normal" { + let tmp = std::env::temp_dir().join(format!("trace_{}.mp4", uuid::Uuid::new_v4())); + let tmp_str = tmp.to_str().unwrap_or("").to_string(); + let result = ffmpeg_cmd() + .args(["-ss", &seek.to_string(), "-i", &video_path, "-t", &duration.to_string(), + "-c", "copy", "-an", "-y", &tmp_str]) + .output() + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + if !result.status.success() { + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + let data = tokio::fs::read(&tmp).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let _ = std::fs::remove_file(&tmp); + return Ok(Response::builder() + .header(header::CONTENT_TYPE, "video/mp4") + .header(header::CONTENT_LENGTH, data.len()) + .body(Body::from(data)) + .unwrap()); + } + + // === DEBUG MODE: with overlays === let total_frames = (duration * fps).round() as i32; let pad_frames = (padding * fps) as i32; - // Build filters: bbox+drawtext (1 filter + 1 drawtext per detection) + // Query identity info for this trace + let identity_name: String = sqlx::query_scalar(&format!( + "SELECT COALESCE(i.name, 'unknown') FROM {} fd LEFT JOIN dev.identities i ON i.id = fd.identity_id WHERE fd.file_uuid = $1 AND fd.trace_id = $2 LIMIT 1", + face_table + )) + .bind(&file_uuid).bind(trace_id) + .fetch_optional(state.db.pool()).await + .unwrap_or(None) + .unwrap_or_else(|| "unknown".to_string()); + + // Query cut_id for the first frame + let cut_id: i32 = sqlx::query_scalar( + "SELECT scene_number FROM public.cut WHERE file_uuid = $1 AND start_frame <= $2 AND end_frame >= $2 LIMIT 1" + ) + .bind(&file_uuid).bind(first_frame) + .fetch_optional(state.db.pool()).await + .unwrap_or(None) + .unwrap_or(0); + let mut parts: Vec = Vec::new(); - // Global frame number overlay (top-left corner, always visible) + // Top-left info block let frame_offset = first_frame as i64 - (padding * fps) as i64; - let global_frame_text = format!( - "drawtext=text='Frame\\: %{{eif:n+{}:d}}':fontsize=16:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y=10", - frame_offset + let info_lines = format!( + "drawtext=text='Trace: {} Cut: {} {}':fontsize=14:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y=12", + trace_id, cut_id, identity_name ); - parts.push(global_frame_text); + parts.push(info_lines); + parts.push(format!( + "drawtext=text='Frame: %{{eif:n+{}:d}} UUID: {}':fontsize=14:fontcolor=white:box=1:boxcolor=black@0.6:x=10:y=34", + frame_offset, file_uuid + )); + for (i, (frame, x, y, w, h)) in rows.iter().enumerate() { let start_off = frame - first_frame + pad_frames; - // End at next detection, or at the last frame of the video (not beyond) let end_off = if i + 1 < rows.len() { rows[i + 1].0 - first_frame + pad_frames } else { - total_frames // last bbox ends with video, no extra padding + total_frames }; - // Bbox parts.push(format!( "drawbox=x={}:y={}:w={}:h={}:color=red@0.8:thickness=8:enable='between(n,{},{})'", - x, - y, - w, - h, - start_off, - end_off - 1 + x, y, w, h, start_off, end_off - 1 )); - // Text label (drawtext, 1 filter vs ~175 bitmap drawboxes) parts.push(format!( "drawtext=text='{}':x={}:y={}:fontsize=20:fontcolor=white:box=1:boxcolor=red@0.8:enable='between(n,{},{})'", trace_id, x + 4, y + 4, start_off, end_off - 1 )); } - // Write filter to temp file to bypass ARG_MAX let filter_text = parts.join(","); let filter_file = std::env::temp_dir().join(format!("vf_{}.txt", uuid::Uuid::new_v4())); let _ = std::fs::write(&filter_file, &filter_text); @@ -352,33 +381,12 @@ async fn trace_video( let tmp = std::env::temp_dir().join(format!("trace_{}.mp4", uuid::Uuid::new_v4())); let tmp_str = tmp.to_str().unwrap_or("").to_string(); let result = ffmpeg_cmd() - .args([ - "-ss", - &seek.to_string(), - "-i", - &video_path, - "-t", - &duration.to_string(), - "-/filter_complex", - &filter_path, - "-c:v", - "libx264", - "-preset", - "ultrafast", - "-crf", - "28", - "-c:a", - "aac", - "-movflags", - "+faststart", - "-y", - &tmp_str, - ]) + .args(["-ss", &seek.to_string(), "-i", &video_path, "-t", &duration.to_string(), + "-/filter_complex", &filter_path, + "-c:v", "libx264", "-preset", "ultrafast", "-crf", "28", + "-c:a", "aac", "-movflags", "+faststart", "-y", &tmp_str]) .output() - .map_err(|e| { - tracing::error!("ffmpeg spawn: {}", e); - StatusCode::INTERNAL_SERVER_ERROR - })?; + .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; if !result.status.success() { let stderr = String::from_utf8_lossy(&result.stderr); tracing::error!("ffmpeg failed: {}", &stderr[..stderr.len().min(300)]); @@ -387,9 +395,7 @@ async fn trace_video( return Err(StatusCode::INTERNAL_SERVER_ERROR); } - let data = tokio::fs::read(&tmp) - .await - .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; + let data = tokio::fs::read(&tmp).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?; let _ = std::fs::remove_file(&filter_file); let _ = std::fs::remove_file(&tmp); Ok(Response::builder()