use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use sqlx::FromRow; // ========================================== // 舊版結構體 (保留以向後兼容) // ========================================== #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct PersonIdentity { pub id: i32, pub person_id: String, pub face_identity_id: Option, pub speaker_id: Option, pub file_uuid: String, pub confidence: f64, pub name: Option, pub metadata: serde_json::Value, pub first_appearance_time: Option, pub last_appearance_time: Option, pub total_appearance_duration: f64, pub appearance_count: i32, pub created_at: DateTime, pub updated_at: DateTime, pub is_confirmed: bool, } // ========================================== // 新版結構體 (V5 身份綁定系統) // ========================================== /// 人物身份 (Identity) - 統一管理演員、公眾人物、家人朋友等 #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct Identity { pub id: i32, pub name: String, pub embedding: Option, pub metadata: Option, pub created_at: DateTime, pub uuid: Option, pub identity_type: Option, pub source: Option, pub status: Option, pub face_embedding: Option>, pub voice_embedding: Option>, pub identity_embedding: Option>, pub reference_data: Option, pub tmdb_id: Option, pub tmdb_profile: Option, pub tmdb_poster: Option, pub file_uuid: Option, } /// 身份綁定記錄 (Identity Binding) /// 將機器 ID (face_x, speaker_y) 綁定到 Identity #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct IdentityBinding { pub id: i64, pub identity_id: i64, pub binding_type: String, // 'face', 'speaker' pub binding_value: String, // e.g. "face_1", "speaker_3" pub source: String, // 'auto', 'manual' pub confidence: f64, pub is_active: bool, pub created_at: DateTime, } /// 綁定請求 (用於 API) #[derive(Debug, Clone, Deserialize, Serialize)] pub struct BindIdentityRequest { pub file_uuid: String, pub face_id: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct UnbindIdentityRequest { pub file_uuid: String, pub face_id: String, } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct MergeIdentitiesRequest { pub into_uuid: String, pub keep_history: Option, } /// 建議綁定請求 (由系統自動產生,人工確認) #[derive(Debug, Clone, Deserialize, Serialize)] pub struct SuggestedBinding { pub binding_type: String, pub binding_value: String, pub suggested_identity_id: i64, pub suggested_identity_name: String, pub confidence: f64, pub reason: String, } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct PersonAppearance { pub id: i32, pub person_id: String, pub file_uuid: String, pub start_time: f64, pub end_time: f64, pub duration: f64, pub face_detection_id: Option, pub asrx_segment_start: Option, pub asrx_segment_end: Option, pub confidence: f64, pub metadata: serde_json::Value, pub created_at: DateTime, } #[derive(Debug, Clone, Serialize, Deserialize, FromRow)] pub struct PersonMatch { pub face_id: String, pub speaker_id: String, pub confidence: f64, pub match_count: i64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersonTimelineEntry { pub start_time: f64, pub end_time: f64, pub duration: f64, pub confidence: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersonStatistics { pub total_appearances: i32, pub total_duration: f64, pub first_appearance: Option, pub last_appearance: Option, pub average_confidence: f64, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CreatePersonIdentityRequest { pub file_uuid: String, pub face_identity_id: Option, pub speaker_id: Option, pub name: Option, pub metadata: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UpdatePersonIdentityRequest { pub name: Option, pub metadata: Option, pub is_confirmed: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersonIdentityResponse { pub person_id: String, pub name: Option, pub face_identity_id: Option, pub speaker_id: Option, pub confidence: f64, pub appearance_count: i32, pub total_appearance_duration: f64, pub first_appearance_time: Option, pub last_appearance_time: Option, pub is_confirmed: bool, } impl From for PersonIdentityResponse { fn from(person: PersonIdentity) -> Self { Self { person_id: person.person_id, name: person.name, face_identity_id: person.face_identity_id, speaker_id: person.speaker_id, confidence: person.confidence, appearance_count: person.appearance_count, total_appearance_duration: person.total_appearance_duration, first_appearance_time: person.first_appearance_time, last_appearance_time: person.last_appearance_time, is_confirmed: person.is_confirmed, } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PersonTimelineResponse { pub person_id: String, pub name: Option, pub timeline: Vec, pub statistics: PersonStatistics, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChunkPersonInfo { pub person_id: String, pub name: Option, pub confidence: f64, pub overlap_duration: f64, } #[cfg(test)] mod tests { use super::*; #[test] fn test_person_identity_serialization() { let person = PersonIdentity { id: 1, person_id: "person_001".to_string(), face_identity_id: Some(123), speaker_id: Some("SPEAKER_00".to_string()), file_uuid: "video_abc".to_string(), confidence: 0.85, name: Some("张三".to_string()), metadata: serde_json::json!({"role": "host"}), first_appearance_time: Some(10.5), last_appearance_time: Some(350.2), total_appearance_duration: 120.5, appearance_count: 15, created_at: Utc::now(), updated_at: Utc::now(), is_confirmed: true, }; let json = serde_json::to_string(&person).unwrap(); assert!(json.contains("person_001")); assert!(json.contains("SPEAKER_00")); assert!(json.contains("张三")); } #[test] fn test_person_appearance_serialization() { let appearance = PersonAppearance { id: 1, person_id: "person_001".to_string(), file_uuid: "video_abc".to_string(), start_time: 10.5, end_time: 25.3, duration: 14.8, face_detection_id: Some(456), asrx_segment_start: Some(10.0), asrx_segment_end: Some(26.0), confidence: 0.92, metadata: serde_json::json!({}), created_at: Utc::now(), }; let json = serde_json::to_string(&appearance).unwrap(); assert!(json.contains("person_001")); assert!(json.contains("14.8")); } #[test] fn test_person_match() { let match_result = PersonMatch { face_id: "face_123".to_string(), speaker_id: "SPEAKER_00".to_string(), confidence: 0.85, match_count: 15, }; assert_eq!(match_result.face_id, "face_123"); assert!(match_result.confidence >= 0.0 && match_result.confidence <= 1.0); } #[test] fn test_person_statistics() { let stats = PersonStatistics { total_appearances: 15, total_duration: 120.5, first_appearance: Some(10.5), last_appearance: Some(350.2), average_confidence: 0.88, }; assert_eq!(stats.total_appearances, 15); assert!(stats.total_duration > 0.0); } }