feat: dual input (start_frame/end_frame + start_time/end_time) + all outputs include frames, time, fps
This commit is contained in:
@@ -114,12 +114,36 @@ fn render_text(
|
|||||||
|
|
||||||
#[derive(Debug, serde::Deserialize)]
|
#[derive(Debug, serde::Deserialize)]
|
||||||
struct BboxParams {
|
struct BboxParams {
|
||||||
|
// Legacy (deprecated): single param, frames
|
||||||
start: Option<i32>,
|
start: Option<i32>,
|
||||||
end: Option<i32>,
|
end: Option<i32>,
|
||||||
|
// Explicit params: input either or both
|
||||||
|
start_frame: Option<i32>,
|
||||||
|
end_frame: Option<i32>,
|
||||||
|
start_time: Option<f64>,
|
||||||
|
end_time: Option<f64>,
|
||||||
face_uuid: Option<String>,
|
face_uuid: Option<String>,
|
||||||
duration: Option<f64>,
|
duration: Option<f64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve (start_frame, end_frame) from dual input.
|
||||||
|
/// Priority: start_frame/end_frame > start/end > start_time/end_time.
|
||||||
|
/// If only time is given, convert via fps.
|
||||||
|
fn resolve_frame_range(
|
||||||
|
start_frame: Option<i32>, end_frame: Option<i32>,
|
||||||
|
start: Option<i32>, end: Option<i32>,
|
||||||
|
start_time: Option<f64>, end_time: Option<f64>,
|
||||||
|
fps: f64,
|
||||||
|
) -> (i32, i32) {
|
||||||
|
if let (Some(sf), Some(ef)) = (start_frame.or(start), end_frame.or(end)) {
|
||||||
|
return (sf, ef);
|
||||||
|
}
|
||||||
|
if let (Some(st), Some(et)) = (start_time, end_time) {
|
||||||
|
return ((st * fps) as i32, (et * fps) as i32);
|
||||||
|
}
|
||||||
|
(0, i32::MAX)
|
||||||
|
}
|
||||||
|
|
||||||
async fn bbox_overlay_video(
|
async fn bbox_overlay_video(
|
||||||
State(state): State<crate::api::server::AppState>,
|
State(state): State<crate::api::server::AppState>,
|
||||||
Path(file_uuid): Path<String>,
|
Path(file_uuid): Path<String>,
|
||||||
@@ -136,8 +160,6 @@ async fn bbox_overlay_video(
|
|||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
let (video_path,) = row.ok_or(StatusCode::NOT_FOUND)?;
|
let (video_path,) = row.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
let start_f = p.start.unwrap_or(0);
|
|
||||||
let end_f = p.end.unwrap_or(i32::MAX);
|
|
||||||
let face_fuid = p.face_uuid.as_deref().unwrap_or(&file_uuid);
|
let face_fuid = p.face_uuid.as_deref().unwrap_or(&file_uuid);
|
||||||
let duration = p.duration.unwrap_or(10.0);
|
let duration = p.duration.unwrap_or(10.0);
|
||||||
|
|
||||||
@@ -152,6 +174,8 @@ async fn bbox_overlay_video(
|
|||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
.unwrap_or(24.0);
|
.unwrap_or(24.0);
|
||||||
|
|
||||||
|
let (start_f, end_f) = resolve_frame_range(p.start_frame, p.end_frame, p.start, p.end, p.start_time, p.end_time, fps);
|
||||||
|
|
||||||
let start_sec = start_f as f64 / fps;
|
let start_sec = start_f as f64 / fps;
|
||||||
|
|
||||||
// Get face bboxes
|
// Get face bboxes
|
||||||
@@ -487,11 +511,30 @@ async fn stream_video(
|
|||||||
return Err(StatusCode::NOT_FOUND);
|
return Err(StatusCode::NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chunk extraction with start/end params
|
// Chunk extraction with dual time/frame params
|
||||||
if let (Some(s), Some(e)) = (params.get("start"), params.get("end")) {
|
let start_time_param = params.get("start_time").and_then(|v| v.parse::<f64>().ok());
|
||||||
let start: f64 = s.parse().unwrap_or(0.0);
|
let end_time_param = params.get("end_time").and_then(|v| v.parse::<f64>().ok());
|
||||||
let end: f64 = e.parse().unwrap_or(0.0);
|
let start_frame_param = params.get("start_frame").and_then(|v| v.parse::<f64>().ok());
|
||||||
let dur = end - start;
|
let end_frame_param = params.get("end_frame").and_then(|v| v.parse::<f64>().ok());
|
||||||
|
let start_legacy = params.get("start").and_then(|v| v.parse::<f64>().ok());
|
||||||
|
let end_legacy = params.get("end").and_then(|v| v.parse::<f64>().ok());
|
||||||
|
|
||||||
|
let has_range = start_frame_param.is_some() || start_time_param.is_some() || start_legacy.is_some();
|
||||||
|
|
||||||
|
if has_range {
|
||||||
|
let (start_sec, dur) = if let (Some(sf), Some(ef)) = (start_frame_param, end_frame_param) {
|
||||||
|
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);
|
||||||
|
(sf / _fps, (ef - sf) / _fps)
|
||||||
|
} else if let (Some(st), Some(et)) = (start_time_param, end_time_param) {
|
||||||
|
(st, et - st)
|
||||||
|
} else if let (Some(s), Some(e)) = (start_legacy, end_legacy) {
|
||||||
|
(s, e - s)
|
||||||
|
} else {
|
||||||
|
return Err(StatusCode::BAD_REQUEST);
|
||||||
|
};
|
||||||
if dur <= 0.0 {
|
if dur <= 0.0 {
|
||||||
return Err(StatusCode::BAD_REQUEST);
|
return Err(StatusCode::BAD_REQUEST);
|
||||||
}
|
}
|
||||||
@@ -499,29 +542,15 @@ async fn stream_video(
|
|||||||
let tmp = std::env::temp_dir().join(format!("chunk_{}.mp4", uuid::Uuid::new_v4()));
|
let tmp = std::env::temp_dir().join(format!("chunk_{}.mp4", uuid::Uuid::new_v4()));
|
||||||
let tmp_str = tmp.to_str().unwrap_or("").to_string();
|
let tmp_str = tmp.to_str().unwrap_or("").to_string();
|
||||||
let status = ffmpeg_cmd()
|
let status = ffmpeg_cmd()
|
||||||
.args([
|
.args(["-ss", &start_sec.to_string(), "-i", &file_path, "-t", &dur.to_string(),
|
||||||
"-ss",
|
"-c", "copy", "-movflags", "+faststart", "-y", &tmp_str])
|
||||||
&start.to_string(),
|
|
||||||
"-i",
|
|
||||||
&file_path,
|
|
||||||
"-t",
|
|
||||||
&dur.to_string(),
|
|
||||||
"-c",
|
|
||||||
"copy",
|
|
||||||
"-movflags",
|
|
||||||
"+faststart",
|
|
||||||
"-y",
|
|
||||||
&tmp_str,
|
|
||||||
])
|
|
||||||
.status()
|
.status()
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
if !status.success() {
|
if !status.success() {
|
||||||
let _ = std::fs::remove_file(&tmp);
|
let _ = std::fs::remove_file(&tmp);
|
||||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||||
}
|
}
|
||||||
let data = tokio::fs::read(&tmp)
|
let data = tokio::fs::read(&tmp).await.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
.await
|
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
|
||||||
let _ = std::fs::remove_file(&tmp);
|
let _ = std::fs::remove_file(&tmp);
|
||||||
return Ok(Response::builder()
|
return Ok(Response::builder()
|
||||||
.header(header::CONTENT_TYPE, "video/mp4")
|
.header(header::CONTENT_TYPE, "video/mp4")
|
||||||
@@ -530,6 +559,7 @@ async fn stream_video(
|
|||||||
.unwrap());
|
.unwrap());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Full file streaming with range request support
|
||||||
let file_size = src.metadata().map(|m| m.len()).unwrap_or(0);
|
let file_size = src.metadata().map(|m| m.len()).unwrap_or(0);
|
||||||
let content_type = "video/mp4";
|
let content_type = "video/mp4";
|
||||||
|
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ struct TracesRequest {
|
|||||||
struct TraceInfo {
|
struct TraceInfo {
|
||||||
trace_id: i32,
|
trace_id: i32,
|
||||||
face_count: i64,
|
face_count: i64,
|
||||||
first_frame: i32,
|
start_frame: i32,
|
||||||
last_frame: i32,
|
end_frame: i32,
|
||||||
first_sec: f64,
|
start_time: f64,
|
||||||
last_sec: f64,
|
end_time: f64,
|
||||||
duration_sec: f64,
|
duration_sec: f64,
|
||||||
avg_confidence: f64,
|
avg_confidence: f64,
|
||||||
sample_face_id: Option<String>,
|
sample_face_id: Option<String>,
|
||||||
@@ -49,6 +49,7 @@ struct TraceInfo {
|
|||||||
struct TracesResponse {
|
struct TracesResponse {
|
||||||
success: bool,
|
success: bool,
|
||||||
file_uuid: String,
|
file_uuid: String,
|
||||||
|
fps: f64,
|
||||||
total_traces: i64,
|
total_traces: i64,
|
||||||
total_faces: i64,
|
total_faces: i64,
|
||||||
page: i64,
|
page: i64,
|
||||||
@@ -73,8 +74,8 @@ async fn list_traces_sorted(
|
|||||||
|
|
||||||
let order_clause = match sort {
|
let order_clause = match sort {
|
||||||
"face_count" => "face_count DESC",
|
"face_count" => "face_count DESC",
|
||||||
"duration" => "(MAX(frame_number) - MIN(frame_number)) DESC",
|
"duration" => "duration_sec DESC",
|
||||||
_ => "first_frame ASC",
|
_ => "start_frame ASC",
|
||||||
};
|
};
|
||||||
|
|
||||||
let fps: f64 =
|
let fps: f64 =
|
||||||
@@ -87,12 +88,13 @@ async fn list_traces_sorted(
|
|||||||
.unwrap_or(24.0);
|
.unwrap_or(24.0);
|
||||||
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
"SELECT tt.*, fd.id AS sample_face_id, {} AS video_fps FROM (
|
"SELECT tt.*, fd.id AS sample_face_id FROM (
|
||||||
SELECT trace_id,
|
SELECT trace_id,
|
||||||
COUNT(*) AS face_count,
|
COUNT(*) AS face_count,
|
||||||
MIN(frame_number) AS first_frame,
|
MIN(frame_number) AS start_frame,
|
||||||
MAX(frame_number) AS last_frame,
|
MAX(frame_number) AS end_frame,
|
||||||
AVG(confidence) AS avg_confidence
|
(MAX(frame_number) - MIN(frame_number))::float8 AS duration_sec,
|
||||||
|
AVG(confidence)::float8 AS avg_confidence
|
||||||
FROM {}
|
FROM {}
|
||||||
WHERE file_uuid = $1 AND trace_id IS NOT NULL
|
WHERE file_uuid = $1 AND trace_id IS NOT NULL
|
||||||
AND confidence >= $5 AND confidence <= $6
|
AND confidence >= $5 AND confidence <= $6
|
||||||
@@ -106,13 +108,12 @@ async fn list_traces_sorted(
|
|||||||
WHERE trace_id = tt.trace_id AND file_uuid = $1
|
WHERE trace_id = tt.trace_id AND file_uuid = $1
|
||||||
ORDER BY confidence DESC LIMIT 1
|
ORDER BY confidence DESC LIMIT 1
|
||||||
) fd ON true",
|
) fd ON true",
|
||||||
crate::core::db::schema::table_name("videos"),
|
|
||||||
crate::core::db::schema::table_name("face_detections"),
|
crate::core::db::schema::table_name("face_detections"),
|
||||||
order_clause,
|
order_clause,
|
||||||
crate::core::db::schema::table_name("face_detections"),
|
crate::core::db::schema::table_name("face_detections"),
|
||||||
);
|
);
|
||||||
|
|
||||||
let rows: Vec<(i32, i64, i32, i32, f64, f64, f64, f64, Option<String>)> =
|
let rows: Vec<(i32, i64, i32, i32, f64, f64, Option<i32>)> =
|
||||||
sqlx::query_as(&query)
|
sqlx::query_as(&query)
|
||||||
.bind(&file_uuid)
|
.bind(&file_uuid)
|
||||||
.bind(min_faces)
|
.bind(min_faces)
|
||||||
@@ -126,16 +127,16 @@ async fn list_traces_sorted(
|
|||||||
|
|
||||||
let traces: Vec<TraceInfo> = rows
|
let traces: Vec<TraceInfo> = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(tid, fc, ff, lf, fs, ls, dur, conf, fid)| TraceInfo {
|
.map(|(tid, fc, sf, ef, dur, conf, fid)| TraceInfo {
|
||||||
trace_id: tid,
|
trace_id: tid,
|
||||||
face_count: fc,
|
face_count: fc,
|
||||||
first_frame: ff,
|
start_frame: sf,
|
||||||
last_frame: lf,
|
end_frame: ef,
|
||||||
first_sec: fs,
|
start_time: sf as f64 / fps,
|
||||||
last_sec: ls,
|
end_time: ef as f64 / fps,
|
||||||
duration_sec: dur,
|
duration_sec: dur / fps,
|
||||||
avg_confidence: conf,
|
avg_confidence: conf,
|
||||||
sample_face_id: fid,
|
sample_face_id: fid.map(|v| v.to_string()),
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
@@ -151,6 +152,7 @@ async fn list_traces_sorted(
|
|||||||
Ok(Json(TracesResponse {
|
Ok(Json(TracesResponse {
|
||||||
success: true,
|
success: true,
|
||||||
file_uuid,
|
file_uuid,
|
||||||
|
fps,
|
||||||
total_traces,
|
total_traces,
|
||||||
total_faces,
|
total_faces,
|
||||||
page,
|
page,
|
||||||
@@ -174,7 +176,9 @@ struct TraceFacesQuery {
|
|||||||
struct TraceFaceItem {
|
struct TraceFaceItem {
|
||||||
id: i32,
|
id: i32,
|
||||||
start_frame: i32,
|
start_frame: i32,
|
||||||
|
end_frame: i32,
|
||||||
start_time: f64,
|
start_time: f64,
|
||||||
|
end_time: f64,
|
||||||
x: Option<i32>,
|
x: Option<i32>,
|
||||||
y: Option<i32>,
|
y: Option<i32>,
|
||||||
width: Option<i32>,
|
width: Option<i32>,
|
||||||
@@ -188,6 +192,7 @@ struct TraceFacesResponse {
|
|||||||
success: bool,
|
success: bool,
|
||||||
file_uuid: String,
|
file_uuid: String,
|
||||||
trace_id: i32,
|
trace_id: i32,
|
||||||
|
fps: f64,
|
||||||
total: i64,
|
total: i64,
|
||||||
faces: Vec<TraceFaceItem>,
|
faces: Vec<TraceFaceItem>,
|
||||||
}
|
}
|
||||||
@@ -274,10 +279,13 @@ async fn list_trace_faces(
|
|||||||
let mid_w = lerp_i32(prev.4, *w, t);
|
let mid_w = lerp_i32(prev.4, *w, t);
|
||||||
let mid_h = lerp_i32(prev.5, *h, t);
|
let mid_h = lerp_i32(prev.5, *h, t);
|
||||||
let mid_frame = prev_frame + mid;
|
let mid_frame = prev_frame + mid;
|
||||||
|
let mt = (mid_frame as f64 / fps * 10.0).round() / 10.0;
|
||||||
faces.push(TraceFaceItem {
|
faces.push(TraceFaceItem {
|
||||||
id: 0,
|
id: 0,
|
||||||
start_frame: mid_frame,
|
start_frame: mid_frame,
|
||||||
start_time: (mid_frame as f64 / fps * 10.0).round() / 10.0,
|
end_frame: mid_frame,
|
||||||
|
start_time: mt,
|
||||||
|
end_time: mt,
|
||||||
x: mid_x,
|
x: mid_x,
|
||||||
y: mid_y,
|
y: mid_y,
|
||||||
width: mid_w,
|
width: mid_w,
|
||||||
@@ -291,10 +299,13 @@ async fn list_trace_faces(
|
|||||||
|
|
||||||
// Add the real detection
|
// Add the real detection
|
||||||
let frame_val = *frame;
|
let frame_val = *frame;
|
||||||
|
let ft = (frame_val as f64 / fps * 10.0).round() / 10.0;
|
||||||
faces.push(TraceFaceItem {
|
faces.push(TraceFaceItem {
|
||||||
id: *id,
|
id: *id,
|
||||||
start_frame: frame_val,
|
start_frame: frame_val,
|
||||||
start_time: (frame_val as f64 / fps * 10.0).round() / 10.0,
|
end_frame: frame_val,
|
||||||
|
start_time: ft,
|
||||||
|
end_time: ft,
|
||||||
x: *x,
|
x: *x,
|
||||||
y: *y,
|
y: *y,
|
||||||
width: *w,
|
width: *w,
|
||||||
@@ -314,6 +325,7 @@ async fn list_trace_faces(
|
|||||||
success: true,
|
success: true,
|
||||||
file_uuid,
|
file_uuid,
|
||||||
trace_id,
|
trace_id,
|
||||||
|
fps,
|
||||||
total,
|
total,
|
||||||
faces,
|
faces,
|
||||||
}))
|
}))
|
||||||
|
|||||||
Reference in New Issue
Block a user