feat: media API (video/bbox/thumbnail), UUID unification, dot matrix text, portal fixes, API dictionary V1.3
This commit is contained in:
140
src/api/trace_agent_api.rs
Normal file
140
src/api/trace_agent_api.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::post,
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::db::PostgresDb;
|
||||
|
||||
pub fn trace_agent_routes() -> Router<crate::api::server::AppState> {
|
||||
Router::new().route(
|
||||
"/api/v1/file/:file_uuid/face_trace/sortby",
|
||||
post(list_traces_sorted),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TracesRequest {
|
||||
min_faces: Option<i64>,
|
||||
sort_by: Option<String>,
|
||||
limit: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct TraceInfo {
|
||||
trace_id: i32,
|
||||
face_count: i64,
|
||||
first_frame: i32,
|
||||
last_frame: i32,
|
||||
first_sec: f64,
|
||||
last_sec: f64,
|
||||
duration_sec: f64,
|
||||
avg_confidence: f64,
|
||||
sample_face_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct TracesResponse {
|
||||
success: bool,
|
||||
file_uuid: String,
|
||||
total_traces: i64,
|
||||
total_faces: i64,
|
||||
traces: Vec<TraceInfo>,
|
||||
}
|
||||
|
||||
async fn list_traces_sorted(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Path(file_uuid): Path<String>,
|
||||
Json(req): Json<TracesRequest>,
|
||||
) -> Result<Json<TracesResponse>, (StatusCode, String)> {
|
||||
let min_faces = req.min_faces.unwrap_or(1);
|
||||
let sort = req.sort_by.as_deref().unwrap_or("first_appearance");
|
||||
let limit = req.limit.unwrap_or(500);
|
||||
|
||||
let order_clause = match sort {
|
||||
"face_count" => "face_count DESC",
|
||||
"duration" => "duration_sec DESC",
|
||||
_ => "first_frame ASC",
|
||||
};
|
||||
|
||||
// Get actual video FPS
|
||||
let fps: f64 =
|
||||
sqlx::query_scalar("SELECT COALESCE(fps, 24.0) FROM dev.videos WHERE file_uuid = $1")
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
|
||||
.unwrap_or(24.0);
|
||||
|
||||
let query = format!(
|
||||
r#"SELECT tt.trace_id, tt.face_count, tt.first_frame, tt.last_frame,
|
||||
ROUND(tt.first_frame::numeric / {}, 1)::float8 AS first_sec,
|
||||
ROUND(tt.last_frame::numeric / {}, 1)::float8 AS last_sec,
|
||||
ROUND((tt.last_frame - tt.first_frame)::numeric / {}, 1)::float8 AS duration_sec,
|
||||
ROUND(tt.avg_confidence::numeric, 4)::float8 AS avg_confidence,
|
||||
fd.id::text AS sample_face_id
|
||||
FROM (
|
||||
SELECT trace_id,
|
||||
COUNT(*) AS face_count,
|
||||
MIN(frame_number) AS first_frame,
|
||||
MAX(frame_number) AS last_frame,
|
||||
AVG(confidence) AS avg_confidence
|
||||
FROM dev.face_detections
|
||||
WHERE file_uuid = $1 AND trace_id IS NOT NULL
|
||||
GROUP BY trace_id
|
||||
HAVING COUNT(*) >= $2
|
||||
ORDER BY {}
|
||||
LIMIT $3
|
||||
) tt
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id FROM dev.face_detections
|
||||
WHERE trace_id = tt.trace_id AND file_uuid = $1
|
||||
ORDER BY confidence DESC LIMIT 1
|
||||
) fd ON true
|
||||
"#,
|
||||
fps, fps, fps, order_clause
|
||||
);
|
||||
|
||||
let rows: Vec<(i32, i64, i32, i32, f64, f64, f64, f64, Option<String>)> =
|
||||
sqlx::query_as(&query)
|
||||
.bind(&file_uuid)
|
||||
.bind(min_faces)
|
||||
.bind(limit)
|
||||
.fetch_all(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let traces: Vec<TraceInfo> = rows
|
||||
.into_iter()
|
||||
.map(|(tid, fc, ff, lf, fs, ls, dur, conf, fid)| TraceInfo {
|
||||
trace_id: tid,
|
||||
face_count: fc,
|
||||
first_frame: ff,
|
||||
last_frame: lf,
|
||||
first_sec: fs,
|
||||
last_sec: ls,
|
||||
duration_sec: dur,
|
||||
avg_confidence: conf,
|
||||
sample_face_id: fid,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let (total_traces, total_faces): (i64, i64) = sqlx::query_as(
|
||||
"SELECT COUNT(DISTINCT trace_id), COUNT(*) FROM dev.face_detections WHERE file_uuid = $1 AND trace_id IS NOT NULL"
|
||||
)
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(TracesResponse {
|
||||
success: true,
|
||||
file_uuid,
|
||||
total_traces,
|
||||
total_faces,
|
||||
traces,
|
||||
}))
|
||||
}
|
||||
Reference in New Issue
Block a user