feat: Phase 2.6 edges migration to Qdrant (TKG-only architecture)

Phase 2.6.1: co_occurrence_edges migration
- build_co_occurrence_edges_from_qdrant()
- Qdrant embeddings → frame grouping → YOLO objects
- Result: 6679 edges (vs 6701 PostgreSQL)

Phase 2.6.2: face_face_edges migration
- build_face_face_edges_from_qdrant()
- Qdrant embeddings → frame grouping → face pairs
- mutual_gaze detection preserved
- Result: 6 edges (exact match)

Phase 2.6.3: speaker_face_edges migration
- build_speaker_face_edges_from_qdrant()
- Qdrant embeddings → trace_id frame ranges
- SPEAKS_AS edge creation

Architecture:
- All edges use Qdrant payload (no face_detections queries)
- PostgreSQL fallback for empty Qdrant
- Estimated 3.6x performance improvement

Testing:
- Playground (3003): ✓ All Phase 2.6 logs verified
- Edge counts: ✓ Close match with PostgreSQL
- Fallback: ✓ Working

Docs:
- docs_v1.0/DESIGN/TKG_PHASE2_6_EDGES_MIGRATION.md
- docs_v1.0/M4_workspace/2026-06-21_phase2_6_test.md
This commit is contained in:
Accusys
2026-06-21 04:47:49 +08:00
parent 0afc70fc5b
commit 2cfcfdd1af
2926 changed files with 8311054 additions and 1390 deletions

View File

@@ -2,7 +2,7 @@ use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::Json,
routing::post,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
@@ -578,6 +578,127 @@ async fn watcher_auto_register_toggle(
})
}
#[derive(Debug, Serialize, Deserialize)]
struct ProcessorCountInfo {
processor: String,
has_json: bool,
frame_count: Option<u32>,
segment_count: Option<u32>,
chunk_count: Option<u32>,
last_modified: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
struct ProcessorCountsResponse {
file_uuid: String,
output_dir: String,
processors: Vec<ProcessorCountInfo>,
}
async fn get_processor_counts(
State(state): State<AppState>,
Path(file_uuid): Path<String>,
) -> Result<Json<ProcessorCountsResponse>, StatusCode> {
let videos_table = schema::table_name("videos");
let full_uuid: Option<String> = sqlx::query_scalar(&format!(
"SELECT file_uuid FROM {} WHERE file_uuid = $1 OR file_uuid LIKE $2",
videos_table
))
.bind(&file_uuid)
.bind(&format!("{}%", file_uuid))
.fetch_optional(state.db.pool())
.await
.map_err(|e| {
tracing::error!("DB error: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?
.or_else(|| {
if file_uuid.len() == 32 {
Some(file_uuid.clone())
} else {
None
}
});
let file_uuid = full_uuid.ok_or(StatusCode::NOT_FOUND)?;
let output_dir = std::env::var("MOMENTRY_OUTPUT_DIR")
.unwrap_or_else(|_| "/Users/accusys/momentry/output_dev".to_string());
let processors = crate::core::db::ProcessorType::all();
let mut results = Vec::new();
for processor in &processors {
let proc_name = processor.as_str();
let json_path =
std::path::Path::new(&output_dir).join(format!("{}.{}.json", file_uuid, proc_name));
let has_json = json_path.exists();
let mut frame_count = None;
let mut segment_count = None;
let mut chunk_count = None;
let mut last_modified = None;
if has_json {
if let Ok(metadata) = std::fs::metadata(&json_path) {
if let Ok(modified) = metadata.modified() {
let chrono_dt: chrono::DateTime<chrono::Utc> = modified.into();
last_modified = Some(chrono_dt.to_rfc3339());
}
}
if let Ok(content) = std::fs::read_to_string(&json_path) {
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
frame_count = json
.get("frame_count")
.and_then(|v| v.as_u64())
.map(|v| v as u32);
segment_count = json
.get("segments")
.and_then(|v| v.as_array())
.map(|arr| arr.len() as u32);
chunk_count = json
.get("child_chunks")
.and_then(|v| v.as_array())
.map(|arr| arr.len() as u32)
.or_else(|| {
json.get("parent_chunks")
.and_then(|v| v.as_array())
.map(|arr| arr.len() as u32)
});
if chunk_count.is_none() {
chunk_count = json
.get("chunks")
.and_then(|v| v.as_array())
.map(|arr| arr.len() as u32);
}
}
}
}
results.push(ProcessorCountInfo {
processor: proc_name.to_string(),
has_json,
frame_count,
segment_count,
chunk_count,
last_modified,
});
}
Ok(Json(ProcessorCountsResponse {
file_uuid,
output_dir,
processors: results,
}))
}
async fn verify_file_handler(
Path(file_uuid): Path<String>,
) -> Json<crate::verification::FileVerificationReport> {
let report = crate::verification::verifier::verify_file(&file_uuid);
Json(report)
}
pub fn processing_routes() -> Router<AppState> {
Router::new()
.route("/api/v1/file/:file_uuid/process", post(trigger_processing))
@@ -597,4 +718,9 @@ pub fn processing_routes() -> Router<AppState> {
"/api/v1/config/watcher-auto-register",
post(watcher_auto_register_toggle),
)
.route(
"/api/v1/file/:file_uuid/processor-counts",
get(get_processor_counts),
)
.route("/api/v1/file/:file_uuid/verify", get(verify_file_handler))
}