diff --git a/docs_v1.0/API_WORKSPACE/modules/18_profile.md b/docs_v1.0/API_WORKSPACE/modules/18_profile.md new file mode 100644 index 0000000..b07a8ab --- /dev/null +++ b/docs_v1.0/API_WORKSPACE/modules/18_profile.md @@ -0,0 +1,267 @@ + + + + +## Profile Management + +Endpoints for managing trace profiles (face track metadata stored in TKG) and file profiles (video metadata stored in PostgreSQL). + +### `GET /api/v1/trace-profile` + +**Auth**: Required +**Scope**: file-level + +Read a single face trace's profile including name, key frame, key face, and multi-language aliases. + +#### Request Parameters + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID | +| `trace_id` | integer | Yes | Trace ID (numeric) | + +#### Example + +```bash +curl -s "$API/api/v1/trace-profile?file_uuid=$FILE_UUID&trace_id=7" \ + -H "X-API-Key: $KEY" +``` + +#### Response (200) + +```json +{ + "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5", + "trace_id": 7, + "name": "John Doe", + "key_frame": 640, + "key_face": "face_12345", + "aliases": { + "en": "John Doe", + "zh": "約翰" + }, + "properties": { + "status": "bound", + "avg_bbox": {"x": 899, "y": 212, "width": 342, "height": 342}, + "start_frame": 624, + "end_frame": 669, + "frame_count": 5 + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `file_uuid` | string | File UUID | +| `trace_id` | integer | Trace ID | +| `name` | string | Display name (from `tkg_nodes.label`) | +| `key_frame` | integer | Representative frame number, or null | +| `key_face` | string | Representative face ID, or null | +| `aliases` | object | Multi-language name aliases | +| `properties` | object | Full TKG node properties (bbox, frames, etc.) | + +#### Error Responses + +| HTTP | When | +|------|------| +| `404` | Trace not found | +| `401` | Missing or invalid API key | + +--- + +### `PUT /api/v1/trace-profile` + +**Auth**: Required +**Scope**: file-level + +Update a single face trace's profile fields. Only provided fields are updated; others remain unchanged. + +#### Request Body + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID | +| `trace_id` | integer | Yes | Trace ID to update | +| `name` | string | No | New display name | +| `key_frame` | integer | No | Representative frame number | +| `key_face` | string | No | Representative face ID | +| `aliases` | object | No | Multi-language aliases `{"en": "...", "zh": "..."}` | +| `properties` | object | No | Additional properties to merge into existing JSONB | + +#### Example + +```bash +curl -s -X PUT "$API/api/v1/trace-profile" \ + -H "X-API-Key: $KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5", + "trace_id": 7, + "name": "John Doe", + "key_frame": 640, + "key_face": "face_12345", + "aliases": {"en": "John Doe", "zh": "約翰"} + }' +``` + +#### Response (200) + +```json +{ + "success": true, + "message": "Trace profile updated", + "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5", + "trace_id": 7 +} +``` + +--- + +### `PUT /api/v1/trace-profile/group` + +**Auth**: Required +**Scope**: file-level + +Batch update the `name` (label) for multiple traces in a face group. Used when renaming a group. + +#### Request Body + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID | +| `trace_ids` | integer[] | Yes | List of trace IDs to update | +| `name` | string | Yes | New group name for all traces | + +#### Example + +```bash +curl -s -X PUT "$API/api/v1/trace-profile/group" \ + -H "X-API-Key: $KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5", + "trace_ids": [7, 2, 13], + "name": "Group A" + }' +``` + +#### Response (200) + +```json +{ + "success": true, + "message": "Updated 3 traces in group", + "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5", + "updated_count": 3 +} +``` + +--- + +### `GET /api/v1/file-profile` + +**Auth**: Required +**Scope**: file-level + +Read a file's metadata including path, name, status, and technical details. + +#### Request Parameters + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID | + +#### Example + +```bash +curl -s "$API/api/v1/file-profile?file_uuid=$FILE_UUID" \ + -H "X-API-Key: $KEY" +``` + +#### Response (200) + +```json +{ + "file_uuid": "49884ce1c341953d1ad7bf67a77c30cc", + "file_name": "Dedicatoria.mp4", + "file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Dedicatoria.mp4", + "status": "completed", + "duration": 93.33, + "width": 1280, + "height": 720, + "fps": 30.0, + "total_frames": 0 +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `file_uuid` | string | File UUID | +| `file_name` | string | File name | +| `file_path` | string | Full filesystem path | +| `status` | string | `pending`, `processing`, `completed`, `failed` | +| `duration` | float | Duration in seconds | +| `width` | integer | Video width in pixels | +| `height` | integer | Video height in pixels | +| `fps` | float | Frames per second | +| `total_frames` | integer | Total frame count | + +--- + +### `PUT /api/v1/file-profile` + +**Auth**: Required +**Scope**: file-level + +Update file metadata, typically used when a file is moved to a new location. + +#### Request Body + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID | +| `file_path` | string | No | New filesystem path | +| `file_name` | string | No | New file name | + +#### Example + +```bash +curl -s -X PUT "$API/api/v1/file-profile" \ + -H "X-API-Key: $KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "file_uuid": "49884ce1c341953d1ad7bf67a77c30cc", + "file_path": "/new/location/Dedicatoria.mp4" + }' +``` + +#### Response (200) + +```json +{ + "success": true, + "message": "File profile updated", + "file_uuid": "49884ce1c341953d1ad7bf67a77c30cc" +} +``` + +--- + +## Data Storage + +| Profile Type | Storage | Table | +|-------------|---------|-------| +| **trace_profile** | PostgreSQL (TKG) | `tkg_nodes` where `node_type='face_track'` | +| **file_profile** | PostgreSQL | `videos` | + +### Trace Profile Fields + +| Field | TKG Column | Description | +|-------|-----------|-------------| +| `name` | `label` | Display name for the trace | +| `key_frame` | `properties->'key_frame'` | Representative frame number | +| `key_face` | `properties->'key_face'` | Representative face ID | +| `aliases` | `properties->'aliases'` | Multi-language name aliases | + +--- +*Updated: 2026-07-18 — New profile module: trace-profile (GET, PUT, PUT group) and file-profile (GET, PUT)* diff --git a/docs_v1.0/DESIGN/IDENTITY_AGENT_REDESIGN_V2.0.md b/docs_v1.0/DESIGN/IDENTITY_AGENT_REDESIGN_V2.0.md new file mode 100644 index 0000000..ab390e2 --- /dev/null +++ b/docs_v1.0/DESIGN/IDENTITY_AGENT_REDESIGN_V2.0.md @@ -0,0 +1,538 @@ +--- +title: Identity Agent V2.0 Redesign +version: 2.0 +date: 2026-07-15 +author: OpenCode +status: Draft +--- + +# Identity Agent V2.0 重新設計 + +## 1. 概述 + +### 目的 +將 face trace 與 seed photos 透過 Qdrant 向量比對,建立 pending people 綁定建議,由用戶確認後升級為 known people。 + +### 核心原則 + +| 原則 | 說明 | +|------|------| +| **只綁定,不確認** | Identity Agent 只建立 suggested 綁定,不升級為 confirmed | +| **所有綁定都需確認** | 即使 matching 的是已知人物(known people),也要建立 pending 綁定 | +| **用戶決定** | 每個新綁定都需要用戶在 UI 上點擊確認 | +| **多重綁定** | 一個 trace 可同時匹配多個 seeds,都記錄為建議 | + +### 流程圖 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Identity Agent V2.0 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Data Stores: │ +│ ├─ Qdrant _faces (512D, trace-level faces) │ +│ ├─ Qdrant _seeds (512D, seed photos per file_uuid) │ +│ ├─ PG identities (metadata: name, tmdb_id, status) │ +│ └─ PG tkg_nodes (face_track, status + suggestions) │ +│ │ +│ Flow: │ +│ 1. TMDb/Upload/Name → Seeds (_seeds collection) │ +│ 2. Identity Agent → Match _seeds vs _faces │ +│ 3. Write suggestions → TKG + Qdrant (suggested) │ +│ 4. User Confirm → status = confirmed │ +│ 5. User Reject → status = stranger │ +│ │ +│ Rules: │ +│ - All bindings start as "suggested" │ +│ - Even known people get new pending bindings │ +│ - Multiple suggestions per trace allowed │ +│ - User confirms each binding individually │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 2. 種子管理 + +### 種子來源 + +| 來源 | 觸發方式 | file_uuid 綁定 | +|------|----------|---------------| +| TMDb probe | 檔案註冊時自動探測 | 探測檔案的 uuid | +| 用戶上傳照片 | UI 上傳 face photo | 當前查看檔案的 uuid | +| Pending Face 命名 | 選擇 trace 並命名 | 該 face 所在檔案的 uuid | + +### Qdrant _seeds 結構 + +```json +{ + "vector": [512 floats], + "payload": { + "identity_id": 3, + "identity_uuid": "69e50666-0c2d-4215-968b-d1fdd820fb7d", + "name": "Walter Matthau", + "source": "tmdb", + "file_uuid": "4bb70f2b0d6d9c1a2900298666189a73", + "tmdb_id": 3490, + "status": "confirmed", + "angles": [ + {"frame": 100, "embedding": [...]}, + {"frame": 150, "embedding": [...]} + ] + } +} +``` + +### 種子產生流程 + +``` +TMDb Probe + → 下載演員照片 + → 提取 face embedding + → Push to _seeds (file_uuid 綁定) + +用戶上傳 + → 上傳照片 + → 檢測 face + 提取 embedding + → Push to _seeds (file_uuid 綁定) + +Pending Face 命名 + → 選擇 trace + → 輸入名稱 + → 從 trace 提取 representative embedding + → Push to _seeds (file_uuid 綁定) +``` + +--- + +## 3. 匹配流程 + +### 匹配閾值 + +| Round | Threshold | Seed Source | +|-------|-----------|-------------| +| Round 1 | 0.55 | TMDb seeds | +| Round 2 | 0.55 | Confirmed traces (propagation) | +| Round 3+ | 0.50 | More confirmed traces | +| Stranger | 0.40 | Unmatched traces (clustering) | + +### 匹配邏輯 + +```python +def match_faces_round_1(file_uuid: str) -> dict: + """ + Returns: {trace_id: [suggestions]} + 每個 trace 可有多個 suggestions(多重綁定) + """ + traces = get_trace_representatives(file_uuid) + seeds = get_seeds(file_uuid=file_uuid) # 只查該檔案的 seeds + + suggestions = {} + + for trace_id, reps in traces.items(): + trace_suggestions = [] + + for seed in seeds: + score = multi_angle_match(seed["embedding"], reps) + if score >= TH_ROUND_1: + trace_suggestions.append({ + "identity_id": seed["identity_id"], + "identity_uuid": seed["identity_uuid"], + "name": seed["name"], + "score": round(score, 4), + "source": seed["source"], + "is_known": seed.get("status") == "confirmed" + }) + + if trace_suggestions: + trace_suggestions.sort(key=lambda x: x["score"], reverse=True) + suggestions[trace_id] = trace_suggestions + + return suggestions +``` + +--- + +## 4. 多重綁定 + +### 設計原則 + +一個 face trace 可以同時匹配多個 seeds(分數都很高),所有匹配都記錄為建議,由用戶決定選擇哪一個。 + +### 範例 + +``` +Trace #432 (7 frames, confidence: 0.83) +├─ Walter Matthau (58.6%) ← is_known: true +├─ George Kennedy (57.2%) ← is_known: true +└─ [Not yet assigned] +``` + +--- + +## 5. 資料儲存 + +### Qdrant _faces payload(多重綁定) + +```json +{ + "file_uuid": "9f880a88e9f027255714906d84865fd4", + "trace_id": 432, + "frame": 13494, + "confidence": 0.829, + "bbox": {"x": 272, "y": 38, "width": 87, "height": 87}, + + "identity_id": null, + "identity_uuid": null, + + "suggestions": [ + {"identity_id": 3, "identity_uuid": "69e50666...", "name": "Walter Matthau", "score": 0.586, "source": "tmdb", "is_known": true}, + {"identity_id": 5, "identity_uuid": "102e8ed0...", "name": "George Kennedy", "score": 0.572, "source": "tmdb", "is_known": true} + ], + "suggested_by": "tmdb" +} +``` + +### TKG face_track properties(多重綁定) + +```json +{ + "trace_id": 432, + "frame_count": 7, + "start_frame": 100, + "end_frame": 300, + + "status": "suggested", + "suggestions": [ + {"identity_id": 3, "identity_uuid": "69e50666...", "name": "Walter Matthau", "score": 0.586, "source": "tmdb", "is_known": true}, + {"identity_id": 5, "identity_uuid": "102e8ed0...", "name": "George Kennedy", "score": 0.572, "source": "tmdb", "is_known": true} + ], + "suggested_by": "tmdb" +} +``` + +### 雙寫入邏輯 + +```python +def write_suggestions(file_uuid: str, suggestions: Dict): + """同時寫入 TKG 和 Qdrant""" + # 1. 寫入 TKG + tkg_updated = batch_mark_suggestions(file_uuid, suggestions) + + # 2. 寫入 Qdrant _faces + qdrant_updated = update_faces_suggestions(file_uuid, suggestions) + + return {"tkg": tkg_updated, "qdrant": qdrant_updated} +``` + +--- + +## 6. Pending People + +### 查詢邏輯 + +**API:** `GET /api/v1/file/:file_uuid/pending-persons` + +**查詢:** PG tkg_nodes WHERE status = 'suggested' + +```sql +SELECT + jsonb_array_elements(properties->'suggestions') as suggestion, + COUNT(DISTINCT (properties->>'trace_id')) as trace_count, + MIN(created_at) as created_at +FROM tkg_nodes +WHERE file_uuid = $1 + AND node_type = 'face_track' + AND properties->>'status' = 'suggested' + AND jsonb_array_length(properties->'suggestions') > 0 +GROUP BY suggestion +ORDER BY trace_count DESC +``` + +### 回傳格式 + +```json +{ + "success": true, + "message": "Found 5 pending persons", + "data": [ + { + "identity_uuid": "69e50666-0c2d-...", + "identity_id": 3, + "name": "Walter Matthau", + "trace_count": 4, + "is_known": true, + "created_at": "2026-07-15 12:00:00" + } + ] +} +``` + +--- + +## 7. Known Person 複製邏輯 + +### 規則 + +當 Identity Agent 匹配到已知人物(status = 'confirmed')時: +1. **不自動確認** — 仍建立 suggested 綁定 +2. **標記 is_known** — 讓 UI 顯示為已知人物 +3. **用戶仍需確認** — 每個新綁定都需要用戶操作 + +### 匹配結果結構 + +```json +{ + "trace_id": 432, + "suggestions": [ + { + "identity_id": 3, + "name": "Walter Matthau", + "score": 0.586, + "is_known": true // ← 已知人物標記 + } + ] +} +``` + +### UI 顯示 + +``` +Pending People (Charade) + +Trace #432 +├─ Walter Matthau (58.6%) ✓ Known +│ [Confirm] [Reject] +├─ George Kennedy (57.2%) ✓ Known +│ [Confirm] [Reject] +``` + +--- + +## 8. Speaker Binding + +### 改查 TKG suggested + +**舊邏輯:** 查 Qdrant _faces WHERE identity_id EXISTS(永遠 0 筆) + +**新邏輯:** 查 PG tkg_nodes WHERE status = 'suggested' + +### 具體實現 + +```rust +pub async fn bind_speakers(pool: &PgPool, file_uuid: &str) -> Result { + // 1. 查 TKG suggested 節點 + let rows: Vec<(i32, i32, i64, i64, f64)> = sqlx::query_as( + "SELECT (properties->>'trace_id')::int, + (properties->'suggestions'->0->>'identity_id')::int, + (properties->>'start_frame')::bigint, + (properties->>'end_frame')::bigint, + COALESCE((properties->>'confidence')::float, 0.0) + FROM tkg_nodes + WHERE file_uuid = $1 AND node_type = 'face_track' + AND properties->>'status' = 'suggested' + AND jsonb_array_length(properties->'suggestions') > 0" + ).bind(file_uuid).fetch_all(pool).await?; + + // 2. 讀取 ASRX segments + let asrx_data = load_asrx(file_uuid).await?; + let speakers = extract_speakers(&asrx_data); + + // 3. 計算重疊 + 寫入 identity_bindings + let mut bindings = 0; + for (trace_id, identity_id, start_frame, end_frame, confidence) in rows { + let best_speaker = compute_overlap(start_frame, end_frame, &speakers, fps); + if best_speaker.overlap_ratio > 0.3 { + insert_identity_binding(identity_id, &best_speaker.speaker_id, ...); + bindings += 1; + } + } + + Ok(bindings) +} +``` + +--- + +## 9. API 設計 + +### 完整 API 清單 + +| Method | Path | 說明 | +|--------|------|------| +| POST | `/api/v1/agents/identity/run` | 執行 Identity Agent(全部 seeds) | +| POST | `/api/v1/agents/identity/run-for-seed` | 執行單一 seed 匹配 | +| POST | `/api/v1/agents/identity/cluster` | 執行 trace clustering(無 seed) | +| POST | `/api/v1/file/:file_uuid/cluster-agent` | 執行 trace clustering(檔案專用) | +| POST | `/api/v1/agents/identity/generate-seeds` | 產生種子 embeddings | +| GET | `/api/v1/file/:file_uuid/pending-persons` | 列出 pending people | +| POST | `/api/v1/file/:file_uuid/pending-person` | 建立 pending person | +| POST | `/api/v1/identity/:uuid/bind` | 綁定 identity | +| POST | `/api/v1/identity/:uuid/unbind` | 解綁 identity | +| POST | `/api/v1/identity/:uuid/confirm` | 確認 pending → confirmed | +| POST | `/api/v1/identity/:uuid/reject` | 拒絕 pending → stranger | + +### run-for-seed API + +**Request:** +```json +{ + "file_uuid": "9f880a88e9f027255714906d84865fd4", + "identity_uuid": "c3545906-c82d-4b66-aa1d-150bc02decce" +} +``` + +**Response:** +```json +{ + "success": true, + "message": "Found 19 new matches for seed", + "matches": 19 +} +``` + +--- + +## 10. 前端 UI + +### Pending People 顯示 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Pending People (Charade) │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ Audrey Hepburn (6 traces) ✓ Known Person │ +│ [Confirm All] [Review] [Skip] │ +│ │ +│ George Kennedy (4 traces) ✓ Known Person │ +│ [Confirm All] [Review] [Skip] │ +│ │ +│ Walter Matthau (4 traces) ✓ Known Person │ +│ [Confirm All] [Review] [Skip] │ +│ │ +│ Bernard Musson (1 trace) │ +│ [Confirm] [Skip] │ +│ │ +│ James Coburn (1 trace) │ +│ [Confirm] [Skip] │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +### Trace 層級多重綁定 + +``` +┌─────────────────────────────────────────────────────────┐ +│ Trace #432 (7 frames) │ +├─────────────────────────────────────────────────────────┤ +│ │ +│ Suggestions: │ +│ ○ Walter Matthau (58.6%) ✓ Known │ +│ ○ George Kennedy (57.2%) ✓ Known │ +│ │ +│ [Select Walter Matthau] [Select George Kennedy] │ +│ [Skip - Not Recognized] │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +--- + +## 11. Cluster Agent(無 Seed 分群) + +### 目的 +不需要 seed photos,直接對現有 face traces 進行相似度分群,找出相似臉並分組。 + +### 與 Identity Agent 的區別 + +| | Identity Agent | Cluster Agent | +|---|---|---| +| Seed | 需要 seed person | 不需要 seed | +| 目標 | TMDB 匹配已知演員 | 用現有 face traces 找出相似臉分群 | +| 輸出 | identity_match_round1.json | cluster_result.json | +| 算法 | Cosine similarity vs seeds | Greedy clustering with centroids | + +### API + +**Request:** +```json +POST /api/v1/file/:file_uuid/cluster-agent +{} +``` + +**Response:** +```json +{ + "success": true, + "message": "Found 30 clusters from 412 traces", + "clusters": 30, + "total_traces": 412, + "output_path": "/Users/accusys/momentry/output/{uuid}/{uuid}.cluster_result.json", + "cluster_details": [ + {"cluster_id": 1, "trace_count": 49, "trace_ids": [...], "representative_trace": 2136}, + {"cluster_id": 2, "trace_count": 36, "trace_ids": [...], "representative_trace": 2} + ] +} +``` + +### 輸出檔案 + +`{output}/{uuid}/{uuid}.cluster_result.json` + +```json +{ + "file_uuid": "9f880a88e9f027255714906d84865fd4", + "threshold": 0.40, + "total_traces": 412, + "clusters": [ + {"cluster_id": 1, "trace_count": 49, "trace_ids": [2136, ...], "representative_trace": 2136} + ] +} +``` + +--- + +## 12. 修改計畫 + +### Python Scripts + +| 檔案 | 改動 | +|------|------| +| `identity_matcher.py` | 輸出 suggestions 陣列,支援多重綁定 | +| `qdrant_faces.py` | 新增 `update_faces_suggestions()` | +| `tkg_helper.py` | 修改 `batch_mark_suggestions()` 寫入 suggestions 陣列 | + +### Rust 後端 + +| 檔案 | 改動 | +|------|------| +| `identity_agent_api.rs` | `bind_speakers()` 改查 TKG suggested | +| `identity_binding.rs` | `list_pending_persons()` 已改為查 TKG ✓ | + +### 前端 + +| 檔案 | 改動 | +|------|------| +| `PeopleView.vue` | 顯示多重綁定、is_known 標記、觸發按鈕 | +| `store.ts` | `ensureFaceCandidates` 傳 fileUuid ✓ | +| `api/index.ts` | 新增 `run_identity_for_seed` API 映射 | + +### Pending People Card UI + +每個 pending person 卡片新增觸發按鈕(右上角 refresh icon): +- 點擊後觸發單一 seed 匹配 +- 顯示匹配結果數量 +- 自動重新載入 pending people 列表 + +--- + +## 12. 版本歷史 + +| 版本 | 日期 | 說明 | +|------|------|------| +| V1.0 | 2026-05-07 | 初始設計 | +| V2.0 | 2026-07-15 | 重新設計:多重綁定、只綁定不確認、TKG 雙寫入 | +| V2.1 | 2026-07-16 | 新增 run-for-seed API、前端觸發按鈕 | +| V2.2 | 2026-07-16 | 新增 Cluster Agent(無 seed 分群)、cluster-agent API | diff --git a/docs_v1.0/OPERATIONS/LLM_MODELS_GUIDE.md b/docs_v1.0/OPERATIONS/LLM_MODELS_GUIDE.md new file mode 100644 index 0000000..f481dcc --- /dev/null +++ b/docs_v1.0/OPERATIONS/LLM_MODELS_GUIDE.md @@ -0,0 +1,274 @@ +--- +document_type: "reference_doc" +service: "MOMENTRY_CORE" +title: "LLM 模型服務管理與配置指南" +date: "2026-07-16" +version: "V1.0" +status: "active" +owner: "Warren" +created_by: "OpenCode" +tags: + - "momentry" + - "llm" + - "model" + - "configuration" +ai_query_hints: + - "查詢 LLM 模型服務管理與配置指南 的內容" + - "如何切換 momentry 使用的 LLM 模型?" + - "有哪些 LLM 模型可以選擇?" + - "如何啟動或關閉 LLM 模型服務?" +--- + +# LLM 模型服務管理與配置指南 + +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-07-16 | +| 文件版本 | V1.0 | + +--- + +## 概述 + +Momentry Core 支援多種 LLM 後端服務。本文說明如何: +- 啟動 / 停止各模型服務 +- 配置 momentry_core 使用不同模型 +- 管理模型記憶體用量(M5 Max 128GB) + +--- + +## 服務總覽 + +| 端口 | 服務類型 | 模型 | 記憶體 | 文字 | Vision | 速度 | +|------|----------|------|--------|------|--------|------| +| `:11434` | **Ollama** (常駐) | Qwen2.5-VL:7b | 8.5GB | ✅ 中文 | ✅ 正確 | 快 | +| `:11434` | **Ollama** (常駐) | LLaVA | 合併 | ⚠️ 英文 | ⚠️ 部分正確 | 快 | +| `:11434` | **Ollama** (常駐) | Qwen2.5:7b | 合併 | ✅ 最佳中文 | — | 快 | +| `:8090` | **llama.cpp** (按需) | Qwen2.5-VL-7B | 12.4GB | ✅ 中文 | ❌ 亂碼(bug) | 109 t/s | +| `:8091` | **llama.cpp** (按需) | LLaVA 1.6 Vicuna 13B | 11.2GB | ✅ 中文 | ✅ 顏色正確 | 62 t/s | +| `:8092` | **llama.cpp** (按需) | Gemma 3 12B | 16.0GB | ⚠️ 英文 | ✅ **中英雙語** | 60 t/s | +| `:8081-8083` | **MarkBaseEngine** (既有) | Gemma-4 系列 | 各 0.1-2.9GB | ⚠️ 已棄用 | ❌ | — | + +**結論**:日常使用 **Ollama** 即可滿足多數需求。需要更強 Vision 時才啟動 `llama.cpp` 的 Gemma 3 12B 或 LLaVA 13B。 + +--- + +## 服務管理 + +### Ollama(預設常駐) + +Ollama 已配置為 launchd 服務,開機自動啟動。 + +```bash +# 啟動 +sudo launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist + +# 停止 +sudo launchctl unload /Library/LaunchDaemons/com.momentry.ollama.plist + +# 狀態 +launchctl list | grep ollama + +# 可用模型 +ollama ls + +# 手動執行 +ollama run qwen2.5vl:7b # 多語言 + Vision +ollama run llava # Vision(英文) +ollama run qwen2.5:7b # 純文字(最佳中文) +ollama run llama3.1 # 純文字(英文) +ollama run gemma2:9b # 純文字(多語言) +``` + +### llama.cpp(按需啟動) + +llama.cpp 模型預設**不開機啟動**(節省記憶體)。使用時手動啟動,用完關閉。 + +**一鍵管理腳本**: + +```bash +# 啟動模型(第一次會自動從 HuggingFace 下載) +~/models/llama-cpp/run_model.sh qwen2.5-vl # Qwen2.5-VL-7B on :8090 +~/models/llama-cpp/run_model.sh llava-13b # LLaVA 1.6 13B on :8091 +~/models/llama-cpp/run_model.sh gemma3-12b # Gemma 3 12B on :8092 + +# 關閉全部 +~/models/llama-cpp/run_model.sh stop + +# 指定端口 +~/models/llama-cpp/run_model.sh gemma3-12b 8085 +``` + +**手動啟動**: + +```bash +llama-server -hf ggml-org/Qwen2.5-VL-7B-Instruct-GGUF --port 8090 -ngl 99 +llama-server -hf cjpais/llava-v1.6-vicuna-13b-gguf --port 8091 -ngl 99 +llama-server -hf ggml-org/gemma-3-12b-it-GGUF --port 8092 -ngl 99 +``` + +**模型快取位置**(已下載後不需重複下載): +``` +~/.cache/huggingface/hub/models--ggml-org--Qwen2.5-VL-7B-Instruct-GGUF/ +~/.cache/huggingface/hub/models--cjpais--llava-v1.6-vicuna-13b-gguf/ +~/.cache/huggingface/hub/models--ggml-org--gemma-3-12b-it-GGUF/ +``` + +### MLX-VLM(可選) + +```bash +# 安裝 +pip install mlx-vlm + +# 啟動 +nohup python3 /tmp/mlx_server.py \ + --model mlx-community/LLaVA-1.5-7B-4bit --port 8093 \ + > ~/models/llama-cpp/mlx-vlm.log 2>&1 & +``` + +> **注意**:MLX-VLM 目前 vision API 不完整,僅支援純文字。建議優先使用 Ollama 或 llama.cpp。 + +--- + +## Momentry Core 配置 + +### 環境變數(`.env` / `.env.development`) + +momentry_core 透過以下環境變數決定使用的 LLM 後端: + +| 變數 | 預設值 | 說明 | +|------|--------|------| +| `MOMENTRY_LLM_CHAT_URL` | `http://127.0.0.1:8082/v1/chat/completions` | 聊天/工具呼叫端點 | +| `MOMENTRY_LLM_CHAT_MODEL` | `google_gemma-4-26B-A4B-it-Q5_K_M.gguf` | 聊天模型名稱 | +| `MOMENTRY_LLM_VISION_URL` | 同 `CHAT_URL` | Vision 端點 | +| `MOMENTRY_LLM_VISION_MODEL` | 同 `CHAT_MODEL` | Vision 模型名稱 | +| `MOMENTRY_LLM_SUMMARY_URL` | 同 `CHAT_URL` | 摘要端點 | +| `MOMENTRY_LLM_SUMMARY_MODEL` | 同 `CHAT_MODEL` | 摘要模型名稱 | +| `MOMENTRY_LLM_SUMMARY_TIMEOUT` | 120 | 摘要超時(秒) | +| `MOMENTRY_LLM_SUMMARY_ENABLED` | true | 啟用摘要 | + +### 配置範例 + +**情境一:使用 Ollama Qwen2.5:7b(推薦日常)** +```bash +# .env.development 加入 +MOMENTRY_LLM_CHAT_URL=http://localhost:11434/v1/chat/completions +MOMENTRY_LLM_CHAT_MODEL=qwen2.5:7b +MOMENTRY_LLM_VISION_URL=http://localhost:11434/v1/chat/completions +MOMENTRY_LLM_VISION_MODEL=qwen2.5vl:7b +``` + +**情境二:使用 llama.cpp Gemma 3 12B(最佳 vision)** +```bash +# 先啟動模型 +~/models/llama-cpp/run_model.sh gemma3-12b + +# .env.development 加入 +MOMENTRY_LLM_CHAT_URL=http://localhost:8092/v1/chat/completions +MOMENTRY_LLM_CHAT_MODEL=ggml-org/gemma-3-12b-it-GGUF +MOMENTRY_LLM_VISION_URL=http://localhost:8092/v1/chat/completions +MOMENTRY_LLM_VISION_MODEL=ggml-org/gemma-3-12b-it-GGUF +``` + +**情境三:混合使用(聊天用 Ollama,Vision 用 llama.cpp)** +```bash +MOMENTRY_LLM_CHAT_URL=http://localhost:11434/v1/chat/completions +MOMENTRY_LLM_CHAT_MODEL=qwen2.5:7b +MOMENTRY_LLM_VISION_URL=http://localhost:8092/v1/chat/completions +MOMENTRY_LLM_VISION_MODEL=ggml-org/gemma-3-12b-it-GGUF +MOMENTRY_LLM_SUMMARY_URL=http://localhost:11434/v1/chat/completions +MOMENTRY_LLM_SUMMARY_MODEL=qwen2.5:7b +``` + +--- + +## 記憶體管理 + +M5 Max 配備 128GB RAM,所有服務同時運行約使用 66GB: + +| 服務 | 記憶體 | 建議 | +|------|--------|------| +| Ollama (常駐) | ~8.5GB | ✅ 保持開啟 | +| MarkBaseEngine x4 | ~4.5GB | ✅ 保持開啟(既有服務) | +| llama.cpp Qwen2.5-VL | 12.4GB | ❌ 按需啟動 | +| llama.cpp LLaVA 13B | 11.2GB | ❌ 按需啟動 | +| llama.cpp Gemma 3 12B | 16.0GB | ❌ 按需啟動 | +| MLX-VLM | 4.2GB | ❌ 按需啟動 | + +**建議只保留 Ollama 常駐**,llama.cpp 模型在用完後立即關閉: +```bash +~/models/llama-cpp/run_model.sh stop +``` + +--- + +## API 相容性 + +所有服務皆支援 OpenAI-compatible API: + +### 純文字請求 +```bash +curl http://localhost:{PORT}/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{ + "messages": [{"role": "user", "content": "你好"}], + "max_tokens": 100 + }' +``` + +### Vision 請求 +```bash +B64=$(base64 -i image.png) +curl http://localhost:{PORT}/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d "{ + \"messages\": [{\"role\": \"user\", \"content\": [ + {\"type\": \"image_url\", \"image_url\": {\"url\": \"data:image/png;base64,${B64}\"}}, + {\"type\": \"text\", \"text\": \"請描述這張圖片\"} + ]}], + \"max_tokens\": 500 + }" +``` + +--- + +## 健康檢查 + +```bash +# 檢查 Ollama +curl -s http://localhost:11434/api/tags | python3 -c "import sys,json; [print(m['name']) for m in json.load(sys.stdin)['models']]" + +# 檢查 llama.cpp +for port in 8090 8091 8092; do + result=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:$port/v1/chat/completions \ + -H "Content-Type: application/json" \ + -d '{"messages":[{"role":"user","content":"hi"}],"max_tokens":1,"stream":false}' 2>/dev/null) + echo "Port $port: HTTP $result" +done +``` + +--- + +## 常見問題 + +### Q: 啟動 llama.cpp 時 port 已被佔用? +現有服務佔用 port 對照: +| Port | 服務 | +|------|------| +| 8080-8083 | MarkBaseEngine (Gemma-4) | +| 8084 | Embedding Server | +| 11434 | Ollama | + +使用其他 port:`~/models/llama-cpp/run_model.sh gemma3-12b 8095` + +### Q: 模型下載很慢? +第一次下載後會快取在 `~/.cache/huggingface/hub/`,之後不需重複下載。可預先下載: +```bash +ls ~/.cache/huggingface/hub/ | grep models-- +``` + +### Q: 哪個模型最適合中文 + Vision? +1. **Ollama Qwen2.5-VL:7b** — 中文 vision 最準確,已常駐 +2. **Gemma 3 12B (llama.cpp)** — 能以中文描述 vision 結果(含注音) +3. **LLaVA 1.6 13B (llama.cpp)** — vision 正確但僅英文輸出 diff --git a/docs_v1.0/doc_developer/17_progress.html b/docs_v1.0/doc_developer/17_progress.html new file mode 100644 index 0000000..6fca6d5 --- /dev/null +++ b/docs_v1.0/doc_developer/17_progress.html @@ -0,0 +1,1120 @@ + + + + +17 Progress - Momentry API Docs + + + +
+ + + + + +

