feat: backup architecture docs, source code, and scripts
This commit is contained in:
266
src/core/person_identity.rs
Normal file
266
src/core/person_identity.rs
Normal file
@@ -0,0 +1,266 @@
|
||||
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<i32>,
|
||||
pub speaker_id: Option<String>,
|
||||
pub video_uuid: String,
|
||||
pub confidence: f64,
|
||||
pub name: Option<String>,
|
||||
pub metadata: serde_json::Value,
|
||||
pub first_appearance_time: Option<f64>,
|
||||
pub last_appearance_time: Option<f64>,
|
||||
pub total_appearance_duration: f64,
|
||||
pub appearance_count: i32,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub is_confirmed: bool,
|
||||
}
|
||||
|
||||
// ==========================================
|
||||
// 新版結構體 (V5 身份綁定系統)
|
||||
// ==========================================
|
||||
|
||||
/// 人物身份 (Identity) - 統一管理演員、公眾人物、家人朋友等
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
|
||||
pub struct Identity {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
pub embedding: Option<String>, // Vector embedding stored as text/json
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
/// 身份綁定記錄 (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<Utc>,
|
||||
}
|
||||
|
||||
/// 綁定請求 (用於 API)
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct BindIdentityRequest {
|
||||
pub identity_id: Option<i64>,
|
||||
pub name: Option<String>, // 若未提供 identity_id,則建立新 Identity
|
||||
pub binding_type: String, // 'face' 或 'speaker'
|
||||
pub binding_value: String, // e.g. "face_1"
|
||||
pub source: Option<String>, // 預設 'manual'
|
||||
}
|
||||
|
||||
/// 解綁請求
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct UnbindIdentityRequest {
|
||||
pub binding_type: String,
|
||||
pub binding_value: String,
|
||||
}
|
||||
|
||||
/// 建議綁定請求 (由系統自動產生,人工確認)
|
||||
#[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 video_uuid: String,
|
||||
pub start_time: f64,
|
||||
pub end_time: f64,
|
||||
pub duration: f64,
|
||||
pub face_detection_id: Option<i32>,
|
||||
pub asrx_segment_start: Option<f64>,
|
||||
pub asrx_segment_end: Option<f64>,
|
||||
pub confidence: f64,
|
||||
pub metadata: serde_json::Value,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[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<f64>,
|
||||
pub last_appearance: Option<f64>,
|
||||
pub average_confidence: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CreatePersonIdentityRequest {
|
||||
pub video_uuid: String,
|
||||
pub face_identity_id: Option<i32>,
|
||||
pub speaker_id: Option<String>,
|
||||
pub name: Option<String>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UpdatePersonIdentityRequest {
|
||||
pub name: Option<String>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub is_confirmed: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PersonIdentityResponse {
|
||||
pub person_id: String,
|
||||
pub name: Option<String>,
|
||||
pub face_identity_id: Option<i32>,
|
||||
pub speaker_id: Option<String>,
|
||||
pub confidence: f64,
|
||||
pub appearance_count: i32,
|
||||
pub total_appearance_duration: f64,
|
||||
pub first_appearance_time: Option<f64>,
|
||||
pub last_appearance_time: Option<f64>,
|
||||
pub is_confirmed: bool,
|
||||
}
|
||||
|
||||
impl From<PersonIdentity> 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<String>,
|
||||
pub timeline: Vec<PersonTimelineEntry>,
|
||||
pub statistics: PersonStatistics,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChunkPersonInfo {
|
||||
pub person_id: String,
|
||||
pub name: Option<String>,
|
||||
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()),
|
||||
video_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(),
|
||||
video_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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user