use axum::{ extract::{Path, Query}, http::StatusCode, response::Json, routing::{get, post}, Router, }; use serde::{Deserialize, Serialize}; use crate::core::db::{Database, PostgresDb}; use crate::core::person_identity::{BindIdentityRequest, Identity, UnbindIdentityRequest}; #[derive(Debug, Clone, Serialize)] pub struct ApiResponse { pub success: bool, pub message: String, pub data: Option, } // ============================================================================ // API Handlers // ============================================================================ async fn get_db() -> Result)> { PostgresDb::init().await.map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": format!("DB init failed: {}", e) })), ) }) } /// 獲取 Identity (人物) 列表 pub async fn list_identities( Query(params): Query, ) -> Result>>, (StatusCode, Json)> { let db = get_db().await?; let limit = params.limit.unwrap_or(100); let offset = params.offset.unwrap_or(0); let search = params.search.unwrap_or_default(); let identities = db .list_identities(&search, limit, offset) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() })), ) })?; Ok(Json(ApiResponse { success: true, message: format!("Found {} identities", identities.len()), data: Some(identities), })) } /// 綁定身份 (Face/Speaker -> Identity) pub async fn bind_identity( Json(req): Json, ) -> Result>, (StatusCode, Json)> { let db = get_db().await?; let identity = if let Some(id_id) = req.identity_id { db.get_identity_by_id(id_id).await.ok().flatten() } else if let Some(name) = &req.name { db.get_or_create_identity(name).await.ok() } else { None }; let identity = match identity { Some(t) => t, None => { return Err(( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Identity not found or name required" })), )); } }; let source = req.source.unwrap_or("manual".to_string()); db.bind_identity( identity.id as i64, &req.binding_type, &req.binding_value, &source, 1.0, ) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() })), ) })?; Ok(Json(ApiResponse { success: true, message: format!( "Bound {} '{}' to Identity '{}'", req.binding_type, req.binding_value, identity.name ), data: None, })) } /// 解綁身份 pub async fn unbind_identity( Json(req): Json, ) -> Result>, (StatusCode, Json)> { let db = get_db().await?; db.unbind_identity(&req.binding_type, &req.binding_value) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() })), ) })?; Ok(Json(ApiResponse { success: true, message: format!("Unbound {} '{}'", req.binding_type, req.binding_value), data: None, })) } /// 查詢機器 ID 對應的 Identity (人物) pub async fn get_identity_info( Path((binding_type, binding_value)): Path<(String, String)>, ) -> Result>, (StatusCode, Json)> { let db = get_db().await?; let identity = db .get_identity_by_binding(&binding_type, &binding_value) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() })), ) })?; let identity = identity.ok_or_else(|| { ( StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Identity not found" })), ) })?; Ok(Json(ApiResponse { success: true, message: "Identity info retrieved".to_string(), data: Some(identity), })) } /// 列出未綁定的信號 (待標註列表) pub async fn list_unbound_signals( Query(params): Query, ) -> Result>>, (StatusCode, Json)> { let db = get_db().await?; let signals = db .list_unbound_signals(¶ms.uuid, ¶ms.binding_type) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() })), ) })?; Ok(Json(ApiResponse { success: true, message: format!( "Found {} unbound {} signals", signals.len(), params.binding_type ), data: Some(signals), })) } /// 獲取特定信號 (Face ID 或 Speaker ID) 出現的所有 Chunk (時間軸) pub async fn get_signal_timeline( Path((uuid, binding_type, binding_value)): Path<(String, String, String)>, ) -> Result>>, (StatusCode, Json)> { let db = get_db().await?; let chunks = db .get_chunks_by_signal(&uuid, &binding_type, &binding_value) .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() })), ) })?; Ok(Json(ApiResponse { success: true, message: format!( "Found {} chunks for {} '{}'", chunks.len(), binding_type, binding_value ), data: Some(chunks), })) } #[derive(Debug, Deserialize)] pub struct AVSuggestRequest { pub video_uuid: String, pub overlap_threshold: Option, // default 0.6 } #[derive(Debug, Serialize)] pub struct AVSuggestion { pub face_id: String, pub speaker_id: String, pub overlap_score: f64, pub face_talent_id: Option, pub speaker_talent_id: Option, } /// Suggests Face-Speaker bindings based on temporal overlap pub async fn suggest_audio_visual_bindings( Json(req): Json, ) -> Result>>, (StatusCode, Json)> { let db = get_db().await?; let threshold = req.overlap_threshold.unwrap_or(0.6); // 1. Get Face signals and their time ranges let face_signals = db .list_unbound_signals(&req.video_uuid, "face") .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": format!("Face signals: {}", e) })), ) })?; let speaker_signals = db .list_unbound_signals(&req.video_uuid, "speaker") .await .map_err(|e| { ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": format!("Speaker signals: {}", e) })), ) })?; let mut suggestions = Vec::new(); for face_id in &face_signals { for speaker_id in &speaker_signals { // Calculate overlap // In a real implementation, we would query the exact timestamps from DB. // For now, we'll use a placeholder or simple heuristic if timestamps are available in signal timeline. // Let's assume we fetch timelines. // Placeholder: Calculate overlap by fetching timelines let face_timeline = db .get_chunks_by_signal(&req.video_uuid, "face", face_id) .await .unwrap_or_default(); let speaker_timeline = db .get_chunks_by_signal(&req.video_uuid, "speaker", speaker_id) .await .unwrap_or_default(); // Simplified overlap calculation based on chunk count/ids (assuming chunk_id contains time info or we have ranges) // Since chunk IDs are generic, we rely on content JSON having 'start_time' let overlap = calculate_overlap(&face_timeline, &speaker_timeline); if overlap >= threshold { // Check if they are already bound to the same identity let face_identity = db .get_identity_by_binding("face", face_id) .await .ok() .flatten(); let speaker_identity = db .get_identity_by_binding("speaker", speaker_id) .await .ok() .flatten(); // If both bound to different identities, don't suggest (conflict) if let (Some(fi), Some(si)) = (&face_identity, &speaker_identity) { if fi.id != si.id { continue; } } suggestions.push(AVSuggestion { face_id: face_id.clone(), speaker_id: speaker_id.clone(), overlap_score: overlap, face_talent_id: face_identity.as_ref().map(|i| i.id as i32), speaker_talent_id: speaker_identity.as_ref().map(|i| i.id as i32), }); } } } suggestions.sort_by(|a, b| b.overlap_score.partial_cmp(&a.overlap_score).unwrap()); Ok(Json(ApiResponse { success: true, message: format!("Found {} AV suggestions", suggestions.len()), data: Some(suggestions), })) } fn calculate_overlap( face_chunks: &[serde_json::Value], speaker_chunks: &[serde_json::Value], ) -> f64 { // Simplified: Extract start/end times and calculate intersection over union // Assuming chunks have start_frame or start_time in content JSON // If content is raw string, we might need to parse it. // In our schema, content is JSONB. let mut face_ranges: Vec<(f64, f64)> = Vec::new(); for c in face_chunks { if let Some(content) = c.get("content") { if let (Some(start), Some(end)) = ( content.get("start_time").and_then(|v| v.as_f64()), content.get("end_time").and_then(|v| v.as_f64()), ) { face_ranges.push((start, end)); } } } let mut speaker_ranges: Vec<(f64, f64)> = Vec::new(); for c in speaker_chunks { if let Some(content) = c.get("content") { if let (Some(start), Some(end)) = ( content.get("start_time").and_then(|v| v.as_f64()), content.get("end_time").and_then(|v| v.as_f64()), ) { speaker_ranges.push((start, end)); } } } let mut overlap_duration = 0.0; for (fs, fe) in &face_ranges { for (ss, se) in &speaker_ranges { let start = fs.max(*ss); let end = fe.min(*se); if start < end { overlap_duration += end - start; } } } // Return normalized overlap (0.0 to 1.0+), simple version: overlap / min_duration let min_duration = face_ranges .iter() .map(|(_, e)| e) .sum::() .min(speaker_ranges.iter().map(|(_, e)| e).sum::()); if min_duration > 0.0 { (overlap_duration / min_duration).min(1.0) } else { 0.0 } } // ============================================================================ // Router Setup // ============================================================================ #[derive(Debug, Deserialize)] pub struct ListIdentitiesParams { pub search: Option, pub limit: Option, pub offset: Option, } #[derive(Debug, Deserialize)] pub struct ListSignalsParams { pub uuid: String, pub binding_type: String, // "face" or "speaker" } pub fn identity_binding_routes() -> Router { Router::new() .route("/api/v1/identities/bind", post(bind_identity)) .route("/api/v1/identities/unbind", post(unbind_identity)) .route( "/api/v1/identity/:binding_type/:binding_value", get(get_identity_info), ) // 信號發現 (Discovery) .route("/api/v1/signals/unbound", get(list_unbound_signals)) // 信號時間軸 (Timeline) .route( "/api/v1/signals/:uuid/:binding_type/:binding_value/timeline", get(get_signal_timeline), ) .route( "/api/v1/identities/suggest-av", post(suggest_audio_visual_bindings), ) }