Progress Tracking — API Workspace Module

+

Overview

+

The progress tracking system provides real-time visibility into all processing stages:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
SystemRedis KeyCoverage
Processor Progress{prefix}progress:{file_uuid}7 main processors (cut, asr, asrx, ocr, face, pose, appearance)
TKG Progress{prefix}progress:{file_uuid}:tkg18 TKG build phases (9 node types + 8 edge types + face_tracing)
Agent Progress{prefix}progress:{file_uuid}:agent5 Identity Agent phases
+
+

POST /api/v1/progress/:file_uuid

+

Auth: Required +Scope: file-level

+

Get real-time processing progress including processor status, TKG build phases, and identity agent phases.

+

Example

+
curl -s -X POST "$API/api/v1/progress/$FILE_UUID" \
+  -H "X-API-Key: $KEY" | jq '.'
+
+ +

Response (200)

+
{
+  "file_uuid": "3a6c1865...",
+  "overall_progress": 71,
+  "cpu_percent": 45.2,
+  "gpu_percent": 30.1,
+  "memory_percent": 62.4,
+  "processors": [
+    {"name": "asr", "status": "complete", "progress": 100, "current": 0, "total": 0, "message": "done"},
+    {"name": "face", "status": "complete", "progress": 100, "current": 0, "total": 0, "message": "done"},
+    {"name": "pose", "status": "complete", "progress": 100, "current": 0, "total": 0, "message": "done"}
+  ],
+  "tkg_progress": {
+    "file_uuid": "3a6c1865...",
+    "phase": "mutual_gaze_edges",
+    "phase_index": 13,
+    "total_phases": 18,
+    "phase_progress": 0.8,
+    "overall_progress": 0.72,
+    "stats": {
+      "total_faces": 1250,
+      "traced_faces": 1250,
+      "total_traces": 45,
+      "face_track_nodes": 45,
+      "gaze_track_nodes": 45,
+      "lip_track_nodes": 12,
+      "text_region_nodes": 8,
+      "appearance_nodes": 38,
+      "accessory_nodes": 5,
+      "object_nodes": 156,
+      "hand_nodes": 22,
+      "speaker_nodes": 14,
+      "co_occurrence_edges": 890,
+      "speaker_face_edges": 120,
+      "face_face_edges": 234,
+      "mutual_gaze_edges": 67,
+      "total_nodes": 345,
+      "total_edges": 1311
+    },
+    "message": "67 mutual gaze edges",
+    "updated_at": "2026-07-02T10:30:00Z"
+  },
+  "agent_progress": {
+    "file_uuid": "3a6c1865...",
+    "phase": "completed",
+    "phase_index": 5,
+    "total_phases": 5,
+    "phase_progress": 1.0,
+    "overall_progress": 1.0,
+    "stats": {
+      "total_faces": 1250,
+      "total_traces": 45,
+      "clusters": 18,
+      "identities_created": 18,
+      "tmdb_matches": 5,
+      "speaker_bindings": 12,
+      "confirmations": 18
+    },
+    "message": "Identity Agent processing completed",
+    "updated_at": "2026-07-02T10:28:00Z"
+  }
+}
+
+ +

