feat: update core API, database layer, and worker modules
- Remove unused imports (n8n_search, universal_search, Client, Arc, etc.) - Update API endpoints for identity, face recognition, search - Fix postgres_db.rs search_videos parent_uuid column - Add snapshot API and identity agent API - Clean up backup files (.bak, .bak2)
This commit is contained in:
@@ -10,7 +10,6 @@ use axum::{
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::core::db::schema;
|
||||
use crate::core::db::{Database, PostgresDb};
|
||||
use crate::core::person_identity::{
|
||||
ChunkPersonInfo, CreatePersonIdentityRequest, PersonIdentity, PersonIdentityResponse,
|
||||
@@ -20,7 +19,7 @@ use crate::core::person_identity::{
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct IdentifyPersonsRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub auto_match: Option<bool>,
|
||||
pub match_threshold: Option<f64>,
|
||||
}
|
||||
@@ -39,19 +38,19 @@ pub struct FaceListQuery {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct VideoUuidQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PersonTimelineQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FaceThumbnailQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
#[serde(default)]
|
||||
pub index: Option<usize>, // Which face detection to use (default: 0)
|
||||
pub index: Option<usize>,
|
||||
}
|
||||
|
||||
// Structs for parsing face_clustered.json
|
||||
@@ -85,7 +84,7 @@ pub struct ChunkPersonsResponse {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MergePersonsRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub target_person_id: String,
|
||||
pub source_person_ids: Vec<String>,
|
||||
}
|
||||
@@ -115,7 +114,7 @@ pub struct MergeHistoryResponse {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PersonListQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub page: Option<usize>,
|
||||
pub page_size: Option<usize>,
|
||||
pub min_appearances: Option<i32>,
|
||||
@@ -124,13 +123,13 @@ pub struct PersonListQuery {
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct AutoIdentifyRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub min_speaker_confidence: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SimilarPersonsQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub threshold: Option<f64>,
|
||||
pub limit: Option<i32>,
|
||||
}
|
||||
@@ -210,7 +209,7 @@ pub struct AutoIdentifyResponse {
|
||||
}
|
||||
|
||||
pub struct AggregateBySpeakerRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub auto_merge: bool, // If true, automatically merge duplicates
|
||||
}
|
||||
|
||||
@@ -311,14 +310,14 @@ async fn identify_persons(
|
||||
|
||||
tracing::info!(
|
||||
"[PERSON_IDENTITY] Identifying persons for video: {}",
|
||||
request.video_uuid
|
||||
request.file_uuid
|
||||
);
|
||||
|
||||
let auto_match = request.auto_match.unwrap_or(true);
|
||||
let threshold = request.match_threshold.unwrap_or(0.5);
|
||||
|
||||
if auto_match {
|
||||
let matches = match auto_match_face_speaker(&db, &request.video_uuid, threshold).await {
|
||||
let matches = match auto_match_face_speaker(&db, &request.file_uuid, threshold).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
return Err((
|
||||
@@ -333,7 +332,7 @@ async fn identify_persons(
|
||||
let person = match create_person_identity(
|
||||
&db,
|
||||
CreatePersonIdentityRequest {
|
||||
video_uuid: request.video_uuid.clone(),
|
||||
file_uuid: request.file_uuid.clone(),
|
||||
face_identity_id: None,
|
||||
speaker_id: Some(match_result.speaker_id.clone()),
|
||||
name: None,
|
||||
@@ -372,7 +371,7 @@ async fn identify_persons(
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PersonDetailQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -381,7 +380,7 @@ pub struct SearchIdentitiesRequest {
|
||||
pub speaker_id: Option<String>,
|
||||
pub gender: Option<String>,
|
||||
pub min_appearances: Option<i32>,
|
||||
pub video_uuid: Option<String>, // Optional: search only in specific video
|
||||
pub file_uuid: Option<String>, // Optional: search only in specific video
|
||||
pub limit: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -404,7 +403,7 @@ async fn search_identities(
|
||||
let mut sql = format!(
|
||||
r#"
|
||||
SELECT
|
||||
id, person_id, face_identity_id, speaker_id, video_uuid,
|
||||
id, person_id, face_identity_id, speaker_id, file_uuid,
|
||||
name, original_name, character_name, gender, age,
|
||||
appearance_count, total_appearance_duration,
|
||||
first_appearance_time, last_appearance_time, is_confirmed
|
||||
@@ -435,8 +434,8 @@ async fn search_identities(
|
||||
conditions.push(format!("appearance_count >= {}", min_count));
|
||||
}
|
||||
|
||||
if let Some(vid) = &req.video_uuid {
|
||||
conditions.push(format!("video_uuid = '{}'", vid.replace('\'', "''")));
|
||||
if let Some(vid) = &req.file_uuid {
|
||||
conditions.push(format!("file_uuid = '{}'", vid.replace('\'', "''")));
|
||||
}
|
||||
|
||||
if !conditions.is_empty() {
|
||||
@@ -482,7 +481,7 @@ async fn search_identities(
|
||||
person_id,
|
||||
face_id,
|
||||
speaker_id,
|
||||
video_uuid,
|
||||
file_uuid,
|
||||
name,
|
||||
original_name,
|
||||
character_name,
|
||||
@@ -498,7 +497,7 @@ async fn search_identities(
|
||||
"id": id,
|
||||
"person_id": person_id,
|
||||
"face_identity_id": face_id,
|
||||
"video_uuid": video_uuid,
|
||||
"file_uuid": file_uuid,
|
||||
"profile": {
|
||||
"name": name,
|
||||
"original_name": original_name,
|
||||
@@ -579,12 +578,12 @@ async fn get_identity_videos(
|
||||
// Each video has its own local person_id, speaker_id, and character_name
|
||||
let videos_query = r#"
|
||||
SELECT
|
||||
pi.video_uuid, v.file_name, v.file_path,
|
||||
pi.file_uuid, v.file_name, v.file_path,
|
||||
pi.person_id, pi.speaker_id, pi.character_name,
|
||||
pi.appearance_count,
|
||||
pi.total_appearance_duration, pi.first_appearance_time, pi.last_appearance_time
|
||||
FROM '"{}' pi
|
||||
LEFT JOIN videos v ON pi.video_uuid = v.uuid
|
||||
LEFT JOIN videos v ON pi.file_uuid = v.uuid
|
||||
WHERE pi.face_identity_id = $1
|
||||
ORDER BY pi.last_appearance_time DESC
|
||||
"#;
|
||||
@@ -599,7 +598,7 @@ async fn get_identity_videos(
|
||||
.map(|row| {
|
||||
use sqlx::Row;
|
||||
serde_json::json!({
|
||||
"video_uuid": row.get::<String, _>("video_uuid"),
|
||||
"file_uuid": row.get::<String, _>("file_uuid"),
|
||||
"file_name": row.get::<Option<String>, _>("file_name"),
|
||||
"file_path": row.get::<Option<String>, _>("file_path"),
|
||||
"person_id": row.get::<String, _>("person_id"),
|
||||
@@ -659,12 +658,12 @@ async fn get_identity_faces(
|
||||
// Fetch distinct face detections for this identity
|
||||
let sql = r#"
|
||||
SELECT
|
||||
fd.id as detection_id, fd.video_uuid, fd.frame_number,
|
||||
fd.id as detection_id, fd.file_uuid, fd.frame_number,
|
||||
fd.timestamp_secs, fd.x, fd.y, fd.width, fd.height,
|
||||
fd.cluster_id, v.file_name
|
||||
FROM face_detections fd
|
||||
JOIN person_identities pi ON fd.video_uuid = pi.video_uuid
|
||||
LEFT JOIN videos v ON fd.video_uuid = v.uuid
|
||||
JOIN person_identities pi ON fd.file_uuid = pi.file_uuid
|
||||
LEFT JOIN videos v ON fd.file_uuid = v.uuid
|
||||
WHERE pi.face_identity_id = $1
|
||||
ORDER BY fd.timestamp_secs DESC
|
||||
LIMIT $2
|
||||
@@ -682,7 +681,7 @@ async fn get_identity_faces(
|
||||
use sqlx::Row;
|
||||
serde_json::json!({
|
||||
"detection_id": row.get::<i32, _>("detection_id"),
|
||||
"video_uuid": row.get::<String, _>("video_uuid"),
|
||||
"file_uuid": row.get::<String, _>("file_uuid"),
|
||||
"file_name": row.get::<Option<String>, _>("file_name"),
|
||||
"frame_number": row.get::<i64, _>("frame_number"),
|
||||
"timestamp": row.get::<f64, _>("timestamp_secs"),
|
||||
@@ -715,7 +714,7 @@ async fn get_identity_faces(
|
||||
/// List all faces in a video (both registered and unregistered)
|
||||
async fn get_video_faces(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Path(video_uuid): Path<String>,
|
||||
Path(file_uuid): Path<String>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
@@ -735,13 +734,13 @@ async fn get_video_faces(
|
||||
pi.person_id, pi.face_identity_id, pi.name, pi.is_confirmed
|
||||
FROM face_clusters fc
|
||||
LEFT JOIN person_identities pi ON fc.cluster_id = pi.person_id
|
||||
AND pi.video_uuid = fc.video_uuid
|
||||
WHERE fc.video_uuid = $1
|
||||
AND pi.file_uuid = fc.file_uuid
|
||||
WHERE fc.file_uuid = $1
|
||||
ORDER BY fc.size DESC
|
||||
"#;
|
||||
|
||||
let clusters: Vec<serde_json::Value> = match sqlx::query(clusters_query)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -785,7 +784,7 @@ async fn get_video_faces(
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"success": true,
|
||||
"video_uuid": video_uuid,
|
||||
"file_uuid": file_uuid,
|
||||
"total_faces": total_faces,
|
||||
"registered_count": registered_count,
|
||||
"unregistered_count": total_faces - registered_count,
|
||||
@@ -799,7 +798,7 @@ async fn register_identity(
|
||||
Path(person_id): Path<String>,
|
||||
Query(query): Query<VideoUuidQuery>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let video_uuid = query.video_uuid;
|
||||
let file_uuid = query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -813,11 +812,11 @@ async fn register_identity(
|
||||
// 1. Fetch person info
|
||||
let person_query = r#"
|
||||
SELECT id, name, face_identity_id FROM '"{}'
|
||||
WHERE person_id = $1 AND video_uuid = $2
|
||||
WHERE person_id = $1 AND file_uuid = $2
|
||||
"#;
|
||||
let person: Option<(i32, Option<String>, Option<i32>)> = match sqlx::query_as(person_query)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -835,7 +834,7 @@ async fn register_identity(
|
||||
None => {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
format!("Person '{}' not found in video '{}'", person_id, video_uuid),
|
||||
format!("Person '{}' not found in video '{}'", person_id, file_uuid),
|
||||
))
|
||||
}
|
||||
};
|
||||
@@ -851,11 +850,11 @@ async fn register_identity(
|
||||
|
||||
// 2. Get cluster centroid or detections embedding
|
||||
let cluster_query = r#"
|
||||
SELECT centroid FROM face_clusters WHERE cluster_id = $1 AND video_uuid = $2
|
||||
SELECT centroid FROM face_clusters WHERE cluster_id = $1 AND file_uuid = $2
|
||||
"#;
|
||||
let centroid: Option<Vec<f32>> = sqlx::query_scalar(cluster_query)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
.ok()
|
||||
@@ -923,7 +922,7 @@ async fn get_person_details(
|
||||
Path(person_id): Path<String>,
|
||||
Query(query): Query<PersonDetailQuery>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let video_uuid = query.video_uuid;
|
||||
let file_uuid = query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -941,7 +940,7 @@ async fn get_person_details(
|
||||
first_appearance_time, last_appearance_time,
|
||||
is_confirmed, created_at, updated_at
|
||||
FROM '"{}'
|
||||
WHERE person_id = $1 AND video_uuid = $2
|
||||
WHERE person_id = $1 AND file_uuid = $2
|
||||
"#;
|
||||
|
||||
let person: Option<(
|
||||
@@ -959,7 +958,7 @@ async fn get_person_details(
|
||||
chrono::DateTime<chrono::Utc>,
|
||||
)> = match sqlx::query_as(query)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1001,7 +1000,7 @@ async fn get_person_details(
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdatePersonQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
async fn update_person_identity(
|
||||
@@ -1010,7 +1009,7 @@ async fn update_person_identity(
|
||||
Query(query): Query<UpdatePersonQuery>,
|
||||
Json(request): Json<UpdatePersonIdentityRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let video_uuid = query.video_uuid;
|
||||
let file_uuid = query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -1083,10 +1082,10 @@ async fn get_person_timeline(
|
||||
}
|
||||
};
|
||||
|
||||
let name_query = "SELECT name FROM person_identities WHERE person_id = $1 AND video_uuid = $2";
|
||||
let name_query = "SELECT name FROM person_identities WHERE person_id = $1 AND file_uuid = $2";
|
||||
let name: Option<String> = match sqlx::query_scalar(name_query)
|
||||
.bind(&person_id)
|
||||
.bind(&query.video_uuid)
|
||||
.bind(&query.file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1102,13 +1101,13 @@ async fn get_person_timeline(
|
||||
let timeline_query = r#"
|
||||
SELECT start_time, end_time, duration, confidence
|
||||
FROM person_appearances
|
||||
WHERE person_id = $1 AND video_uuid = $2
|
||||
WHERE person_id = $1 AND file_uuid = $2
|
||||
ORDER BY start_time ASC
|
||||
"#;
|
||||
|
||||
let timeline: Vec<(f64, f64, f64, f64)> = match sqlx::query_as(timeline_query)
|
||||
.bind(&person_id)
|
||||
.bind(&query.video_uuid)
|
||||
.bind(&query.file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1139,13 +1138,13 @@ async fn get_person_timeline(
|
||||
MAX(end_time) as last_appearance,
|
||||
AVG(confidence) as average_confidence
|
||||
FROM person_appearances
|
||||
WHERE person_id = $1 AND video_uuid = $2
|
||||
WHERE person_id = $1 AND file_uuid = $2
|
||||
"#;
|
||||
|
||||
let stats: (i64, Option<f64>, Option<f64>, Option<f64>, Option<f64>) =
|
||||
match sqlx::query_as(stats_query)
|
||||
.bind(&person_id)
|
||||
.bind(&query.video_uuid)
|
||||
.bind(&query.file_uuid)
|
||||
.fetch_one(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1179,7 +1178,7 @@ async fn get_person_appearances(
|
||||
Path(person_id): Path<String>,
|
||||
Query(query): Query<PersonDetailQuery>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let video_uuid = query.video_uuid;
|
||||
let file_uuid = query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -1192,18 +1191,18 @@ async fn get_person_appearances(
|
||||
|
||||
let query = r#"
|
||||
SELECT
|
||||
person_id, video_uuid, start_time, end_time, duration,
|
||||
person_id, file_uuid, start_time, end_time, duration,
|
||||
face_detection_id, asrx_segment_start, asrx_segment_end,
|
||||
confidence, created_at
|
||||
FROM person_appearances
|
||||
WHERE person_id = $1 AND video_uuid = $2
|
||||
WHERE person_id = $1 AND file_uuid = $2
|
||||
ORDER BY start_time DESC
|
||||
LIMIT 100
|
||||
"#;
|
||||
|
||||
let appearances: Vec<serde_json::Value> = match sqlx::query(query)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1213,7 +1212,7 @@ async fn get_person_appearances(
|
||||
use sqlx::Row;
|
||||
serde_json::json!({
|
||||
"person_id": row.get::<String, _>("person_id"),
|
||||
"video_uuid": row.get::<String, _>("video_uuid"),
|
||||
"file_uuid": row.get::<String, _>("file_uuid"),
|
||||
"start_time": row.get::<f64, _>("start_time"),
|
||||
"end_time": row.get::<f64, _>("end_time"),
|
||||
"duration": row.get::<f64, _>("duration"),
|
||||
@@ -1240,7 +1239,7 @@ async fn get_person_appearances(
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ChunkPersonsQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
async fn get_chunk_persons(
|
||||
@@ -1248,7 +1247,7 @@ async fn get_chunk_persons(
|
||||
Path(chunk_id): Path<String>,
|
||||
Query(query): Query<ChunkPersonsQuery>,
|
||||
) -> Result<Json<ChunkPersonsResponse>, (StatusCode, String)> {
|
||||
let video_uuid = &query.video_uuid;
|
||||
let file_uuid = &query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -1290,7 +1289,7 @@ async fn get_chunk_persons(
|
||||
}
|
||||
};
|
||||
|
||||
let (video_uuid, start_time, end_time, _metadata) = chunk;
|
||||
let (file_uuid, start_time, end_time, _metadata) = chunk;
|
||||
|
||||
let persons_query = r#"
|
||||
SELECT
|
||||
@@ -1300,7 +1299,7 @@ async fn get_chunk_persons(
|
||||
LEAST(pa.end_time, $3) - GREATEST(pa.start_time, $2) as overlap_duration
|
||||
FROM person_appearances pa
|
||||
JOIN person_identities pi ON pa.person_id = pi.person_id
|
||||
WHERE pa.video_uuid = $1
|
||||
WHERE pa.file_uuid = $1
|
||||
AND pa.start_time < $3
|
||||
AND pa.end_time > $2
|
||||
ORDER BY overlap_duration DESC
|
||||
@@ -1308,7 +1307,7 @@ async fn get_chunk_persons(
|
||||
|
||||
let persons: Vec<ChunkPersonInfo> =
|
||||
match sqlx::query_as::<_, (String, Option<String>, f64, f64)>(persons_query)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.bind(start_time)
|
||||
.bind(end_time)
|
||||
.fetch_all(db.pool())
|
||||
@@ -1349,15 +1348,15 @@ async fn get_person_thumbnail(
|
||||
// 1. Locate the face_clustered.json file
|
||||
let json_path = format!(
|
||||
"output/{}/{}_face_clustered.json",
|
||||
query.video_uuid, query.video_uuid
|
||||
query.file_uuid, query.file_uuid
|
||||
);
|
||||
let json_path2 = format!(
|
||||
"output/{}/{}.face_clustered.json",
|
||||
query.video_uuid, query.video_uuid
|
||||
query.file_uuid, query.file_uuid
|
||||
);
|
||||
|
||||
// Fallback path if the naming convention is slightly different
|
||||
let fallback_path = format!("output/{}/face_clustered.json", query.video_uuid);
|
||||
let fallback_path = format!("output/{}/face_clustered.json", query.file_uuid);
|
||||
|
||||
let path = if std::path::Path::new(&json_path).exists() {
|
||||
json_path
|
||||
@@ -1370,7 +1369,7 @@ async fn get_person_thumbnail(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!(
|
||||
"Face data not found for video: {}. Tried: {}, {}, {}",
|
||||
query.video_uuid, json_path, json_path2, fallback_path
|
||||
query.file_uuid, json_path, json_path2, fallback_path
|
||||
),
|
||||
));
|
||||
};
|
||||
@@ -1418,7 +1417,7 @@ async fn get_person_thumbnail(
|
||||
let (timestamp, face) = detections[index];
|
||||
|
||||
// 3. Locate the video file
|
||||
let video_path = format!("output/{}/{}.mp4", query.video_uuid, query.video_uuid);
|
||||
let video_path = format!("output/{}/{}.mp4", query.file_uuid, query.file_uuid);
|
||||
if !std::path::Path::new(&video_path).exists() {
|
||||
return Err((
|
||||
StatusCode::NOT_FOUND,
|
||||
@@ -1489,12 +1488,12 @@ async fn create_person_identity(
|
||||
|
||||
let query = r#"
|
||||
INSERT INTO person_identities (
|
||||
person_id, video_uuid, face_identity_id, speaker_id,
|
||||
person_id, file_uuid, face_identity_id, speaker_id,
|
||||
name, metadata, confidence
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING
|
||||
id, person_id, face_identity_id, speaker_id,
|
||||
video_uuid, confidence, name, metadata,
|
||||
file_uuid, confidence, name, metadata,
|
||||
first_appearance_time, last_appearance_time,
|
||||
total_appearance_duration, appearance_count,
|
||||
created_at, updated_at, is_confirmed
|
||||
@@ -1502,11 +1501,11 @@ async fn create_person_identity(
|
||||
|
||||
let person: PersonIdentity = sqlx::query_as(query)
|
||||
.bind(&person_id)
|
||||
.bind(&request.video_uuid)
|
||||
.bind(&request.file_uuid)
|
||||
.bind(&request.face_identity_id)
|
||||
.bind(&request.speaker_id)
|
||||
.bind(&request.name)
|
||||
.bind(&request.metadata.unwrap_or(serde_json::json!({})))
|
||||
.bind(serde_json::to_string(&request.metadata.unwrap_or(serde_json::json!({}))).unwrap())
|
||||
.bind(0.0)
|
||||
.fetch_one(db.pool())
|
||||
.await?;
|
||||
@@ -1516,13 +1515,13 @@ async fn create_person_identity(
|
||||
|
||||
async fn auto_match_face_speaker(
|
||||
db: &PostgresDb,
|
||||
video_uuid: &str,
|
||||
file_uuid: &str,
|
||||
threshold: f64,
|
||||
) -> Result<Vec<PersonMatch>, anyhow::Error> {
|
||||
let query = "SELECT * FROM auto_match_face_speaker($1, $2)";
|
||||
|
||||
let matches: Vec<PersonMatch> = sqlx::query_as(query)
|
||||
.bind(video_uuid)
|
||||
.bind(file_uuid)
|
||||
.bind(threshold)
|
||||
.fetch_all(db.pool())
|
||||
.await?;
|
||||
@@ -1548,9 +1547,9 @@ async fn auto_identify_persons(
|
||||
// 1. Load face_clustered.json
|
||||
let clustered_path = format!(
|
||||
"output/{}/{}.face_clustered.json",
|
||||
request.video_uuid, request.video_uuid
|
||||
request.file_uuid, request.file_uuid
|
||||
);
|
||||
let fallback_path = format!("output/{}/face_clustered.json", request.video_uuid);
|
||||
let fallback_path = format!("output/{}/face_clustered.json", request.file_uuid);
|
||||
let path = if std::path::Path::new(&clustered_path).exists() {
|
||||
clustered_path
|
||||
} else if std::path::Path::new(&fallback_path).exists() {
|
||||
@@ -1560,7 +1559,7 @@ async fn auto_identify_persons(
|
||||
StatusCode::NOT_FOUND,
|
||||
format!(
|
||||
"face_clustered.json not found for video: {}",
|
||||
request.video_uuid
|
||||
request.file_uuid
|
||||
),
|
||||
));
|
||||
};
|
||||
@@ -1615,7 +1614,7 @@ async fn auto_identify_persons(
|
||||
// 3. Load ASRX from chunks
|
||||
let asrx_query = "SELECT chunk_id, content::text FROM chunks WHERE uuid = $1 AND chunk_type = 'trace' AND chunk_id LIKE 'trace_asrx_%'";
|
||||
let asrx_chunks: Vec<(String, String)> = match sqlx::query_as(asrx_query)
|
||||
.bind(&request.video_uuid)
|
||||
.bind(&request.file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1631,7 +1630,7 @@ async fn auto_identify_persons(
|
||||
// Also check sentence chunks for speaker_id
|
||||
let sentence_query = "SELECT content::text FROM chunks WHERE uuid = $1 AND chunk_type = 'sentence' AND content ? 'speaker_id'";
|
||||
let sentence_chunks: Vec<String> = match sqlx::query_scalar(sentence_query)
|
||||
.bind(&request.video_uuid)
|
||||
.bind(&request.file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -1801,7 +1800,7 @@ async fn list_persons(
|
||||
}
|
||||
};
|
||||
|
||||
let video_uuid = query.video_uuid.replace("'", "''");
|
||||
let file_uuid = query.file_uuid.replace("'", "''");
|
||||
let page = query.page.unwrap_or(1);
|
||||
let page_size = query.page_size.unwrap_or(20);
|
||||
let offset = ((page - 1) as i64) * (page_size as i64);
|
||||
@@ -1811,25 +1810,25 @@ async fn list_persons(
|
||||
let (sql, count_sql) = if has_speaker {
|
||||
if min_appearances > 0 {
|
||||
(
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE video_uuid = '{}' AND speaker_id IS NOT NULL AND appearance_count >= $1 ORDER BY appearance_count DESC LIMIT $2 OFFSET $3", video_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE video_uuid = '{}' AND speaker_id IS NOT NULL AND appearance_count >= $1", video_uuid),
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE file_uuid = '{}' AND speaker_id IS NOT NULL AND appearance_count >= $1 ORDER BY appearance_count DESC LIMIT $2 OFFSET $3", file_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE file_uuid = '{}' AND speaker_id IS NOT NULL AND appearance_count >= $1", file_uuid),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE video_uuid = '{}' AND speaker_id IS NOT NULL ORDER BY appearance_count DESC LIMIT $1 OFFSET $2", video_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE video_uuid = '{}' AND speaker_id IS NOT NULL", video_uuid),
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE file_uuid = '{}' AND speaker_id IS NOT NULL ORDER BY appearance_count DESC LIMIT $1 OFFSET $2", file_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE file_uuid = '{}' AND speaker_id IS NOT NULL", file_uuid),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if min_appearances > 0 {
|
||||
(
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE video_uuid = '{}' AND appearance_count >= $1 ORDER BY appearance_count DESC LIMIT $2 OFFSET $3", video_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE video_uuid = '{}' AND appearance_count >= $1", video_uuid),
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE file_uuid = '{}' AND appearance_count >= $1 ORDER BY appearance_count DESC LIMIT $2 OFFSET $3", file_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE file_uuid = '{}' AND appearance_count >= $1", file_uuid),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE video_uuid = '{}' ORDER BY appearance_count DESC LIMIT $1 OFFSET $2", video_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE video_uuid = '{}'", video_uuid),
|
||||
format!("SELECT person_id, name, speaker_id, appearance_count, total_appearance_duration, first_appearance_time, last_appearance_time, is_confirmed, metadata::text FROM person_identities WHERE file_uuid = '{}' ORDER BY appearance_count DESC LIMIT $1 OFFSET $2", file_uuid),
|
||||
format!("SELECT COUNT(*) FROM person_identities WHERE file_uuid = '{}'", file_uuid),
|
||||
)
|
||||
}
|
||||
};
|
||||
@@ -1925,7 +1924,7 @@ async fn merge_persons(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Json(request): Json<MergePersonsRequest>,
|
||||
) -> Result<Json<MergePersonsResponse>, (StatusCode, String)> {
|
||||
let video_uuid = &request.video_uuid;
|
||||
let file_uuid = &request.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -2150,7 +2149,7 @@ async fn undo_merge(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Json(request): Json<UndoMergeRequest>,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
// video_uuid is validated through merge_history lookup
|
||||
// file_uuid is validated through merge_history lookup
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -2285,7 +2284,10 @@ async fn undo_merge(
|
||||
.bind(source_duration)
|
||||
.bind(source_first)
|
||||
.bind(source_last)
|
||||
.bind(&serde_json::json!({"restored_from_merge": _merge_id}))
|
||||
.bind(
|
||||
serde_json::to_string(&serde_json::json!({"restored_from_merge": _merge_id}))
|
||||
.unwrap(),
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await
|
||||
{
|
||||
@@ -2333,14 +2335,14 @@ async fn undo_merge(
|
||||
/// Get merge history
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MergeHistoryQuery {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
}
|
||||
|
||||
async fn get_merge_history(
|
||||
State(_state): State<crate::api::server::AppState>,
|
||||
Query(query): Query<MergeHistoryQuery>,
|
||||
) -> Result<Json<MergeHistoryResponse>, (StatusCode, String)> {
|
||||
let _video_uuid = &query.video_uuid;
|
||||
let _file_uuid = &query.file_uuid;
|
||||
let db = match PostgresDb::init().await {
|
||||
Ok(db) => db,
|
||||
Err(e) => {
|
||||
@@ -2412,16 +2414,16 @@ async fn get_similar_persons(
|
||||
}
|
||||
};
|
||||
|
||||
let video_uuid = query.video_uuid;
|
||||
let file_uuid = query.file_uuid;
|
||||
let threshold = query.threshold.unwrap_or(0.5);
|
||||
let limit = query.limit.unwrap_or(10);
|
||||
|
||||
// Find the speaker_id of the requested person
|
||||
let get_speaker_query =
|
||||
"SELECT speaker_id FROM person_identities WHERE person_id = $1 AND video_uuid = $2";
|
||||
"SELECT speaker_id FROM person_identities WHERE person_id = $1 AND file_uuid = $2";
|
||||
let current_speaker_id: Option<String> = match sqlx::query_scalar(get_speaker_query)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -2437,7 +2439,7 @@ async fn get_similar_persons(
|
||||
let results = match current_speaker_id {
|
||||
Some(sid) => {
|
||||
// Find others with same speaker_id
|
||||
let similar_query = "SELECT person_id, name, speaker_id, appearance_count, first_appearance_time, last_appearance_time FROM person_identities WHERE speaker_id = $1 AND person_id != $2 AND video_uuid = $3 ORDER BY appearance_count DESC LIMIT $4";
|
||||
let similar_query = "SELECT person_id, name, speaker_id, appearance_count, first_appearance_time, last_appearance_time FROM person_identities WHERE speaker_id = $1 AND person_id != $2 AND file_uuid = $3 ORDER BY appearance_count DESC LIMIT $4";
|
||||
let rows: Vec<(
|
||||
String,
|
||||
Option<String>,
|
||||
@@ -2448,7 +2450,7 @@ async fn get_similar_persons(
|
||||
)> = match sqlx::query_as(similar_query)
|
||||
.bind(&sid)
|
||||
.bind(&person_id)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.bind(limit)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
@@ -2509,13 +2511,13 @@ async fn get_person_suggestions(
|
||||
// 1. Naming suggestions: Persons with NULL name but high appearance count.
|
||||
// 2. Merge suggestions: Persons sharing the same speaker_id.
|
||||
|
||||
let video_uuid = req.video_uuid;
|
||||
let file_uuid = req.file_uuid;
|
||||
|
||||
// Naming suggestions
|
||||
let naming_query = "SELECT person_id, name, speaker_id, appearance_count FROM person_identities WHERE video_uuid = $1 AND (name IS NULL OR name = person_id) AND appearance_count > 50 ORDER BY appearance_count DESC LIMIT 10";
|
||||
let naming_query = "SELECT person_id, name, speaker_id, appearance_count FROM person_identities WHERE file_uuid = $1 AND (name IS NULL OR name = person_id) AND appearance_count > 50 ORDER BY appearance_count DESC LIMIT 10";
|
||||
let naming_rows: Vec<(String, Option<String>, Option<String>, i32)> =
|
||||
match sqlx::query_as(naming_query)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -2538,9 +2540,9 @@ async fn get_person_suggestions(
|
||||
}
|
||||
|
||||
// Merge suggestions (Speaker overlap)
|
||||
let merge_query = "SELECT person_id, speaker_id, appearance_count FROM person_identities WHERE video_uuid = $1 AND speaker_id IS NOT NULL ORDER BY speaker_id, appearance_count DESC";
|
||||
let merge_query = "SELECT person_id, speaker_id, appearance_count FROM person_identities WHERE file_uuid = $1 AND speaker_id IS NOT NULL ORDER BY speaker_id, appearance_count DESC";
|
||||
let merge_rows: Vec<(String, String, i32)> = match sqlx::query_as(merge_query)
|
||||
.bind(&video_uuid)
|
||||
.bind(&file_uuid)
|
||||
.fetch_all(db.pool())
|
||||
.await
|
||||
{
|
||||
@@ -2644,14 +2646,14 @@ async fn confirm_person_suggestion(
|
||||
/// Request to unbind speaker from person
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UnbindSpeakerRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Request to reassign speaker to person
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ReassignSpeakerRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub speaker_id: String,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
@@ -2659,7 +2661,7 @@ pub struct ReassignSpeakerRequest {
|
||||
/// Request to remove a specific appearance
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct RemoveAppearanceRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub appearance_id: i32,
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
@@ -2667,7 +2669,7 @@ pub struct RemoveAppearanceRequest {
|
||||
/// Request to reassign appearance to another person
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ReassignAppearanceRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub appearance_id: i32,
|
||||
pub target_person_id: String,
|
||||
pub reason: Option<String>,
|
||||
@@ -2676,7 +2678,7 @@ pub struct ReassignAppearanceRequest {
|
||||
/// Request to split a person into two
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct SplitPersonRequest {
|
||||
pub video_uuid: String,
|
||||
pub file_uuid: String,
|
||||
pub new_person_id: String,
|
||||
pub appearance_ids_to_move: Vec<i32>,
|
||||
pub new_person_name: Option<String>,
|
||||
|
||||
Reference in New Issue
Block a user