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:
@@ -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 |
|
||||
@@ -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
@@ -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">← 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">"</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">&trace_id=7"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</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">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"9f6a9cd55a5809f977f5a6589b9045c5"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"trace_id"</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">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"key_frame"</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">"key_face"</span><span class="p">:</span><span class="w"> </span><span class="s2">"face_12345"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"aliases"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"en"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Doe"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"zh"</span><span class="p">:</span><span class="w"> </span><span class="s2">"約翰"</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bound"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"avg_bbox"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"x"</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">"y"</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">"width"</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">"height"</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">"start_frame"</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">"end_frame"</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">"frame_count"</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">"</span><span class="nv">$API</span><span class="s2">/api/v1/trace-profile"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{</span>
|
||||
<span class="s1"> "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5",</span>
|
||||
<span class="s1"> "trace_id": 7,</span>
|
||||
<span class="s1"> "name": "John Doe",</span>
|
||||
<span class="s1"> "key_frame": 640,</span>
|
||||
<span class="s1"> "key_face": "face_12345",</span>
|
||||
<span class="s1"> "aliases": {"en": "John Doe", "zh": "約翰"}</span>
|
||||
<span class="s1"> }'</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">"success"</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">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Trace profile updated"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"9f6a9cd55a5809f977f5a6589b9045c5"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"trace_id"</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">"</span><span class="nv">$API</span><span class="s2">/api/v1/trace-profile/group"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{</span>
|
||||
<span class="s1"> "file_uuid": "9f6a9cd55a5809f977f5a6589b9045c5",</span>
|
||||
<span class="s1"> "trace_ids": [7, 2, 13],</span>
|
||||
<span class="s1"> "name": "Group A"</span>
|
||||
<span class="s1"> }'</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">"success"</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">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Updated 3 traces in group"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"9f6a9cd55a5809f977f5a6589b9045c5"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"updated_count"</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">"</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">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</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">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"49884ce1c341953d1ad7bf67a77c30cc"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Dedicatoria.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/Users/accusys/momentry/var/sftpgo/data/demo/Dedicatoria.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</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">"width"</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">"height"</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">"fps"</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">"total_frames"</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">"</span><span class="nv">$API</span><span class="s2">/api/v1/file-profile"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{</span>
|
||||
<span class="s1"> "file_uuid": "49884ce1c341953d1ad7bf67a77c30cc",</span>
|
||||
<span class="s1"> "file_path": "/new/location/Dedicatoria.mp4"</span>
|
||||
<span class="s1"> }'</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">"success"</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">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"File profile updated"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"49884ce1c341953d1ad7bf67a77c30cc"</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->'key_frame'</code></td>
|
||||
<td>Representative frame number</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>key_face</code></td>
|
||||
<td><code>properties->'key_face'</code></td>
|
||||
<td>Representative face ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>aliases</code></td>
|
||||
<td><code>properties->'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>
|
||||
@@ -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 |
|
||||
@@ -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,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(¶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<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 != ¤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<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(¶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<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
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user