Field Descriptions

+

Top Level

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
file_uuidstring32-char hex UUID
overall_progressintegerOverall processor progress (0–100)
processorsarrayPer-processor status
tkg_progressobjectTKG build progress (null if not started)
agent_progressobjectIdentity Agent progress (null if not started)
+

TKG Progress Fields

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
phasestringCurrent phase name (see TKG Phases below)
phase_indexintegerCurrent phase index (0–17)
total_phasesintegerTotal phases: 18
phase_progressfloatProgress within current phase (0.0–1.0)
overall_progressfloatOverall TKG progress (0.0–1.0)
statsobjectCounts for all node and edge types
messagestringHuman-readable status message
+

TKG Phases (18 total)

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IndexPhaseDescription
0face_tracingPopulate trace_id from face.json
1face_track_nodesBuild face_track nodes
2gaze_track_nodesBuild gaze_track nodes
3lip_track_nodesBuild lip_track nodes
4text_region_nodesBuild text_region nodes
5appearance_nodesBuild appearance_trace nodes
6accessory_nodesBuild accessory nodes
7object_nodesBuild yolo_object nodes
8hand_nodesBuild hand nodes
9speaker_nodesBuild speaker nodes
10co_occurrence_edgesBuild co_occurrence edges
11speaker_face_edgesBuild speaker_face edges
12face_face_edgesBuild face_face edges
13mutual_gaze_edgesBuild mutual_gaze edges
14lip_sync_edgesBuild lip_sync edges
15has_appearance_edgesBuild has_appearance edges
16wears_edgesBuild wears edges
17hand_object_edgesBuild hand_object edges
+

