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:
@@ -7,6 +7,7 @@ use axum::{
|
||||
Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::core::db::PostgresDb;
|
||||
|
||||
@@ -73,6 +74,7 @@ struct TraceInfo {
|
||||
duration_sec: f64,
|
||||
avg_confidence: f64,
|
||||
sample_face_id: Option<String>,
|
||||
thumbnail_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
@@ -118,46 +120,76 @@ async fn list_traces_sorted(
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
|
||||
.unwrap_or(24.0);
|
||||
|
||||
let query = format!(
|
||||
"SELECT tt.*, fd.id AS sample_face_id FROM (
|
||||
SELECT trace_id::int AS trace_id,
|
||||
COUNT(*) AS face_count,
|
||||
MIN(frame_number)::bigint AS start_frame,
|
||||
MAX(frame_number)::bigint AS end_frame,
|
||||
(MAX(frame_number) - MIN(frame_number))::float8 AS duration_sec,
|
||||
AVG(confidence)::float8 AS avg_confidence
|
||||
FROM {}
|
||||
WHERE file_uuid = $1 AND trace_id IS NOT NULL
|
||||
AND confidence >= $5 AND confidence <= $6
|
||||
GROUP BY trace_id
|
||||
HAVING COUNT(*) >= $2
|
||||
ORDER BY {}
|
||||
LIMIT $3 OFFSET $4
|
||||
) tt
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT id FROM {}
|
||||
WHERE trace_id = tt.trace_id AND file_uuid = $1
|
||||
ORDER BY confidence DESC LIMIT 1
|
||||
) fd ON true",
|
||||
crate::core::db::schema::table_name("face_detections"),
|
||||
order_clause,
|
||||
crate::core::db::schema::table_name("face_detections"),
|
||||
);
|
||||
// Get face points from Qdrant _faces
|
||||
use crate::core::db::qdrant_db::QdrantDb;
|
||||
use serde_json::json;
|
||||
use std::collections::HashMap;
|
||||
|
||||
let rows: Vec<(i32, i64, i64, i64, f64, f64, Option<i32>)> = sqlx::query_as(&query)
|
||||
.bind(&file_uuid)
|
||||
.bind(min_faces)
|
||||
.bind(effective_limit)
|
||||
.bind(db_offset)
|
||||
.bind(min_confidence)
|
||||
.bind(max_confidence)
|
||||
.fetch_all(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
let qdrant = QdrantDb::new();
|
||||
let face_filter = json!({
|
||||
"must": [
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", face_filter, 2000).await.unwrap_or_default();
|
||||
|
||||
let traces: Vec<TraceInfo> = rows
|
||||
// Aggregate by trace_id
|
||||
struct TraceAgg {
|
||||
face_count: i64,
|
||||
start_frame: i64,
|
||||
end_frame: i64,
|
||||
avg_confidence: f64,
|
||||
sum_confidence: f64,
|
||||
}
|
||||
|
||||
let mut trace_data: HashMap<i32, TraceAgg> = HashMap::new();
|
||||
for point in &points {
|
||||
let payload = &point["payload"];
|
||||
let trace_id = payload["trace_id"].as_i64().unwrap_or(0) as i32;
|
||||
let frame = payload["frame"].as_i64().unwrap_or(0);
|
||||
let confidence = payload["confidence"].as_f64().unwrap_or(0.5);
|
||||
|
||||
if confidence < min_confidence || confidence > max_confidence {
|
||||
continue;
|
||||
}
|
||||
|
||||
let entry = trace_data.entry(trace_id).or_insert(TraceAgg {
|
||||
face_count: 0,
|
||||
start_frame: i64::MAX,
|
||||
end_frame: i64::MIN,
|
||||
avg_confidence: 0.0,
|
||||
sum_confidence: 0.0,
|
||||
});
|
||||
entry.face_count += 1;
|
||||
entry.start_frame = entry.start_frame.min(frame);
|
||||
entry.end_frame = entry.end_frame.max(frame);
|
||||
entry.sum_confidence += confidence;
|
||||
}
|
||||
|
||||
// Filter by min_faces and sort
|
||||
let mut traces_vec: Vec<(i32, i64, i64, i64, f64, f64)> = trace_data.into_iter()
|
||||
.filter(|(_, agg)| agg.face_count >= min_faces)
|
||||
.map(|(tid, agg)| {
|
||||
let duration = (agg.end_frame - agg.start_frame) as f64;
|
||||
let avg_conf = if agg.face_count > 0 { agg.sum_confidence / agg.face_count as f64 } else { 0.0 };
|
||||
(tid, agg.face_count, agg.start_frame, agg.end_frame, duration, avg_conf)
|
||||
})
|
||||
.collect();
|
||||
|
||||
match order_clause {
|
||||
"face_count DESC" => traces_vec.sort_by(|a, b| b.1.cmp(&a.1)),
|
||||
"duration_sec DESC" => traces_vec.sort_by(|a, b| b.4.partial_cmp(&a.4).unwrap_or(std::cmp::Ordering::Equal)),
|
||||
_ => traces_vec.sort_by(|a, b| a.2.cmp(&b.2)),
|
||||
}
|
||||
|
||||
// Apply pagination
|
||||
let total_traces = traces_vec.len() as i64;
|
||||
let total_faces: i64 = points.len() as i64;
|
||||
let traces_vec: Vec<_> = traces_vec.into_iter().skip(db_offset as usize).take(effective_limit as usize).collect();
|
||||
|
||||
let traces: Vec<TraceInfo> = traces_vec
|
||||
.into_iter()
|
||||
.map(|(tid, fc, sf, ef, dur, conf, fid)| TraceInfo {
|
||||
.map(|(tid, fc, sf, ef, dur, conf)| TraceInfo {
|
||||
trace_id: tid,
|
||||
face_count: fc,
|
||||
start_frame: sf,
|
||||
@@ -166,19 +198,11 @@ async fn list_traces_sorted(
|
||||
end_time: ef as f64 / fps,
|
||||
duration_sec: dur / fps,
|
||||
avg_confidence: conf,
|
||||
sample_face_id: fid.map(|v| v.to_string()),
|
||||
sample_face_id: None,
|
||||
thumbnail_url: format!("/api/v1/file/{}/trace/{}/thumbnail", file_uuid, tid),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let (total_traces, total_faces): (i64, i64) = sqlx::query_as(
|
||||
&format!("SELECT COUNT(DISTINCT trace_id), COUNT(*) FROM {} WHERE file_uuid = $1 AND trace_id IS NOT NULL",
|
||||
crate::core::db::schema::table_name("face_detections"))
|
||||
)
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
Ok(Json(TracesResponse {
|
||||
success: true,
|
||||
file_uuid,
|
||||
@@ -260,55 +284,57 @@ async fn list_trace_faces(
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?
|
||||
.unwrap_or(24.0);
|
||||
|
||||
let total_detected: i64 = sqlx::query_scalar(&format!(
|
||||
"SELECT COUNT(*) FROM {} WHERE file_uuid = $1 AND trace_id = $2",
|
||||
crate::core::db::schema::table_name("face_detections")
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.bind(trace_id)
|
||||
.fetch_one(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
// Get face points from Qdrant _faces for this trace
|
||||
use crate::core::db::qdrant_db::QdrantDb;
|
||||
use serde_json::json;
|
||||
|
||||
let rows: Vec<(
|
||||
i32,
|
||||
i64,
|
||||
Option<i32>,
|
||||
Option<i32>,
|
||||
Option<i32>,
|
||||
Option<i32>,
|
||||
f32,
|
||||
)> = sqlx::query_as(&format!(
|
||||
"SELECT id, frame_number, x, y, width, height, confidence::float4 \
|
||||
FROM {} WHERE file_uuid = $1 AND trace_id = $2 \
|
||||
ORDER BY frame_number ASC LIMIT $3 OFFSET $4",
|
||||
crate::core::db::schema::table_name("face_detections")
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.bind(trace_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
let qdrant = QdrantDb::new();
|
||||
let trace_filter = json!({
|
||||
"must": [
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}},
|
||||
{"key": "trace_id", "match": {"value": trace_id}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", trace_filter, 1000).await.unwrap_or_default();
|
||||
|
||||
let total_detected: i64 = points.len() as i64;
|
||||
|
||||
// Apply pagination
|
||||
let paged: Vec<_> = points.into_iter().skip(offset as usize).take(limit as usize).collect();
|
||||
|
||||
let mut faces: Vec<TraceFaceItem> = Vec::new();
|
||||
|
||||
for (i, (id, frame, x, y, w, h, conf)) in rows.iter().enumerate() {
|
||||
for (i, point) in paged.iter().enumerate() {
|
||||
let payload = &point["payload"];
|
||||
let frame = payload["frame"].as_i64().unwrap_or(0);
|
||||
let bbox = &payload["bbox"];
|
||||
let x = bbox["x"].as_f64().unwrap_or(0.0) as i32;
|
||||
let y = bbox["y"].as_f64().unwrap_or(0.0) as i32;
|
||||
let w = bbox["width"].as_f64().unwrap_or(0.0) as i32;
|
||||
let h = bbox["height"].as_f64().unwrap_or(0.0) as i32;
|
||||
let conf = payload["confidence"].as_f64().unwrap_or(0.5) as f32;
|
||||
let id = i as i32;
|
||||
|
||||
let cur = (x, y, w, h);
|
||||
|
||||
// Add interpolated frames between previous and current detection
|
||||
if interpolate && i > 0 {
|
||||
let prev = &rows[i - 1];
|
||||
let prev_frame = prev.1;
|
||||
let prev_point = &paged[i - 1];
|
||||
let prev_payload = &prev_point["payload"];
|
||||
let prev_bbox = &prev_payload["bbox"];
|
||||
let prev_frame = prev_payload["frame"].as_i64().unwrap_or(0);
|
||||
let prev_x = prev_bbox["x"].as_f64().unwrap_or(0.0) as i32;
|
||||
let prev_y = prev_bbox["y"].as_f64().unwrap_or(0.0) as i32;
|
||||
let prev_w = prev_bbox["width"].as_f64().unwrap_or(0.0) as i32;
|
||||
let prev_h = prev_bbox["height"].as_f64().unwrap_or(0.0) as i32;
|
||||
let gap = frame - prev_frame;
|
||||
if gap > 1 {
|
||||
for mid in 1..gap {
|
||||
let t = mid as f64 / gap as f64;
|
||||
let mid_x = lerp_i32(prev.2, *x, t);
|
||||
let mid_y = lerp_i32(prev.3, *y, t);
|
||||
let mid_w = lerp_i32(prev.4, *w, t);
|
||||
let mid_h = lerp_i32(prev.5, *h, t);
|
||||
let mid_x = lerp_i32(Some(prev_x), Some(x), t).unwrap_or(0);
|
||||
let mid_y = lerp_i32(Some(prev_y), Some(y), t).unwrap_or(0);
|
||||
let mid_w = lerp_i32(Some(prev_w), Some(w), t).unwrap_or(0);
|
||||
let mid_h = lerp_i32(Some(prev_h), Some(h), t).unwrap_or(0);
|
||||
let mid_frame = prev_frame + mid;
|
||||
let mt = (mid_frame as f64 / fps * 10.0).round() / 10.0;
|
||||
faces.push(TraceFaceItem {
|
||||
@@ -317,10 +343,10 @@ async fn list_trace_faces(
|
||||
end_frame: mid_frame,
|
||||
start_time: mt,
|
||||
end_time: mt,
|
||||
x: mid_x,
|
||||
y: mid_y,
|
||||
width: mid_w,
|
||||
height: mid_h,
|
||||
x: Some(mid_x),
|
||||
y: Some(mid_y),
|
||||
width: Some(mid_w),
|
||||
height: Some(mid_h),
|
||||
confidence: 0.0,
|
||||
interpolated: true,
|
||||
});
|
||||
@@ -329,19 +355,19 @@ async fn list_trace_faces(
|
||||
}
|
||||
|
||||
// Add the real detection
|
||||
let frame_val = *frame;
|
||||
let frame_val = frame;
|
||||
let ft = (frame_val as f64 / fps * 10.0).round() / 10.0;
|
||||
faces.push(TraceFaceItem {
|
||||
id: *id,
|
||||
id,
|
||||
start_frame: frame_val,
|
||||
end_frame: frame_val,
|
||||
start_time: ft,
|
||||
end_time: ft,
|
||||
x: *x,
|
||||
y: *y,
|
||||
width: *w,
|
||||
height: *h,
|
||||
confidence: *conf as f64,
|
||||
x: Some(x),
|
||||
y: Some(y),
|
||||
width: Some(w),
|
||||
height: Some(h),
|
||||
confidence: conf as f64,
|
||||
interpolated: false,
|
||||
});
|
||||
}
|
||||
@@ -413,7 +439,8 @@ where
|
||||
F: Fn(anyhow::Error) -> T,
|
||||
{
|
||||
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 video_table = schema::table_name("videos");
|
||||
|
||||
let fps: f64 = sqlx::query_scalar(&format!(
|
||||
@@ -426,15 +453,16 @@ where
|
||||
.map_err(|e| err_fn(anyhow::anyhow!("{}", e)))?
|
||||
.unwrap_or(25.0);
|
||||
|
||||
let face_count: (i64,) = sqlx::query_as(&format!(
|
||||
"SELECT COUNT(*) FROM {} WHERE file_uuid = $1 AND trace_id = $2",
|
||||
fd_table
|
||||
))
|
||||
.bind(file_uuid)
|
||||
.bind(trace_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| err_fn(anyhow::anyhow!("{}", e)))?;
|
||||
// Get face count from Qdrant
|
||||
let qdrant = QdrantDb::new();
|
||||
let trace_filter = json!({
|
||||
"must": [
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}},
|
||||
{"key": "trace_id", "match": {"value": trace_id}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", trace_filter, 1000).await.unwrap_or_default();
|
||||
let face_count: (i64,) = (points.len() as i64,);
|
||||
|
||||
struct Candidate {
|
||||
frame: i64,
|
||||
@@ -446,38 +474,35 @@ where
|
||||
score: f64,
|
||||
}
|
||||
|
||||
let rows = sqlx::query_as::<_, (i64, i32, i32, i32, i32, f64)>(&format!(
|
||||
"SELECT frame_number::bigint, x, y, width, height, confidence::float8 \
|
||||
FROM {} WHERE file_uuid = $1 AND trace_id = $2 AND confidence > 0.7 \
|
||||
AND ((metadata->>'qc_ok')::boolean IS NULL OR (metadata->>'qc_ok')::boolean = true) \
|
||||
ORDER BY (width::float8 * height::float8) * confidence::float8 DESC LIMIT 10",
|
||||
fd_table
|
||||
))
|
||||
.bind(file_uuid)
|
||||
.bind(trace_id)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| err_fn(anyhow::anyhow!("{}", e)))?;
|
||||
// Get top faces by quality from Qdrant
|
||||
let mut candidates: Vec<Candidate> = points.iter()
|
||||
.filter_map(|p| {
|
||||
let payload = &p["payload"];
|
||||
let bbox = &payload["bbox"];
|
||||
let w = bbox["width"].as_f64()? as i32;
|
||||
let h = bbox["height"].as_f64()? as i32;
|
||||
let conf = payload["confidence"].as_f64()?;
|
||||
if conf <= 0.7 { return None; }
|
||||
let score = (w as f64 * h as f64) * conf;
|
||||
Some(Candidate {
|
||||
frame: payload["frame"].as_i64().unwrap_or(0),
|
||||
x: bbox["x"].as_f64()? as i32,
|
||||
y: bbox["y"].as_f64()? as i32,
|
||||
w,
|
||||
h,
|
||||
conf,
|
||||
score,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
candidates.sort_by(|a, b| b.score.partial_cmp(&a.score).unwrap_or(std::cmp::Ordering::Equal));
|
||||
let rows: Vec<_> = candidates.into_iter().take(10).collect();
|
||||
|
||||
if rows.is_empty() {
|
||||
return Err(err_fn(anyhow::anyhow!("No suitable face found")));
|
||||
}
|
||||
|
||||
let candidates: Vec<Candidate> = rows
|
||||
.into_iter()
|
||||
.map(|(frame, x, y, w, h, conf)| {
|
||||
let score = (w as f64 * h as f64) * conf;
|
||||
Candidate {
|
||||
frame,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
conf,
|
||||
score,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let candidates: Vec<Candidate> = rows;
|
||||
|
||||
let video_path: String = sqlx::query_scalar(&format!(
|
||||
"SELECT file_path FROM {} WHERE file_uuid = $1",
|
||||
@@ -759,8 +784,9 @@ async fn get_cooccurrence(
|
||||
Path((file_uuid, identity_uuid_a, identity_uuid_b)): Path<(String, String, String)>,
|
||||
) -> Result<Json<CoOccurResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
use crate::core::db::schema;
|
||||
use crate::core::db::qdrant_db::QdrantDb;
|
||||
use serde_json::json;
|
||||
let id_table = schema::table_name("identities");
|
||||
let fd_table = schema::table_name("face_detections");
|
||||
|
||||
// Stage 1: Get identity names and IDs
|
||||
let id_a = sqlx::query_as::<_, (i32, String)>(&format!(
|
||||
@@ -803,27 +829,33 @@ async fn get_cooccurrence(
|
||||
)
|
||||
})?;
|
||||
|
||||
// Stage 2: Find first frame where both identity_ids appear
|
||||
let cooccur: Option<(i64,)> = sqlx::query_as(&format!(
|
||||
"SELECT MIN(fd.frame_number)::bigint FROM {} fd \
|
||||
WHERE fd.file_uuid = $1 AND fd.identity_id = $2 \
|
||||
AND fd.frame_number IN ( \
|
||||
SELECT frame_number FROM {} \
|
||||
WHERE file_uuid = $1 AND identity_id = $3 \
|
||||
)",
|
||||
fd_table, fd_table
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.bind(id_a.0)
|
||||
.bind(id_b.0)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": e.to_string()})),
|
||||
)
|
||||
})?;
|
||||
// Stage 2: Find first frame where both identity_ids appear (from Qdrant _faces)
|
||||
let qdrant = QdrantDb::new();
|
||||
|
||||
// Get frames for identity A
|
||||
let filter_a = json!({
|
||||
"must": [
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}},
|
||||
{"key": "identity_id", "match": {"value": id_a.0}}
|
||||
]
|
||||
});
|
||||
let points_a = qdrant.scroll_all_points("_faces", filter_a, 1000).await.unwrap_or_default();
|
||||
let frames_a: std::collections::HashSet<i64> = points_a.iter()
|
||||
.filter_map(|p| p["payload"]["frame"].as_i64())
|
||||
.collect();
|
||||
|
||||
// Get frames for identity B and find first co-occurrence
|
||||
let filter_b = json!({
|
||||
"must": [
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}},
|
||||
{"key": "identity_id", "match": {"value": id_b.0}}
|
||||
]
|
||||
});
|
||||
let points_b = qdrant.scroll_all_points("_faces", filter_b, 1000).await.unwrap_or_default();
|
||||
let cooccur: Option<(i64,)> = points_b.iter()
|
||||
.filter_map(|p| p["payload"]["frame"].as_i64())
|
||||
.find(|f| frames_a.contains(f))
|
||||
.map(|f| (f,));
|
||||
|
||||
let (first_frame,) = cooccur.ok_or_else(|| {
|
||||
(StatusCode::NOT_FOUND, Json(serde_json::json!({"error": "These two identities never appear together in this file"})))
|
||||
@@ -846,24 +878,16 @@ async fn get_cooccurrence(
|
||||
})?
|
||||
.unwrap_or(25.0);
|
||||
|
||||
// Stage 3: Get trace_ids for both at this frame
|
||||
let trace_a: Option<(i32,)> = sqlx::query_as(
|
||||
&format!("SELECT trace_id FROM {} WHERE file_uuid = $1 AND frame_number = $2 AND identity_id = $3 AND trace_id IS NOT NULL LIMIT 1", fd_table)
|
||||
)
|
||||
.bind(&file_uuid).bind(first_frame).bind(id_a.0)
|
||||
.fetch_optional(state.db.pool()).await
|
||||
.map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})))
|
||||
})?;
|
||||
// Stage 3: Get trace_ids for both at this frame (from Qdrant _faces)
|
||||
let trace_a: Option<(i32,)> = points_a.iter()
|
||||
.find(|p| p["payload"]["frame"].as_i64() == Some(first_frame))
|
||||
.and_then(|p| p["payload"]["trace_id"].as_i64())
|
||||
.map(|t| (t as i32,));
|
||||
|
||||
let trace_b: Option<(i32,)> = sqlx::query_as(
|
||||
&format!("SELECT trace_id FROM {} WHERE file_uuid = $1 AND frame_number = $2 AND identity_id = $3 AND trace_id IS NOT NULL LIMIT 1", fd_table)
|
||||
)
|
||||
.bind(&file_uuid).bind(first_frame).bind(id_b.0)
|
||||
.fetch_optional(state.db.pool()).await
|
||||
.map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})))
|
||||
})?;
|
||||
let trace_b: Option<(i32,)> = points_b.iter()
|
||||
.find(|p| p["payload"]["frame"].as_i64() == Some(first_frame))
|
||||
.and_then(|p| p["payload"]["trace_id"].as_i64())
|
||||
.map(|t| (t as i32,));
|
||||
|
||||
// Stage 4: Get representative faces for both traces (reusing select_rep_face)
|
||||
let rep_a = if let Some((tid,)) = trace_a {
|
||||
@@ -914,22 +938,14 @@ async fn get_cooccurrence(
|
||||
None
|
||||
};
|
||||
|
||||
// Total co-occurrence frames (from TKG if available, otherwise from face_detections)
|
||||
let total_cooccurrence_frames: i64 = sqlx::query_scalar(&format!(
|
||||
"SELECT COUNT(DISTINCT fd.frame_number)::bigint FROM {} fd \
|
||||
WHERE fd.file_uuid = $1 AND fd.identity_id = $2 \
|
||||
AND fd.frame_number IN ( \
|
||||
SELECT frame_number FROM {} \
|
||||
WHERE file_uuid = $1 AND identity_id = $3 \
|
||||
)",
|
||||
fd_table, fd_table
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.bind(id_a.0)
|
||||
.bind(id_b.0)
|
||||
.fetch_one(state.db.pool())
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
// Total co-occurrence frames (from Qdrant _faces)
|
||||
let frames_b: std::collections::HashSet<i64> = points_b.iter()
|
||||
.filter_map(|p| p["payload"]["frame"].as_i64())
|
||||
.collect();
|
||||
let total_cooccurrence_frames: i64 = points_a.iter()
|
||||
.filter_map(|p| p["payload"]["frame"].as_i64())
|
||||
.filter(|f| frames_b.contains(f))
|
||||
.count() as i64;
|
||||
|
||||
Ok(Json(CoOccurResponse {
|
||||
success: true,
|
||||
@@ -971,7 +987,8 @@ async fn rebuild_tkg(
|
||||
use crate::core::chunk::rule2_ingest::ingest_rule2;
|
||||
use tracing::info;
|
||||
|
||||
let result = crate::core::processor::tkg::build_tkg(&state.db, &file_uuid, &OUTPUT_DIR).await;
|
||||
let redis = crate::core::db::RedisClient::new().ok();
|
||||
let result = crate::core::processor::tkg::build_tkg(&state.db, &file_uuid, &OUTPUT_DIR, redis.map(Arc::new)).await;
|
||||
|
||||
match result {
|
||||
Ok(r) => {
|
||||
@@ -987,7 +1004,7 @@ async fn rebuild_tkg(
|
||||
"[TKG] {} relationship edges found, triggering Rule 2 ingestion...",
|
||||
total_edges
|
||||
);
|
||||
match ingest_rule2(state.db.pool(), &file_uuid).await {
|
||||
match ingest_rule2(state.db.pool(), &file_uuid, None, None).await {
|
||||
Ok(count) => info!("[TKG] Rule 2 created {} relationship chunks", count),
|
||||
Err(e) => info!("[TKG] Rule 2 ingestion failed: {}", e),
|
||||
}
|
||||
@@ -1087,26 +1104,26 @@ async fn get_stranger_representative_face(
|
||||
State(state): State<crate::api::types::AppState>,
|
||||
Path((file_uuid, stranger_id)): Path<(String, i32)>,
|
||||
) -> Result<Json<RepFaceResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
let faces_table = crate::core::db::schema::table_name("face_detections");
|
||||
// Get trace_id from Qdrant _faces by stranger_id
|
||||
use crate::core::db::qdrant_db::QdrantDb;
|
||||
use serde_json::json;
|
||||
|
||||
let trace_id: i32 = sqlx::query_scalar(&format!(
|
||||
"SELECT trace_id FROM {} WHERE file_uuid = $1 AND stranger_id = $2 LIMIT 1",
|
||||
faces_table
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.bind(stranger_id)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": e.to_string()})),
|
||||
)
|
||||
})?
|
||||
.ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({"error": "Stranger not found"})),
|
||||
))?;
|
||||
let qdrant = QdrantDb::new();
|
||||
let filter = json!({
|
||||
"must": [
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}},
|
||||
{"key": "stranger_id", "match": {"value": stranger_id}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", filter, 1).await.unwrap_or_default();
|
||||
|
||||
let trace_id: i32 = points.first()
|
||||
.and_then(|p| p["payload"]["trace_id"].as_i64())
|
||||
.map(|t| t as i32)
|
||||
.ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({"error": "Stranger not found"})),
|
||||
))?;
|
||||
|
||||
get_representative_face_inner(&state, &file_uuid, trace_id).await
|
||||
}
|
||||
@@ -1115,26 +1132,25 @@ async fn get_stranger_thumbnail(
|
||||
State(state): State<crate::api::types::AppState>,
|
||||
Path((file_uuid, stranger_id)): Path<(String, i32)>,
|
||||
) -> Result<Response, (StatusCode, Json<serde_json::Value>)> {
|
||||
let faces_table = crate::core::db::schema::table_name("face_detections");
|
||||
use crate::core::db::qdrant_db::QdrantDb;
|
||||
use serde_json::json;
|
||||
|
||||
let trace_id: i32 = sqlx::query_scalar(&format!(
|
||||
"SELECT trace_id FROM {} WHERE file_uuid = $1 AND stranger_id = $2 LIMIT 1",
|
||||
faces_table
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.bind(stranger_id)
|
||||
.fetch_optional(state.db.pool())
|
||||
.await
|
||||
.map_err(|e| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": e.to_string()})),
|
||||
)
|
||||
})?
|
||||
.ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({"error": "Stranger not found"})),
|
||||
))?;
|
||||
let qdrant = QdrantDb::new();
|
||||
let filter = json!({
|
||||
"must": [
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}},
|
||||
{"key": "stranger_id", "match": {"value": stranger_id}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", filter, 1).await.unwrap_or_default();
|
||||
|
||||
let trace_id: i32 = points.first()
|
||||
.and_then(|p| p["payload"]["trace_id"].as_i64())
|
||||
.map(|t| t as i32)
|
||||
.ok_or((
|
||||
StatusCode::NOT_FOUND,
|
||||
Json(serde_json::json!({"error": "Stranger not found"})),
|
||||
))?;
|
||||
|
||||
get_trace_thumbnail_inner(&state, &file_uuid, trace_id).await
|
||||
}
|
||||
@@ -1526,7 +1542,7 @@ async fn ingest_rule2(
|
||||
use crate::core::embedding::Embedder;
|
||||
use tracing::info;
|
||||
|
||||
let result = ingest_rule2(state.db.pool(), &file_uuid).await;
|
||||
let result = ingest_rule2(state.db.pool(), &file_uuid, None, None).await;
|
||||
|
||||
match result {
|
||||
Ok(rule2_chunks) => {
|
||||
|
||||
Reference in New Issue
Block a user