Files
momentry_core/src/core/person_identity.rs
Accusys ffc30d7377 M4 handover: coordinate fixes, detector registry, deploy v2, YOLOv8s, identity lifecycle
- Fix swift_pose/swift_ocr Y-flip bugs (BUG-003~006)
- Add heuristic_scene module + post-processing trigger (replaces Places365)
- YOLOv5nu → YOLOv8s CoreML (+33% detections, +390% scene indicators)
- Per-table SQL export (split 4.7GB single file → 478MB max per table)
- Version/build check in deploy.sh (compare /health vs file_info.json)
- Add file_uuid column to identities table + backfill
- Identity pre-clean step in deploy (avoids UNIQUE conflicts on re-deploy)
- Stranger_xxx naming fix with UUID context
- Add DETECTOR_REGISTRY.md (25 detectors), DETECTOR_SELECTION_SOP.md
- Update SPATIAL_COORDINATE_REGISTRY.md (P layer, 6-layer architecture)
- New IDENTITY_LIFECYCLE.md
- M4 response docs for deploy_script_fix and 111614 test report
2026-05-13 20:00:47 +08:00

281 lines
8.3 KiB
Rust

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 file_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>,
pub metadata: Option<serde_json::Value>,
pub created_at: DateTime<Utc>,
pub uuid: Option<uuid::Uuid>,
pub identity_type: Option<String>,
pub source: Option<String>,
pub status: Option<String>,
pub face_embedding: Option<Vec<f32>>,
pub voice_embedding: Option<Vec<f32>>,
pub identity_embedding: Option<Vec<f32>>,
pub reference_data: Option<serde_json::Value>,
pub tmdb_id: Option<i32>,
pub tmdb_profile: Option<String>,
pub tmdb_poster: Option<String>,
pub file_uuid: Option<String>,
}
/// 身份綁定記錄 (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 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<bool>,
}
/// 建議綁定請求 (由系統自動產生,人工確認)
#[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<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 file_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()),
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);
}
}