TKG Stats Fields

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
total_facesintegerTotal face detections
traced_facesintegerFaces with trace_id assigned
total_tracesintegerUnique trace count
face_track_nodesintegerFace track nodes created
gaze_track_nodesintegerGaze track nodes created
lip_track_nodesintegerLip track nodes created
text_region_nodesintegerText region nodes created
appearance_nodesintegerAppearance trace nodes created
accessory_nodesintegerAccessory nodes created
object_nodesintegerYOLO object nodes created
hand_nodesintegerHand nodes created
speaker_nodesintegerSpeaker nodes created
co_occurrence_edgesintegerCo-occurrence edges created
speaker_face_edgesintegerSpeaker-face edges created
face_face_edgesintegerFace-face edges created
mutual_gaze_edgesintegerMutual gaze edges created
lip_sync_edgesintegerLip sync edges created
has_appearance_edgesintegerHas-appearance edges created
wears_edgesintegerWears edges created
hand_object_edgesintegerHand-object edges created
total_nodesintegerTotal nodes (sum of all node types)
total_edgesintegerTotal edges (sum of all edge types)
+
+

GET /api/v1/stats/ingestion-status/:file_uuid

+

Auth: Required +Scope: file-level

+

Get detailed ingestion status showing completion of all 24 processing steps.

+

Example

+
curl -s "$API/api/v1/stats/ingestion-status/$FILE_UUID" \
+  -H "X-API-Key: $KEY" | jq '.steps[] | {name, status, detail}'
+
+ +

Response (200)

+
{
+  "file_uuid": "3a6c1865...",
+  "steps": [
+    {"name": "rule1_sentence", "status": "done", "detail": "156 sentence chunks"},
+    {"name": "auto_vectorize", "status": "done", "detail": "156 embedded"},
+    {"name": "face_track", "status": "done", "detail": "45 traces / 1250 detections"},
+    {"name": "trace_chunks", "status": "done", "detail": "45 trace chunks"},
+    {"name": "tkg_face_track", "status": "done", "detail": "45 nodes"},
+    {"name": "tkg_gaze_track", "status": "done", "detail": "45 nodes"},
+    {"name": "tkg_lip_track", "status": "done", "detail": "12 nodes"},
+    {"name": "tkg_text_region", "status": "done", "detail": "8 nodes"},
+    {"name": "tkg_appearance", "status": "done", "detail": "38 nodes"},
+    {"name": "tkg_accessory", "status": "done", "detail": "5 nodes"},
+    {"name": "tkg_object", "status": "done", "detail": "156 nodes"},
+    {"name": "tkg_hand", "status": "done", "detail": "22 nodes"},
+    {"name": "tkg_speaker", "status": "done", "detail": "14 nodes"},
+    {"name": "tkg_co_occurrence", "status": "done", "detail": "890 edges"},
+    {"name": "tkg_speaker_face", "status": "done", "detail": "120 edges"},
+    {"name": "tkg_face_face", "status": "done", "detail": "234 edges"},
+    {"name": "tkg_mutual_gaze", "status": "done", "detail": "67 edges"},
+    {"name": "tkg_lip_sync", "status": "done", "detail": "12 edges"},
+    {"name": "tkg_has_appearance", "status": "done", "detail": "38 edges"},
+    {"name": "tkg_wears", "status": "done", "detail": "22 edges"},
+    {"name": "tkg_hand_object", "status": "done", "detail": "18 edges"},
+    {"name": "rule2_relationship", "status": "done", "detail": "1331 relationship chunks"},
+    {"name": "identity_match", "status": "done", "detail": "18 identities matched"},
+    {"name": "scene_metadata", "status": "done", "detail": null}
+  ],
+  "related_identities": [
+    {"uuid": "a9a901056d6b46ff92da0c3c1a57dff4", "name": "John Smith"}
+  ],
+  "strangers": 3
+}
+
+ +

Step Descriptions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StepStatus When Done
rule1_sentencesentence_count > 0
auto_vectorizesentence_embedded > 0
face_tracktrace_count > 0
trace_chunkstrace_chunks > 0
tkg_face_track → tkg_speakerNode count > 0 (9 steps)
tkg_co_occurrence → tkg_hand_objectEdge count > 0 (8 steps)
rule2_relationshiprelationship_chunks > 0
identity_matchidentity_count > 0
scene_metadatascene_meta.json exists
+
+

POST /api/v1/file/:file_uuid/tkg/rebuild

+

Auth: Required +Scope: file-level

+

Manually trigger TKG rebuild. Automatically triggers Rule 2 ingestion after TKG completes.

+

Example

+
curl -s -X POST "$API/api/v1/file/$FILE_UUID/tkg/rebuild" \
+  -H "X-API-Key: $KEY" \
+  -H "Content-Type: application/json" -d '{}'
+
+ +

Response (200)

+
{
+  "success": true,
+  "message": "TKG rebuild started",
+  "nodes": 345,
+  "edges": 1311
+}
+
+ +
+

POST /api/v1/file/:file_uuid/rule2

+

Auth: Required +Scope: file-level

+

Manually trigger Rule 2 ingestion (TKG edges → relationship chunks).

+

Example

+
curl -s -X POST "$API/api/v1/file/$FILE_UUID/rule2" \
+  -H "X-API-Key: $KEY" \
+  -H "Content-Type: application/json" -d '{}'
+
+ +

Response (200)

+
{
+  "success": true,
+  "message": "Rule 2 ingestion: 1331 relationship chunks created",
+  "rule2_count": 1331
+}
+
+ +
+

Processing Pipeline Flow

+
1. Processors (concurrent)
+   ├── cut, asr, ocr, face, pose, appearance → complete
+   └── asrx → after cut+asr
+
+2. Post-Processor Triggers (automatic)
+   ├── Rule 1 Ingestion (ASR+OCR → sentence chunks)
+   ├── Face Trace + DB Store (face_traced.json → Qdrant trace_id)
+   ├── TMDb Face Matching (if enabled)
+   ├── Heuristic Scene Metadata
+   ├── Identity Agent (face + ASRX)
+   └── TKG Build (automatic after processors complete)
+       └── Rule 2 Ingestion (automatic after TKG)
+           └── Relationship chunks vectorized
+
+3. Completion
+   └── Job marked completed when all ingestion steps done
+
+ +

Error Codes

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CodeHTTPWhen
E001400Invalid file_uuid format
E002404File not found
E003404No TKG data available
E010500Qdrant connection failed
E011500Database connection failed
+
+

GET /api/v1/stats/pipeline/:file_uuid

+

Auth: Required +Scope: file-level

+

Get segmented pipeline progress with weighted stage breakdown. Shows overall progress as weighted sum of all pipeline stages.

+

Pipeline Stages and Weights

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
StageWeightDescription
processors30%7 concurrent processors (cut, asr, asrx, ocr, face, pose, appearance)
rule1_ingestion5%ASR+OCR → sentence chunks
face_tracing5%Face trace_id assignment
identity_agent10%Identity creation, TMDb matching, speaker binding
tkg_nodes20%TKG node building (9 node types)
tkg_edges15%TKG edge building (8 edge types)
rule2_ingestion15%TKG edges → relationship chunks
+

Example

+
curl -s "$API/api/v1/stats/pipeline/$FILE_UUID" \
+  -H "X-API-Key: $KEY" | jq '.'
+
+ +

Response (200)

+
{
+  "file_uuid": "3a6c1865...",
+  "overall_progress": 0.65,
+  "stages": [
+    {"name": "processors", "weight": 0.30, "progress": 1.0, "status": "completed", "detail": "7/7 complete"},
+    {"name": "rule1_ingestion", "weight": 0.05, "progress": 1.0, "status": "completed", "detail": "156 chunks"},
+    {"name": "face_tracing", "weight": 0.05, "progress": 1.0, "status": "completed", "detail": "45 traces"},
+    {"name": "identity_agent", "weight": 0.10, "progress": 1.0, "status": "completed", "detail": "18 identities"},
+    {"name": "tkg_nodes", "weight": 0.20, "progress": 1.0, "status": "completed", "detail": "345 nodes"},
+    {"name": "tkg_edges", "weight": 0.15, "progress": 0.5, "status": "running", "detail": "mutual_gaze_edges: 67/8 expected"},
+    {"name": "rule2_ingestion", "weight": 0.15, "progress": 0.0, "status": "pending", "detail": null}
+  ],
+  "updated_at": "2026-07-02T10:30:00Z"
+}
+
+ +

Field Descriptions

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
file_uuidstring32-char hex UUID
overall_progressfloatWeighted sum of all stage progress (0.0–1.0)
stagesarrayPer-stage progress breakdown
stages[].namestringStage name
stages[].weightfloatStage weight in overall progress
stages[].progressfloatStage completion (0.0–1.0)
stages[].statusstring"pending", "running", "completed", "failed"
stages[].detailstringHuman-readable detail (optional)
updated_atstringISO 8601 timestamp
+

Overall Progress Calculation

+
overall_progress = Σ(stage.weight × stage.progress) for all stages
+
+ +

Example calculation: +- processors: 0.30 × 1.0 = 0.30 +- rule1_ingestion: 0.05 × 1.0 = 0.05 +- face_tracing: 0.05 × 1.0 = 0.05 +- identity_agent: 0.10 × 1.0 = 0.10 +- tkg_nodes: 0.20 × 1.0 = 0.20 +- tkg_edges: 0.15 × 0.5 = 0.075 +- rule2_ingestion: 0.15 × 0.0 = 0.0 +- Total: 0.775 (77.5%)

+
+

GET /api/v1/stats/file/:file_uuid

+

Auth: Required +Scope: file-level

+

Get comprehensive file statistics from all data sources: JSON processing status, PostgreSQL counts, Qdrant collections, TKG nodes/edges, and Identity Agent stats.

+

Example

+
curl -s "$API/api/v1/stats/file/$FILE_UUID" \
+  -H "X-API-Key: $KEY" | jq '.'
+
+ +

Response (200)

