feat: implement skin_tone_trace node builder and standardize TKG node naming

- Add build_skin_tone_trace_nodes() to tkg.rs (Fitzpatrick I-VI classification)
- Add skin_tone_trace_nodes field to TkgResult
- Standardize node naming: _trace -> _track (text uses _region)
- Add external_id format column to Node Types table
- Add storage names to Edge Types table
- Create TKG_FORMATION_V1.0.md with Phase 0-4 definition, flow diagram, queries
- Add cross-reference from identity_agent_v4.0.md to TKG Formation
- Update Python scripts to executable mode
This commit is contained in:
Accusys
2026-06-25 03:09:16 +08:00
parent 406b2d5524
commit 4273576612
8 changed files with 680 additions and 72 deletions

View File

@@ -459,12 +459,13 @@ struct FaceDetectionRow {
// ── Public API ────────────────────────────────────────────────────
pub struct TkgResult {
pub face_track_nodes: usize,
pub gaze_track_nodes: usize,
pub lip_track_nodes: usize,
pub text_region_nodes: usize,
pub appearance_trace_nodes: usize,
pub accessory_nodes: usize,
pub face_track_nodes: usize,
pub gaze_track_nodes: usize,
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,
pub speaker_nodes: usize,
@@ -507,9 +508,10 @@ pub async fn build_tkg(db: &PostgresDb, file_uuid: &str, output_dir: &str) -> Re
let n_gaze = build_gaze_track_nodes(pool, file_uuid, &pose_data).await?;
let n_lip = build_lip_track_nodes(pool, file_uuid, output_dir, &pose_data).await?;
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_accessories = build_accessory_nodes(pool, file_uuid, output_dir).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?;
let n_speakers = build_speaker_nodes(pool, file_uuid, output_dir).await?;
@@ -523,14 +525,15 @@ let n_accessories = build_accessory_nodes(pool, file_uuid, output_dir).await?;
let e_w = build_wears_edges(pool, file_uuid).await?;
let e_ho = build_hand_object_edges(pool, file_uuid, output_dir).await?;
Ok(TkgResult {
face_track_nodes: n_face,
gaze_track_nodes: n_gaze,
lip_track_nodes: n_lip,
text_region_nodes: n_text,
appearance_trace_nodes: n_appearance,
accessory_nodes: n_accessories,
object_nodes: n_objects,
Ok(TkgResult {
face_track_nodes: n_face,
gaze_track_nodes: n_gaze,
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,
speaker_nodes: n_speakers,
co_occurrence_edges: e_co,
@@ -2559,21 +2562,109 @@ fn compute_skin_h_from_face(face: &serde_json::Value) -> f64 {
}
fn classify_fitzpatrick(h_mean: f64) -> &'static str {
if h_mean < 5.0 {
"Type I - Very Fair"
} else if h_mean < 12.0 {
"Type II - Fair"
} else if h_mean < 18.0 {
"Type III - Medium-Fair"
} else if h_mean < 25.0 {
"Type IV - Medium"
} else if h_mean < 35.0 {
"Type V - Dark"
if h_mean < 10.0 {
"I"
} else if h_mean < 20.0 {
"II"
} else if h_mean < 30.0 {
"III"
} else if h_mean < 40.0 {
"IV"
} else if h_mean < 50.0 {
"V"
} else {
"Type VI - Very Dark"
"VI"
}
}
async fn build_skin_tone_trace_nodes(
pool: &PgPool,
file_uuid: &str,
output_dir: &str,
) -> Result<usize> {
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, COUNT(*) \
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::<serde_json::Value>(&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::<f64>() / 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<usize> {
@@ -3124,10 +3215,11 @@ mod tests {
let r = TkgResult {
face_track_nodes: 5,
gaze_track_nodes: 5,
lip_track_nodes: 4,
text_region_nodes: 20,
appearance_trace_nodes: 3,
accessory_nodes: 0,
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,
speaker_nodes: 3,