refactor: cargo fmt across 27 files + behavioral fixes
Behavioral changes: - postgres_db: reorder default processors (cut first), prevent overwriting completed status - qdrant_db: fix scroll pagination exit condition (next.is_none()) - job_worker: add idempotency guards for face trace / TKG build; better error logging - main.rs: add LineWriter for stdout buffering Remaining diff is cargo fmt reformatting (line wrapping, import ordering).
This commit is contained in:
+41
-13
@@ -339,9 +339,16 @@ async fn register_single_file(
|
||||
existing_uuid
|
||||
);
|
||||
if let Err(e) = unregister_internal(&state, &existing_uuid).await {
|
||||
tracing::error!("[REGISTER] Force unregister failed for {}: {:?}", existing_uuid, e);
|
||||
tracing::error!(
|
||||
"[REGISTER] Force unregister failed for {}: {:?}",
|
||||
existing_uuid,
|
||||
e
|
||||
);
|
||||
} else {
|
||||
tracing::info!("[REGISTER] Force unregister completed for {}", existing_uuid);
|
||||
tracing::info!(
|
||||
"[REGISTER] Force unregister completed for {}",
|
||||
existing_uuid
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let existing_info: Option<(String, String, f64, i32, i32, f64, i64, Option<String>)> = sqlx::query_as(
|
||||
@@ -726,7 +733,11 @@ async fn register_file(
|
||||
existing_uuid
|
||||
);
|
||||
if let Err(e) = unregister_internal(&state, &existing_uuid).await {
|
||||
tracing::error!("[REGISTER] Force unregister failed for {}: {:?}", existing_uuid, e);
|
||||
tracing::error!(
|
||||
"[REGISTER] Force unregister failed for {}: {:?}",
|
||||
existing_uuid,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
// Also check by content_hash if provided
|
||||
@@ -744,7 +755,11 @@ async fn register_file(
|
||||
existing_uuid
|
||||
);
|
||||
if let Err(e) = unregister_internal(&state, &existing_uuid).await {
|
||||
tracing::error!("[REGISTER] Force unregister failed for {}: {:?}", existing_uuid, e);
|
||||
tracing::error!(
|
||||
"[REGISTER] Force unregister failed for {}: {:?}",
|
||||
existing_uuid,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1241,25 +1256,31 @@ async fn unregister(
|
||||
match RedisClient::new() {
|
||||
Ok(redis) => {
|
||||
let mut deleted = 0;
|
||||
|
||||
|
||||
// Delete worker job keys
|
||||
if redis.delete_worker_job(&uuid).await.is_ok() {
|
||||
tracing::info!("[UNREGISTER] Deleted Redis worker job keys for {}", uuid);
|
||||
deleted += 1;
|
||||
}
|
||||
|
||||
|
||||
// Delete PipelineProgress key
|
||||
let progress_key = format!("{}progress:{}:pipeline",
|
||||
crate::core::config::REDIS_KEY_PREFIX.as_str(), uuid);
|
||||
let progress_key = format!(
|
||||
"{}progress:{}:pipeline",
|
||||
crate::core::config::REDIS_KEY_PREFIX.as_str(),
|
||||
uuid
|
||||
);
|
||||
if let Ok(mut conn) = redis.get_conn().await {
|
||||
let _: Option<String> = redis::cmd("DEL").arg(&progress_key)
|
||||
.query_async(&mut conn).await.ok();
|
||||
let _: Option<String> = redis::cmd("DEL")
|
||||
.arg(&progress_key)
|
||||
.query_async(&mut conn)
|
||||
.await
|
||||
.ok();
|
||||
tracing::info!("[UNREGISTER] Deleted Redis PipelineProgress for {}", uuid);
|
||||
deleted += 1;
|
||||
}
|
||||
|
||||
|
||||
Some(deleted)
|
||||
},
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::warn!("[UNREGISTER] Failed to create Redis client: {}", e);
|
||||
None
|
||||
@@ -1440,11 +1461,18 @@ async fn unregister_internal(state: &AppState, uuid: &str) -> Result<(), StatusC
|
||||
uuid,
|
||||
)
|
||||
.await;
|
||||
// Delete from speaker collection
|
||||
let schema = std::env::var("DATABASE_SCHEMA").unwrap_or_else(|_| "dev".to_string());
|
||||
let speaker_collection = if schema == "public" {
|
||||
"momentry_public_speaker"
|
||||
} else {
|
||||
&format!("momentry_{}_speaker", schema)
|
||||
};
|
||||
let _ = QdrantDb::delete_by_uuid_from_collection(
|
||||
&qdrant.client,
|
||||
&qdrant.base_url,
|
||||
&qdrant.api_key,
|
||||
&format!("{}_voice", uuid),
|
||||
speaker_collection,
|
||||
uuid,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -11,11 +11,11 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::api::types::AppState;
|
||||
use crate::core::db::redis_client::RedisClient;
|
||||
use crate::core::db::schema;
|
||||
use crate::core::db::PostgresDb;
|
||||
use crate::core::db::QdrantDb;
|
||||
use crate::core::progress::{AgentPhase, AgentProgress, AgentStats, publish_agent_progress};
|
||||
use crate::core::db::redis_client::RedisClient;
|
||||
use crate::core::progress::{publish_agent_progress, AgentPhase, AgentProgress, AgentStats};
|
||||
|
||||
pub fn identity_agent_routes() -> Router<AppState> {
|
||||
Router::new()
|
||||
@@ -213,17 +213,23 @@ async fn match_from_photo(
|
||||
// 4. Find best matching trace via Qdrant _faces search
|
||||
let qdrant = QdrantDb::new();
|
||||
|
||||
let best_match: Option<(i32, f64)> = match qdrant.search_face_collection(
|
||||
"_faces",
|
||||
&embedding_f32,
|
||||
1,
|
||||
"file_uuid",
|
||||
"",
|
||||
Some(&file_uuid),
|
||||
).await {
|
||||
let best_match: Option<(i32, f64)> = match qdrant
|
||||
.search_face_collection(
|
||||
"_faces",
|
||||
&embedding_f32,
|
||||
1,
|
||||
"file_uuid",
|
||||
"",
|
||||
Some(&file_uuid),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(hits) if !hits.is_empty() => {
|
||||
let (score, payload) = &hits[0];
|
||||
let trace_id = payload.get("trace_id").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
|
||||
let trace_id = payload
|
||||
.get("trace_id")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0) as i32;
|
||||
Some((trace_id, *score))
|
||||
}
|
||||
_ => None,
|
||||
@@ -295,18 +301,27 @@ async fn match_from_trace(
|
||||
{"key": "trace_id", "match": {"value": req.trace_id}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", trace_filter, 500).await.unwrap_or_default();
|
||||
let points = qdrant
|
||||
.scroll_all_points("_faces", trace_filter, 500)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let all_faces: Vec<(Vec<f32>, i64)> = points.iter().filter_map(|p| {
|
||||
let vector = p.get("vector").and_then(|v| v.as_array())?;
|
||||
let embedding: Vec<f32> = vector.iter().filter_map(|v| v.as_f64().map(|f| f as f32)).collect();
|
||||
let frame = p["payload"]["frame"].as_i64()?;
|
||||
if embedding.len() == 512 {
|
||||
Some((embedding, frame))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect();
|
||||
let all_faces: Vec<(Vec<f32>, i64)> = points
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let vector = p.get("vector").and_then(|v| v.as_array())?;
|
||||
let embedding: Vec<f32> = vector
|
||||
.iter()
|
||||
.filter_map(|v| v.as_f64().map(|f| f as f32))
|
||||
.collect();
|
||||
let frame = p["payload"]["frame"].as_i64()?;
|
||||
if embedding.len() == 512 {
|
||||
Some((embedding, frame))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if all_faces.is_empty() {
|
||||
return Err((
|
||||
@@ -328,13 +343,16 @@ async fn match_from_trace(
|
||||
let mut query_embeddings: Vec<Vec<f32>> = Vec::new();
|
||||
|
||||
// Get bbox size info from Qdrant payload
|
||||
let face_sizes: Vec<(i64, i32)> = points.iter().filter_map(|p| {
|
||||
let frame = p["payload"]["frame"].as_i64()?;
|
||||
let bbox = &p["payload"]["bbox"];
|
||||
let w = bbox["width"].as_f64().unwrap_or(0.0) as i32;
|
||||
let h = bbox["height"].as_f64().unwrap_or(0.0) as i32;
|
||||
Some((frame, w * h))
|
||||
}).collect();
|
||||
let face_sizes: Vec<(i64, i32)> = points
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let frame = p["payload"]["frame"].as_i64()?;
|
||||
let bbox = &p["payload"]["bbox"];
|
||||
let w = bbox["width"].as_f64().unwrap_or(0.0) as i32;
|
||||
let h = bbox["height"].as_f64().unwrap_or(0.0) as i32;
|
||||
Some((frame, w * h))
|
||||
})
|
||||
.collect();
|
||||
|
||||
let face_sizes_map: std::collections::HashMap<i64, i32> = face_sizes.into_iter().collect();
|
||||
|
||||
@@ -373,14 +391,17 @@ async fn match_from_trace(
|
||||
]
|
||||
});
|
||||
|
||||
let hits = match qdrant.search_face_collection(
|
||||
"_faces",
|
||||
qemb,
|
||||
1,
|
||||
"trace_id",
|
||||
&req.trace_id.to_string(),
|
||||
Some(&req.file_uuid),
|
||||
).await {
|
||||
let hits = match qdrant
|
||||
.search_face_collection(
|
||||
"_faces",
|
||||
qemb,
|
||||
1,
|
||||
"trace_id",
|
||||
&req.trace_id.to_string(),
|
||||
Some(&req.file_uuid),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(h) => h,
|
||||
Err(e) => {
|
||||
tracing::warn!("[match_from_trace] Qdrant search failed: {}", e);
|
||||
@@ -389,7 +410,10 @@ async fn match_from_trace(
|
||||
};
|
||||
|
||||
if let Some((score, payload)) = hits.first() {
|
||||
let trace_id = payload.get("trace_id").and_then(|v| v.as_i64()).unwrap_or(0) as i32;
|
||||
let trace_id = payload
|
||||
.get("trace_id")
|
||||
.and_then(|v| v.as_i64())
|
||||
.unwrap_or(0) as i32;
|
||||
if seen_trace_ids.insert(trace_id) {
|
||||
validated.push((0, trace_id, *score));
|
||||
}
|
||||
@@ -787,7 +811,10 @@ pub async fn bind_speakers(pool: &sqlx::PgPool, file_uuid: &str) -> anyhow::Resu
|
||||
{"key": "trace_id", "match": {"value": 1}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", trace_filter, 500).await.unwrap_or_default();
|
||||
let points = qdrant
|
||||
.scroll_all_points("_faces", trace_filter, 500)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
// Group by trace_id, collect frames
|
||||
let mut traces: HashMap<i32, Vec<i64>> = HashMap::new();
|
||||
@@ -855,15 +882,14 @@ pub async fn bind_speakers(pool: &sqlx::PgPool, file_uuid: &str) -> anyhow::Resu
|
||||
}
|
||||
|
||||
// Compute fps from video table
|
||||
let fps: f64 = sqlx::query_scalar::<_, f64>(
|
||||
"SELECT COALESCE(fps, 25.0) FROM videos WHERE file_uuid=$1"
|
||||
)
|
||||
.bind(file_uuid)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(25.0);
|
||||
let fps: f64 =
|
||||
sqlx::query_scalar::<_, f64>("SELECT COALESCE(fps, 25.0) FROM videos WHERE file_uuid=$1")
|
||||
.bind(file_uuid)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(25.0);
|
||||
|
||||
tracing::info!(
|
||||
"[SpeakerBind] Using fps={:.3} for {} ({} traces)",
|
||||
@@ -880,7 +906,8 @@ pub async fn bind_speakers(pool: &sqlx::PgPool, file_uuid: &str) -> anyhow::Resu
|
||||
}
|
||||
|
||||
// Get identity_id for this trace from Qdrant payload
|
||||
let identity_id: Option<i32> = points.iter()
|
||||
let identity_id: Option<i32> = points
|
||||
.iter()
|
||||
.find(|p| {
|
||||
p["payload"]["trace_id"].as_i64() == Some(*trace_id as i64)
|
||||
&& p["payload"]["identity_id"].as_i64().is_some()
|
||||
@@ -993,7 +1020,11 @@ pub async fn run_identity_agent(
|
||||
|
||||
let matched = match_faces_iterative(pool, file_uuid).await.unwrap_or(0);
|
||||
progress.stats.tmdb_matches = matched as i64;
|
||||
progress.update_phase(AgentPhase::TmdbMatching, 1.0, &format!("Face matching: {} matches", matched));
|
||||
progress.update_phase(
|
||||
AgentPhase::TmdbMatching,
|
||||
1.0,
|
||||
&format!("Face matching: {} matches", matched),
|
||||
);
|
||||
if let Some(r) = redis.as_ref() {
|
||||
publish_agent_progress(&r, file_uuid, &progress).await;
|
||||
}
|
||||
@@ -1051,9 +1082,11 @@ pub async fn run_identity_agent(
|
||||
.await;
|
||||
}
|
||||
progress.stats.identities_created = identities.len() as i64;
|
||||
progress.update_phase(AgentPhase::IdentityCreation, 1.0, &format!(
|
||||
"Created {} identities from clusters", identities.len()
|
||||
));
|
||||
progress.update_phase(
|
||||
AgentPhase::IdentityCreation,
|
||||
1.0,
|
||||
&format!("Created {} identities from clusters", identities.len()),
|
||||
);
|
||||
if let Some(r) = redis.as_ref() {
|
||||
publish_agent_progress(&r, file_uuid, &progress).await;
|
||||
}
|
||||
@@ -1081,7 +1114,11 @@ pub async fn run_identity_agent(
|
||||
|
||||
let bound = bind_speakers(pool, file_uuid).await.unwrap_or(0);
|
||||
progress.stats.speaker_bindings = bound as i64;
|
||||
progress.update_phase(AgentPhase::SpeakerBinding, 1.0, &format!("Speaker binding: {} bound", bound));
|
||||
progress.update_phase(
|
||||
AgentPhase::SpeakerBinding,
|
||||
1.0,
|
||||
&format!("Speaker binding: {} bound", bound),
|
||||
);
|
||||
if let Some(r) = redis.as_ref() {
|
||||
publish_agent_progress(&r, file_uuid, &progress).await;
|
||||
}
|
||||
@@ -1162,7 +1199,10 @@ async fn find_ready_files(pool: &sqlx::PgPool) -> anyhow::Result<Vec<String>> {
|
||||
{"key": "identity_id", "match": {"value": null}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", filter, 1000).await.unwrap_or_default();
|
||||
let points = qdrant
|
||||
.scroll_all_points("_faces", filter, 1000)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut file_uuids: std::collections::HashSet<String> = std::collections::HashSet::new();
|
||||
for point in &points {
|
||||
|
||||
+65
-32
@@ -164,8 +164,7 @@ async fn list_files(
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let count_map: std::collections::HashMap<String, i64> =
|
||||
chunk_counts.into_iter().collect();
|
||||
let count_map: std::collections::HashMap<String, i64> = chunk_counts.into_iter().collect();
|
||||
|
||||
for item in &mut data {
|
||||
if let Some(cnt) = count_map.get(&item.file_uuid) {
|
||||
@@ -483,13 +482,20 @@ async fn delete_identity(
|
||||
{"key": "identity_id", "match": {"value": identity_id}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", face_filter, 1000).await.unwrap_or_default();
|
||||
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()
|
||||
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 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))
|
||||
})
|
||||
@@ -629,8 +635,12 @@ async fn get_identity_files(
|
||||
{"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()
|
||||
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;
|
||||
@@ -730,7 +740,10 @@ async fn get_identity_faces(
|
||||
{"key": "identity_id", "match": {"value": identity_id}}
|
||||
]
|
||||
});
|
||||
let points2 = qdrant2.scroll_all_points("_faces", face_filter2, 2000).await.unwrap_or_default();
|
||||
let points2 = qdrant2
|
||||
.scroll_all_points("_faces", face_filter2, 2000)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let total = points2.len() as i64;
|
||||
|
||||
Ok(Json(IdentityFacesResponse {
|
||||
@@ -827,9 +840,7 @@ async fn get_file_faces(
|
||||
use serde_json::json;
|
||||
|
||||
let qdrant = QdrantDb::new();
|
||||
let mut filter_conditions = vec![
|
||||
json!({"key": "file_uuid", "match": {"value": file_uuid}})
|
||||
];
|
||||
let mut filter_conditions = vec![json!({"key": "file_uuid", "match": {"value": file_uuid}})];
|
||||
|
||||
if let Some(ref binding) = params.binding {
|
||||
match binding.as_str() {
|
||||
@@ -851,30 +862,46 @@ async fn get_file_faces(
|
||||
}
|
||||
|
||||
let face_filter = json!({"must": filter_conditions});
|
||||
let points = qdrant.scroll_all_points("_faces", face_filter, 2000).await.unwrap_or_default();
|
||||
let points = qdrant
|
||||
.scroll_all_points("_faces", face_filter, 2000)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
// 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 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);
|
||||
|
||||
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();
|
||||
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();
|
||||
|
||||
let total = filtered.len() as i64;
|
||||
|
||||
// Apply pagination
|
||||
let paged: Vec<_> = filtered.into_iter().skip(offset as usize).take(page_size as usize).collect();
|
||||
let paged: Vec<_> = filtered
|
||||
.into_iter()
|
||||
.skip(offset as usize)
|
||||
.take(page_size as usize)
|
||||
.collect();
|
||||
|
||||
// Build response items
|
||||
let mut data = Vec::new();
|
||||
@@ -889,7 +916,10 @@ async fn get_file_faces(
|
||||
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()),
|
||||
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),
|
||||
@@ -1315,8 +1345,8 @@ async fn set_profile_from_face(
|
||||
Path(identity_uuid): Path<String>,
|
||||
Json(req): Json<SetProfileFromFaceRequest>,
|
||||
) -> Result<Json<ProfileImageResponse>, (StatusCode, Json<serde_json::Value>)> {
|
||||
use crate::core::db::schema;
|
||||
use crate::core::db::qdrant_db::QdrantDb;
|
||||
use crate::core::db::schema;
|
||||
use serde_json::json;
|
||||
let videos_table = schema::table_name("videos");
|
||||
|
||||
@@ -1341,13 +1371,16 @@ async fn set_profile_from_face(
|
||||
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}})
|
||||
json!({"key": "trace_id", "match": {"value": trace_id}}),
|
||||
];
|
||||
if let Some(frame) = use_frame {
|
||||
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();
|
||||
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"];
|
||||
|
||||
+29
-12
@@ -249,7 +249,10 @@ pub async fn unbind_identity(
|
||||
{"key": "trace_id", "match": {"value": tid}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_points("_faces", filter, 1, None).await.unwrap_or_default();
|
||||
let points = qdrant
|
||||
.scroll_points("_faces", filter, 1, None)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
points.0.first().map(|p| {
|
||||
let identity_id = p["payload"]["identity_id"].as_i64().map(|i| i as i32);
|
||||
(identity_id, Some(tid))
|
||||
@@ -260,7 +263,9 @@ pub async fn unbind_identity(
|
||||
None
|
||||
} else {
|
||||
// face_id lookup not supported in Qdrant
|
||||
tracing::warn!("[unbind_identity] face_id field lookup not supported, use trace_id instead");
|
||||
tracing::warn!(
|
||||
"[unbind_identity] face_id field lookup not supported, use trace_id instead"
|
||||
);
|
||||
None
|
||||
};
|
||||
|
||||
@@ -440,7 +445,6 @@ pub async fn merge_identities(
|
||||
Path(identity_uuid): Path<String>,
|
||||
Json(req): Json<MergeIdentitiesRequest>,
|
||||
) -> Result<Json<ApiResponse<serde_json::Value>>, (StatusCode, Json<serde_json::Value>)> {
|
||||
|
||||
let id_table = crate::core::db::schema::table_name("identities");
|
||||
|
||||
let db = sqlx::PgPool::connect(&crate::core::config::DATABASE_URL)
|
||||
@@ -523,10 +527,17 @@ pub async fn merge_identities(
|
||||
{"key": "identity_id", "match": {"value": from_id}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", face_filter, 1000).await.unwrap_or_default();
|
||||
let face_ids: Vec<(Option<String>, Option<i32>)> = points.iter()
|
||||
let points = qdrant
|
||||
.scroll_all_points("_faces", face_filter, 1000)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let face_ids: Vec<(Option<String>, Option<i32>)> = points
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let face_id = p["payload"].get("face_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
let face_id = p["payload"]
|
||||
.get("face_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
let trace_id = p["payload"]["trace_id"].as_i64().map(|t| t as i32);
|
||||
(face_id, trace_id)
|
||||
})
|
||||
@@ -537,7 +548,8 @@ pub async fn merge_identities(
|
||||
let faces_count = face_id_list.len() as i64;
|
||||
|
||||
// Get file_uuid for these faces (from Qdrant)
|
||||
let file_uuid: Option<String> = points.first()
|
||||
let file_uuid: Option<String> = points
|
||||
.first()
|
||||
.and_then(|p| p["payload"]["file_uuid"].as_str())
|
||||
.map(|s| s.to_string());
|
||||
|
||||
@@ -1651,7 +1663,9 @@ async fn apply_bind_snapshot(
|
||||
// Legacy face_id path not supported - no trace_id in snapshot
|
||||
Err((
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(serde_json::json!({"error": "Snapshot has no trace_id, legacy face_id path not supported"})),
|
||||
Json(
|
||||
serde_json::json!({"error": "Snapshot has no trace_id, legacy face_id path not supported"}),
|
||||
),
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -1789,8 +1803,7 @@ pub async fn bind_redo(
|
||||
let after = &records.last().unwrap().2;
|
||||
|
||||
let identity_id_after = after.get("identity_id_after").cloned();
|
||||
let affected_rows =
|
||||
apply_bind_snapshot(state.db.pool(), "", after, identity_id_after).await?;
|
||||
let affected_rows = apply_bind_snapshot(state.db.pool(), "", after, identity_id_after).await?;
|
||||
|
||||
// Mark all as redone
|
||||
for (history_id, _, _) in &records {
|
||||
@@ -2098,8 +2111,12 @@ pub async fn list_pending_persons(
|
||||
{"key": "identity_id", "match": {"value": id}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", face_filter, 500).await.unwrap_or_default();
|
||||
let unique_traces: std::collections::HashSet<i64> = points.iter()
|
||||
let points = qdrant
|
||||
.scroll_all_points("_faces", face_filter, 500)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let unique_traces: std::collections::HashSet<i64> = points
|
||||
.iter()
|
||||
.filter_map(|p| p["payload"]["trace_id"].as_i64())
|
||||
.collect();
|
||||
let trace_count = unique_traces.len() as i64;
|
||||
|
||||
+74
-45
@@ -232,20 +232,29 @@ async fn bbox_overlay_video(
|
||||
{"key": "trace_id", "match": {"value": 1}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", face_filter, 500).await.unwrap_or_default();
|
||||
let points = qdrant
|
||||
.scroll_all_points("_faces", face_filter, 500)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let rows: Vec<(i64, i32, i32, i32, i32, Option<i32>, Option<String>)> = points.iter().filter_map(|p| {
|
||||
let payload = &p["payload"];
|
||||
let frame = payload["frame"].as_i64()?;
|
||||
let bbox = &payload["bbox"];
|
||||
let x = bbox["x"].as_f64()? as i32;
|
||||
let y = bbox["y"].as_f64()? as i32;
|
||||
let w = bbox["width"].as_f64()? as i32;
|
||||
let h = bbox["height"].as_f64()? as i32;
|
||||
let trace_id = payload["trace_id"].as_i64().map(|t| t as i32);
|
||||
let face_id = payload.get("face_id").and_then(|v| v.as_str()).map(|s| s.to_string());
|
||||
Some((frame, x, y, w, h, trace_id, face_id))
|
||||
}).collect();
|
||||
let rows: Vec<(i64, i32, i32, i32, i32, Option<i32>, Option<String>)> = points
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let payload = &p["payload"];
|
||||
let frame = payload["frame"].as_i64()?;
|
||||
let bbox = &payload["bbox"];
|
||||
let x = bbox["x"].as_f64()? as i32;
|
||||
let y = bbox["y"].as_f64()? as i32;
|
||||
let w = bbox["width"].as_f64()? as i32;
|
||||
let h = bbox["height"].as_f64()? as i32;
|
||||
let trace_id = payload["trace_id"].as_i64().map(|t| t as i32);
|
||||
let face_id = payload
|
||||
.get("face_id")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(|s| s.to_string());
|
||||
Some((frame, x, y, w, h, trace_id, face_id))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Build filters — each bbox enabled only on its frame
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
@@ -362,18 +371,24 @@ async fn trace_video_inner(
|
||||
{"key": "trace_id", "match": {"value": trace_id}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", trace_filter, 500).await.unwrap_or_default();
|
||||
let points = qdrant
|
||||
.scroll_all_points("_faces", trace_filter, 500)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let rows: Vec<(i64, i32, i32, i32, i32)> = points.iter().filter_map(|p| {
|
||||
let payload = &p["payload"];
|
||||
let frame = payload["frame"].as_i64()?;
|
||||
let bbox = &payload["bbox"];
|
||||
let x = bbox["x"].as_f64()? as i32;
|
||||
let y = bbox["y"].as_f64()? as i32;
|
||||
let w = bbox["width"].as_f64()? as i32;
|
||||
let h = bbox["height"].as_f64()? as i32;
|
||||
Some((frame, x, y, w, h))
|
||||
}).collect();
|
||||
let rows: Vec<(i64, i32, i32, i32, i32)> = points
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let payload = &p["payload"];
|
||||
let frame = payload["frame"].as_i64()?;
|
||||
let bbox = &payload["bbox"];
|
||||
let x = bbox["x"].as_f64()? as i32;
|
||||
let y = bbox["y"].as_f64()? as i32;
|
||||
let w = bbox["width"].as_f64()? as i32;
|
||||
let h = bbox["height"].as_f64()? as i32;
|
||||
Some((frame, x, y, w, h))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if rows.is_empty() {
|
||||
return Err(StatusCode::NOT_FOUND);
|
||||
@@ -424,13 +439,20 @@ async fn trace_video_inner(
|
||||
|
||||
// Query all traces with identity names and bbox positions in the visible frame range
|
||||
let identities_table = schema::table_name("identities");
|
||||
let all_points = qdrant.scroll_all_points("_faces", json!({
|
||||
"must": [
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}},
|
||||
{"key": "frame", "range": {"gte": start_fn, "lte": end_fn}},
|
||||
{"key": "trace_id", "match": {"value": 1}}
|
||||
]
|
||||
}), 1000).await.unwrap_or_default();
|
||||
let all_points = qdrant
|
||||
.scroll_all_points(
|
||||
"_faces",
|
||||
json!({
|
||||
"must": [
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}},
|
||||
{"key": "frame", "range": {"gte": start_fn, "lte": end_fn}},
|
||||
{"key": "trace_id", "match": {"value": 1}}
|
||||
]
|
||||
}),
|
||||
1000,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
// Get identity names for traces that have identity_id
|
||||
let mut identity_names: HashMap<i32, String> = HashMap::new();
|
||||
@@ -455,18 +477,21 @@ async fn trace_video_inner(
|
||||
}
|
||||
}
|
||||
|
||||
let all_rows: Vec<(i32, i64, i32, i32, i32, i32, Option<String>)> = all_points.iter().filter_map(|p| {
|
||||
let payload = &p["payload"];
|
||||
let trace_id = payload["trace_id"].as_i64()? as i32;
|
||||
let frame = payload["frame"].as_i64()?;
|
||||
let bbox = &payload["bbox"];
|
||||
let x = bbox["x"].as_f64()? as i32;
|
||||
let y = bbox["y"].as_f64()? as i32;
|
||||
let w = bbox["width"].as_f64()? as i32;
|
||||
let h = bbox["height"].as_f64()? as i32;
|
||||
let name = identity_names.get(&trace_id).cloned();
|
||||
Some((trace_id, frame, x, y, w, h, name))
|
||||
}).collect();
|
||||
let all_rows: Vec<(i32, i64, i32, i32, i32, i32, Option<String>)> = all_points
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let payload = &p["payload"];
|
||||
let trace_id = payload["trace_id"].as_i64()? as i32;
|
||||
let frame = payload["frame"].as_i64()?;
|
||||
let bbox = &payload["bbox"];
|
||||
let x = bbox["x"].as_f64()? as i32;
|
||||
let y = bbox["y"].as_f64()? as i32;
|
||||
let w = bbox["width"].as_f64()? as i32;
|
||||
let h = bbox["height"].as_f64()? as i32;
|
||||
let name = identity_names.get(&trace_id).cloned();
|
||||
Some((trace_id, frame, x, y, w, h, name))
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Group frames by trace_id, compute start_frame per trace; collect bbox per frame
|
||||
// frame_number is i64 (BIGINT), so HashMaps need i64 for frame values
|
||||
@@ -1169,9 +1194,13 @@ async fn stranger_video_inner(
|
||||
{"key": "stranger_id", "match": {"value": stranger_id}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", face_filter, 1000).await.unwrap_or_default();
|
||||
let points = qdrant
|
||||
.scroll_all_points("_faces", face_filter, 1000)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let rows: Vec<(i64, i32, i32, i32, i32)> = points.iter()
|
||||
let rows: Vec<(i64, i32, i32, i32, i32)> = points
|
||||
.iter()
|
||||
.filter_map(|p| {
|
||||
let payload = &p["payload"];
|
||||
let frame = payload["frame"].as_i64()?;
|
||||
|
||||
+10
-2
@@ -471,14 +471,22 @@ async fn get_progress(file_uuid: Path<String>) -> Result<Json<serde_json::Value>
|
||||
let agent_key = format!("{}progress:{}:agent", REDIS_KEY_PREFIX.as_str(), file_uuid);
|
||||
|
||||
let tkg_progress: Option<serde_json::Value> = if let Ok(mut c) = redis.get_conn().await {
|
||||
let val: Option<String> = redis::cmd("GET").arg(&tkg_key).query_async(&mut c).await.ok();
|
||||
let val: Option<String> = redis::cmd("GET")
|
||||
.arg(&tkg_key)
|
||||
.query_async(&mut c)
|
||||
.await
|
||||
.ok();
|
||||
val.and_then(|s| serde_json::from_str(&s).ok())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let agent_progress: Option<serde_json::Value> = if let Ok(mut c) = redis.get_conn().await {
|
||||
let val: Option<String> = redis::cmd("GET").arg(&agent_key).query_async(&mut c).await.ok();
|
||||
let val: Option<String> = redis::cmd("GET")
|
||||
.arg(&agent_key)
|
||||
.query_async(&mut c)
|
||||
.await
|
||||
.ok();
|
||||
val.and_then(|s| serde_json::from_str(&s).ok())
|
||||
} else {
|
||||
None
|
||||
|
||||
+254
-81
@@ -460,7 +460,10 @@ async fn get_ingestion_status(
|
||||
{"key": "file_uuid", "match": {"value": file_uuid}}
|
||||
]
|
||||
});
|
||||
let points = qdrant.scroll_all_points("_faces", face_filter, 1000).await.unwrap_or_default();
|
||||
let points = qdrant
|
||||
.scroll_all_points("_faces", face_filter, 1000)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let face_total = points.len() as i64;
|
||||
let mut trace_ids: std::collections::HashSet<i64> = std::collections::HashSet::new();
|
||||
@@ -564,7 +567,11 @@ async fn get_ingestion_status(
|
||||
// Get related identities from Qdrant _faces
|
||||
let related_identity_ids: Vec<i64> = identity_ids.into_iter().collect();
|
||||
let related_identities: Vec<IdentityRef> = if !related_identity_ids.is_empty() {
|
||||
let id_list: String = related_identity_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(",");
|
||||
let id_list: String = related_identity_ids
|
||||
.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
match sqlx::query_as::<_, (String, String)>(&format!(
|
||||
"SELECT DISTINCT uuid::text, name FROM {identities} \
|
||||
WHERE id IN ({id_list}) ORDER BY name"
|
||||
@@ -592,21 +599,25 @@ async fn get_ingestion_status(
|
||||
|
||||
// Check if job is completed - if so, all ingestion steps are considered done
|
||||
let mj_table = schema::table_name("monitor_jobs");
|
||||
let job_completed: bool = sqlx::query_scalar::<_, String>(&format!(
|
||||
"SELECT status FROM {mj_table} WHERE uuid = $1"
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
.map(|s| s == "completed")
|
||||
.unwrap_or(false);
|
||||
let job_completed: bool =
|
||||
sqlx::query_scalar::<_, String>(&format!("SELECT status FROM {mj_table} WHERE uuid = $1"))
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.unwrap_or(None)
|
||||
.map(|s| s == "completed")
|
||||
.unwrap_or(false);
|
||||
|
||||
macro_rules! step {
|
||||
($name:expr, $done:expr, $detail:expr) => {
|
||||
IngestionStep {
|
||||
name: $name.into(),
|
||||
status: if $done || job_completed { "done" } else { "pending" }.into(),
|
||||
status: if $done || job_completed {
|
||||
"done"
|
||||
} else {
|
||||
"pending"
|
||||
}
|
||||
.into(),
|
||||
detail: $detail,
|
||||
}
|
||||
};
|
||||
@@ -639,24 +650,92 @@ async fn get_ingestion_status(
|
||||
Some(format!("{trace_count} traces / {face_total} detections"))
|
||||
),
|
||||
// TKG Nodes
|
||||
step!("tkg_face_track", face_track_nodes > 0, Some(format!("{face_track_nodes} nodes"))),
|
||||
step!("tkg_gaze_track", gaze_track_nodes > 0, Some(format!("{gaze_track_nodes} nodes"))),
|
||||
step!("tkg_lip_track", lip_track_nodes > 0, Some(format!("{lip_track_nodes} nodes"))),
|
||||
step!("tkg_text_region", text_region_nodes > 0, Some(format!("{text_region_nodes} nodes"))),
|
||||
step!("tkg_appearance", appearance_nodes > 0, Some(format!("{appearance_nodes} nodes"))),
|
||||
step!("tkg_accessory", accessory_nodes > 0, Some(format!("{accessory_nodes} nodes"))),
|
||||
step!("tkg_object", object_nodes > 0, Some(format!("{object_nodes} nodes"))),
|
||||
step!("tkg_hand", hand_nodes > 0, Some(format!("{hand_nodes} nodes"))),
|
||||
step!("tkg_speaker", speaker_nodes > 0, Some(format!("{speaker_nodes} nodes"))),
|
||||
step!(
|
||||
"tkg_face_track",
|
||||
face_track_nodes > 0,
|
||||
Some(format!("{face_track_nodes} nodes"))
|
||||
),
|
||||
step!(
|
||||
"tkg_gaze_track",
|
||||
gaze_track_nodes > 0,
|
||||
Some(format!("{gaze_track_nodes} nodes"))
|
||||
),
|
||||
step!(
|
||||
"tkg_lip_track",
|
||||
lip_track_nodes > 0,
|
||||
Some(format!("{lip_track_nodes} nodes"))
|
||||
),
|
||||
step!(
|
||||
"tkg_text_region",
|
||||
text_region_nodes > 0,
|
||||
Some(format!("{text_region_nodes} nodes"))
|
||||
),
|
||||
step!(
|
||||
"tkg_appearance",
|
||||
appearance_nodes > 0,
|
||||
Some(format!("{appearance_nodes} nodes"))
|
||||
),
|
||||
step!(
|
||||
"tkg_accessory",
|
||||
accessory_nodes > 0,
|
||||
Some(format!("{accessory_nodes} nodes"))
|
||||
),
|
||||
step!(
|
||||
"tkg_object",
|
||||
object_nodes > 0,
|
||||
Some(format!("{object_nodes} nodes"))
|
||||
),
|
||||
step!(
|
||||
"tkg_hand",
|
||||
hand_nodes > 0,
|
||||
Some(format!("{hand_nodes} nodes"))
|
||||
),
|
||||
step!(
|
||||
"tkg_speaker",
|
||||
speaker_nodes > 0,
|
||||
Some(format!("{speaker_nodes} nodes"))
|
||||
),
|
||||
// TKG Edges
|
||||
step!("tkg_co_occurrence", co_occurrence_edges > 0, Some(format!("{co_occurrence_edges} edges"))),
|
||||
step!("tkg_speaker_face", speaker_face_edges > 0, Some(format!("{speaker_face_edges} edges"))),
|
||||
step!("tkg_face_face", face_face_edges > 0, Some(format!("{face_face_edges} edges"))),
|
||||
step!("tkg_mutual_gaze", mutual_gaze_edges > 0, Some(format!("{mutual_gaze_edges} edges"))),
|
||||
step!("tkg_lip_sync", lip_sync_edges > 0, Some(format!("{lip_sync_edges} edges"))),
|
||||
step!("tkg_has_appearance", has_appearance_edges > 0, Some(format!("{has_appearance_edges} edges"))),
|
||||
step!("tkg_wears", wears_edges > 0, Some(format!("{wears_edges} edges"))),
|
||||
step!("tkg_hand_object", hand_object_edges > 0, Some(format!("{hand_object_edges} edges"))),
|
||||
step!(
|
||||
"tkg_co_occurrence",
|
||||
co_occurrence_edges > 0,
|
||||
Some(format!("{co_occurrence_edges} edges"))
|
||||
),
|
||||
step!(
|
||||
"tkg_speaker_face",
|
||||
speaker_face_edges > 0,
|
||||
Some(format!("{speaker_face_edges} edges"))
|
||||
),
|
||||
step!(
|
||||
"tkg_face_face",
|
||||
face_face_edges > 0,
|
||||
Some(format!("{face_face_edges} edges"))
|
||||
),
|
||||
step!(
|
||||
"tkg_mutual_gaze",
|
||||
mutual_gaze_edges > 0,
|
||||
Some(format!("{mutual_gaze_edges} edges"))
|
||||
),
|
||||
step!(
|
||||
"tkg_lip_sync",
|
||||
lip_sync_edges > 0,
|
||||
Some(format!("{lip_sync_edges} edges"))
|
||||
),
|
||||
step!(
|
||||
"tkg_has_appearance",
|
||||
has_appearance_edges > 0,
|
||||
Some(format!("{has_appearance_edges} edges"))
|
||||
),
|
||||
step!(
|
||||
"tkg_wears",
|
||||
wears_edges > 0,
|
||||
Some(format!("{wears_edges} edges"))
|
||||
),
|
||||
step!(
|
||||
"tkg_hand_object",
|
||||
hand_object_edges > 0,
|
||||
Some(format!("{hand_object_edges} edges"))
|
||||
),
|
||||
// Rule 2
|
||||
step!(
|
||||
"rule2_relationship",
|
||||
@@ -703,34 +782,45 @@ async fn get_file_stats(
|
||||
.unwrap_or_else(|| (file_uuid.clone(), None, None));
|
||||
|
||||
// 2. Get processor status from processing_status JSONB
|
||||
let processing_status: serde_json::Value =
|
||||
sqlx::query_scalar(&format!(
|
||||
"SELECT processing_status FROM {} WHERE file_uuid = $1",
|
||||
videos_table
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.unwrap_or(serde_json::json!({}));
|
||||
let processing_status: serde_json::Value = sqlx::query_scalar(&format!(
|
||||
"SELECT processing_status FROM {} WHERE file_uuid = $1",
|
||||
videos_table
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||
.unwrap_or(serde_json::json!({}));
|
||||
|
||||
let processors: Vec<ProcessorStatus> = processing_status
|
||||
let progress_map = processing_status
|
||||
.get("progress")
|
||||
.and_then(|p| p.as_object())
|
||||
.map(|progress| {
|
||||
progress
|
||||
.iter()
|
||||
.filter_map(|(name, info)| {
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
|
||||
let active_order: Vec<String> = processing_status
|
||||
.get("active_processors")
|
||||
.and_then(|a| a.as_array())
|
||||
.map(|arr| {
|
||||
arr.iter()
|
||||
.filter_map(|v| v.as_str().map(|s| s.to_uppercase()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let processors: Vec<ProcessorStatus> = if !active_order.is_empty() {
|
||||
active_order
|
||||
.iter()
|
||||
.filter_map(|name| {
|
||||
progress_map.get(name).and_then(|info| {
|
||||
info.as_object().map(|obj| {
|
||||
let status = obj
|
||||
.get("status")
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("pending")
|
||||
.to_string();
|
||||
let progress_val = obj
|
||||
.get("percentage")
|
||||
.and_then(|p| p.as_u64())
|
||||
.unwrap_or(0) as u32;
|
||||
let progress_val =
|
||||
obj.get("percentage").and_then(|p| p.as_u64()).unwrap_or(0) as u32;
|
||||
let message = obj
|
||||
.get("message")
|
||||
.and_then(|m| m.as_str())
|
||||
@@ -743,9 +833,34 @@ async fn get_file_stats(
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
progress_map
|
||||
.iter()
|
||||
.filter_map(|(name, info)| {
|
||||
info.as_object().map(|obj| {
|
||||
let status = obj
|
||||
.get("status")
|
||||
.and_then(|s| s.as_str())
|
||||
.unwrap_or("pending")
|
||||
.to_string();
|
||||
let progress_val =
|
||||
obj.get("percentage").and_then(|p| p.as_u64()).unwrap_or(0) as u32;
|
||||
let message = obj
|
||||
.get("message")
|
||||
.and_then(|m| m.as_str())
|
||||
.map(|s| s.to_string());
|
||||
ProcessorStatus {
|
||||
name: name.clone(),
|
||||
status,
|
||||
progress: progress_val,
|
||||
message,
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
// 3. Get PostgreSQL counts
|
||||
let chunk_table = schema::table_name("chunk");
|
||||
@@ -853,10 +968,22 @@ async fn get_file_stats(
|
||||
let tkg_nodes_table = schema::table_name("tkg_nodes");
|
||||
let tkg_edges_table = schema::table_name("tkg_edges");
|
||||
|
||||
let tkg_nodes_total: i64 = sqlx::query_scalar::<_, i64>(&format!("SELECT COUNT(*) FROM {} WHERE file_uuid = $1", tkg_nodes_table))
|
||||
.bind(&file_uuid).fetch_one(pool).await.unwrap_or(0);
|
||||
let tkg_edges_total: i64 = sqlx::query_scalar::<_, i64>(&format!("SELECT COUNT(*) FROM {} WHERE file_uuid = $1", tkg_edges_table))
|
||||
.bind(&file_uuid).fetch_one(pool).await.unwrap_or(0);
|
||||
let tkg_nodes_total: i64 = sqlx::query_scalar::<_, i64>(&format!(
|
||||
"SELECT COUNT(*) FROM {} WHERE file_uuid = $1",
|
||||
tkg_nodes_table
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
let tkg_edges_total: i64 = sqlx::query_scalar::<_, i64>(&format!(
|
||||
"SELECT COUNT(*) FROM {} WHERE file_uuid = $1",
|
||||
tkg_edges_table
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let tkg = TkgFileStats {
|
||||
total_nodes: tkg_nodes_total,
|
||||
@@ -870,12 +997,14 @@ async fn get_file_stats(
|
||||
object_nodes: count_nodes(pool, &tkg_nodes_table, &file_uuid, "yolo_object").await,
|
||||
hand_nodes: count_nodes(pool, &tkg_nodes_table, &file_uuid, "hand").await,
|
||||
speaker_nodes: count_nodes(pool, &tkg_nodes_table, &file_uuid, "speaker").await,
|
||||
co_occurrence_edges: count_edges(pool, &tkg_edges_table, &file_uuid, "CO_OCCURS_WITH").await,
|
||||
co_occurrence_edges: count_edges(pool, &tkg_edges_table, &file_uuid, "CO_OCCURS_WITH")
|
||||
.await,
|
||||
speaker_face_edges: count_edges(pool, &tkg_edges_table, &file_uuid, "SPEAKS_AS").await,
|
||||
face_face_edges: count_edges(pool, &tkg_edges_table, &file_uuid, "FACE_TO_FACE").await,
|
||||
mutual_gaze_edges: count_edges(pool, &tkg_edges_table, &file_uuid, "MUTUAL_GAZE").await,
|
||||
lip_sync_edges: count_edges(pool, &tkg_edges_table, &file_uuid, "LIP_SYNC").await,
|
||||
has_appearance_edges: count_edges(pool, &tkg_edges_table, &file_uuid, "HAS_APPEARANCE").await,
|
||||
has_appearance_edges: count_edges(pool, &tkg_edges_table, &file_uuid, "HAS_APPEARANCE")
|
||||
.await,
|
||||
wears_edges: count_edges(pool, &tkg_edges_table, &file_uuid, "WEARS").await,
|
||||
hand_object_edges: count_edges(pool, &tkg_edges_table, &file_uuid, "HAND_OBJECT").await,
|
||||
..Default::default()
|
||||
@@ -895,7 +1024,8 @@ async fn get_file_stats(
|
||||
let identity_agent = IdentityAgentStats {
|
||||
clusters: 0, // From face_clustered.json if available
|
||||
identities_created: face_identities.len() as i64,
|
||||
tmdb_matches: seed_points.iter()
|
||||
tmdb_matches: seed_points
|
||||
.iter()
|
||||
.filter(|p| p["payload"]["source"].as_str() == Some("tmdb"))
|
||||
.count() as i64,
|
||||
speaker_bindings: speaker_points.len() as i64,
|
||||
@@ -946,10 +1076,7 @@ pub fn scan_routes() -> Router<AppState> {
|
||||
"/api/v1/stats/ingestion-status/:file_uuid",
|
||||
get(get_ingestion_status),
|
||||
)
|
||||
.route(
|
||||
"/api/v1/stats/file/:file_uuid",
|
||||
get(get_file_stats),
|
||||
)
|
||||
.route("/api/v1/stats/file/:file_uuid", get(get_file_stats))
|
||||
.route(
|
||||
"/api/v1/stats/pipeline/:file_uuid",
|
||||
get(get_pipeline_progress_handler),
|
||||
@@ -969,9 +1096,13 @@ async fn get_pipeline_progress_handler(
|
||||
let mj_table = schema::table_name("monitor_jobs");
|
||||
|
||||
// Compute actual progress from DB state
|
||||
let sentence_count: i64 = sqlx::query_scalar::<_, i64>(
|
||||
&format!("SELECT COUNT(*) FROM {chunk_table} WHERE file_uuid = $1 AND chunk_type = 'sentence'")
|
||||
).bind(&file_uuid).fetch_one(pool).await.unwrap_or(0);
|
||||
let sentence_count: i64 = sqlx::query_scalar::<_, i64>(&format!(
|
||||
"SELECT COUNT(*) FROM {chunk_table} WHERE file_uuid = $1 AND chunk_type = 'sentence'"
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let sentence_embedded: i64 = sqlx::query_scalar::<_, i64>(
|
||||
&format!("SELECT COUNT(*) FROM {chunk_table} WHERE file_uuid = $1 AND chunk_type = 'sentence' AND embedding IS NOT NULL")
|
||||
@@ -981,17 +1112,29 @@ async fn get_pipeline_progress_handler(
|
||||
&format!("SELECT COUNT(*) FROM {pr_table} pr JOIN {mj_table} mj ON pr.job_id = mj.id WHERE mj.uuid = $1 AND pr.processor = 'face' AND pr.status = 'completed'")
|
||||
).bind(&file_uuid).fetch_one(pool).await.unwrap_or(0);
|
||||
|
||||
let tkg_node_count: i64 = sqlx::query_scalar::<_, i64>(
|
||||
&format!("SELECT COUNT(*) FROM {tkg_nodes_table} WHERE file_uuid = $1")
|
||||
).bind(&file_uuid).fetch_one(pool).await.unwrap_or(0);
|
||||
let tkg_node_count: i64 = sqlx::query_scalar::<_, i64>(&format!(
|
||||
"SELECT COUNT(*) FROM {tkg_nodes_table} WHERE file_uuid = $1"
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let tkg_edge_count: i64 = sqlx::query_scalar::<_, i64>(
|
||||
&format!("SELECT COUNT(*) FROM {tkg_edges_table} WHERE file_uuid = $1")
|
||||
).bind(&file_uuid).fetch_one(pool).await.unwrap_or(0);
|
||||
let tkg_edge_count: i64 = sqlx::query_scalar::<_, i64>(&format!(
|
||||
"SELECT COUNT(*) FROM {tkg_edges_table} WHERE file_uuid = $1"
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let relationship_count: i64 = sqlx::query_scalar::<_, i64>(
|
||||
&format!("SELECT COUNT(*) FROM {chunk_table} WHERE file_uuid = $1 AND chunk_type = 'relationship'")
|
||||
).bind(&file_uuid).fetch_one(pool).await.unwrap_or(0);
|
||||
let relationship_count: i64 = sqlx::query_scalar::<_, i64>(&format!(
|
||||
"SELECT COUNT(*) FROM {chunk_table} WHERE file_uuid = $1 AND chunk_type = 'relationship'"
|
||||
))
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
||||
let asrx_completed: i64 = sqlx::query_scalar::<_, i64>(
|
||||
&format!("SELECT COUNT(*) FROM {pr_table} pr JOIN {mj_table} mj ON pr.job_id = mj.id WHERE mj.uuid = $1 AND pr.processor = 'asrx' AND pr.status = 'completed'")
|
||||
@@ -1007,7 +1150,10 @@ async fn get_pipeline_progress_handler(
|
||||
}
|
||||
if sentence_count > 0 {
|
||||
let detail = if sentence_embedded > 0 {
|
||||
Some(format!("{} chunks, {} embedded", sentence_count, sentence_embedded))
|
||||
Some(format!(
|
||||
"{} chunks, {} embedded",
|
||||
sentence_count, sentence_embedded
|
||||
))
|
||||
} else {
|
||||
Some(format!("{} chunks", sentence_count))
|
||||
};
|
||||
@@ -1017,13 +1163,28 @@ async fn get_pipeline_progress_handler(
|
||||
pp.update_stage("face_tracing", 1.0, "completed", None);
|
||||
}
|
||||
if tkg_node_count > 0 {
|
||||
pp.update_stage("tkg_nodes", 1.0, "completed", Some(format!("{} nodes", tkg_node_count)));
|
||||
pp.update_stage(
|
||||
"tkg_nodes",
|
||||
1.0,
|
||||
"completed",
|
||||
Some(format!("{} nodes", tkg_node_count)),
|
||||
);
|
||||
}
|
||||
if tkg_edge_count > 0 {
|
||||
pp.update_stage("tkg_edges", 1.0, "completed", Some(format!("{} edges", tkg_edge_count)));
|
||||
pp.update_stage(
|
||||
"tkg_edges",
|
||||
1.0,
|
||||
"completed",
|
||||
Some(format!("{} edges", tkg_edge_count)),
|
||||
);
|
||||
}
|
||||
if relationship_count > 0 {
|
||||
pp.update_stage("rule2_ingestion", 1.0, "completed", Some(format!("{} chunks", relationship_count)));
|
||||
pp.update_stage(
|
||||
"rule2_ingestion",
|
||||
1.0,
|
||||
"completed",
|
||||
Some(format!("{} chunks", relationship_count)),
|
||||
);
|
||||
}
|
||||
|
||||
// Check identity agent from _seeds
|
||||
@@ -1031,11 +1192,23 @@ async fn get_pipeline_progress_handler(
|
||||
use serde_json::json;
|
||||
let qdrant = QdrantDb::new();
|
||||
let schema = std::env::var("DATABASE_SCHEMA").unwrap_or_else(|_| "dev".to_string());
|
||||
let seeds_collection = if schema == "public" { "momentry_public_speaker" } else { &format!("momentry_{}_speaker", schema) };
|
||||
let seeds_collection = if schema == "public" {
|
||||
"momentry_public_speaker"
|
||||
} else {
|
||||
&format!("momentry_{}_speaker", schema)
|
||||
};
|
||||
let seeds_filter = json!({"must": [{"key": "file_uuid", "match": {"value": &file_uuid}}]});
|
||||
let seed_points = qdrant.scroll_all_points("_seeds", seeds_filter, 100).await.unwrap_or_default();
|
||||
let seed_points = qdrant
|
||||
.scroll_all_points("_seeds", seeds_filter, 100)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
if !seed_points.is_empty() {
|
||||
pp.update_stage("identity_agent", 1.0, "completed", Some(format!("{} seeds", seed_points.len())));
|
||||
pp.update_stage(
|
||||
"identity_agent",
|
||||
1.0,
|
||||
"completed",
|
||||
Some(format!("{} seeds", seed_points.len())),
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Json(pp))
|
||||
|
||||
+4
-2
@@ -386,9 +386,11 @@ pub async fn smart_search(
|
||||
// 取更多結果以便過濾
|
||||
// Use no_embedding version for keyword results, regular for semantic
|
||||
let pg_opt = if mr.keyword_score.is_some() && mr.semantic_score.is_none() {
|
||||
db.get_chunk_by_id_no_embedding(&mr.file_uuid, &mr.chunk_id).await
|
||||
db.get_chunk_by_id_no_embedding(&mr.file_uuid, &mr.chunk_id)
|
||||
.await
|
||||
} else {
|
||||
db.get_chunk_by_file_and_chunk_id(&mr.file_uuid, &mr.chunk_id).await
|
||||
db.get_chunk_by_file_and_chunk_id(&mr.file_uuid, &mr.chunk_id)
|
||||
.await
|
||||
};
|
||||
if let Some(pg) = pg_opt.ok().flatten() {
|
||||
// 關鍵字結果跳過 text_match 過濾(search_bm25 已經匹配過)
|
||||
|
||||
Reference in New Issue
Block a user