+
{
+  "file_uuid": "3a6c1865...",
+  "file_name": "video.mp4",
+  "status": "processing",
+  "processors": [
+    {"name": "asr", "status": "complete", "progress": 100, "message": "done"},
+    {"name": "face", "status": "complete", "progress": 100, "message": "done"}
+  ],
+  "postgres": {
+    "sentence_chunks": 156,
+    "trace_chunks": 45,
+    "relationship_chunks": 1331,
+    "identities": 18,
+    "file_identities": 18
+  },
+  "qdrant": {
+    "faces": 1250,
+    "face_traces": 45,
+    "face_identities": 18,
+    "text_chunks": 4562,
+    "speakers": 434
+  },
+  "tkg": {
+    "total_nodes": 345,
+    "total_edges": 1311,
+    "face_track_nodes": 45,
+    "gaze_track_nodes": 45,
+    "lip_track_nodes": 12,
+    "text_region_nodes": 8,
+    "appearance_nodes": 38,
+    "accessory_nodes": 5,
+    "object_nodes": 156,
+    "hand_nodes": 22,
+    "speaker_nodes": 14,
+    "co_occurrence_edges": 890,
+    "speaker_face_edges": 120,
+    "face_face_edges": 234,
+    "mutual_gaze_edges": 67,
+    "lip_sync_edges": 12,
+    "has_appearance_edges": 38,
+    "wears_edges": 22,
+    "hand_object_edges": 18
+  },
+  "identity_agent": {
+    "clusters": 18,
+    "identities_created": 18,
+    "tmdb_matches": 5,
+    "speaker_bindings": 12,
+    "confirmations": 18
+  }
+}
+
+ +

Field Descriptions

+

Top Level

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
file_uuidstring32-char hex UUID
file_namestringOriginal filename
statusstringFile status: registered, processing, completed, failed
processorsarrayPer-processor status from processing_status JSONB
postgresobjectPostgreSQL table counts
qdrantobjectQdrant collection point counts
tkgobjectTKG node and edge counts by type
identity_agentobjectIdentity Agent statistics
+

PostgreSQL Stats

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
sentence_chunksintegerRule 1 sentence chunks count
trace_chunksintegerFace trace chunks count
relationship_chunksintegerRule 2 relationship chunks count
identitiesintegerUnique identities bound to this file
file_identitiesintegerFile-identity mapping records
+

Qdrant Stats

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
facesintegerTotal face points in _faces collection
face_tracesintegerUnique trace IDs in _faces
face_identitiesintegerUnique identity IDs bound in _faces
text_chunksintegerText chunk vectors in momentry_*_rule1_v2
speakersintegerSpeaker segments in momentry_*_speaker
+

TKG Stats

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
total_nodesintegerSum of all node types
total_edgesintegerSum of all edge types
face_track_nodesintegerFace track nodes
gaze_track_nodesintegerGaze track nodes
lip_track_nodesintegerLip track nodes
text_region_nodesintegerText region nodes
appearance_nodesintegerAppearance trace nodes
accessory_nodesintegerAccessory nodes
object_nodesintegerYOLO object nodes
hand_nodesintegerHand nodes
speaker_nodesintegerSpeaker nodes
co_occurrence_edgesintegerCo-occurrence edges
speaker_face_edgesintegerSpeaker-face edges
face_face_edgesintegerFace-face edges
mutual_gaze_edgesintegerMutual gaze edges
lip_sync_edgesintegerLip sync edges
has_appearance_edgesintegerHas-appearance edges
wears_edgesintegerWears edges
hand_object_edgesintegerHand-object edges
+

Identity Agent Stats

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
clustersintegerFace clusters from face_clustered.json
identities_createdintegerIdentities created from clusters
tmdb_matchesintegerTMDb identity matches
speaker_bindingsintegerSpeaker-to-identity bindings
confirmationsintegerConfirmed identity bindings
+
+ + \ No newline at end of file diff --git a/docs_v1.0/doc_developer/18_profile.html b/docs_v1.0/doc_developer/18_profile.html new file mode 100644 index 0000000..6ce8f19 --- /dev/null +++ b/docs_v1.0/doc_developer/18_profile.html @@ -0,0 +1,511 @@ + + + + +18 Profile - Momentry API Docs + + + +
+ + + + + +

Profile Management

+

Endpoints for managing trace profiles (face track metadata stored in TKG) and file profiles (video metadata stored in PostgreSQL).

+

GET /api/v1/trace-profile

+

Auth: Required +Scope: file-level

+

Read a single face trace's profile including name, key frame, key face, and multi-language aliases.

+

Request Parameters

+ + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeRequiredDescription
file_uuidstringYesFile UUID
trace_idintegerYesTrace ID (numeric)
+

Example

+
curl -s "$API/api/v1/trace-profile?file_uuid=$FILE_UUID&trace_id=7" \
+  -H "X-API-Key: $KEY"
+
+ +

Response (200)

+
{
+  "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5",
+  "trace_id": 7,
+  "name": "John Doe",
+  "key_frame": 640,
+  "key_face": "face_12345",
+  "aliases": {
+    "en": "John Doe",
+    "zh": "約翰"
+  },
+  "properties": {
+    "status": "bound",
+    "avg_bbox": {"x": 899, "y": 212, "width": 342, "height": 342},
+    "start_frame": 624,
+    "end_frame": 669,
+    "frame_count": 5
+  }
+}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
file_uuidstringFile UUID
trace_idintegerTrace ID
namestringDisplay name (from tkg_nodes.label)
key_frameintegerRepresentative frame number, or null
key_facestringRepresentative face ID, or null
aliasesobjectMulti-language name aliases
propertiesobjectFull TKG node properties (bbox, frames, etc.)
+

Error Responses

+ + + + + + + + + + + + + + + + + +
HTTPWhen
404Trace not found
401Missing or invalid API key
+
+

PUT /api/v1/trace-profile

+

Auth: Required +Scope: file-level

+

Update a single face trace's profile fields. Only provided fields are updated; others remain unchanged.

+

Request Body

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeRequiredDescription
file_uuidstringYesFile UUID
trace_idintegerYesTrace ID to update
namestringNoNew display name
key_frameintegerNoRepresentative frame number
key_facestringNoRepresentative face ID
aliasesobjectNoMulti-language aliases {"en": "...", "zh": "..."}
propertiesobjectNoAdditional properties to merge into existing JSONB
+

Example

+
curl -s -X PUT "$API/api/v1/trace-profile" \
+  -H "X-API-Key: $KEY" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5",
+    "trace_id": 7,
+    "name": "John Doe",
+    "key_frame": 640,
+    "key_face": "face_12345",
+    "aliases": {"en": "John Doe", "zh": "約翰"}
+  }'
+
+ +

Response (200)

+
{
+  "success": true,
+  "message": "Trace profile updated",
+  "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5",
+  "trace_id": 7
+}
+
+ +
+

PUT /api/v1/trace-profile/group

+

Auth: Required +Scope: file-level

+

Batch update the name (label) for multiple traces in a face group. Used when renaming a group.

+

Request Body

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeRequiredDescription
file_uuidstringYesFile UUID
trace_idsinteger[]YesList of trace IDs to update
namestringYesNew group name for all traces
+

Example

+
curl -s -X PUT "$API/api/v1/trace-profile/group" \
+  -H "X-API-Key: $KEY" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5",
+    "trace_ids": [7, 2, 13],
+    "name": "Group A"
+  }'
+
+ +

Response (200)

+
{
+  "success": true,
+  "message": "Updated 3 traces in group",
+  "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5",
+  "updated_count": 3
+}
+
+ +
+

GET /api/v1/file-profile

+

Auth: Required +Scope: file-level

+

Read a file's metadata including path, name, status, and technical details.

+

Request Parameters

+ + + + + + + + + + + + + + + + + +
FieldTypeRequiredDescription
file_uuidstringYesFile UUID
+

Example

+
curl -s "$API/api/v1/file-profile?file_uuid=$FILE_UUID" \
+  -H "X-API-Key: $KEY"
+
+ +

Response (200)

+
{
+  "file_uuid": "49884ce1c341953d1ad7bf67a77c30cc",
+  "file_name": "Dedicatoria.mp4",
+  "file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Dedicatoria.mp4",
+  "status": "completed",
+  "duration": 93.33,
+  "width": 1280,
+  "height": 720,
+  "fps": 30.0,
+  "total_frames": 0
+}
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
file_uuidstringFile UUID
file_namestringFile name
file_pathstringFull filesystem path
statusstringpending, processing, completed, failed
durationfloatDuration in seconds
widthintegerVideo width in pixels
heightintegerVideo height in pixels
fpsfloatFrames per second
total_framesintegerTotal frame count
+
+

PUT /api/v1/file-profile

+

Auth: Required +Scope: file-level

+

Update file metadata, typically used when a file is moved to a new location.

+

Request Body

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeRequiredDescription
file_uuidstringYesFile UUID
file_pathstringNoNew filesystem path
file_namestringNoNew file name
+

Example

+
curl -s -X PUT "$API/api/v1/file-profile" \
+  -H "X-API-Key: $KEY" \
+  -H "Content-Type: application/json" \
+  -d '{
+    "file_uuid": "49884ce1c341953d1ad7bf67a77c30cc",
+    "file_path": "/new/location/Dedicatoria.mp4"
+  }'
+
+ +

Response (200)

+
{
+  "success": true,
+  "message": "File profile updated",
+  "file_uuid": "49884ce1c341953d1ad7bf67a77c30cc"
+}
+
+ +
+

Data Storage

+ + + + + + + + + + + + + + + + + + + + +
Profile TypeStorageTable
trace_profilePostgreSQL (TKG)tkg_nodes where node_type='face_track'
file_profilePostgreSQLvideos
+

Trace Profile Fields

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTKG ColumnDescription
namelabelDisplay name for the trace
key_frameproperties->'key_frame'Representative frame number
key_faceproperties->'key_face'Representative face ID
aliasesproperties->'aliases'Multi-language name aliases
+
+

Updated: 2026-07-18 — New profile module: trace-profile (GET, PUT, PUT group) and file-profile (GET, PUT)

