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

@@ -6,19 +6,23 @@
TKG is a time-aligned knowledge graph built from multi-processor outputs (face, yolo, ocr, pose, asrx, gaze, lip, appearance). It produces 9 node types and 14 edge types stored in `dev.tkg_nodes` and `dev.tkg_edges`.
**Node naming convention:** All trace types use `_track` suffix. Text uses `_region` (non-temporal).
**See also:** `docs_v1.0/DESIGN/TKG_FORMATION_V1.0.md` for formation phases, flow diagrams, and query examples.
### Node Types
| Node Type | Description | Key Properties |
|-----------|-------------|----------------|
| `face_track` | A tracked face identity over time | `trace_id`, `frame_count`, `status`, `pending_identity_name`, `confidence`, `identity_uuid` (see Identity Agent section below) |
| `gaze_track` | Gaze direction over time | `direction` (frontal/left/right/up/down + diagonals) |
| `lip_track` | Lip movement synced with speech | `speaker_id`, `lip_area_range` |
| `text_region` | Spoken text aligned to time | `speaker_id`, `text`, `start_time`, `end_time` |
| `appearance_trace` | Human appearance (clothing) over time | `clothing_color`, `upper_cloth`, `lower_cloth` |
| `skin_tone_trace` | Fitzpatrick skin tone classification | `fitzpatrick_type` (IVI) |
| `accessory` | Detected accessories | `type` (glasses/hat/etc.), `confidence` |
| `object` | YOLO-detected object | `class`, `confidence`, `frame_count` |
| `speaker` | ASRX speaker segment | `speaker_id`, `segment_count`, `total_duration` |
| Node Type | External ID Format | Description | Key Properties |
|-----------|-------------------|-------------|----------------|
| `face_track` | `face_track_{trace_id}` | A tracked face identity over time | `trace_id`, `frame_count`, `status`, `pending_identity_name`, `confidence`, `identity_uuid` |
| `gaze_track` | `gaze_track_{id}` | Gaze direction over time | `direction` (frontal/left/right/up/down + diagonals) |
| `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` (IVI) |
| `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` |
---
@@ -69,15 +73,16 @@ Identity Agent marks face_track nodes with identity binding status.
### Edge Types
| Edge Type | Source → Target | Description |
|-----------|-----------------|-------------|
| `co_occurs` | object ↔ object | Two objects appear together in same frame |
| `speaker_face` | speaker face_trace | Speaker matched to face trace via lip sync |
| `face_face` | face_trace ↔ face_trace | Two face traces interact (mutual gaze) |
| `mutual_gaze` | gaze_trace ↔ gaze_trace | Two people looking at each other |
| `lip_sync` | lip_trace ↔ text_trace | Lip movement aligned with spoken text |
| `has_appearance` | face_trace ↔ appearance_trace | Face has specific appearance |
| `wears` | face_trace ↔ accessory | Face wears an accessory |
| Edge Type | Storage Name | Source → Target | Description |
|-----------|--------------|-----------------|-------------|
| `co_occurs` | `CO_OCCURS_WITH` | object ↔ object | Two objects appear together in same frame |
| `speaker_face` | `SPEAKS_AS` | speaker face_track | Speaker matched to face track via lip sync |
| `face_face` | `INTERACTS_WITH` | face_track ↔ face_track | Two face tracks interact (mutual gaze) |
| `mutual_gaze` | `MUTUAL_GAZE` | gaze_track ↔ gaze_track | Two people looking at each other |
| `lip_sync` | `LIP_SYNC` | lip_track → text_region | Lip movement aligned with spoken text |
| `has_appearance` | `HAS_APPEARANCE` | face_track → appearance_trace | Face has specific appearance |
| `wears` | `WEARS` | face_track → accessory | Face wears an accessory |
| `hand_object` | `HOLDS` | hand → object | Hand holding object |
---
@@ -102,10 +107,10 @@ curl -s -X POST "$API/api/v1/file/$FILE_UUID/tkg/rebuild" \
"success": true,
"file_uuid": "d3f9ae8e471a1fc4d47022c66091b920",
"result": {
"face_trace_nodes": 16,
"gaze_trace_nodes": 16,
"lip_trace_nodes": 12,
"text_trace_nodes": 24,
"face_track_nodes": 16,
"gaze_track_nodes": 16,
"lip_track_nodes": 12,
"text_region_nodes": 24,
"appearance_trace_nodes": 8,
"skin_tone_trace_nodes": 5,
"accessory_nodes": 3,
@@ -143,18 +148,18 @@ Query TKG nodes with pagination and optional type filter.
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `node_type` | string | No | all | Filter by node type: `face_trace`, `gaze_trace`, `lip_trace`, `text_trace`, `appearance_trace`, `skin_tone_trace`, `accessory`, `object`, `speaker` |
| `node_type` | string | No | all | Filter by node type: `face_track`, `gaze_track`, `lip_track`, `text_region`, `appearance_trace`, `skin_tone_trace`, `accessory`, `object`, `speaker` |
| `page` | integer | No | 1 | Page number |
| `page_size` | integer | No | 100 | Items per page (max 500) |
#### Example
```bash
# Get all face_trace nodes
# Get all face_track nodes
curl -s -X POST "$API/api/v1/file/$FILE_UUID/tkg/nodes" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"node_type": "face_trace", "page": 1, "page_size": 50}'
-d '{"node_type": "face_track", "page": 1, "page_size": 50}'
# Get all nodes
curl -s -X POST "$API/api/v1/file/$FILE_UUID/tkg/nodes" \
@@ -175,12 +180,12 @@ curl -s -X POST "$API/api/v1/file/$FILE_UUID/tkg/nodes" \
"nodes": [
{
"id": 1,
"node_type": "face_trace",
"external_id": "trace_0",
"label": "Face Trace 0",
"node_type": "face_track",
"external_id": "face_track_0",
"label": "Face Track 0",
"properties": {
"trace_id": 0,
"face_count": 142,
"frame_count": 142,
"avg_confidence": 0.87
}
}
@@ -224,17 +229,17 @@ Query TKG edges with pagination and optional filters.
#### Example
```bash
# Get all co_occurrence edges
# Get all co_occurs edges
curl -s -X POST "$API/api/v1/file/$FILE_UUID/tkg/edges" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"edge_type": "co_occurs"}'
# Get edges between face_trace and speaker nodes
# Get edges between face_track and speaker nodes
curl -s -X POST "$API/api/v1/file/$FILE_UUID/tkg/edges" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"source_type": "speaker", "target_type": "face_trace"}'
-d '{"source_type": "speaker", "target_type": "face_track"}'
```
#### Response (200)
@@ -298,12 +303,12 @@ curl -s "$API/api/v1/file/$FILE_UUID/tkg/node/1" \
"success": true,
"node": {
"id": 1,
"node_type": "face_trace",
"external_id": "trace_0",
"label": "Face Trace 0",
"node_type": "face_track",
"external_id": "face_track_0",
"label": "Face Track 0",
"properties": {
"trace_id": 0,
"face_count": 142,
"frame_count": 142,
"avg_confidence": 0.87
}
},
@@ -422,4 +427,4 @@ curl -s "$API/api/v1/file/$FILE_UUID/processor-counts" \
---
*Updated: 2026-06-20 12:00:00*
*Updated: 2026-06-25 17:00:00*

View File

@@ -0,0 +1,499 @@
---
title: TKG Formation V1.0
version: 1.0
date: 2026-06-25
author: OpenCode
status: draft
---
## Overview
Temporal Knowledge Graph (TKG) is built from multi-processor outputs to create a time-aligned knowledge graph. This document defines the formation phases, node/edge types, data flow, and integration with Identity Agent.
---
## Phase Definition
| Phase | Name | Trigger | Input | Output | Code Location |
|-------|------|---------|-------|--------|---------------|
| **Phase 0** | Populate | TKG rebuild | `face.json` | `PG face_detections.trace_id` | `tkg.rs:20-100` |
| **Phase 1** | Extract | Video register | Video frames | `Qdrant _faces` (512D embeddings) | `face_processor.py` |
| **Phase 2** | Build Nodes | TKG rebuild | `face.json`, PG tables | `tkg_nodes` (9 types) | `tkg.rs:506-515` |
| **Phase 3** | Build Edges | TKG rebuild | `tkg_nodes`, pose data | `tkg_edges` (9 types) | `tkg.rs:517-524` |
| **Phase 4** | Identity | Manual/API call | `Qdrant _faces`, `_seeds` | `tkg_nodes.status` updated | `identity_matcher.py` |
### Phase Details
#### Phase 0: Populate
**Purpose:** Assign `trace_id` to face detections.
**Flow:**
1. Check if `trace_id IS NOT NULL` already exists in `face_detections`
2. If not, call `store_traced_faces.py`
3. `store_traced_faces.py` runs `face_tracker.py` (IoU-only)
4. Update `face_detections.trace_id`
**Dependency:** `face.json` must exist (from Phase 1)
---
#### Phase 1: Extract
**Purpose:** Generate face embeddings and push to Qdrant.
**Flow:**
1. `face_processor.py` runs Vision detection (ANE)
2. Crop faces from video frames
3. CoreML FaceNet → 512D embedding
4. Push to Qdrant `_faces` collection
5. Write `face.json` (metadata only, no embedding)
**Output:**
- `face.json` in output directory
- Qdrant `_faces` collection with embeddings
---
#### Phase 2: Build Nodes
**Purpose:** Create TKG nodes from processor outputs.
**Node Builders:**
| Builder | Node Type | Data Source |
|---------|-----------|-------------|
| `build_face_track_nodes` | `face_track` | `face.json`, `face_detections` |
| `build_gaze_track_nodes` | `gaze_track` | `face.json` (pose data) |
| `build_lip_track_nodes` | `lip_track` | `face.json` (lips), `asrx.json` |
| `build_text_region_nodes` | `text_region` | `asrx.json` |
| `build_appearance_trace_nodes` | `appearance_trace` | `yolo.json` (person) |
| `build_accessory_nodes` | `accessory` | `yolo.json` |
| `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** |
---
#### Phase 3: Build Edges
**Purpose:** Create TKG edges from node relationships.
**Edge Builders:**
| Builder | Edge Type | Source → Target |
|---------|-----------|-----------------|
| `build_co_occurrence_edges` | `co_occurs` | `object ↔ object` |
| `build_speaker_face_edges` | `speaker_face` | `speaker ↔ face_track` |
| `build_face_face_edges` | `face_face` | `face_track ↔ face_track` |
| `build_mutual_gaze_edges` | `mutual_gaze` | `gaze_track ↔ gaze_track` |
| `build_lip_sync_edges` | `lip_sync` | `lip_track ↔ text_region` |
| `build_has_appearance_edges` | `has_appearance` | `face_track ↔ appearance_trace` |
| `build_wears_edges` | `wears` | `face_track ↔ accessory` |
| `build_hand_object_edges` | `hand_object` | `hand ↔ object` |
---
#### Phase 4: Identity
**Purpose:** Mark face_track nodes with identity binding status.
**Flow:**
1. Identity Agent queries `_seeds` collection (TMDb/manual/propagation)
2. Queries `_faces` collection for trace representatives
3. Multi-angle matching (3 reps per trace)
4. Mark TKG nodes: `status='suggested'`, `confidence`, `pending_identity_name`
5. User confirms → update TKG, Qdrant, PG
6. Confirmed trace becomes propagation seed in `_seeds`
---
## Node Types (Naming Standardized)
**Naming Rule:**
- All trace types use `_track` suffix
- Text uses `_region` (non-temporal)
| Node Type | External ID Format | Key Properties |
|-----------|---------------------|----------------|
| `face_track` | `face_track_{trace_id}` | `trace_id`, `frame_count`, `start_frame`, `end_frame`, `avg_bbox`, `status`, `confidence`, `identity_uuid` |
| `gaze_track` | `gaze_track_{id}` | `direction` (frontal/left/right/up/down + diagonals) |
| `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` |
### face_track Identity Properties
| Property | Type | Values |
|----------|------|--------|
| `status` | string | `pending` | `suggested` | `confirmed` | `stranger` |
| `pending_identity_name` | string/null | Suggested identity name |
| `pending_identity_uuid` | string/null | Suggested identity UUID |
| `suggested_by` | string/null | `tmdb` | `propagation` | `manual` |
| `confidence` | float/null | Matching score (0.0-1.0) |
| `identity_uuid` | string/null | Confirmed identity UUID |
| `identity_id` | integer/null | Confirmed PG identity.id |
| `identity_ref` | string/null | Reference string (e.g., `file_uuid:identity_1`) |
| `stranger_id` | integer/null | Stranger cluster ID |
| `stranger_ref` | string/null | Reference string (e.g., `stranger_1`) |
---
## Edge Types
| Edge Type | Storage Name | Source → Target | Properties |
|-----------|--------------|-----------------|------------|
| `co_occurs` | `CO_OCCURS_WITH` | `object ↔ object` | `frame_count`, `confidence` |
| `speaker_face` | `SPEAKS_AS` | `speaker → face_track` | `overlap_frames`, `confidence` |
| `face_face` | `INTERACTS_WITH` | `face_track ↔ face_track` | `co_occurrence_frames` |
| `mutual_gaze` | `MUTUAL_GAZE` | `gaze_track ↔ gaze_track` | `frame_count` |
| `lip_sync` | `LIP_SYNC` | `lip_track → text_region` | `speaker_id` |
| `has_appearance` | `HAS_APPEARANCE` | `face_track → appearance_trace` | `frame_count` |
| `wears` | `WEARS` | `face_track → accessory` | `confidence` |
| `hand_object` | `HOLDS` | `hand → object` | `frame_count`, `confidence` |
---
## Data Flow Diagram
```mermaid
graph TB
subgraph Phase0[Phase 0: Populate]
A[face.json] --> B[store_traced_faces.py]
B --> C[PG face_detections.trace_id]
end
subgraph Phase1[Phase 1: Extract]
D[Video Frames] --> E[face_processor.py]
E --> F[face.json metadata]
E --> G[Qdrant _faces 512D]
end
subgraph Phase2[Phase 2: Build Nodes]
C --> H[TKG Builder]
F --> H
I[pose.json] --> H
J[asrx.json] --> H
K[yolo.json] --> H
H --> L[tkg_nodes<br/>9 node types]
end
subgraph Phase3[Phase 3: Build Edges]
L --> M[TKG Builder]
M --> N[tkg_edges<br/>9 edge types]
end
subgraph Phase4[Phase 4: Identity]
G --> O[identity_matcher.py]
L --> O
P[Qdrant _seeds] --> O
O --> Q[mark_tkg_suggested]
Q --> L
O --> R[confirm_identity.py]
R --> L
R --> G
R --> P
end
style Phase0 fill:#e1f5fe
style Phase1 fill:#fff9c4
style Phase2 fill:#e8f5e9
style Phase3 fill:#f3e5f5
style Phase4 fill:#fce4ec
```
---
## 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<usize> {
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
```sql
-- Get all face_track nodes for a file
SELECT id, external_id, label, properties
FROM dev.tkg_nodes
WHERE node_type = 'face_track' AND file_uuid = 'xxx'
ORDER BY external_id;
-- Get pending faces (no identity suggestion)
SELECT id, external_id, properties->>'trace_id' as trace_id
FROM dev.tkg_nodes
WHERE node_type = 'face_track'
AND file_uuid = 'xxx'
AND (properties->>'status' IS NULL OR properties->>'status' = 'pending');
-- Get suggested faces (Identity Agent suggested)
SELECT id, external_id,
properties->>'pending_identity_name' as name,
properties->>'confidence' as confidence,
properties->>'suggested_by' as source
FROM dev.tkg_nodes
WHERE node_type = 'face_track'
AND file_uuid = 'xxx'
AND properties->>'status' = 'suggested'
ORDER BY (properties->>'confidence')::float DESC;
-- Get confirmed faces
SELECT id, external_id,
properties->>'identity_uuid' as identity_uuid,
properties->>'identity_name' as name
FROM dev.tkg_nodes
WHERE node_type = 'face_track'
AND file_uuid = 'xxx'
AND properties->>'status' = 'confirmed';
-- Get stranger cluster members
SELECT id, external_id, properties->>'stranger_id' as cluster
FROM dev.tkg_nodes
WHERE node_type = 'face_track'
AND file_uuid = 'xxx'
AND properties->>'status' = 'stranger'
ORDER BY (properties->>'stranger_id')::int;
```
### Identity Queries
```sql
-- Find all traces bound to an identity
SELECT id, external_id, properties->>'trace_id' as trace_id
FROM dev.tkg_nodes
WHERE node_type = 'face_track'
AND properties->>'identity_uuid' = 'xxx-xxx';
-- Count identities per file
SELECT properties->>'identity_uuid' as identity_uuid,
COUNT(*) as trace_count
FROM dev.tkg_nodes
WHERE node_type = 'face_track'
AND file_uuid = 'xxx'
AND properties->>'status' = 'confirmed'
GROUP BY properties->>'identity_uuid';
```
### Statistics Queries
```sql
-- Status distribution for a file
SELECT properties->>'status' as status, COUNT(*) as count
FROM dev.tkg_nodes
WHERE node_type = 'face_track' AND file_uuid = 'xxx'
GROUP BY properties->>'status';
-- Confidence distribution
SELECT
CASE
WHEN (properties->>'confidence')::float >= 0.9 THEN 'high'
WHEN (properties->>'confidence')::float >= 0.7 THEN 'medium'
ELSE 'low'
END as confidence_level,
COUNT(*) as count
FROM dev.tkg_nodes
WHERE node_type = 'face_track'
AND file_uuid = 'xxx'
AND properties->>'status' = 'suggested'
GROUP BY confidence_level;
-- Top suggested identities
SELECT properties->>'pending_identity_name' as name,
COUNT(*) as trace_count,
AVG((properties->>'confidence')::float) as avg_confidence
FROM dev.tkg_nodes
WHERE node_type = 'face_track'
AND properties->>'status' = 'suggested'
GROUP BY properties->>'pending_identity_name'
ORDER BY trace_count DESC
LIMIT 10;
```
### Cross-node Queries
```sql
-- Speaker ↔ face_track edges
SELECT
s.external_id as speaker,
f.external_id as face_track,
e.properties->>'overlap_frames' as overlap
FROM dev.tkg_edges e
JOIN dev.tkg_nodes s ON e.source_node_id = s.id
JOIN dev.tkg_nodes f ON e.target_node_id = f.id
WHERE e.file_uuid = 'xxx'
AND e.edge_type = 'SPEAKS_AS';
-- Objects co-occurrence
SELECT
o1.external_id as obj1,
o2.external_id as obj2,
e.properties->>'frame_count' as co_frames
FROM dev.tkg_edges e
JOIN dev.tkg_nodes o1 ON e.source_node_id = o1.id
JOIN dev.tkg_nodes o2 ON e.target_node_id = o2.id
WHERE e.file_uuid = 'xxx'
AND e.edge_type = 'CO_OCCURS_WITH'
ORDER BY (e.properties->>'frame_count')::int DESC;
```
---
## Integration with Identity Agent
### Identity Agent Flow
```
Identity Agent Pipeline:
├─ Round 1 (TH=0.55):
│ Query _seeds (source='tmdb')
│ Query _faces (file_uuid) → get trace representatives
│ Multi-angle match → suggestions
│ Mark TKG: status='suggested', confidence
├─ User Confirmation:
│ Update TKG: status='confirmed'
│ Update _faces: identity_uuid for all points
│ Update PG face_detections: identity_id
│ Add _seeds: source='propagation'
├─ Round 2 (TH=0.55):
│ Use confirmed traces as seeds
│ Match remaining pending traces
└─ Round 3+ (TH=0.50):
Continue propagation
Stranger clustering (TH=0.40)
```
### TKG Node Status Transitions
```
pending → suggested → confirmed → (final)
↘ stranger ↘ (final)
```
### Status Transition Triggers
| Transition | Trigger | Action |
|------------|---------|--------|
| `pending → suggested` | Identity Agent Round 1-3 | `mark_face_track_suggested()` |
| `suggested → confirmed` | User confirmation API | `mark_face_track_confirmed()` |
| `pending → stranger` | Stranger clustering | `mark_face_track_stranger()` |
| `confirmed → pending` | Undo binding | `clear_face_track_status()` |
---
## Version History
| Version | Date | Changes |
|---------|------|---------|
| 1.0 | 2026-06-25 | Initial version with Phase 0-4 definition, node naming, flow diagram, query examples |

View File

@@ -163,4 +163,16 @@ status: Completed
- **Rust API integration**: Call Python scripts from stubbed Rust functions
- **Production deployment**: Generate TMDb seeds for all identities
- **Workflow documentation**: User guide for Identity Agent CLI usage
- **Workflow documentation**: User guide for Identity Agent CLI usage
## Related Documents
- `docs_v1.0/DESIGN/TKG_FORMATION_V1.0.md` — TKG formation phases, node/edge types, data flow diagram
- `docs_v1.0/API_WORKSPACE/modules/15_tkg.md` — TKG API endpoints
## Version History
| Version | Date | Changes |
|---------|------|--------|
| 1.0 | 2026-06-25 | Initial architecture doc, all phases completed |
| 1.1 | 2026-06-25 | Added reference to TKG Formation V1.0 |

0
scripts/confirm_identity.py Normal file → Executable file
View File

0
scripts/generate_seed_embeddings.py Normal file → Executable file
View File

0
scripts/identity_matcher.py Normal file → Executable file
View File

0
scripts/manual_seed.py Normal file → Executable file
View File

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,