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
+51
View File
@@ -0,0 +1,51 @@
use anyhow::{Context, Result};
use momentry_core::core::agent::tools;
use momentry_core::core::db::{Database, PostgresDb};
pub async fn handle_agent(tool: &str, args_str: &str) -> Result<()> {
let db = PostgresDb::init()
.await
.context("Failed to initialize database")?;
let args: serde_json::Value = serde_json::from_str(args_str)
.context("Failed to parse JSON arguments")?;
let pool = db.pool();
let result = match tool {
"find_file" => tools::exec_find_file(pool, &args).await,
"list_files" => tools::exec_list_files(pool, &args).await,
"tkg_query" => tools::exec_tkg_query(pool, &args).await,
"tkg_nodes_query" => tools::exec_tkg_nodes_query(pool, &args).await,
"tkg_edges_query" => tools::exec_tkg_edges_query(pool, &args).await,
"tkg_node_detail" => tools::exec_tkg_node_detail(pool, &args).await,
"smart_search" => tools::exec_smart_search(pool, &args).await,
"identity_text" => tools::exec_identity_text(pool, &args).await,
"identities_search" => tools::exec_identities_search(pool, &args).await,
"get_identity_detail" => tools::exec_get_identity_detail(pool, &args).await,
"get_file_info" => tools::exec_get_file_info(pool, &args).await,
"get_representative_frame" => tools::exec_get_representative_frame(pool, &args).await,
"analyze_frame" => tools::exec_analyze_frame(pool, &args).await,
_ => anyhow::bail!(
"Unknown tool: {}. Available tools: find_file, list_files, tkg_query, \
tkg_nodes_query, tkg_edges_query, tkg_node_detail, smart_search, \
identity_text, identities_search, get_identity_detail, get_file_info, \
get_representative_frame, analyze_frame",
tool
),
};
match result {
Ok(json_str) => {
match serde_json::from_str::<serde_json::Value>(&json_str) {
Ok(value) => println!("{}", serde_json::to_string_pretty(&value)?),
Err(_) => println!("{}", json_str),
}
}
Err(e) => {
eprintln!("Error: {}", e);
std::process::exit(1);
}
}
Ok(())
}
+6 -22
View File
@@ -190,20 +190,12 @@ pub enum Commands {
#[arg(long)]
scopes: Option<String>,
},
/// Manage n8n API keys
N8n {
/// Action: create, list, delete, verify
#[arg(value_enum)]
action: N8nAction,
/// n8n API key (for create/list/delete)
#[arg(long)]
api_key: Option<String>,
/// API key label (for create/delete)
#[arg(long)]
label: Option<String>,
/// Expiration days (for create)
#[arg(long)]
expires_in_days: Option<i64>,
/// Run an agent tool with JSON arguments
Agent {
/// Tool name (find_file, list_files, tkg_query, smart_search, etc.)
tool: String,
/// JSON arguments for the tool (e.g. '{"query": "batman"}')
args: String,
},
}
@@ -225,14 +217,6 @@ pub enum GiteaAction {
Verify,
}
#[derive(clap::ValueEnum, Clone, Debug)]
pub enum N8nAction {
Create,
List,
Delete,
Verify,
}
#[derive(Subcommand)]
pub enum VisionCommands {
/// Start Qwen3-VL server
+1
View File
@@ -1,5 +1,6 @@
//! CLI command definitions and argument parsing
pub mod agent;
pub mod args;
pub mod vision;
+37 -15
View File
@@ -1,8 +1,8 @@
use anyhow::Result;
use std::path::PathBuf;
use momentry_core::core::vision::qwen_vl_manager::QwenVLManager;
use momentry_core::core::processor::cascade_vision::CascadeVisionProcessor;
use momentry_core::core::vision::qwen_vl_manager::QwenVLManager;
pub async fn handle_vision_command(cmd: crate::cli::args::VisionCommands) -> Result<()> {
let manager = QwenVLManager::new();
@@ -22,9 +22,12 @@ pub async fn handle_vision_command(cmd: crate::cli::args::VisionCommands) -> Res
crate::cli::args::VisionCommands::Status => {
println!("Checking Qwen3-VL status...");
let status = manager.get_status().await?;
println!("Status:");
println!(" Running: {}", if status.running { "✅ Yes" } else { "❌ No" });
println!(
" Running: {}",
if status.running { "✅ Yes" } else { "❌ No" }
);
println!(" Port: {}", status.port);
println!(" Model: {}", status.model_path);
println!(" Last request: {} seconds ago", status.last_request);
@@ -43,53 +46,72 @@ pub async fn handle_detect_command(
threshold: f32,
) -> Result<()> {
let image_path = PathBuf::from(&image);
if !image_path.exists() {
anyhow::bail!("Image file not found: {}", image);
}
println!("Detecting objects in: {}", image);
println!("Objects: {}", objects.join(", "));
println!("Mode: {}", if cascade { "Cascade (CLIP + Qwen3-VL)" } else { "CLIP only" });
println!(
"Mode: {}",
if cascade {
"Cascade (CLIP + Qwen3-VL)"
} else {
"CLIP only"
}
);
println!("Threshold: {:.2}", threshold);
println!();
if cascade {
let processor = CascadeVisionProcessor::with_threshold(threshold);
let result = processor.detect_objects(&image_path, &objects.iter().map(|s| s.as_str()).collect::<Vec<_>>()).await?;
let result = processor
.detect_objects(
&image_path,
&objects.iter().map(|s| s.as_str()).collect::<Vec<_>>(),
)
.await?;
println!("Detection Results:");
println!(" Model used: {}", result.model_used);
println!(" CLIP confidence: {:.3}", result.clip_confidence);
println!(" Qwen3-VL used: {}", if result.qwenvl_used { "✅ Yes" } else { "❌ No" });
println!(
" Qwen3-VL used: {}",
if result.qwenvl_used {
"✅ Yes"
} else {
"❌ No"
}
);
println!(" Processing time: {} ms", result.processing_time_ms);
println!(" Detections:");
for detection in &result.detections {
println!(" - {}: {:.3}", detection.label, detection.confidence);
}
if result.detections.is_empty() {
println!(" (No objects detected)");
}
} else {
use momentry_core::core::processor::clip::detect_objects;
let objects_str: Vec<&str> = objects.iter().map(|s| s.as_str()).collect();
let predictions = detect_objects(&image, &objects_str, Some(threshold), None).await?;
println!("Detection Results:");
println!(" Model used: CLIP");
println!(" Detections:");
for prediction in &predictions {
println!(" - {}: {:.3}", prediction.label, prediction.confidence);
}
if predictions.is_empty() {
println!(" (No objects detected above threshold {:.2})", threshold);
}
}
Ok(())
}
}