feat: Phase 2-3 TKG-only architecture

Phase 2.1: build_face_trace_nodes_from_qdrant()
- Read trace_id, frame, bbox directly from Qdrant payload
- No dependency on face_detections table

Phase 2.3: Rule2 queries TKG nodes
- identity resolution from tkg_nodes.properties.identity_id
- TKG-only architecture (Phase 2.3)

Phase 3: Identity Agent updates TKG nodes
- match_faces_iterative() updates tkg_nodes.properties
- bind_identity_trace() syncs identity_id to TKG
- unbind_identity() removes identity_id from TKG

Test results:
- 23 face_trace nodes from Qdrant (Phase 2.1)
- 75 relationship chunks (Rule2)
- TKG rebuild: Phase0 → Phase1 → Phase2
This commit is contained in:
Accusys
2026-06-21 01:30:04 +08:00
parent 2f2ccc94f7
commit 23c440104b
5 changed files with 305 additions and 23 deletions
+122
View File
@@ -549,6 +549,128 @@ async fn build_face_trace_nodes(
pool: &PgPool,
file_uuid: &str,
pose_data: &[FacePose],
) -> Result<usize> {
use crate::core::db::face_embedding_db::FaceEmbeddingDb;
// Try Qdrant first (Phase 2: TKG-only)
let face_db = FaceEmbeddingDb::new();
let qdrant_embeddings = face_db.get_all_embeddings_for_file(file_uuid).await?;
if !qdrant_embeddings.is_empty() {
tracing::info!("[TKG-Phase2] Building face_trace nodes from Qdrant ({} embeddings)", qdrant_embeddings.len());
return build_face_trace_nodes_from_qdrant(pool, file_uuid, pose_data, qdrant_embeddings).await;
}
// Fallback to PostgreSQL
tracing::info!("[TKG-Phase2] No Qdrant embeddings, falling back to PostgreSQL");
build_face_trace_nodes_from_pg(pool, file_uuid, pose_data).await
}
async fn build_face_trace_nodes_from_qdrant(
pool: &PgPool,
file_uuid: &str,
pose_data: &[FacePose],
qdrant_embeddings: Vec<(String, Vec<f32>, crate::core::db::face_embedding_db::FaceEmbeddingPayload)>,
) -> Result<usize> {
use crate::core::db::face_embedding_db::FaceEmbeddingPayload;
let nodes_table = t("tkg_nodes");
// Group by trace_id
let mut trace_frames: HashMap<i64, Vec<(i64, f64, f64, f64, f64)>> = HashMap::new();
for (_, _, payload) in &qdrant_embeddings {
trace_frames
.entry(payload.trace_id as i64)
.or_default()
.push((
payload.frame,
payload.bbox_x,
payload.bbox_y,
payload.bbox_w,
payload.bbox_h,
));
}
if trace_frames.is_empty() {
tracing::warn!("[TKG-Phase2] No trace data in Qdrant");
return Ok(0);
}
// Build aggregates
let mut count = 0;
for (tid, frames) in &trace_frames {
let external_id = format!("trace_{}", tid);
let label = format!("Face Trace {}", tid);
let frame_count = frames.len() as i64;
let start_f = frames.iter().map(|(f, _, _, _, _)| *f).min().unwrap_or(0);
let end_f = frames.iter().map(|(f, _, _, _, _)| *f).max().unwrap_or(0);
let avg_x = frames.iter().map(|(_, x, _, _, _)| *x).sum::<f64>() / frame_count as f64;
let avg_y = frames.iter().map(|(_, _, y, _, _)| *y).sum::<f64>() / frame_count as f64;
let avg_w = frames.iter().map(|(_, _, _, w, _)| *w).sum::<f64>() / frame_count as f64;
let avg_h = frames.iter().map(|(_, _, _, _, h)| *h).sum::<f64>() / frame_count as f64;
// Compute average pose
let mut yaw_sum = 0.0f64;
let mut pitch_sum = 0.0f64;
let mut roll_sum = 0.0f64;
let mut pose_count = 0i64;
for (frame, x, y, w, h) in frames {
if let Some((yaw, pitch, roll)) = get_pose_for_face(*frame, *x, *y, *w, *h, pose_data) {
yaw_sum += yaw;
pitch_sum += pitch;
roll_sum += roll;
pose_count += 1;
}
}
let (avg_yaw, avg_pitch, avg_roll) = if pose_count > 0 {
(yaw_sum / pose_count as f64, pitch_sum / pose_count as f64, roll_sum / pose_count as f64)
} else {
(0.0, 0.0, 0.0)
};
let props = serde_json::json!({
"trace_id": tid,
"frame_count": frame_count,
"first_frame": start_f,
"last_frame": end_f,
"avg_x": avg_x,
"avg_y": avg_y,
"avg_width": avg_w,
"avg_height": avg_h,
"pose_count": pose_count,
"avg_yaw": avg_yaw,
"avg_pitch": avg_pitch,
"avg_roll": avg_roll,
});
sqlx::query(&format!(
"INSERT INTO {} (file_uuid, node_type, external_id, label, properties) \
VALUES ($1, $2, $3, $4, $5::jsonb) \
ON CONFLICT (file_uuid, node_type, external_id) \
DO UPDATE SET properties = COALESCE(EXCLUDED.properties, tkg_nodes.properties)",
nodes_table
))
.bind(file_uuid)
.bind("face_trace")
.bind(&external_id)
.bind(&label)
.bind(serde_json::to_string(&props)?)
.execute(pool)
.await?;
count += 1;
}
tracing::info!("[TKG-Phase2] Built {} face_trace nodes from Qdrant", count);
Ok(count)
}
async fn build_face_trace_nodes_from_pg(
pool: &PgPool,
file_uuid: &str,
pose_data: &[FacePose],
) -> Result<usize> {
let face_table = t("face_detections");
let nodes_table = t("tkg_nodes");