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 8311058 additions and 1394 deletions

View File

@@ -426,6 +426,7 @@ async fn process_yolo_module(
video_path,
yolo_path.to_str().unwrap(),
Some(uuid),
None,
)
.await?;
let yolo_json = serde_json::to_string_pretty(&yolo_result)?;
@@ -460,6 +461,7 @@ async fn process_ocr_module(
video_path,
ocr_path.to_str().unwrap(),
Some(uuid),
None,
)
.await?;
let ocr_json = serde_json::to_string_pretty(&ocr_result)?;
@@ -497,6 +499,7 @@ async fn process_face_module(
video_path,
face_path.to_str().unwrap(),
Some(uuid),
None,
)
.await?;
let face_json = serde_json::to_string_pretty(&face_result)?;
@@ -531,6 +534,7 @@ async fn process_pose_module(
video_path,
pose_path.to_str().unwrap(),
Some(uuid),
None,
)
.await?;
let pose_json = serde_json::to_string_pretty(&pose_result)?;
@@ -551,6 +555,47 @@ async fn process_pose_module(
Ok(())
}
async fn process_appearance_module(
appearance_path: &Path,
video_path: &str,
pose_path: &Path,
uuid: &str,
progress_state: &Arc<Mutex<ProgressState>>,
ui: &Arc<Mutex<Option<ProgressUi>>>,
) -> anyhow::Result<()> {
{
let mut state = progress_state.lock().unwrap();
state.get_processor(ProcessorType::Appearance).start(1);
}
let appearance_result = momentry_core::core::processor::process_appearance(
video_path,
pose_path.to_str().unwrap(),
appearance_path.to_str().unwrap(),
Some(uuid),
None,
)
.await?;
let appearance_json = serde_json::to_string_pretty(&appearance_result)?;
std::fs::write(appearance_path, &appearance_json)?;
let output_dir = OutputDir::new();
let _ = output_dir.backup_file(uuid, "appearance.json");
println!(
" ✓ Appearance saved: {} frames",
appearance_result.frame_count
);
{
let mut state = progress_state.lock().unwrap();
state
.get_processor(ProcessorType::Appearance)
.complete(&format!("{} frames", appearance_result.frame_count));
state.stop();
}
if let Some(ref mut ui) = *ui.lock().unwrap() {
let _ = ui.render();
}
Ok(())
}
async fn process_story_module(
story_path: &Path,
video_path: &str,
@@ -643,7 +688,7 @@ enum Commands {
Process {
/// UUID or path
target: String,
/// Modules to process (comma separated: asr,cut,asrx,yolo,ocr,face,pose,story,caption)
/// Modules to process (comma separated: appearance,asr,cut,asrx,yolo,ocr,face,pose,story,caption)
/// If not specified, processes all modules
#[arg(short, long, value_delimiter = ',')]
modules: Option<Vec<String>>,
@@ -826,9 +871,11 @@ enum N8nAction {
#[tokio::main]
async fn main() -> Result<()> {
// Load development environment — try absolute path first
if dotenv::from_filename("/Users/accusys/momentry_core_0.1/.env.development").is_err() {
// Fallback to relative path (for development)
let _ = dotenv::from_filename(".env.development");
let env_loaded = dotenv::from_filename("/Users/accusys/momentry_core_0.1/.env.development")
.is_ok()
|| dotenv::from_filename(".env.development").is_ok();
if !env_loaded {
eprintln!("[WARN] No .env.development found, using defaults or env vars");
}
tracing_subscriber::fmt::init();
@@ -839,6 +886,10 @@ async fn main() -> Result<()> {
"Redis prefix: {}",
*momentry_core::core::config::REDIS_KEY_PREFIX
);
tracing::info!(
"Database schema: {}",
*momentry_core::core::config::DATABASE_SCHEMA
);
let cli = Cli::parse();
@@ -1011,6 +1062,7 @@ async fn main() -> Result<()> {
.filter_map(|name| {
let name_lower = name.to_lowercase();
match name_lower.as_str() {
"appearance" => Some(ProcessorType::Appearance),
"asr" => Some(ProcessorType::Asr),
"cut" => Some(ProcessorType::Cut),
"asrx" => Some(ProcessorType::Asrx),
@@ -1037,6 +1089,7 @@ async fn main() -> Result<()> {
.filter_map(|name| {
let name_lower = name.to_lowercase();
match name_lower.as_str() {
"appearance" => Some(ProcessorType::Appearance),
"asr" => Some(ProcessorType::Asr),
"cut" => Some(ProcessorType::Cut),
"asrx" => Some(ProcessorType::Asrx),
@@ -1618,6 +1671,69 @@ async fn main() -> Result<()> {
}
}
// Process Appearance (color/histogram analysis, depends on Pose)
if should_process(ProcessorType::Appearance) {
let appearance_path = output_dir.get_output_path(&uuid, "appearance.json");
let pose_path = output_dir.get_output_path(&uuid, "pose.json");
let decision = decide_processing(&appearance_path, force, resume);
match decision {
ProcessingDecision::SkipComplete => {
println!("\nAppearance: ✓ Already complete, skipping");
}
ProcessingDecision::ForceReprocess => {
println!("\nAppearance: ⟳ Force reprocessing from scratch...");
std::fs::remove_file(&appearance_path).ok();
if is_cloud(ProcessorType::Appearance) {
println!(" [Cloud processing not implemented yet - run locally]");
} else {
process_appearance_module(
&appearance_path,
video_path,
&pose_path,
&uuid,
&progress_state,
&ui,
)
.await?;
}
}
ProcessingDecision::ResumePartial => {
println!("\nAppearance: ↻ Resuming from checkpoint...");
if is_cloud(ProcessorType::Appearance) {
println!(" [Cloud processing not implemented yet - run locally]");
} else {
process_appearance_module(
&appearance_path,
video_path,
&pose_path,
&uuid,
&progress_state,
&ui,
)
.await?;
}
}
ProcessingDecision::Process => {
if is_cloud(ProcessorType::Appearance) {
println!("\nAppearance: ☁️ Running via cloud...");
println!(" [Cloud processing not implemented yet - run locally]");
} else {
println!("\nAppearance: ⚙️ Processing...");
process_appearance_module(
&appearance_path,
video_path,
&pose_path,
&uuid,
&progress_state,
&ui,
)
.await?;
}
}
}
}
// Process Story (video narrative)
if should_process(ProcessorType::Story) {
let story_path = output_dir.get_output_path(&uuid, "story.json");
@@ -1770,6 +1886,10 @@ async fn main() -> Result<()> {
let path = output_dir.get_output_path(&uuid, "pose.json");
println!(" - Pose JSON: {}", path.display());
}
if should_process(ProcessorType::Appearance) {
let path = output_dir.get_output_path(&uuid, "appearance.json");
println!(" - Appearance JSON: {}", path.display());
}
if should_process(ProcessorType::Story) {
let path = output_dir.get_output_path(&uuid, "story.json");
println!(" - Story JSON: {}", path.display());