feat: add source prefix to search results

- Add [OCR], [ASRX], [ASRX+OCR] prefix to text_content
- Add content field to SemanticSearchResult struct
- Update SQL queries to include content field
- Helps users distinguish the source of search results
This commit is contained in:
Accusys
2026-07-19 14:06:06 +08:00
parent 5e83ee7dac
commit 87aa7e0c40
10 changed files with 3947 additions and 6 deletions
@@ -0,0 +1,267 @@
<!-- module: profile -->
<!-- description: Trace profile and file profile management — read/update face trace names, key frames, aliases, and file paths -->
<!-- depends: 01_auth, 07_identity, 15_tkg -->
## 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)*
@@ -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<usize> {
// 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 |
+274
View File
@@ -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 正確但僅英文輸出
File diff suppressed because it is too large Load Diff
+511
View File
@@ -0,0 +1,511 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>18 Profile - Momentry API Docs</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
h1 { font-size: 24px; margin: 24px 0 12px; }
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
p { line-height: 1.6; margin: 8px 0; }
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
th { background: #f0f0f0; font-weight: 600; }
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
pre code { background: none; padding: 0; }
a { color: #0066cc; }
.back { display: inline-block; margin-bottom: 20px; color: #666; }
.back:hover { color: #333; }
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
.logout-btn { font-size: 13px; color: #999; text-decoration: none; }
.logout-btn:hover { color: #cc0000; }
</style>
</head>
<body>
<div class="container">
<div class="topbar">
<a class="back" href="index.html">&larr; Back to index</a>
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{method:'POST'}).then(()=>window.location.reload());return false">Logout</a>
</div>
<!-- module: profile -->
<!-- description: Trace profile and file profile management — read/update face trace names, key frames, aliases, and file paths -->
<!-- depends: 01_auth, 07_identity, 15_tkg -->
<h2>Profile Management</h2>
<p>Endpoints for managing trace profiles (face track metadata stored in TKG) and file profiles (video metadata stored in PostgreSQL).</p>
<h3><code>GET /api/v1/trace-profile</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Read a single face trace's profile including name, key frame, key face, and multi-language aliases.</p>
<h4>Request Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>Yes</td>
<td>File UUID</td>
</tr>
<tr>
<td><code>trace_id</code></td>
<td>integer</td>
<td>Yes</td>
<td>Trace ID (numeric)</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/trace-profile?file_uuid=</span><span class="nv">$FILE_UUID</span><span class="s2">&amp;trace_id=7&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;9f6a9cd55a5809f977f5a6589b9045c5&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;trace_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">7</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;name&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;John Doe&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;key_frame&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">640</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;key_face&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;face_12345&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;aliases&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;en&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;John Doe&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;zh&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;約翰&quot;</span>
<span class="w"> </span><span class="p">},</span>
<span class="w"> </span><span class="nt">&quot;properties&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;status&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;bound&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;avg_bbox&quot;</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">&quot;x&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">899</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;y&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">212</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;width&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">342</span><span class="p">,</span><span class="w"> </span><span class="nt">&quot;height&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">342</span><span class="p">},</span>
<span class="w"> </span><span class="nt">&quot;start_frame&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">624</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;end_frame&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">669</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;frame_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span>
<span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>File UUID</td>
</tr>
<tr>
<td><code>trace_id</code></td>
<td>integer</td>
<td>Trace ID</td>
</tr>
<tr>
<td><code>name</code></td>
<td>string</td>
<td>Display name (from <code>tkg_nodes.label</code>)</td>
</tr>
<tr>
<td><code>key_frame</code></td>
<td>integer</td>
<td>Representative frame number, or null</td>
</tr>
<tr>
<td><code>key_face</code></td>
<td>string</td>
<td>Representative face ID, or null</td>
</tr>
<tr>
<td><code>aliases</code></td>
<td>object</td>
<td>Multi-language name aliases</td>
</tr>
<tr>
<td><code>properties</code></td>
<td>object</td>
<td>Full TKG node properties (bbox, frames, etc.)</td>
</tr>
</tbody>
</table>
<h4>Error Responses</h4>
<table class="table">
<thead>
<tr>
<th>HTTP</th>
<th>When</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>404</code></td>
<td>Trace not found</td>
</tr>
<tr>
<td><code>401</code></td>
<td>Missing or invalid API key</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>PUT /api/v1/trace-profile</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Update a single face trace's profile fields. Only provided fields are updated; others remain unchanged.</p>
<h4>Request Body</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>Yes</td>
<td>File UUID</td>
</tr>
<tr>
<td><code>trace_id</code></td>
<td>integer</td>
<td>Yes</td>
<td>Trace ID to update</td>
</tr>
<tr>
<td><code>name</code></td>
<td>string</td>
<td>No</td>
<td>New display name</td>
</tr>
<tr>
<td><code>key_frame</code></td>
<td>integer</td>
<td>No</td>
<td>Representative frame number</td>
</tr>
<tr>
<td><code>key_face</code></td>
<td>string</td>
<td>No</td>
<td>Representative face ID</td>
</tr>
<tr>
<td><code>aliases</code></td>
<td>object</td>
<td>No</td>
<td>Multi-language aliases <code>{"en": "...", "zh": "..."}</code></td>
</tr>
<tr>
<td><code>properties</code></td>
<td>object</td>
<td>No</td>
<td>Additional properties to merge into existing JSONB</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>PUT<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/trace-profile&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{</span>
<span class="s1"> &quot;file_uuid&quot;: &quot;9f6a9cd55a5809f977f5a6589b9045c5&quot;,</span>
<span class="s1"> &quot;trace_id&quot;: 7,</span>
<span class="s1"> &quot;name&quot;: &quot;John Doe&quot;,</span>
<span class="s1"> &quot;key_frame&quot;: 640,</span>
<span class="s1"> &quot;key_face&quot;: &quot;face_12345&quot;,</span>
<span class="s1"> &quot;aliases&quot;: {&quot;en&quot;: &quot;John Doe&quot;, &quot;zh&quot;: &quot;約翰&quot;}</span>
<span class="s1"> }&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;message&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Trace profile updated&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;9f6a9cd55a5809f977f5a6589b9045c5&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;trace_id&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">7</span>
<span class="p">}</span>
</code></pre></div>
<hr />
<h3><code>PUT /api/v1/trace-profile/group</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Batch update the <code>name</code> (label) for multiple traces in a face group. Used when renaming a group.</p>
<h4>Request Body</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>Yes</td>
<td>File UUID</td>
</tr>
<tr>
<td><code>trace_ids</code></td>
<td>integer[]</td>
<td>Yes</td>
<td>List of trace IDs to update</td>
</tr>
<tr>
<td><code>name</code></td>
<td>string</td>
<td>Yes</td>
<td>New group name for all traces</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>PUT<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/trace-profile/group&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{</span>
<span class="s1"> &quot;file_uuid&quot;: &quot;9f6a9cd55a5809f977f5a6589b9045c5&quot;,</span>
<span class="s1"> &quot;trace_ids&quot;: [7, 2, 13],</span>
<span class="s1"> &quot;name&quot;: &quot;Group A&quot;</span>
<span class="s1"> }&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;message&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Updated 3 traces in group&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;9f6a9cd55a5809f977f5a6589b9045c5&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;updated_count&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span>
<span class="p">}</span>
</code></pre></div>
<hr />
<h3><code>GET /api/v1/file-profile</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Read a file's metadata including path, name, status, and technical details.</p>
<h4>Request Parameters</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>Yes</td>
<td>File UUID</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file-profile?file_uuid=</span><span class="nv">$FILE_UUID</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;49884ce1c341953d1ad7bf67a77c30cc&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_name&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;Dedicatoria.mp4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_path&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;/Users/accusys/momentry/var/sftpgo/data/demo/Dedicatoria.mp4&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;status&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;completed&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;duration&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">93.33</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;width&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">1280</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;height&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">720</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;fps&quot;</span><span class="p">:</span><span class="w"> </span><span class="mf">30.0</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;total_frames&quot;</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span>
<span class="p">}</span>
</code></pre></div>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>File UUID</td>
</tr>
<tr>
<td><code>file_name</code></td>
<td>string</td>
<td>File name</td>
</tr>
<tr>
<td><code>file_path</code></td>
<td>string</td>
<td>Full filesystem path</td>
</tr>
<tr>
<td><code>status</code></td>
<td>string</td>
<td><code>pending</code>, <code>processing</code>, <code>completed</code>, <code>failed</code></td>
</tr>
<tr>
<td><code>duration</code></td>
<td>float</td>
<td>Duration in seconds</td>
</tr>
<tr>
<td><code>width</code></td>
<td>integer</td>
<td>Video width in pixels</td>
</tr>
<tr>
<td><code>height</code></td>
<td>integer</td>
<td>Video height in pixels</td>
</tr>
<tr>
<td><code>fps</code></td>
<td>float</td>
<td>Frames per second</td>
</tr>
<tr>
<td><code>total_frames</code></td>
<td>integer</td>
<td>Total frame count</td>
</tr>
</tbody>
</table>
<hr />
<h3><code>PUT /api/v1/file-profile</code></h3>
<p><strong>Auth</strong>: Required
<strong>Scope</strong>: file-level</p>
<p>Update file metadata, typically used when a file is moved to a new location.</p>
<h4>Request Body</h4>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>Type</th>
<th>Required</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>file_uuid</code></td>
<td>string</td>
<td>Yes</td>
<td>File UUID</td>
</tr>
<tr>
<td><code>file_path</code></td>
<td>string</td>
<td>No</td>
<td>New filesystem path</td>
</tr>
<tr>
<td><code>file_name</code></td>
<td>string</td>
<td>No</td>
<td>New file name</td>
</tr>
</tbody>
</table>
<h4>Example</h4>
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>PUT<span class="w"> </span><span class="s2">&quot;</span><span class="nv">$API</span><span class="s2">/api/v1/file-profile&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;X-API-Key: </span><span class="nv">$KEY</span><span class="s2">&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-H<span class="w"> </span><span class="s2">&quot;Content-Type: application/json&quot;</span><span class="w"> </span><span class="se">\</span>
<span class="w"> </span>-d<span class="w"> </span><span class="s1">&#39;{</span>
<span class="s1"> &quot;file_uuid&quot;: &quot;49884ce1c341953d1ad7bf67a77c30cc&quot;,</span>
<span class="s1"> &quot;file_path&quot;: &quot;/new/location/Dedicatoria.mp4&quot;</span>
<span class="s1"> }&#39;</span>
</code></pre></div>
<h4>Response (200)</h4>
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
<span class="w"> </span><span class="nt">&quot;success&quot;</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;message&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;File profile updated&quot;</span><span class="p">,</span>
<span class="w"> </span><span class="nt">&quot;file_uuid&quot;</span><span class="p">:</span><span class="w"> </span><span class="s2">&quot;49884ce1c341953d1ad7bf67a77c30cc&quot;</span>
<span class="p">}</span>
</code></pre></div>
<hr />
<h2>Data Storage</h2>
<table class="table">
<thead>
<tr>
<th>Profile Type</th>
<th>Storage</th>
<th>Table</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>trace_profile</strong></td>
<td>PostgreSQL (TKG)</td>
<td><code>tkg_nodes</code> where <code>node_type='face_track'</code></td>
</tr>
<tr>
<td><strong>file_profile</strong></td>
<td>PostgreSQL</td>
<td><code>videos</code></td>
</tr>
</tbody>
</table>
<h3>Trace Profile Fields</h3>
<table class="table">
<thead>
<tr>
<th>Field</th>
<th>TKG Column</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>name</code></td>
<td><code>label</code></td>
<td>Display name for the trace</td>
</tr>
<tr>
<td><code>key_frame</code></td>
<td><code>properties-&gt;'key_frame'</code></td>
<td>Representative frame number</td>
</tr>
<tr>
<td><code>key_face</code></td>
<td><code>properties-&gt;'key_face'</code></td>
<td>Representative face ID</td>
</tr>
<tr>
<td><code>aliases</code></td>
<td><code>properties-&gt;'aliases'</code></td>
<td>Multi-language name aliases</td>
</tr>
</tbody>
</table>
<hr />
<p><em>Updated: 2026-07-18 — New profile module: trace-profile (GET, PUT, PUT group) and file-profile (GET, PUT)</em></p>
</div>
</body>
</html>
+545
View File
@@ -0,0 +1,545 @@
<!-- module: progress -->
<!-- description: Real-time progress tracking for processing pipeline, TKG build, and identity agent -->
<!-- depends: 01_auth, 03_register, 05_process -->
# 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 |
+267
View File
@@ -0,0 +1,267 @@
<!-- module: profile -->
<!-- description: Trace profile and file profile management — read/update face trace names, key frames, aliases, and file paths -->
<!-- depends: 01_auth, 07_identity, 15_tkg -->
## 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)*
+391
View File
@@ -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<i64>,
pub key_face: Option<String>,
pub aliases: Option<serde_json::Value>,
pub bbox: Option<serde_json::Value>,
pub properties: serde_json::Value,
}
#[derive(Deserialize)]
pub struct UpdateTraceProfileRequest {
pub file_uuid: String,
pub trace_id: i64,
pub name: Option<String>,
pub key_frame: Option<i64>,
pub key_face: Option<String>,
pub aliases: Option<serde_json::Value>,
pub properties: Option<serde_json::Value>,
}
#[derive(Deserialize)]
pub struct UpdateTraceProfileGroupRequest {
pub file_uuid: String,
pub trace_ids: Vec<i64>,
pub name: String,
}
pub async fn get_trace_profile_handler(
State(state): State<AppState>,
Extension(_auth): Extension<UserAuth>,
Query(params): Query<TraceProfileQuery>,
) -> Result<Json<TraceProfile>, 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(&params.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<AppState>,
Extension(_auth): Extension<UserAuth>,
Json(req): Json<UpdateTraceProfileRequest>,
) -> Result<Json<serde_json::Value>, 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<String> = Vec::new();
if let Some(ref new_name) = req.name {
if new_name != &current_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(&current_label))
.bind(&current_props)
.execute(state.db.pool())
.await
} else {
sqlx::query(&query)
.bind(&req.file_uuid)
.bind(&external_id)
.bind(&current_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<AppState>,
Extension(_auth): Extension<UserAuth>,
Json(req): Json<UpdateTraceProfileGroupRequest>,
) -> Result<Json<serde_json::Value>, 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<String>,
pub file_name: Option<String>,
}
pub async fn get_file_profile_handler(
State(state): State<AppState>,
Extension(_auth): Extension<UserAuth>,
Query(params): Query<FileProfileQuery>,
) -> Result<Json<FileProfile>, 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(&params.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<AppState>,
Extension(_auth): Extension<UserAuth>,
Json(req): Json<UpdateFileProfileRequest>,
) -> Result<Json<serde_json::Value>, StatusCode> {
let videos_table = schema::table_name("videos");
let mut updates: Vec<String> = 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<AppState> {
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))
}
+23 -2
View File
@@ -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,
+11 -4
View File
@@ -835,6 +835,7 @@ pub struct SemanticSearchResult {
pub text_content: Option<String>,
pub metadata: Option<serde_json::Value>,
pub similarity: Option<f64>,
pub content: Option<serde_json::Value>,
}
/// 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",