feat: Phase 1 handover - schema migration, correction mechanism, API fixes
Schema changes: dev.chunks->dev.chunk, remove old_chunk_id/chunk_index Correction: asr-1.json format, generate/apply scripts API: 37/37 endpoints fixed and tested Docs: HANDOVER_V2.0.md for M4
This commit is contained in:
+77
-27
@@ -13,11 +13,17 @@ struct TmdbIdentity {
|
||||
}
|
||||
|
||||
fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
|
||||
if a.len() != b.len() || a.is_empty() { return 0.0; }
|
||||
if a.len() != b.len() || a.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
let dot: f32 = a.iter().zip(b).map(|(x, y)| x * y).sum();
|
||||
let na: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
let nb: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
|
||||
if na == 0.0 || nb == 0.0 { 0.0 } else { dot / (na * nb) }
|
||||
if na == 0.0 || nb == 0.0 {
|
||||
0.0
|
||||
} else {
|
||||
dot / (na * nb)
|
||||
}
|
||||
}
|
||||
|
||||
/// Match face detections against TMDb identities using iterative multi-angle propagation.
|
||||
@@ -42,10 +48,11 @@ pub async fn match_faces_against_tmdb(db: &PostgresDb, file_uuid: &str) -> Resul
|
||||
let fd_rows = sqlx::query_as::<_, (i32, Vec<f32>)>(
|
||||
"SELECT trace_id, embedding FROM dev.face_detections \
|
||||
WHERE file_uuid=$1 AND trace_id IS NOT NULL AND embedding IS NOT NULL \
|
||||
ORDER BY trace_id"
|
||||
ORDER BY trace_id",
|
||||
)
|
||||
.bind(file_uuid)
|
||||
.fetch_all(pool).await?;
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
if fd_rows.is_empty() {
|
||||
info!("[TKG-MATCH] No face detections for {}", file_uuid);
|
||||
@@ -77,14 +84,23 @@ pub async fn match_faces_against_tmdb(db: &PostgresDb, file_uuid: &str) -> Resul
|
||||
for (id, name, tmdb_emb) in &tmdb_rows {
|
||||
for face in faces {
|
||||
let s = cosine_similarity(face, tmdb_emb);
|
||||
if s > best_sim { best_sim = s; best_id = *id; best_name = name.clone(); }
|
||||
if s > best_sim {
|
||||
best_sim = s;
|
||||
best_id = *id;
|
||||
best_name = name.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
if best_sim >= TH {
|
||||
matched.insert(tid, (best_id, best_name));
|
||||
}
|
||||
}
|
||||
info!("[TKG-MATCH] Round 1: {} ({}/{})", matched.len(), matched.len() * 100 / total, total);
|
||||
info!(
|
||||
"[TKG-MATCH] Round 1: {} ({}/{})",
|
||||
matched.len(),
|
||||
matched.len() * 100 / total,
|
||||
total
|
||||
);
|
||||
|
||||
// Round 2+: propagate
|
||||
for round_n in 2..=10 {
|
||||
@@ -98,7 +114,9 @@ pub async fn match_faces_against_tmdb(db: &PostgresDb, file_uuid: &str) -> Resul
|
||||
|
||||
let mut new_matches: Vec<(i32, i32, String)> = Vec::new();
|
||||
for (&tid, faces) in &trace_faces {
|
||||
if matched.contains_key(&tid) || faces.is_empty() { continue; }
|
||||
if matched.contains_key(&tid) || faces.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let ref_face = &faces[0];
|
||||
let mut best_id = 0i32;
|
||||
let mut best_name = String::new();
|
||||
@@ -106,13 +124,19 @@ pub async fn match_faces_against_tmdb(db: &PostgresDb, file_uuid: &str) -> Resul
|
||||
for (&id, seed_faces) in &seed_pool {
|
||||
for seed in seed_faces {
|
||||
let s = cosine_similarity(ref_face, seed);
|
||||
if s > best_sim { best_sim = s; best_id = id; }
|
||||
if s > best_sim {
|
||||
best_sim = s;
|
||||
best_id = id;
|
||||
}
|
||||
}
|
||||
}
|
||||
if best_sim >= TH {
|
||||
// Look up name for this id
|
||||
for (id, name, _) in &tmdb_rows {
|
||||
if *id == best_id { best_name = name.clone(); break; }
|
||||
if *id == best_id {
|
||||
best_name = name.clone();
|
||||
break;
|
||||
}
|
||||
}
|
||||
new_matches.push((tid, best_id, best_name));
|
||||
}
|
||||
@@ -121,7 +145,9 @@ pub async fn match_faces_against_tmdb(db: &PostgresDb, file_uuid: &str) -> Resul
|
||||
matched.insert(tid, (id, name));
|
||||
}
|
||||
let new = matched.len() - prev;
|
||||
if new < 5 { break; }
|
||||
if new < 5 {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 4: Quality control
|
||||
@@ -129,41 +155,62 @@ pub async fn match_faces_against_tmdb(db: &PostgresDb, file_uuid: &str) -> Resul
|
||||
let mut after_qc = HashMap::new();
|
||||
for (&tid, &(id, ref name)) in &matched {
|
||||
let cnt: i64 = sqlx::query_scalar(
|
||||
"SELECT COUNT(*) FROM dev.face_detections WHERE file_uuid=$1 AND trace_id=$2"
|
||||
"SELECT COUNT(*) FROM dev.face_detections WHERE file_uuid=$1 AND trace_id=$2",
|
||||
)
|
||||
.bind(file_uuid).bind(tid)
|
||||
.fetch_one(pool).await.unwrap_or(0);
|
||||
.bind(file_uuid)
|
||||
.bind(tid)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
if cnt >= 4 {
|
||||
after_qc.insert(tid, (id, name.clone()));
|
||||
} else {
|
||||
info!("[TKG-QC] trace {} removed: only {} face(s), need >= 4", tid, cnt);
|
||||
info!(
|
||||
"[TKG-QC] trace {} removed: only {} face(s), need >= 4",
|
||||
tid, cnt
|
||||
);
|
||||
}
|
||||
}
|
||||
let matched = after_qc;
|
||||
let removed_low = total - matched.len();
|
||||
if removed_low > 0 {
|
||||
info!("[TKG-QC] Removed {} low-confidence traces (< 4 faces)", removed_low);
|
||||
info!(
|
||||
"[TKG-QC] Removed {} low-confidence traces (< 4 faces)",
|
||||
removed_low
|
||||
);
|
||||
}
|
||||
|
||||
// 4b: Temporal collision check
|
||||
let removed_collisions = quality_check_temporal_collisions(pool, file_uuid).await?;
|
||||
if removed_collisions > 0 {
|
||||
info!("[TKG-QC] Resolved {} temporal collisions", removed_collisions);
|
||||
info!(
|
||||
"[TKG-QC] Resolved {} temporal collisions",
|
||||
removed_collisions
|
||||
);
|
||||
}
|
||||
|
||||
// Step 5: Update DB
|
||||
let mut updated = 0usize;
|
||||
for (&tid, &(id, _)) in &matched {
|
||||
let r = sqlx::query(
|
||||
"UPDATE dev.face_detections SET identity_id=$1 WHERE file_uuid=$2 AND trace_id=$3"
|
||||
"UPDATE dev.face_detections SET identity_id=$1 WHERE file_uuid=$2 AND trace_id=$3",
|
||||
)
|
||||
.bind(id).bind(file_uuid).bind(tid)
|
||||
.execute(pool).await?;
|
||||
if r.rows_affected() > 0 { updated += 1; }
|
||||
.bind(id)
|
||||
.bind(file_uuid)
|
||||
.bind(tid)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
if r.rows_affected() > 0 {
|
||||
updated += 1;
|
||||
}
|
||||
}
|
||||
|
||||
info!("[TKG-MATCH] Done: {}/{} traces matched ({}%)",
|
||||
matched.len(), total, matched.len() * 100 / total);
|
||||
info!(
|
||||
"[TKG-MATCH] Done: {}/{} traces matched ({}%)",
|
||||
matched.len(),
|
||||
total,
|
||||
matched.len() * 100 / total
|
||||
);
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
@@ -185,10 +232,11 @@ async fn quality_check_temporal_collisions(pool: &sqlx::PgPool, file_uuid: &str)
|
||||
AND a.identity_id IS NOT NULL
|
||||
AND a.identity_id = b.identity_id
|
||||
ORDER BY a.identity_id, a.frame_number
|
||||
"#
|
||||
"#,
|
||||
)
|
||||
.bind(file_uuid)
|
||||
.fetch_all(pool).await?;
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
|
||||
if collisions.is_empty() {
|
||||
return Ok(0);
|
||||
@@ -221,10 +269,12 @@ async fn quality_check_temporal_collisions(pool: &sqlx::PgPool, file_uuid: &str)
|
||||
let victim_cnt = if cnt_a <= cnt_b { cnt_a } else { cnt_b };
|
||||
|
||||
sqlx::query(
|
||||
"UPDATE dev.face_detections SET identity_id=NULL WHERE file_uuid=$1 AND trace_id=$2"
|
||||
"UPDATE dev.face_detections SET identity_id=NULL WHERE file_uuid=$1 AND trace_id=$2",
|
||||
)
|
||||
.bind(file_uuid).bind(victim)
|
||||
.execute(pool).await?;
|
||||
.bind(file_uuid)
|
||||
.bind(victim)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
unbound += 1;
|
||||
warn!("[TKG-QC] Collision identity={}: trace {} vs trace {} ({} overlap frames). Unbound trace {} ({} detections)",
|
||||
|
||||
Reference in New Issue
Block a user