Release v1.0.0 candidate
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
extract::{Path, Query, State},
|
||||
http::StatusCode,
|
||||
response::Json,
|
||||
routing::post,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -10,10 +10,15 @@ 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),
|
||||
)
|
||||
Router::new()
|
||||
.route(
|
||||
"/api/v1/file/:file_uuid/face_trace/sortby",
|
||||
post(list_traces_sorted),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/file/:file_uuid/trace/:trace_id/faces",
|
||||
get(list_trace_faces),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -21,6 +26,8 @@ struct TracesRequest {
|
||||
min_faces: Option<i64>,
|
||||
sort_by: Option<String>,
|
||||
limit: Option<i64>,
|
||||
min_confidence: Option<f64>,
|
||||
max_confidence: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -53,14 +60,15 @@ async fn list_traces_sorted(
|
||||
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 min_confidence = req.min_confidence.unwrap_or(0.0);
|
||||
let max_confidence = req.max_confidence.unwrap_or(1.0);
|
||||
|
||||
let order_clause = match sort {
|
||||
"face_count" => "face_count DESC",
|
||||
"duration" => "duration_sec DESC",
|
||||
"duration" => "(MAX(frame_number) - MIN(frame_number)) 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)
|
||||
@@ -84,6 +92,7 @@ async fn list_traces_sorted(
|
||||
AVG(confidence) AS avg_confidence
|
||||
FROM dev.face_detections
|
||||
WHERE file_uuid = $1 AND trace_id IS NOT NULL
|
||||
AND confidence >= $4 AND confidence <= $5
|
||||
GROUP BY trace_id
|
||||
HAVING COUNT(*) >= $2
|
||||
ORDER BY {}
|
||||
@@ -103,6 +112,8 @@ async fn list_traces_sorted(
|
||||
.bind(&file_uuid)
|
||||
.bind(min_faces)
|
||||
.bind(limit)
|
||||
.bind(min_confidence)
|
||||
.bind(max_confidence)
|
||||
.fetch_all(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
@@ -138,3 +149,146 @@ async fn list_traces_sorted(
|
||||
traces,
|
||||
}))
|
||||
}
|
||||
|
||||
// ── Individual face detections for a trace ──
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct TraceFacesQuery {
|
||||
limit: Option<i64>,
|
||||
offset: Option<i64>,
|
||||
interpolate: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct TraceFaceItem {
|
||||
id: i32,
|
||||
start_frame: i32,
|
||||
start_time: f64,
|
||||
x: Option<i32>,
|
||||
y: Option<i32>,
|
||||
width: Option<i32>,
|
||||
height: Option<i32>,
|
||||
confidence: f64,
|
||||
interpolated: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct TraceFacesResponse {
|
||||
success: bool,
|
||||
file_uuid: String,
|
||||
trace_id: i32,
|
||||
total: i64,
|
||||
faces: Vec<TraceFaceItem>,
|
||||
}
|
||||
|
||||
fn lerp_i32(a: Option<i32>, b: Option<i32>, t: f64) -> Option<i32> {
|
||||
match (a, b) {
|
||||
(Some(av), Some(bv)) => Some((av as f64 + (bv - av) as f64 * t).round() as i32),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_trace_faces(
|
||||
State(state): State<crate::api::server::AppState>,
|
||||
Path((file_uuid, trace_id)): Path<(String, i32)>,
|
||||
Query(q): Query<TraceFacesQuery>,
|
||||
) -> Result<Json<TraceFacesResponse>, (StatusCode, String)> {
|
||||
let limit = q.limit.unwrap_or(200).min(1000);
|
||||
let offset = q.offset.unwrap_or(0);
|
||||
let interpolate = q.interpolate.unwrap_or(false);
|
||||
|
||||
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 total_detected: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM dev.face_detections WHERE file_uuid = $1 AND trace_id = $2"
|
||||
)
|
||||
.bind(&file_uuid)
|
||||
.bind(trace_id)
|
||||
.fetch_one(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let rows: Vec<(i32, i32, Option<i32>, Option<i32>, Option<i32>, Option<i32>, f32)> =
|
||||
sqlx::query_as(
|
||||
"SELECT id, frame_number, x, y, width, height, confidence
|
||||
FROM dev.face_detections
|
||||
WHERE file_uuid = $1 AND trace_id = $2
|
||||
ORDER BY frame_number ASC
|
||||
LIMIT $3 OFFSET $4"
|
||||
)
|
||||
.bind(&file_uuid)
|
||||
.bind(trace_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let mut faces: Vec<TraceFaceItem> = Vec::new();
|
||||
|
||||
for (i, (id, frame, x, y, w, h, conf)) in rows.iter().enumerate() {
|
||||
let cur = (x, y, w, h);
|
||||
|
||||
// Add interpolated frames between previous and current detection
|
||||
if interpolate && i > 0 {
|
||||
let prev = &rows[i - 1];
|
||||
let prev_frame = prev.1;
|
||||
let gap = frame - prev_frame;
|
||||
if gap > 1 {
|
||||
for mid in 1..gap {
|
||||
let t = mid as f64 / gap as f64;
|
||||
let mid_x = lerp_i32(prev.2, *x, t);
|
||||
let mid_y = lerp_i32(prev.3, *y, t);
|
||||
let mid_w = lerp_i32(prev.4, *w, t);
|
||||
let mid_h = lerp_i32(prev.5, *h, t);
|
||||
let mid_frame = prev_frame + mid;
|
||||
faces.push(TraceFaceItem {
|
||||
id: 0,
|
||||
start_frame: mid_frame,
|
||||
start_time: (mid_frame as f64 / fps * 10.0).round() / 10.0,
|
||||
x: mid_x,
|
||||
y: mid_y,
|
||||
width: mid_w,
|
||||
height: mid_h,
|
||||
confidence: 0.0,
|
||||
interpolated: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add the real detection
|
||||
let frame_val = *frame;
|
||||
faces.push(TraceFaceItem {
|
||||
id: *id,
|
||||
start_frame: frame_val,
|
||||
start_time: (frame_val as f64 / fps * 10.0).round() / 10.0,
|
||||
x: *x,
|
||||
y: *y,
|
||||
width: *w,
|
||||
height: *h,
|
||||
confidence: *conf as f64,
|
||||
interpolated: false,
|
||||
});
|
||||
}
|
||||
|
||||
let total = if interpolate && faces.len() as i64 > total_detected {
|
||||
faces.len() as i64
|
||||
} else {
|
||||
total_detected
|
||||
};
|
||||
|
||||
Ok(Json(TraceFacesResponse {
|
||||
success: true,
|
||||
file_uuid,
|
||||
trace_id,
|
||||
total,
|
||||
faces,
|
||||
}))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user