diff --git a/docs_v1.0/API_WORKSPACE/modules/15_tkg.md b/docs_v1.0/API_WORKSPACE/modules/15_tkg.md index c5ee228..ca22254 100644 --- a/docs_v1.0/API_WORKSPACE/modules/15_tkg.md +++ b/docs_v1.0/API_WORKSPACE/modules/15_tkg.md @@ -19,7 +19,6 @@ TKG is a time-aligned knowledge graph built from multi-processor outputs (face, | `lip_track` | `lip_track_{id}` | Lip movement synced with speech | `speaker_id`, `lip_area_range` | | `text_region` | `text_region_{id}` | Spoken text aligned to time | `speaker_id`, `text`, `start_time`, `end_time` | | `appearance_trace` | `appearance_{trace_id}` | Human appearance (clothing) over time | `clothing_color`, `upper_cloth`, `lower_cloth` | -| `skin_tone_trace` | `skin_tone_{trace_id}` | Fitzpatrick skin tone classification | `fitzpatrick_type` (I–VI) | | `accessory` | `accessory_{id}` | Detected accessories | `type` (glasses/hat/etc.), `confidence` | | `object` | `object_{class}_{id}` | YOLO-detected object | `class`, `confidence`, `frame_count` | | `speaker` | `speaker_{speaker_id}` | ASRX speaker segment | `speaker_id`, `segment_count`, `total_duration` | diff --git a/docs_v1.0/DESIGN/TKG_FORMATION_V1.0.md b/docs_v1.0/DESIGN/TKG_FORMATION_V1.0.md index 9502479..7da5151 100644 --- a/docs_v1.0/DESIGN/TKG_FORMATION_V1.0.md +++ b/docs_v1.0/DESIGN/TKG_FORMATION_V1.0.md @@ -72,7 +72,6 @@ Temporal Knowledge Graph (TKG) is built from multi-processor outputs to create a | `build_yolo_object_nodes` | `object` | `yolo.json` | | `build_hand_nodes` | `hand` | `pose.json` | | `build_speaker_nodes` | `speaker` | `asrx.json` | -| `build_skin_tone_trace_nodes` | `skin_tone_trace` | **TODO** | --- @@ -122,7 +121,6 @@ Temporal Knowledge Graph (TKG) is built from multi-processor outputs to create a | `lip_track` | `lip_track_{id}` | `speaker_id`, `lip_area_range` | | `text_region` | `text_region_{id}` | `speaker_id`, `text`, `start_time`, `end_time` | | `appearance_trace` | `appearance_{trace_id}` | `clothing_color`, `upper_cloth`, `lower_cloth` | -| `skin_tone_trace` | `skin_tone_{trace_id}` | `fitzpatrick_type` (I-VI) - **TODO** | | `accessory` | `accessory_{id}` | `type` (glasses/hat/etc.), `confidence` | | `object` | `object_{class}_{id}` | `class`, `confidence`, `frame_count` | | `speaker` | `speaker_{speaker_id}` | `speaker_id`, `segment_count`, `total_duration` | @@ -209,113 +207,6 @@ graph TB --- -## skin_tone_trace Implementation - -### Status: ✅ Implemented (2026-06-25) - -See `src/core/processor/tkg.rs` lines 2579-2627 for implementation. - -### Overview - -Skin tone classification using Fitzpatrick scale from face skin color analysis. - -### Fitzpatrick Classification - -| Type | Skin H Range (HSV) | Description | Example | -|------|-------------------|-------------|---------| -| I | H < 10° | Very fair/pale | Northern European | -| II | 10° ≤ H < 20° | Fair | European | -| III | 20° ≤ H < 30° | Medium | Mediterranean | -| IV | 30° ≤ H < 40° | Olive | Asian, Hispanic | -| V | 40° ≤ H < 50° | Brown | Indian, African | -| VI | H ≥ 50° | Dark brown | African | - -### Implementation - -**Rust Code Location:** `src/core/processor/tkg.rs` - -```rust -// Add to build_tkg() -let n_skin = build_skin_tone_trace_nodes(pool, file_uuid).await?; - -// Add to TkgResult -pub skin_tone_trace_nodes: usize, - -// New builder function -async fn build_skin_tone_trace_nodes( - pool: &PgPool, - file_uuid: &str, -) -> Result { - let fd_table = t("face_detections"); - - // Step 1: Get avg skin H per trace - let rows: Vec<(i64, f64)> = sqlx::query_as(&format!( - "SELECT trace_id, AVG(skin_h) as avg_h - FROM {} - WHERE file_uuid = $1 AND trace_id IS NOT NULL AND skin_h IS NOT NULL - GROUP BY trace_id", - fd_table - )) - .bind(file_uuid) - .fetch_all(pool) - .await?; - - // Step 2: Classify Fitzpatrick - let nodes_table = t("tkg_nodes"); - let mut count = 0; - - for (trace_id, avg_h) in &rows { - let fitz_type = classify_fitzpatrick(*avg_h); - let external_id = format!("skin_tone_{}", trace_id); - let label = format!("Skin Tone Trace {}", trace_id); - - sqlx::query(&format!( - "INSERT INTO {} (node_type, external_id, file_uuid, label, properties) - VALUES ('skin_tone_trace', $1, $2, $3, $4::jsonb) - ON CONFLICT (file_uuid, node_type, external_id) DO UPDATE SET properties = EXCLUDED.properties", - nodes_table - )) - .bind(&external_id) - .bind(file_uuid) - .bind(&label) - .bind(serde_json::json!({ - "trace_id": trace_id, - "avg_skin_h": avg_h, - "fitzpatrick_type": fitz_type, - })) - .execute(pool) - .await?; - - count += 1; - } - - Ok(count) -} - -fn classify_fitzpatrick(h: f64) -> &'static str { - if h < 10.0 { "I" } - else if h < 20.0 { "II" } - else if h < 30.0 { "III" } - else if h < 40.0 { "IV" } - else if h < 50.0 { "V" } - else { "VI" } -} -``` - -### Dependencies - -- `face_detections.trace_id` must be populated (from Phase 0) -- `face.json` used for skin_h estimation (placeholder based on attributes) -- `output_dir` path must be passed to builder - -### Limitations - -- Current skin_h estimation is placeholder (based on face attributes) -- For accurate Fitzpatrick classification, face ROI color extraction needed -- Face.json doesn't store skin_h directly (would need video frame analysis) - ---- - ## SQL Query Examples ### Status Queries diff --git a/src/api/trace_agent_api.rs b/src/api/trace_agent_api.rs index 60ba204..7d328a1 100644 --- a/src/api/trace_agent_api.rs +++ b/src/api/trace_agent_api.rs @@ -1002,7 +1002,6 @@ async fn rebuild_tkg( "lip_track_nodes": r.lip_track_nodes, "text_region_nodes": r.text_region_nodes, "appearance_trace_nodes": r.appearance_trace_nodes, - "skin_tone_trace_nodes": r.skin_tone_trace_nodes, "accessory_nodes": r.accessory_nodes, "object_nodes": r.object_nodes, "hand_nodes": r.hand_nodes, diff --git a/src/core/processor/tkg.rs b/src/core/processor/tkg.rs index ad5a844..c3020c9 100644 --- a/src/core/processor/tkg.rs +++ b/src/core/processor/tkg.rs @@ -465,7 +465,6 @@ pub struct TkgResult { pub lip_track_nodes: usize, pub text_region_nodes: usize, pub appearance_trace_nodes: usize, - pub skin_tone_trace_nodes: usize, pub accessory_nodes: usize, pub object_nodes: usize, pub hand_nodes: usize, @@ -511,7 +510,6 @@ pub async fn build_tkg(db: &PostgresDb, file_uuid: &str, output_dir: &str) -> Re let n_text = build_text_region_nodes(pool, file_uuid).await?; let n_appearance = build_appearance_trace_nodes(pool, file_uuid, output_dir, &pose_data).await?; - let n_skin_tone = build_skin_tone_trace_nodes(pool, file_uuid, output_dir).await?; let n_accessories = build_accessory_nodes(pool, file_uuid, output_dir).await?; let n_objects = build_yolo_object_nodes(pool, file_uuid, output_dir).await?; let n_hands = build_hand_nodes(pool, file_uuid, output_dir).await?; @@ -532,7 +530,6 @@ pub async fn build_tkg(db: &PostgresDb, file_uuid: &str, output_dir: &str) -> Re lip_track_nodes: n_lip, text_region_nodes: n_text, appearance_trace_nodes: n_appearance, - skin_tone_trace_nodes: n_skin_tone, accessory_nodes: n_accessories, object_nodes: n_objects, hand_nodes: n_hands, @@ -2578,94 +2575,6 @@ fn classify_fitzpatrick(h_mean: f64) -> &'static str { } } -async fn build_skin_tone_trace_nodes( - pool: &PgPool, - file_uuid: &str, - output_dir: &str, -) -> Result { - let nodes_table = t("tkg_nodes"); - let fd_table = t("face_detections"); - let mut count = 0; - - let rows: Vec<(i64, i64)> = sqlx::query_as(&format!( - "SELECT trace_id::bigint, COUNT(*)::bigint \ - FROM {} \ - WHERE file_uuid = $1 AND trace_id IS NOT NULL \ - GROUP BY trace_id", - fd_table - )) - .bind(file_uuid) - .fetch_all(pool) - .await?; - - if rows.is_empty() { - tracing::info!("[TKG] No traced faces for skin_tone_trace nodes"); - return Ok(0); - } - - let path = Path::new(output_dir).join(format!("{}.face.json", file_uuid)); - let face_json = if path.exists() { - let content = std::fs::read_to_string(&path) - .with_context(|| format!("Failed to read face.json: {}", path.display()))?; - serde_json::from_str::(&content)? - } else { - tracing::warn!("[TKG] No face.json for skin_tone computation"); - serde_json::Value::Null - }; - - for (trace_id, frame_count) in &rows { - let avg_skin_h = if !face_json.is_null() { - let mut skin_h_values = Vec::new(); - if let Some(frames) = face_json.get("frames").and_then(|v| v.as_array()) { - for frame_entry in frames { - if let Some(faces) = frame_entry.get("faces").and_then(|v| v.as_array()) { - for face in faces { - let face_trace_id = face.get("trace_id").and_then(|v| v.as_i64()); - if face_trace_id == Some(*trace_id) { - skin_h_values.push(compute_skin_h_from_face(face)); - } - } - } - } - } - if skin_h_values.is_empty() { - 20.0 - } else { - skin_h_values.iter().sum::() / skin_h_values.len() as f64 - } - } else { - 20.0 - }; - - let fitz_type = classify_fitzpatrick(avg_skin_h); - let external_id = format!("skin_tone_{}", trace_id); - let label = format!("Skin Tone Trace {}", trace_id); - - sqlx::query(&format!( - "INSERT INTO {} (node_type, external_id, file_uuid, label, properties) \ - VALUES ('skin_tone_trace', $1, $2, $3, $4::jsonb) \ - ON CONFLICT (file_uuid, node_type, external_id) DO UPDATE SET properties = EXCLUDED.properties", - nodes_table - )) - .bind(&external_id) - .bind(file_uuid) - .bind(&label) - .bind(serde_json::json!({ - "trace_id": trace_id, - "avg_skin_h": avg_skin_h, - "fitzpatrick_type": fitz_type, - "frame_count": frame_count, - })) - .execute(pool) - .await?; - - count += 1; - } - - tracing::info!("[TKG] Built {} skin_tone_trace nodes", count); - Ok(count) -} - // ── Accessory Nodes ────────────────────────────────────────────── async fn build_accessory_nodes(pool: &PgPool, file_uuid: &str, output_dir: &str) -> Result { @@ -3219,7 +3128,6 @@ let r = TkgResult { lip_track_nodes: 4, text_region_nodes: 20, appearance_trace_nodes: 3, - skin_tone_trace_nodes: 5, accessory_nodes: 0, object_nodes: 10, hand_nodes: 0,