+
+ + \ No newline at end of file diff --git a/docs_v1.0/doc_wasm/modules/17_progress.md b/docs_v1.0/doc_wasm/modules/17_progress.md new file mode 100644 index 0000000..6188025 --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/17_progress.md @@ -0,0 +1,545 @@ + + + + +# Progress Tracking — API Workspace Module + +## Overview + +The progress tracking system provides real-time visibility into all processing stages: + +| System | Redis Key | Coverage | +|--------|-----------|----------| +| **Processor Progress** | `{prefix}progress:{file_uuid}` | 7 main processors (cut, asr, asrx, ocr, face, pose, appearance) | +| **TKG Progress** | `{prefix}progress:{file_uuid}:tkg` | 18 TKG build phases (9 node types + 8 edge types + face_tracing) | +| **Agent Progress** | `{prefix}progress:{file_uuid}:agent` | 5 Identity Agent phases | + +--- + +## `POST /api/v1/progress/:file_uuid` + +**Auth**: Required +**Scope**: file-level + +Get real-time processing progress including processor status, TKG build phases, and identity agent phases. + +### Example + +```bash +curl -s -X POST "$API/api/v1/progress/$FILE_UUID" \ + -H "X-API-Key: $KEY" | jq '.' +``` + +### Response (200) + +```json +{ + "file_uuid": "3a6c1865...", + "overall_progress": 71, + "cpu_percent": 45.2, + "gpu_percent": 30.1, + "memory_percent": 62.4, + "processors": [ + {"name": "asr", "status": "complete", "progress": 100, "current": 0, "total": 0, "message": "done"}, + {"name": "face", "status": "complete", "progress": 100, "current": 0, "total": 0, "message": "done"}, + {"name": "pose", "status": "complete", "progress": 100, "current": 0, "total": 0, "message": "done"} + ], + "tkg_progress": { + "file_uuid": "3a6c1865...", + "phase": "mutual_gaze_edges", + "phase_index": 13, + "total_phases": 18, + "phase_progress": 0.8, + "overall_progress": 0.72, + "stats": { + "total_faces": 1250, + "traced_faces": 1250, + "total_traces": 45, + "face_track_nodes": 45, + "gaze_track_nodes": 45, + "lip_track_nodes": 12, + "text_region_nodes": 8, + "appearance_nodes": 38, + "accessory_nodes": 5, + "object_nodes": 156, + "hand_nodes": 22, + "speaker_nodes": 14, + "co_occurrence_edges": 890, + "speaker_face_edges": 120, + "face_face_edges": 234, + "mutual_gaze_edges": 67, + "total_nodes": 345, + "total_edges": 1311 + }, + "message": "67 mutual gaze edges", + "updated_at": "2026-07-02T10:30:00Z" + }, + "agent_progress": { + "file_uuid": "3a6c1865...", + "phase": "completed", + "phase_index": 5, + "total_phases": 5, + "phase_progress": 1.0, + "overall_progress": 1.0, + "stats": { + "total_faces": 1250, + "total_traces": 45, + "clusters": 18, + "identities_created": 18, + "tmdb_matches": 5, + "speaker_bindings": 12, + "confirmations": 18 + }, + "message": "Identity Agent processing completed", + "updated_at": "2026-07-02T10:28:00Z" + } +} +``` + +### Field Descriptions + +#### Top Level + +| Field | Type | Description | +|-------|------|-------------| +| `file_uuid` | string | 32-char hex UUID | +| `overall_progress` | integer | Overall processor progress (0–100) | +| `processors` | array | Per-processor status | +| `tkg_progress` | object | TKG build progress (null if not started) | +| `agent_progress` | object | Identity Agent progress (null if not started) | + +#### TKG Progress Fields + +| Field | Type | Description | +|-------|------|-------------| +| `phase` | string | Current phase name (see TKG Phases below) | +| `phase_index` | integer | Current phase index (0–17) | +| `total_phases` | integer | Total phases: 18 | +| `phase_progress` | float | Progress within current phase (0.0–1.0) | +| `overall_progress` | float | Overall TKG progress (0.0–1.0) | +| `stats` | object | Counts for all node and edge types | +| `message` | string | Human-readable status message | + +#### TKG Phases (18 total) + +| Index | Phase | Description | +|-------|-------|-------------| +| 0 | `face_tracing` | Populate trace_id from face.json | +| 1 | `face_track_nodes` | Build face_track nodes | +| 2 | `gaze_track_nodes` | Build gaze_track nodes | +| 3 | `lip_track_nodes` | Build lip_track nodes | +| 4 | `text_region_nodes` | Build text_region nodes | +| 5 | `appearance_nodes` | Build appearance_trace nodes | +| 6 | `accessory_nodes` | Build accessory nodes | +| 7 | `object_nodes` | Build yolo_object nodes | +| 8 | `hand_nodes` | Build hand nodes | +| 9 | `speaker_nodes` | Build speaker nodes | +| 10 | `co_occurrence_edges` | Build co_occurrence edges | +| 11 | `speaker_face_edges` | Build speaker_face edges | +| 12 | `face_face_edges` | Build face_face edges | +| 13 | `mutual_gaze_edges` | Build mutual_gaze edges | +| 14 | `lip_sync_edges` | Build lip_sync edges | +| 15 | `has_appearance_edges` | Build has_appearance edges | +| 16 | `wears_edges` | Build wears edges | +| 17 | `hand_object_edges` | Build hand_object edges | + +#### TKG Stats Fields + +| Field | Type | Description | +|-------|------|-------------| +| `total_faces` | integer | Total face detections | +| `traced_faces` | integer | Faces with trace_id assigned | +| `total_traces` | integer | Unique trace count | +| `face_track_nodes` | integer | Face track nodes created | +| `gaze_track_nodes` | integer | Gaze track nodes created | +| `lip_track_nodes` | integer | Lip track nodes created | +| `text_region_nodes` | integer | Text region nodes created | +| `appearance_nodes` | integer | Appearance trace nodes created | +| `accessory_nodes` | integer | Accessory nodes created | +| `object_nodes` | integer | YOLO object nodes created | +| `hand_nodes` | integer | Hand nodes created | +| `speaker_nodes` | integer | Speaker nodes created | +| `co_occurrence_edges` | integer | Co-occurrence edges created | +| `speaker_face_edges` | integer | Speaker-face edges created | +| `face_face_edges` | integer | Face-face edges created | +| `mutual_gaze_edges` | integer | Mutual gaze edges created | +| `lip_sync_edges` | integer | Lip sync edges created | +| `has_appearance_edges` | integer | Has-appearance edges created | +| `wears_edges` | integer | Wears edges created | +| `hand_object_edges` | integer | Hand-object edges created | +| `total_nodes` | integer | Total nodes (sum of all node types) | +| `total_edges` | integer | Total edges (sum of all edge types) | + +--- + +## `GET /api/v1/stats/ingestion-status/:file_uuid` + +**Auth**: Required +**Scope**: file-level + +Get detailed ingestion status showing completion of all 24 processing steps. + +### Example + +```bash +curl -s "$API/api/v1/stats/ingestion-status/$FILE_UUID" \ + -H "X-API-Key: $KEY" | jq '.steps[] | {name, status, detail}' +``` + +### Response (200) + +```json +{ + "file_uuid": "3a6c1865...", + "steps": [ + {"name": "rule1_sentence", "status": "done", "detail": "156 sentence chunks"}, + {"name": "auto_vectorize", "status": "done", "detail": "156 embedded"}, + {"name": "face_track", "status": "done", "detail": "45 traces / 1250 detections"}, + {"name": "trace_chunks", "status": "done", "detail": "45 trace chunks"}, + {"name": "tkg_face_track", "status": "done", "detail": "45 nodes"}, + {"name": "tkg_gaze_track", "status": "done", "detail": "45 nodes"}, + {"name": "tkg_lip_track", "status": "done", "detail": "12 nodes"}, + {"name": "tkg_text_region", "status": "done", "detail": "8 nodes"}, + {"name": "tkg_appearance", "status": "done", "detail": "38 nodes"}, + {"name": "tkg_accessory", "status": "done", "detail": "5 nodes"}, + {"name": "tkg_object", "status": "done", "detail": "156 nodes"}, + {"name": "tkg_hand", "status": "done", "detail": "22 nodes"}, + {"name": "tkg_speaker", "status": "done", "detail": "14 nodes"}, + {"name": "tkg_co_occurrence", "status": "done", "detail": "890 edges"}, + {"name": "tkg_speaker_face", "status": "done", "detail": "120 edges"}, + {"name": "tkg_face_face", "status": "done", "detail": "234 edges"}, + {"name": "tkg_mutual_gaze", "status": "done", "detail": "67 edges"}, + {"name": "tkg_lip_sync", "status": "done", "detail": "12 edges"}, + {"name": "tkg_has_appearance", "status": "done", "detail": "38 edges"}, + {"name": "tkg_wears", "status": "done", "detail": "22 edges"}, + {"name": "tkg_hand_object", "status": "done", "detail": "18 edges"}, + {"name": "rule2_relationship", "status": "done", "detail": "1331 relationship chunks"}, + {"name": "identity_match", "status": "done", "detail": "18 identities matched"}, + {"name": "scene_metadata", "status": "done", "detail": null} + ], + "related_identities": [ + {"uuid": "a9a901056d6b46ff92da0c3c1a57dff4", "name": "John Smith"} + ], + "strangers": 3 +} +``` + +### Step Descriptions + +| Step | Status When Done | +|------|-----------------| +| `rule1_sentence` | sentence_count > 0 | +| `auto_vectorize` | sentence_embedded > 0 | +| `face_track` | trace_count > 0 | +| `trace_chunks` | trace_chunks > 0 | +| `tkg_face_track` → `tkg_speaker` | Node count > 0 (9 steps) | +| `tkg_co_occurrence` → `tkg_hand_object` | Edge count > 0 (8 steps) | +| `rule2_relationship` | relationship_chunks > 0 | +| `identity_match` | identity_count > 0 | +| `scene_metadata` | scene_meta.json exists | + +--- + +## `POST /api/v1/file/:file_uuid/tkg/rebuild` + +**Auth**: Required +**Scope**: file-level + +Manually trigger TKG rebuild. Automatically triggers Rule 2 ingestion after TKG completes. + +### Example + +```bash +curl -s -X POST "$API/api/v1/file/$FILE_UUID/tkg/rebuild" \ + -H "X-API-Key: $KEY" \ + -H "Content-Type: application/json" -d '{}' +``` + +### Response (200) + +```json +{ + "success": true, + "message": "TKG rebuild started", + "nodes": 345, + "edges": 1311 +} +``` + +--- + +## `POST /api/v1/file/:file_uuid/rule2` + +**Auth**: Required +**Scope**: file-level + +Manually trigger Rule 2 ingestion (TKG edges → relationship chunks). + +### Example + +```bash +curl -s -X POST "$API/api/v1/file/$FILE_UUID/rule2" \ + -H "X-API-Key: $KEY" \ + -H "Content-Type: application/json" -d '{}' +``` + +### Response (200) + +```json +{ + "success": true, + "message": "Rule 2 ingestion: 1331 relationship chunks created", + "rule2_count": 1331 +} +``` + +--- + +## Processing Pipeline Flow + +``` +1. Processors (concurrent) + ├── cut, asr, ocr, face, pose, appearance → complete + └── asrx → after cut+asr + +2. Post-Processor Triggers (automatic) + ├── Rule 1 Ingestion (ASR+OCR → sentence chunks) + ├── Face Trace + DB Store (face_traced.json → Qdrant trace_id) + ├── TMDb Face Matching (if enabled) + ├── Heuristic Scene Metadata + ├── Identity Agent (face + ASRX) + └── TKG Build (automatic after processors complete) + └── Rule 2 Ingestion (automatic after TKG) + └── Relationship chunks vectorized + +3. Completion + └── Job marked completed when all ingestion steps done +``` + +## Error Codes + +| Code | HTTP | When | +|------|------|------| +| E001 | 400 | Invalid file_uuid format | +| E002 | 404 | File not found | +| E003 | 404 | No TKG data available | +| E010 | 500 | Qdrant connection failed | +| E011 | 500 | Database connection failed | + +--- + +## `GET /api/v1/stats/pipeline/:file_uuid` + +**Auth**: Required +**Scope**: file-level + +Get segmented pipeline progress with weighted stage breakdown. Shows overall progress as weighted sum of all pipeline stages. + +### Pipeline Stages and Weights + +| Stage | Weight | Description | +|-------|--------|-------------| +| `processors` | 30% | 7 concurrent processors (cut, asr, asrx, ocr, face, pose, appearance) | +| `rule1_ingestion` | 5% | ASR+OCR → sentence chunks | +| `face_tracing` | 5% | Face trace_id assignment | +| `identity_agent` | 10% | Identity creation, TMDb matching, speaker binding | +| `tkg_nodes` | 20% | TKG node building (9 node types) | +| `tkg_edges` | 15% | TKG edge building (8 edge types) | +| `rule2_ingestion` | 15% | TKG edges → relationship chunks | + +### Example + +```bash +curl -s "$API/api/v1/stats/pipeline/$FILE_UUID" \ + -H "X-API-Key: $KEY" | jq '.' +``` + +### Response (200) + +```json +{ + "file_uuid": "3a6c1865...", + "overall_progress": 0.65, + "stages": [ + {"name": "processors", "weight": 0.30, "progress": 1.0, "status": "completed", "detail": "7/7 complete"}, + {"name": "rule1_ingestion", "weight": 0.05, "progress": 1.0, "status": "completed", "detail": "156 chunks"}, + {"name": "face_tracing", "weight": 0.05, "progress": 1.0, "status": "completed", "detail": "45 traces"}, + {"name": "identity_agent", "weight": 0.10, "progress": 1.0, "status": "completed", "detail": "18 identities"}, + {"name": "tkg_nodes", "weight": 0.20, "progress": 1.0, "status": "completed", "detail": "345 nodes"}, + {"name": "tkg_edges", "weight": 0.15, "progress": 0.5, "status": "running", "detail": "mutual_gaze_edges: 67/8 expected"}, + {"name": "rule2_ingestion", "weight": 0.15, "progress": 0.0, "status": "pending", "detail": null} + ], + "updated_at": "2026-07-02T10:30:00Z" +} +``` + +### Field Descriptions + +| Field | Type | Description | +|-------|------|-------------| +| `file_uuid` | string | 32-char hex UUID | +| `overall_progress` | float | Weighted sum of all stage progress (0.0–1.0) | +| `stages` | array | Per-stage progress breakdown | +| `stages[].name` | string | Stage name | +| `stages[].weight` | float | Stage weight in overall progress | +| `stages[].progress` | float | Stage completion (0.0–1.0) | +| `stages[].status` | string | `"pending"`, `"running"`, `"completed"`, `"failed"` | +| `stages[].detail` | string | Human-readable detail (optional) | +| `updated_at` | string | ISO 8601 timestamp | + +### Overall Progress Calculation + +``` +overall_progress = Σ(stage.weight × stage.progress) for all stages +``` + +Example calculation: +- processors: 0.30 × 1.0 = 0.30 +- rule1_ingestion: 0.05 × 1.0 = 0.05 +- face_tracing: 0.05 × 1.0 = 0.05 +- identity_agent: 0.10 × 1.0 = 0.10 +- tkg_nodes: 0.20 × 1.0 = 0.20 +- tkg_edges: 0.15 × 0.5 = 0.075 +- rule2_ingestion: 0.15 × 0.0 = 0.0 +- **Total: 0.775 (77.5%)** + +--- + +## `GET /api/v1/stats/file/:file_uuid` + +**Auth**: Required +**Scope**: file-level + +Get comprehensive file statistics from all data sources: JSON processing status, PostgreSQL counts, Qdrant collections, TKG nodes/edges, and Identity Agent stats. + +### Example + +```bash +curl -s "$API/api/v1/stats/file/$FILE_UUID" \ + -H "X-API-Key: $KEY" | jq '.' +``` + +### Response (200) + +```json +{ + "file_uuid": "3a6c1865...", + "file_name": "video.mp4", + "status": "processing", + "processors": [ + {"name": "asr", "status": "complete", "progress": 100, "message": "done"}, + {"name": "face", "status": "complete", "progress": 100, "message": "done"} + ], + "postgres": { + "sentence_chunks": 156, + "trace_chunks": 45, + "relationship_chunks": 1331, + "identities": 18, + "file_identities": 18 + }, + "qdrant": { + "faces": 1250, + "face_traces": 45, + "face_identities": 18, + "text_chunks": 4562, + "speakers": 434 + }, + "tkg": { + "total_nodes": 345, + "total_edges": 1311, + "face_track_nodes": 45, + "gaze_track_nodes": 45, + "lip_track_nodes": 12, + "text_region_nodes": 8, + "appearance_nodes": 38, + "accessory_nodes": 5, + "object_nodes": 156, + "hand_nodes": 22, + "speaker_nodes": 14, + "co_occurrence_edges": 890, + "speaker_face_edges": 120, + "face_face_edges": 234, + "mutual_gaze_edges": 67, + "lip_sync_edges": 12, + "has_appearance_edges": 38, + "wears_edges": 22, + "hand_object_edges": 18 + }, + "identity_agent": { + "clusters": 18, + "identities_created": 18, + "tmdb_matches": 5, + "speaker_bindings": 12, + "confirmations": 18 + } +} +``` + +### Field Descriptions + +#### Top Level + +| Field | Type | Description | +|-------|------|-------------| +| `file_uuid` | string | 32-char hex UUID | +| `file_name` | string | Original filename | +| `status` | string | File status: `registered`, `processing`, `completed`, `failed` | +| `processors` | array | Per-processor status from processing_status JSONB | +| `postgres` | object | PostgreSQL table counts | +| `qdrant` | object | Qdrant collection point counts | +| `tkg` | object | TKG node and edge counts by type | +| `identity_agent` | object | Identity Agent statistics | + +#### PostgreSQL Stats + +| Field | Type | Description | +|-------|------|-------------| +| `sentence_chunks` | integer | Rule 1 sentence chunks count | +| `trace_chunks` | integer | Face trace chunks count | +| `relationship_chunks` | integer | Rule 2 relationship chunks count | +| `identities` | integer | Unique identities bound to this file | +| `file_identities` | integer | File-identity mapping records | + +#### Qdrant Stats + +| Field | Type | Description | +|-------|------|-------------| +| `faces` | integer | Total face points in `_faces` collection | +| `face_traces` | integer | Unique trace IDs in `_faces` | +| `face_identities` | integer | Unique identity IDs bound in `_faces` | +| `text_chunks` | integer | Text chunk vectors in `momentry_*_rule1_v2` | +| `speakers` | integer | Speaker segments in `momentry_*_speaker` | + +#### TKG Stats + +| Field | Type | Description | +|-------|------|-------------| +| `total_nodes` | integer | Sum of all node types | +| `total_edges` | integer | Sum of all edge types | +| `face_track_nodes` | integer | Face track nodes | +| `gaze_track_nodes` | integer | Gaze track nodes | +| `lip_track_nodes` | integer | Lip track nodes | +| `text_region_nodes` | integer | Text region nodes | +| `appearance_nodes` | integer | Appearance trace nodes | +| `accessory_nodes` | integer | Accessory nodes | +| `object_nodes` | integer | YOLO object nodes | +| `hand_nodes` | integer | Hand nodes | +| `speaker_nodes` | integer | Speaker nodes | +| `co_occurrence_edges` | integer | Co-occurrence edges | +| `speaker_face_edges` | integer | Speaker-face edges | +| `face_face_edges` | integer | Face-face edges | +| `mutual_gaze_edges` | integer | Mutual gaze edges | +| `lip_sync_edges` | integer | Lip sync edges | +| `has_appearance_edges` | integer | Has-appearance edges | +| `wears_edges` | integer | Wears edges | +| `hand_object_edges` | integer | Hand-object edges | + +#### Identity Agent Stats + +| Field | Type | Description | +|-------|------|-------------| +| `clusters` | integer | Face clusters from face_clustered.json | +| `identities_created` | integer | Identities created from clusters | +| `tmdb_matches` | integer | TMDb identity matches | +| `speaker_bindings` | integer | Speaker-to-identity bindings | +| `confirmations` | integer | Confirmed identity bindings | diff --git a/docs_v1.0/doc_wasm/modules/18_profile.md b/docs_v1.0/doc_wasm/modules/18_profile.md new file mode 100644 index 0000000..b07a8ab --- /dev/null +++ b/docs_v1.0/doc_wasm/modules/18_profile.md @@ -0,0 +1,267 @@ + + + + +## Profile Management + +Endpoints for managing trace profiles (face track metadata stored in TKG) and file profiles (video metadata stored in PostgreSQL). + +### `GET /api/v1/trace-profile` + +**Auth**: Required +**Scope**: file-level + +Read a single face trace's profile including name, key frame, key face, and multi-language aliases. + +#### Request Parameters + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID | +| `trace_id` | integer | Yes | Trace ID (numeric) | + +#### Example + +```bash +curl -s "$API/api/v1/trace-profile?file_uuid=$FILE_UUID&trace_id=7" \ + -H "X-API-Key: $KEY" +``` + +#### Response (200) + +```json +{ + "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5", + "trace_id": 7, + "name": "John Doe", + "key_frame": 640, + "key_face": "face_12345", + "aliases": { + "en": "John Doe", + "zh": "約翰" + }, + "properties": { + "status": "bound", + "avg_bbox": {"x": 899, "y": 212, "width": 342, "height": 342}, + "start_frame": 624, + "end_frame": 669, + "frame_count": 5 + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `file_uuid` | string | File UUID | +| `trace_id` | integer | Trace ID | +| `name` | string | Display name (from `tkg_nodes.label`) | +| `key_frame` | integer | Representative frame number, or null | +| `key_face` | string | Representative face ID, or null | +| `aliases` | object | Multi-language name aliases | +| `properties` | object | Full TKG node properties (bbox, frames, etc.) | + +#### Error Responses + +| HTTP | When | +|------|------| +| `404` | Trace not found | +| `401` | Missing or invalid API key | + +--- + +### `PUT /api/v1/trace-profile` + +**Auth**: Required +**Scope**: file-level + +Update a single face trace's profile fields. Only provided fields are updated; others remain unchanged. + +#### Request Body + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID | +| `trace_id` | integer | Yes | Trace ID to update | +| `name` | string | No | New display name | +| `key_frame` | integer | No | Representative frame number | +| `key_face` | string | No | Representative face ID | +| `aliases` | object | No | Multi-language aliases `{"en": "...", "zh": "..."}` | +| `properties` | object | No | Additional properties to merge into existing JSONB | + +#### Example + +```bash +curl -s -X PUT "$API/api/v1/trace-profile" \ + -H "X-API-Key: $KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5", + "trace_id": 7, + "name": "John Doe", + "key_frame": 640, + "key_face": "face_12345", + "aliases": {"en": "John Doe", "zh": "約翰"} + }' +``` + +#### Response (200) + +```json +{ + "success": true, + "message": "Trace profile updated", + "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5", + "trace_id": 7 +} +``` + +--- + +### `PUT /api/v1/trace-profile/group` + +**Auth**: Required +**Scope**: file-level + +Batch update the `name` (label) for multiple traces in a face group. Used when renaming a group. + +#### Request Body + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID | +| `trace_ids` | integer[] | Yes | List of trace IDs to update | +| `name` | string | Yes | New group name for all traces | + +#### Example + +```bash +curl -s -X PUT "$API/api/v1/trace-profile/group" \ + -H "X-API-Key: $KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5", + "trace_ids": [7, 2, 13], + "name": "Group A" + }' +``` + +#### Response (200) + +```json +{ + "success": true, + "message": "Updated 3 traces in group", + "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5", + "updated_count": 3 +} +``` + +--- + +### `GET /api/v1/file-profile` + +**Auth**: Required +**Scope**: file-level + +Read a file's metadata including path, name, status, and technical details. + +#### Request Parameters + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID | + +#### Example + +```bash +curl -s "$API/api/v1/file-profile?file_uuid=$FILE_UUID" \ + -H "X-API-Key: $KEY" +``` + +#### Response (200) + +```json +{ + "file_uuid": "49884ce1c341953d1ad7bf67a77c30cc", + "file_name": "Dedicatoria.mp4", + "file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Dedicatoria.mp4", + "status": "completed", + "duration": 93.33, + "width": 1280, + "height": 720, + "fps": 30.0, + "total_frames": 0 +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `file_uuid` | string | File UUID | +| `file_name` | string | File name | +| `file_path` | string | Full filesystem path | +| `status` | string | `pending`, `processing`, `completed`, `failed` | +| `duration` | float | Duration in seconds | +| `width` | integer | Video width in pixels | +| `height` | integer | Video height in pixels | +| `fps` | float | Frames per second | +| `total_frames` | integer | Total frame count | + +--- + +### `PUT /api/v1/file-profile` + +**Auth**: Required +**Scope**: file-level + +Update file metadata, typically used when a file is moved to a new location. + +#### Request Body + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `file_uuid` | string | Yes | File UUID | +| `file_path` | string | No | New filesystem path | +| `file_name` | string | No | New file name | + +#### Example + +```bash +curl -s -X PUT "$API/api/v1/file-profile" \ + -H "X-API-Key: $KEY" \ + -H "Content-Type: application/json" \ + -d '{ + "file_uuid": "49884ce1c341953d1ad7bf67a77c30cc", + "file_path": "/new/location/Dedicatoria.mp4" + }' +``` + +#### Response (200) + +```json +{ + "success": true, + "message": "File profile updated", + "file_uuid": "49884ce1c341953d1ad7bf67a77c30cc" +} +``` + +--- + +## Data Storage + +| Profile Type | Storage | Table | +|-------------|---------|-------| +| **trace_profile** | PostgreSQL (TKG) | `tkg_nodes` where `node_type='face_track'` | +| **file_profile** | PostgreSQL | `videos` | + +### Trace Profile Fields + +| Field | TKG Column | Description | +|-------|-----------|-------------| +| `name` | `label` | Display name for the trace | +| `key_frame` | `properties->'key_frame'` | Representative frame number | +| `key_face` | `properties->'key_face'` | Representative face ID | +| `aliases` | `properties->'aliases'` | Multi-language name aliases | + +--- +*Updated: 2026-07-18 — New profile module: trace-profile (GET, PUT, PUT group) and file-profile (GET, PUT)* diff --git a/src/api/profile.rs b/src/api/profile.rs new file mode 100644 index 0000000..da38131 --- /dev/null +++ b/src/api/profile.rs @@ -0,0 +1,391 @@ +use axum::{ + Extension, Json, + extract::{Query, State}, + http::StatusCode, +}; +use serde::{Deserialize, Serialize}; +use sqlx::PgPool; + +use crate::core::db::schema; +use crate::api::middleware::UserAuth; +use crate::api::types::AppState; + +// ─── Trace Profile ─── + +#[derive(Deserialize)] +pub struct TraceProfileQuery { + pub file_uuid: String, + pub trace_id: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct TraceProfile { + pub file_uuid: String, + pub trace_id: i64, + pub name: String, + pub key_frame: Option, + pub key_face: Option, + pub aliases: Option, + pub bbox: Option, + pub properties: serde_json::Value, +} + +#[derive(Deserialize)] +pub struct UpdateTraceProfileRequest { + pub file_uuid: String, + pub trace_id: i64, + pub name: Option, + pub key_frame: Option, + pub key_face: Option, + pub aliases: Option, + pub properties: Option, +} + +#[derive(Deserialize)] +pub struct UpdateTraceProfileGroupRequest { + pub file_uuid: String, + pub trace_ids: Vec, + pub name: String, +} + +pub async fn get_trace_profile_handler( + State(state): State, + Extension(_auth): Extension, + Query(params): Query, +) -> Result, StatusCode> { + let tkg_table = schema::table_name("tkg_nodes"); + let external_id = format!("face_track_{}", params.trace_id); + + let row: Option<(String, String, serde_json::Value)> = sqlx::query_as(&format!( + "SELECT label, external_id, properties FROM {} \ + WHERE file_uuid = $1 AND node_type = 'face_track' AND external_id = $2", + tkg_table + )) + .bind(¶ms.file_uuid) + .bind(&external_id) + .fetch_optional(state.db.pool()) + .await + .map_err(|e| { + tracing::error!("[TraceProfile] DB error: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let (label, _ext_id, properties) = row.ok_or(StatusCode::NOT_FOUND)?; + + let key_frame = properties.get("key_frame").and_then(|v| v.as_i64()); + let key_face = properties + .get("key_face") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let aliases = properties.get("aliases").cloned(); + let bbox = properties.get("avg_bbox").cloned(); + + Ok(Json(TraceProfile { + file_uuid: params.file_uuid, + trace_id: params.trace_id, + name: label, + key_frame, + key_face, + aliases, + bbox, + properties, + })) +} + +pub async fn update_trace_profile_handler( + State(state): State, + Extension(_auth): Extension, + Json(req): Json, +) -> Result, StatusCode> { + let tkg_table = schema::table_name("tkg_nodes"); + let external_id = format!("face_track_{}", req.trace_id); + + // Get current node + let current: Option<(String, serde_json::Value)> = sqlx::query_as(&format!( + "SELECT label, properties FROM {} \ + WHERE file_uuid = $1 AND node_type = 'face_track' AND external_id = $2", + tkg_table + )) + .bind(&req.file_uuid) + .bind(&external_id) + .fetch_optional(state.db.pool()) + .await + .map_err(|e| { + tracing::error!("[TraceProfile] DB error: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let (current_label, mut current_props) = current.ok_or(StatusCode::NOT_FOUND)?; + + // Build updates + let mut updates: Vec = Vec::new(); + + if let Some(ref new_name) = req.name { + if new_name != ¤t_label { + updates.push(format!("label = $3")); + } + } + + // Merge properties + if let Some(ref new_props) = req.properties { + if let Some(obj) = new_props.as_object() { + for (k, v) in obj { + current_props[k] = v.clone(); + } + } + } + + if let Some(ref key_frame) = req.key_frame { + current_props["key_frame"] = serde_json::json!(key_frame); + } + + if let Some(ref key_face) = req.key_face { + current_props["key_face"] = serde_json::json!(key_face); + } + + if let Some(ref aliases) = req.aliases { + current_props["aliases"] = aliases.clone(); + } + + if updates.is_empty() && req.properties.is_none() && req.key_frame.is_none() + && req.key_face.is_none() && req.aliases.is_none() + { + return Ok(Json(serde_json::json!({ + "success": true, + "message": "No changes" + }))); + } + + let mut query = format!( + "UPDATE {} SET properties = $4", + tkg_table + ); + if !updates.is_empty() { + query.push_str(", "); + query.push_str(&updates.join(", ")); + } + query.push_str(" WHERE file_uuid = $1 AND node_type = 'face_track' AND external_id = $2"); + + let param_idx = if updates.is_empty() { 3 } else { 4 }; + query.push_str(&format!(" RETURNING id")); + + let result = if !updates.is_empty() { + sqlx::query(&query) + .bind(&req.file_uuid) + .bind(&external_id) + .bind(req.name.as_ref().unwrap_or(¤t_label)) + .bind(¤t_props) + .execute(state.db.pool()) + .await + } else { + sqlx::query(&query) + .bind(&req.file_uuid) + .bind(&external_id) + .bind(¤t_props) + .execute(state.db.pool()) + .await + }; + + match result { + Ok(res) if res.rows_affected() > 0 => Ok(Json(serde_json::json!({ + "success": true, + "message": "Trace profile updated", + "file_uuid": req.file_uuid, + "trace_id": req.trace_id + }))), + Ok(_) => Err(StatusCode::NOT_FOUND), + Err(e) => { + tracing::error!("[TraceProfile] Update failed: {}", e); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } +} + +pub async fn update_trace_profile_group_handler( + State(state): State, + Extension(_auth): Extension, + Json(req): Json, +) -> Result, StatusCode> { + let tkg_table = schema::table_name("tkg_nodes"); + + let mut updated = 0; + for trace_id in &req.trace_ids { + let external_id = format!("face_track_{}", trace_id); + let result = sqlx::query(&format!( + "UPDATE {} SET label = $1 \ + WHERE file_uuid = $2 AND node_type = 'face_track' AND external_id = $3", + tkg_table + )) + .bind(&req.name) + .bind(&req.file_uuid) + .bind(&external_id) + .execute(state.db.pool()) + .await; + + if let Ok(res) = result { + updated += res.rows_affected(); + } + } + + Ok(Json(serde_json::json!({ + "success": true, + "message": format!("Updated {} traces in group", updated), + "file_uuid": req.file_uuid, + "updated_count": updated + }))) +} + +// ─── File Profile ─── + +#[derive(Deserialize)] +pub struct FileProfileQuery { + pub file_uuid: String, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct FileProfile { + pub file_uuid: String, + pub file_name: String, + pub file_path: String, + pub status: String, + pub duration: f64, + pub width: i32, + pub height: i32, + pub fps: f64, + pub total_frames: i64, +} + +#[derive(Deserialize)] +pub struct UpdateFileProfileRequest { + pub file_uuid: String, + pub file_path: Option, + pub file_name: Option, +} + +pub async fn get_file_profile_handler( + State(state): State, + Extension(_auth): Extension, + Query(params): Query, +) -> Result, StatusCode> { + let videos_table = schema::table_name("videos"); + + let row: Option<(String, String, String, String, f64, i32, i32, f64, i64)> = + sqlx::query_as(&format!( + "SELECT file_uuid, file_name, file_path, status, duration, width, height, fps, \ + COALESCE(total_frames, 0) FROM {} WHERE file_uuid = $1", + videos_table + )) + .bind(¶ms.file_uuid) + .fetch_optional(state.db.pool()) + .await + .map_err(|e| { + tracing::error!("[FileProfile] DB error: {}", e); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let (file_uuid, file_name, file_path, status, duration, width, height, fps, total_frames) = + row.ok_or(StatusCode::NOT_FOUND)?; + + Ok(Json(FileProfile { + file_uuid, + file_name, + file_path, + status, + duration, + width, + height, + fps, + total_frames, + })) +} + +pub async fn update_file_profile_handler( + State(state): State, + Extension(_auth): Extension, + Json(req): Json, +) -> Result, StatusCode> { + let videos_table = schema::table_name("videos"); + + let mut updates: Vec = Vec::new(); + let mut param_idx: i32 = 1; + + if req.file_path.is_some() { + updates.push(format!("file_path = ${}", param_idx)); + param_idx += 1; + } + if req.file_name.is_some() { + updates.push(format!("file_name = ${}", param_idx)); + param_idx += 1; + } + + if updates.is_empty() { + return Ok(Json(serde_json::json!({ + "success": true, + "message": "No changes" + }))); + } + + updates.push("updated_at = CURRENT_TIMESTAMP".to_string()); + + let sql = format!( + "UPDATE {} SET {} WHERE file_uuid = ${}", + videos_table, + updates.join(", "), + param_idx + ); + + // Build the SQL and execute with correct bind order + let result = match (&req.file_path, &req.file_name) { + (Some(fp), Some(fn_)) => { + sqlx::query(&sql) + .bind(fp) + .bind(fn_) + .bind(&req.file_uuid) + .execute(state.db.pool()) + .await + } + (Some(fp), None) => { + sqlx::query(&sql) + .bind(fp) + .bind(&req.file_uuid) + .execute(state.db.pool()) + .await + } + (None, Some(fn_)) => { + sqlx::query(&sql) + .bind(fn_) + .bind(&req.file_uuid) + .execute(state.db.pool()) + .await + } + (None, None) => unreachable!(), + }; + + match result { + Ok(res) if res.rows_affected() > 0 => Ok(Json(serde_json::json!({ + "success": true, + "message": "File profile updated", + "file_uuid": req.file_uuid + }))), + Ok(_) => Err(StatusCode::NOT_FOUND), + Err(e) => { + tracing::error!("[FileProfile] Update failed: {}", e); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } +} + +// ─── Routes ─── + +pub fn profile_routes() -> axum::Router { + use axum::routing::{get, put}; + axum::Router::new() + .route("/api/v1/trace-profile", get(get_trace_profile_handler)) + .route("/api/v1/trace-profile", put(update_trace_profile_handler)) + .route( + "/api/v1/trace-profile/group", + put(update_trace_profile_group_handler), + ) + .route("/api/v1/file-profile", get(get_file_profile_handler)) + .route("/api/v1/file-profile", put(update_file_profile_handler)) +} diff --git a/src/api/search.rs b/src/api/search.rs index 095e660..e665220 100644 --- a/src/api/search.rs +++ b/src/api/search.rs @@ -459,6 +459,27 @@ pub async fn smart_search( pg.summary.clone() }; + // Determine source prefix based on content field + let source_prefix = if let Some(ref content) = pg.content { + let text = content.get("text").and_then(|t| t.as_str()).unwrap_or(""); + let ocr_text = content.get("ocr_text").and_then(|t| t.as_str()).unwrap_or(""); + + let has_asrx = !text.trim().is_empty(); + let has_ocr = !ocr_text.trim().is_empty(); + + if has_asrx && has_ocr { + "[ASRX+OCR] " + } else if has_asrx { + "[ASRX] " + } else if has_ocr { + "[OCR] " + } else { + "" + } + } else { + "" + }; + final_results.push(SearchResult { id: 0, file_uuid: pg.file_uuid.clone(), @@ -470,8 +491,8 @@ pub async fn smart_search( start_time: pg.start_time, end_time: pg.end_time, raw_text: None, - summary: Some(pg.summary), - text_content: pg.text_content.clone(), + summary: Some(format!("{}{}", source_prefix, display_text)), + text_content: Some(format!("{}{}", source_prefix, pg.text_content.clone().unwrap_or_default())), metadata: pg.metadata.clone(), similarity: Some(mr.score), file_name: None, diff --git a/src/core/db/postgres_db.rs b/src/core/db/postgres_db.rs index 69f427e..8d99c14 100644 --- a/src/core/db/postgres_db.rs +++ b/src/core/db/postgres_db.rs @@ -835,6 +835,7 @@ pub struct SemanticSearchResult { pub text_content: Option, pub metadata: Option, pub similarity: Option, + pub content: Option, } /// Result structure for child chunks @@ -2515,8 +2516,10 @@ impl PostgresDb { (start_time * fps)::bigint as start_frame, (end_time * fps)::bigint as end_frame, \ fps, start_time, end_time, \ COALESCE(summary_text, text_content, '') as summary, \ + text_content, \ metadata, \ - (1 - (embedding <=> $1::vector)) as similarity \ + (1 - (embedding <=> $1::vector)) as similarity, \ + content \ FROM {} \ WHERE file_uuid = $2 AND chunk_type IN ('sentence', 'story_parent', 'llm_parent') AND embedding IS NOT NULL \ ORDER BY embedding <=> $1::vector \ @@ -2551,8 +2554,10 @@ impl PostgresDb { (start_time * fps)::bigint as start_frame, (end_time * fps)::bigint as end_frame, \ fps, start_time, end_time, \ COALESCE(summary_text, text_content, '') as summary, \ + text_content, \ metadata, \ - (1 - (embedding <=> $1::vector)) as similarity \ + (1 - (embedding <=> $1::vector)) as similarity, \ + content \ FROM {} \ WHERE chunk_type IN ('sentence', 'story_parent', 'llm_parent') AND embedding IS NOT NULL \ ORDER BY embedding <=> $1::vector \ @@ -2584,7 +2589,8 @@ impl PostgresDb { COALESCE(summary_text, text_content, '') as summary, \ text_content as text_content, \ metadata, \ - 1.0::float8 as similarity \ + 1.0::float8 as similarity, \ + content \ FROM {} \ WHERE file_uuid = $1 AND chunk_id = $2 AND embedding IS NOT NULL \ LIMIT 1", @@ -2615,7 +2621,8 @@ impl PostgresDb { COALESCE(summary_text, text_content, '') as summary, \ text_content as text_content, \ metadata, \ - 1.0::float8 as similarity \ + 1.0::float8 as similarity, \ + content \ FROM {} \ WHERE file_uuid = $1 AND chunk_id = $2 \ LIMIT 1",