feat: representative frame - auto-detect thumbnail + JSON endpoint

This commit is contained in:
Accusys
2026-05-22 09:22:15 +08:00
parent 2b025a014e
commit 2b950c985c
4 changed files with 266 additions and 4 deletions

View File

@@ -690,7 +690,7 @@ async fn stream_video(
#[derive(Debug, serde::Deserialize)]
struct ThumbQuery {
frame: i64,
frame: Option<i64>,
x: Option<i32>,
y: Option<i32>,
w: Option<i32>,
@@ -703,6 +703,20 @@ async fn face_thumbnail(
Query(q): Query<ThumbQuery>,
) -> Result<impl IntoResponse, StatusCode> {
let videos_table = schema::table_name("videos");
let frame = match q.frame {
Some(f) => f,
None => {
let result = crate::core::processor::tkg::query_auto_representative_frame(
state.db.pool(),
&file_uuid,
)
.await
.map_err(|_| StatusCode::NOT_FOUND)?;
result.frame_number
}
};
let row: Option<(String,)> = sqlx::query_as(&format!(
"SELECT file_path FROM {} WHERE file_uuid = $1",
videos_table
@@ -713,7 +727,7 @@ async fn face_thumbnail(
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
let (file_path,) = row.ok_or(StatusCode::NOT_FOUND)?;
let select = format!("select=eq(n\\,{})", q.frame);
let select = format!("select=eq(n\\,{})", frame);
let vf = if let (Some(x), Some(y), Some(w), Some(h)) = (q.x, q.y, q.w, q.h) {
format!("{},crop={}:{}:{}:{}", select, w, h, x, y)
} else {

View File

@@ -33,6 +33,10 @@ pub fn trace_agent_routes() -> Router<crate::api::types::AppState> {
"/api/v1/file/:file_uuid/tkg/rebuild",
post(rebuild_tkg),
)
.route(
"/api/v1/file/:file_uuid/representative-frame",
get(get_representative_frame),
)
}
#[derive(Debug, Deserialize)]
@@ -783,3 +787,59 @@ async fn rebuild_tkg(
}),
}
}
// ── Representative Frame (JSON) ───────────────────────────────────
use crate::core::processor::tkg;
#[derive(Serialize)]
struct RepFrameResponse {
success: bool,
file_uuid: String,
frame_number: i64,
timestamp_secs: f64,
face_quality: f64,
main_identities: Vec<tkg::MainIdentityInfo>,
traces: Vec<tkg::FrameTraceInfo>,
}
async fn get_representative_frame(
State(state): State<crate::api::types::AppState>,
Path(file_uuid): Path<String>,
) -> Result<Json<RepFrameResponse>, (StatusCode, Json<serde_json::Value>)> {
let result = tkg::query_auto_representative_frame(
state.db.pool(),
&file_uuid,
)
.await
.map_err(|e| {
(StatusCode::NOT_FOUND, Json(serde_json::json!({"error": e.to_string()})))
})?;
let fps = query_fps(state.db.pool(), &file_uuid).await;
Ok(Json(RepFrameResponse {
success: true,
file_uuid,
frame_number: result.frame_number,
timestamp_secs: result.frame_number as f64 / fps,
face_quality: result.face_quality,
main_identities: result.main_identities,
traces: result.traces,
}))
}
async fn query_fps(pool: &sqlx::PgPool, file_uuid: &str) -> f64 {
use crate::core::db::schema;
let video_table = schema::table_name("videos");
sqlx::query_scalar(&format!(
"SELECT COALESCE(fps, 25.0) FROM {} WHERE file_uuid = $1",
video_table
))
.bind(file_uuid)
.fetch_optional(pool)
.await
.ok()
.flatten()
.unwrap_or(25.0)
}