fix: ASRX duplication, TKG edges, trace ingest, and add pipeline progress publishing

- ASRX handler no longer stores duplicate 'asr' pre_chunks
- Pre_chunks storage made idempotent (delete-before-insert)
- Rule 1 + trace_ingest changed to query 'asrx' not 'asr'
- Trace chunks removed (dynamic from TKG/Qdrant)
- TKG scroll_face_points fixed: trace_id >= 1 (not == 1)
- TKG AsrxSegmentEntry: start/end -> start_time/end_time (match ASRX JSON)
- Unregister error handling: log instead of silent discard
- Add publish_pipeline_progress calls at each pipeline stage
  (processors, rule1, face_trace, identity_agent, TKG, rule2, completion)
This commit is contained in:
Accusys
2026-07-02 10:43:46 +08:00
parent d791d138f2
commit 3eabd45882
65 changed files with 9481 additions and 3856 deletions

View File

@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
use sqlx::Row;
use std::process::Command;
use crate::core::db::ResourceRecord;
use crate::core::db::{QdrantDb, ResourceRecord};
pub fn identity_routes() -> Router<crate::api::types::AppState> {
Router::new()
@@ -269,12 +269,7 @@ async fn get_file_identities(
let fi_table = crate::core::db::schema::table_name("file_identities");
let total = match sqlx::query_scalar::<_, i64>(
&format!(
r#"SELECT COUNT(DISTINCT identity_id) FROM (
SELECT identity_id FROM {} WHERE file_uuid = $1 AND identity_id IS NOT NULL
UNION
SELECT identity_id FROM {} WHERE file_uuid = $1
) combined"#,
crate::core::db::schema::table_name("face_detections"),
r#"SELECT COUNT(DISTINCT identity_id) FROM {} WHERE file_uuid = $1 AND identity_id IS NOT NULL"#,
fi_table
)
)
@@ -419,7 +414,6 @@ async fn delete_identity(
Extension(auth): Extension<crate::api::middleware::UserAuth>,
Path(identity_uuid): Path<String>,
) -> Result<StatusCode, StatusCode> {
let table = crate::core::db::schema::table_name("face_detections");
let id_table = crate::core::db::schema::table_name("identities");
let history_table = crate::core::db::schema::table_name("identity_history");
@@ -440,15 +434,27 @@ async fn delete_identity(
// Delete identity file from disk
let _ = crate::core::identity::storage::delete_identity_file(&uuid_clean);
// Capture unbound faces before unbinding
let unbound_faces: Vec<(String, Option<String>, Option<i32>)> = sqlx::query_as(&format!(
"SELECT file_uuid, face_id, trace_id FROM {} WHERE identity_id = $1",
table
))
.bind(identity_id)
.fetch_all(state.db.pool())
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Capture unbound faces from Qdrant _faces before unbinding
use crate::core::db::qdrant_db::QdrantDb;
use serde_json::json;
let qdrant = QdrantDb::new();
let face_filter = json!({
"must": [
{"key": "identity_id", "match": {"value": identity_id}}
]
});
let points = qdrant.scroll_all_points("_faces", face_filter, 1000).await.unwrap_or_default();
let unbound_faces: Vec<(String, Option<String>, Option<i32>)> = points.iter()
.filter_map(|p| {
let payload = &p["payload"];
let file_uuid = payload["file_uuid"].as_str()?.to_string();
let face_id = payload.get("face_id").and_then(|v| v.as_str()).map(|s| s.to_string());
let trace_id = payload["trace_id"].as_i64().map(|t| t as i32);
Some((file_uuid, face_id, trace_id))
})
.collect();
let face_list: Vec<serde_json::Value> = unbound_faces
.into_iter()
@@ -494,15 +500,17 @@ async fn delete_identity(
.execute(state.db.pool())
.await;
// Unbind all faces
sqlx::query(&format!(
"UPDATE {} SET identity_id = NULL WHERE identity_id = $1",
table
))
.bind(identity_id)
.execute(state.db.pool())
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
// Unbind all faces in Qdrant _faces
let qdrant = QdrantDb::new();
let filter = serde_json::json!({
"must": [
{"key": "identity_id", "match": {"value": identity_id}}
]
});
let payload = serde_json::json!({"identity_id": serde_json::Value::Null});
let _ = qdrant
.update_payload_by_filter("_faces", filter, payload)
.await;
// Delete identity
sqlx::query(&format!("DELETE FROM {} WHERE id = $1", id_table))
@@ -572,17 +580,21 @@ async fn get_identity_files(
})
.collect();
let total = match sqlx::query_scalar::<_, i64>(&format!(
"SELECT COUNT(DISTINCT fd.file_uuid) FROM {} fd WHERE fd.identity_id = $1",
crate::core::db::schema::table_name("face_detections"),
))
.bind(identity_id)
.fetch_one(state.db.pool())
.await
{
Ok(c) => c,
Err(_) => data.len() as i64,
};
// Get total from Qdrant _faces
use crate::core::db::qdrant_db::QdrantDb;
use serde_json::json;
let qdrant = QdrantDb::new();
let face_filter = json!({
"must": [
{"key": "identity_id", "match": {"value": identity_id}}
]
});
let points = qdrant.scroll_all_points("_faces", face_filter, 1000).await.unwrap_or_default();
let unique_files: std::collections::HashSet<String> = points.iter()
.filter_map(|p| p["payload"]["file_uuid"].as_str().map(|s| s.to_string()))
.collect();
let total = unique_files.len() as i64;
Ok(Json(IdentityFilesResponse {
success: true,
@@ -673,17 +685,14 @@ async fn get_identity_faces(
})
.collect();
let total = match sqlx::query_scalar::<_, i64>(&format!(
"SELECT COUNT(*) FROM {} fd WHERE fd.identity_id = $1",
crate::core::db::schema::table_name("face_detections"),
))
.bind(identity_id)
.fetch_one(state.db.pool())
.await
{
Ok(c) => c,
Err(_) => data.len() as i64,
};
let qdrant2 = QdrantDb::new();
let face_filter2 = serde_json::json!({
"must": [
{"key": "identity_id", "match": {"value": identity_id}}
]
});
let points2 = qdrant2.scroll_all_points("_faces", face_filter2, 2000).await.unwrap_or_default();
let total = points2.len() as i64;
Ok(Json(IdentityFacesResponse {
success: true,
@@ -759,151 +768,114 @@ async fn get_file_faces(
let page_size = params.page_size.unwrap_or(50);
let offset = ((page - 1) as i64) * (page_size as i64);
let fd_table = crate::core::db::schema::table_name("face_detections");
let id_table = crate::core::db::schema::table_name("identities");
let st_table = crate::core::db::schema::table_name("strangers");
let video_table = crate::core::db::schema::table_name("videos");
// Build WHERE clauses
let mut where_clauses = vec![format!(
"fd.file_uuid = '{}'",
file_uuid.replace('\'', "''")
)];
// Get fps
let fps: f64 = sqlx::query_scalar(&format!(
"SELECT COALESCE(fps, 25.0) FROM {} WHERE file_uuid = $1",
video_table
))
.bind(&file_uuid)
.fetch_optional(state.db.pool())
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
.unwrap_or(25.0);
// Get face points from Qdrant _faces
use crate::core::db::qdrant_db::QdrantDb;
use serde_json::json;
let qdrant = QdrantDb::new();
let mut filter_conditions = vec![
json!({"key": "file_uuid", "match": {"value": file_uuid}})
];
if let Some(ref binding) = params.binding {
match binding.as_str() {
"identity" => {
where_clauses.push(format!("fd.identity_id IN (SELECT id FROM {})", id_table));
filter_conditions.push(json!({"key": "identity_id", "exists": true}));
}
"stranger" => {
where_clauses.push("fd.stranger_id IS NOT NULL".to_string());
}
"dangling" => {
where_clauses.push(format!(
"fd.identity_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM {} WHERE id = fd.identity_id)",
id_table
));
filter_conditions.push(json!({"key": "stranger_id", "exists": true}));
}
"unbound" => {
where_clauses.push("fd.identity_id IS NULL AND fd.stranger_id IS NULL".to_string());
filter_conditions.push(json!({"key": "identity_id", "match": {"value": null}}));
}
_ => {}
}
}
if let Some(tid) = params.trace_id {
where_clauses.push(format!("fd.trace_id = {}", tid));
}
if let Some(mc) = params.min_confidence {
where_clauses.push(format!("fd.confidence >= {}", mc));
}
if let Some(sf) = params.start_frame {
where_clauses.push(format!("fd.frame_number >= {}", sf));
}
if let Some(ef) = params.end_frame {
where_clauses.push(format!("fd.frame_number <= {}", ef));
filter_conditions.push(json!({"key": "trace_id", "match": {"value": tid}}));
}
let where_sql = where_clauses.join(" AND ");
let face_filter = json!({"must": filter_conditions});
let points = qdrant.scroll_all_points("_faces", face_filter, 2000).await.unwrap_or_default();
let select_sql = format!(
"SELECT fd.id::bigint as id, fd.file_uuid, \
fd.frame_number::bigint as frame_number, \
(fd.frame_number::float8 / NULLIF(v.fps, 0)) as timestamp_secs, \
fd.face_id, fd.trace_id, \
fd.x::float8 as x, fd.y::float8 as y, \
fd.width::float8 as width, fd.height::float8 as height, \
fd.confidence::float8 as confidence, \
fd.identity_id, fd.stranger_id, \
i.uuid::text as identity_uuid, i.name as identity_name, \
s.metadata as stranger_metadata \
FROM {} fd \
JOIN {} v ON v.file_uuid = fd.file_uuid \
LEFT JOIN {} i ON i.id = fd.identity_id \
LEFT JOIN {} s ON s.id = fd.stranger_id \
WHERE {} \
ORDER BY fd.frame_number, fd.trace_id \
LIMIT {} OFFSET {}",
fd_table, video_table, id_table, st_table, where_sql, page_size as i64, offset
);
// Apply additional filters in Rust
let filtered: Vec<_> = points.into_iter().filter(|p| {
let payload = &p["payload"];
let confidence = payload["confidence"].as_f64().unwrap_or(0.0);
let frame = payload["frame"].as_i64().unwrap_or(0);
let count_sql = format!(
"SELECT COUNT(*) FROM {} fd \
WHERE {}",
fd_table, where_sql
);
if let Some(mc) = params.min_confidence {
if confidence < mc { return false; }
}
if let Some(sf) = params.start_frame {
if frame < sf { return false; }
}
if let Some(ef) = params.end_frame {
if frame > ef { return false; }
}
true
}).collect();
use sqlx::Row;
let rows = sqlx::query(&select_sql)
.fetch_all(state.db.pool())
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let total = filtered.len() as i64;
let total: i64 = sqlx::query_scalar(&count_sql)
.fetch_one(state.db.pool())
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// Apply pagination
let paged: Vec<_> = filtered.into_iter().skip(offset as usize).take(page_size as usize).collect();
let data: Vec<FileFaceItem> = rows
.into_iter()
.map(|r| {
let identity_id: Option<i32> = r.get("identity_id");
let identity_uuid: Option<String> = r.get("identity_uuid");
let identity_name: Option<String> = r.get("identity_name");
let stranger_id: Option<i32> = r.get("stranger_id");
// Build response items
let mut data = Vec::new();
for point in &paged {
let payload = &point["payload"];
let bbox = &payload["bbox"];
let frame = payload["frame"].as_i64().unwrap_or(0);
let confidence = payload["confidence"].as_f64().unwrap_or(0.0);
let binding = if let (Some(iid), Some(iuuid), Some(iname)) =
(identity_id, identity_uuid, identity_name)
{
FaceBinding::Identity {
identity_id: iid,
identity_uuid: iuuid,
identity_name: iname,
}
} else if let Some(sid) = stranger_id {
FaceBinding::Stranger {
stranger_id: sid,
metadata: r
.get::<Option<serde_json::Value>, _>("stranger_metadata")
.unwrap_or(serde_json::Value::Null),
}
} else if let Some(iid) = identity_id {
FaceBinding::Dangling {
old_identity_id: iid,
}
} else {
FaceBinding::Unbound
};
FileFaceItem {
id: r.get("id"),
file_uuid: r.get("file_uuid"),
frame_number: r.get("frame_number"),
timestamp_secs: r.get("timestamp_secs"),
face_id: r.get("face_id"),
trace_id: r.get("trace_id"),
bbox: BBox {
x: r.get("x"),
y: r.get("y"),
width: r.get("width"),
height: r.get("height"),
},
confidence: r.get("confidence"),
binding,
}
})
.collect();
let item = FileFaceItem {
id: 0,
file_uuid: file_uuid.clone(),
frame_number: frame,
timestamp_secs: Some(frame as f64 / fps),
face_id: payload.get("face_id").and_then(|v| v.as_str()).map(|s| s.to_string()),
trace_id: payload["trace_id"].as_i64().map(|t| t as i32),
bbox: BBox {
x: bbox["x"].as_f64().unwrap_or(0.0),
y: bbox["y"].as_f64().unwrap_or(0.0),
width: bbox["width"].as_f64().unwrap_or(0.0),
height: bbox["height"].as_f64().unwrap_or(0.0),
},
confidence,
binding: FaceBinding::Unbound,
};
data.push(item);
}
Ok(Json(FileFacesResponse {
success: true,
file_uuid,
total,
page,
page_size,
page: page as usize,
page_size: page_size as usize,
data,
}))
}
// --- List Face Candidates ---
#[derive(Debug, Serialize)]
pub struct IdentityChunksResponse {
pub success: bool,
@@ -1305,76 +1277,62 @@ async fn set_profile_from_face(
Json(req): Json<SetProfileFromFaceRequest>,
) -> Result<Json<ProfileImageResponse>, (StatusCode, Json<serde_json::Value>)> {
use crate::core::db::schema;
let fd_table = schema::table_name("face_detections");
use crate::core::db::qdrant_db::QdrantDb;
use serde_json::json;
let videos_table = schema::table_name("videos");
let uuid_clean = identity_uuid.replace('-', "");
let (face_identifier, use_trace, use_frame) = match (&req.face_id, req.id, req.trace_id) {
(Some(fid), _, _) => (fid.clone(), false, None),
(None, Some(id), _) => (id.to_string(), false, None),
(None, None, Some(trace_id)) => (trace_id.to_string(), true, req.frame_number),
(Some(fid), _, _) => (fid.clone(), None, None),
(None, Some(id), _) => (id.to_string(), None, None),
(None, None, Some(trace_id)) => (trace_id.to_string(), Some(trace_id), req.frame_number),
(None, None, None) => {
return Err((
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"success": false, "message": "Either face_id, id, or trace_id is required"})),
Json(
serde_json::json!({"success": false, "message": "Either face_id, id, or trace_id is required"}),
),
));
}
};
let row: Option<(i64, i32, i32, i32, i32, f64)> = if use_trace {
// Get face data from Qdrant _faces
let qdrant = QdrantDb::new();
let row: Option<(i64, i32, i32, i32, i32, f64)> = if let Some(trace_id) = use_trace {
let mut filter_conds = vec![
json!({"key": "file_uuid", "match": {"value": req.file_uuid}}),
json!({"key": "trace_id", "match": {"value": trace_id}})
];
if let Some(frame) = use_frame {
sqlx::query_as(&format!(
"SELECT frame_number, x, y, width, height, confidence FROM {} WHERE file_uuid = $1 AND trace_id = $2 AND frame_number = $3 LIMIT 1",
fd_table
))
.bind(&req.file_uuid)
.bind(use_trace)
.bind(frame as i32)
.fetch_optional(state.db.pool())
.await
} else {
sqlx::query_as(&format!(
"SELECT frame_number, x, y, width, height, confidence FROM {} WHERE file_uuid = $1 AND trace_id = $2 ORDER BY confidence DESC LIMIT 1",
fd_table
))
.bind(&req.file_uuid)
.bind(use_trace)
.fetch_optional(state.db.pool())
.await
filter_conds.push(json!({"key": "frame", "match": {"value": frame}}));
}
let face_filter = json!({"must": filter_conds});
let points = qdrant.scroll_all_points("_faces", face_filter, 10).await.unwrap_or_default();
points.first().map(|p| {
let payload = &p["payload"];
let bbox = &payload["bbox"];
(
payload["frame"].as_i64().unwrap_or(0),
bbox["x"].as_f64().unwrap_or(0.0) as i32,
bbox["y"].as_f64().unwrap_or(0.0) as i32,
bbox["width"].as_f64().unwrap_or(0.0) as i32,
bbox["height"].as_f64().unwrap_or(0.0) as i32,
payload["confidence"].as_f64().unwrap_or(0.0),
)
})
} else if req.id.is_some() {
sqlx::query_as(&format!(
"SELECT frame_number, x, y, width, height, confidence FROM {} WHERE file_uuid = $1 AND id = $2",
fd_table
))
.bind(&req.file_uuid)
.bind(req.id.unwrap())
.fetch_optional(state.db.pool())
.await
// id lookup not supported in Qdrant - skip
None
} else {
sqlx::query_as(&format!(
"SELECT frame_number, x, y, width, height, confidence FROM {} WHERE file_uuid = $1 AND face_id = $2",
fd_table
))
.bind(&req.file_uuid)
.bind(&face_identifier)
.fetch_optional(state.db.pool())
.await
}
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(serde_json::json!({"success": false, "message": format!("DB error: {}", e)})),
)
})?;
// face_id lookup not supported in Qdrant - skip
None
};
let (frame_number, x, y, width, height, confidence) = row.ok_or_else(|| {
(
StatusCode::NOT_FOUND,
Json(serde_json::json!({"success": false, "message": "Face not found"})),
)
})?;
let (frame_number, x, y, w, h, confidence) = row.ok_or((
StatusCode::NOT_FOUND,
Json(serde_json::json!({"success": false, "message": "Face not found"})),
))?;
let video_row: Option<(String, Option<i32>, Option<i32>)> = sqlx::query_as(&format!(
"SELECT file_path, width, height FROM {} WHERE file_uuid = $1",
@@ -1400,7 +1358,7 @@ async fn set_profile_from_face(
let vw = video_width.unwrap_or(1920);
let vh = video_height.unwrap_or(1080);
crate::core::thumbnail::validator::validate_crop(x, y, width, height, vw, vh).map_err(|e| {
crate::core::thumbnail::validator::validate_crop(x, y, w, h, vw, vh).map_err(|e| {
(
StatusCode::BAD_REQUEST,
Json(serde_json::json!({"success": false, "message": format!("Crop validation failed: {}", e)})),
@@ -1408,7 +1366,7 @@ async fn set_profile_from_face(
})?;
let select = format!("select=eq(n\\,{})", frame_number);
let vf = format!("{},crop={}:{}:{}:{}", select, width, height, x, y);
let vf = format!("{},crop={}:{}:{}:{}", select, w, h, x, y);
let output = Command::new("ffmpeg")
.args([
@@ -1465,7 +1423,10 @@ async fn set_profile_from_face(
success: true,
identity_uuid: uuid_clean,
path: file_path.to_string_lossy().to_string(),
message: format!("Profile image set from face {} (frame {}, confidence {:.2})", face_identifier, frame_number, confidence),
message: format!(
"Profile image set from face {} (frame {}, confidence {:.2})",
face_identifier, frame_number, confidence
),
}))
}
@@ -1567,21 +1528,20 @@ async fn search_identity_text(
) -> Result<Json<IdentityTextResponse>, StatusCode> {
use crate::core::db::schema;
let chunk_table = schema::table_name("chunk");
let fd_table = schema::table_name("face_detections");
let id_table = schema::table_name("identities");
let ib_table = schema::table_name("identity_bindings");
let like_q = format!("%{}%", params.q.replace('%', "%%"));
let limit = params.limit.unwrap_or(50).min(100);
let sd_table = schema::table_name("speaker_detections");
let query = format!(
r#"SELECT c.file_uuid, c.chunk_id, c.start_time, c.end_time, c.text_content,
fd.identity_id, i.name AS identity_name, i.source AS identity_source,
fd.trace_id
i.id AS identity_id, i.name AS identity_name, i.source AS identity_source,
(c.metadata->>'trace_id')::int AS trace_id
FROM {} c
LEFT JOIN {} fd ON fd.file_uuid = c.file_uuid
AND fd.frame_number BETWEEN c.start_frame AND c.end_frame
AND fd.identity_id IS NOT NULL
LEFT JOIN {} i ON i.id = fd.identity_id
LEFT JOIN {} ib ON ib.identity_value = c.metadata->>'trace_id'
AND ib.identity_type = 'trace'
LEFT JOIN {} i ON i.id = ib.identity_id
WHERE ($1::text IS NULL OR c.file_uuid = $1) AND (LOWER(c.text_content) LIKE LOWER($2) OR LOWER(c.content::text) LIKE LOWER($2))
UNION ALL
@@ -1597,7 +1557,7 @@ async fn search_identity_text(
ORDER BY 3
LIMIT $3"#,
chunk_table, fd_table, id_table, sd_table, id_table, chunk_table
chunk_table, ib_table, id_table, sd_table, id_table, chunk_table
);
let rows = sqlx::query_as::<
@@ -1696,7 +1656,6 @@ async fn search_identities_by_text(
) -> Result<Json<IdentitySearchResponse>, StatusCode> {
use crate::core::db::schema;
let id_table = schema::table_name("identities");
let fd_table = schema::table_name("face_detections");
let chunk_table = schema::table_name("chunk");
let like_q = format!("%{}%", params.q.replace('%', "%%"));
let page = params.page.unwrap_or(1).max(1);
@@ -1710,26 +1669,26 @@ async fn search_identities_by_text(
let sd_table = schema::table_name("speaker_detections");
let ib_table = schema::table_name("identity_bindings");
let fi_table = schema::table_name("file_identities");
let query = format!(
r#"WITH matched AS (
SELECT i.id::int, i.name, i.source, i.tmdb_id,
fd.file_uuid, fd.trace_id,
c.file_uuid, (c.metadata->>'trace_id')::int AS trace_id,
c.chunk_id, c.start_frame, c.end_frame, c.fps,
c.start_time, c.end_time, c.text_content
FROM {} i
JOIN {} fi ON fi.identity_id = i.id
JOIN {} ib ON ib.identity_id = i.id AND ib.identity_type = 'trace'
JOIN {} fd ON fd.trace_id = ib.identity_value::int
JOIN {} c ON c.file_uuid = fd.file_uuid
AND c.start_time <= fd.frame_number / COALESCE(c.fps, 25.0)
AND c.end_time >= fd.frame_number / COALESCE(c.fps, 25.0)
JOIN {} c ON c.file_uuid = fi.file_uuid
AND c.metadata->>'trace_id' = ib.identity_value
WHERE (i.name ILIKE $1
OR EXISTS (
SELECT 1 FROM jsonb_array_elements(i.metadata->'aliases') AS a
WHERE a->>'name' ILIKE $1
))
AND ($2::text IS NULL OR fd.file_uuid = $2)
AND ($2::text IS NULL OR c.file_uuid = $2)
UNION ALL
UNION ALL
SELECT i.id::int, i.name, i.source, i.tmdb_id,
sd.file_uuid, NULL::int AS trace_id,
@@ -1755,7 +1714,7 @@ SELECT *, COUNT(*) OVER() AS total_count
FROM deduped
ORDER BY name, start_time
LIMIT $3 OFFSET $4"#,
id_table, ib_table, fd_table, chunk_table, id_table, sd_table, chunk_table
id_table, fi_table, ib_table, chunk_table, id_table, sd_table, chunk_table
);
let rows = sqlx::query(&query)
@@ -2093,7 +2052,6 @@ async fn undo_identity(
let table = crate::core::db::schema::table_name("identities");
let history_table = crate::core::db::schema::table_name("identity_history");
let face_table = crate::core::db::schema::table_name("face_detections");
// Try normal identity lookup
let identity_row: Option<(i32,)> = sqlx::query_as(&format!(
@@ -2174,22 +2132,23 @@ async fn undo_identity(
)
})?;
// Re-bind faces
// Re-bind faces via Qdrant _faces
if let Some(faces) = snapshot.get("unbound_faces").and_then(|v| v.as_array()) {
let qdrant = QdrantDb::new();
for face in faces {
let file_uuid = face.get("file_uuid").and_then(|v| v.as_str());
let face_id = face.get("face_id").and_then(|v| v.as_str());
let trace_id = face.get("trace_id").and_then(|v| v.as_i64());
if let (Some(fu), Some(fid)) = (file_uuid, face_id) {
let _ = sqlx::query(&format!(
"UPDATE {} SET identity_id = $1 WHERE file_uuid = $2 AND face_id = $3",
face_table
))
.bind(new_id)
.bind(fu)
.bind(fid)
.execute(state.db.pool())
.await;
if let (Some(fu), Some(tid)) = (file_uuid, trace_id) {
let filter = serde_json::json!({
"must": [
{"key": "file_uuid", "match": {"value": fu}},
{"key": "trace_id", "match": {"value": tid}}
]
});
let payload = serde_json::json!({"identity_id": new_id});
let _ = qdrant
.update_payload_by_filter("_faces", filter, payload)
.await;
}
}
}
@@ -2377,7 +2336,6 @@ async fn redo_identity(
let table = crate::core::db::schema::table_name("identities");
let history_table = crate::core::db::schema::table_name("identity_history");
let face_table = crate::core::db::schema::table_name("face_detections");
// Get identity_id
let identity_id: i32 = sqlx::query_scalar(&format!(
@@ -2417,14 +2375,17 @@ async fn redo_identity(
// ── Delete redo: re-delete the identity ──
let _ = crate::core::identity::storage::delete_identity_file(&uuid_clean);
// Unbind all faces
let _ = sqlx::query(&format!(
"UPDATE {} SET identity_id = NULL WHERE identity_id = $1",
face_table
))
.bind(identity_id)
.execute(state.db.pool())
.await;
// Unbind all faces in Qdrant _faces
let qdrant = QdrantDb::new();
let filter = serde_json::json!({
"must": [
{"key": "identity_id", "match": {"value": identity_id}}
]
});
let payload = serde_json::json!({"identity_id": serde_json::Value::Null});
let _ = qdrant
.update_payload_by_filter("_faces", filter, payload)
.await;
// Delete identity
sqlx::query(&format!("DELETE FROM {} WHERE id = $1", table))