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()
|
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 {
|
final_results.push(SearchResult {
|
||||||
id: 0,
|
id: 0,
|
||||||
file_uuid: pg.file_uuid.clone(),
|
file_uuid: pg.file_uuid.clone(),
|
||||||
@@ -470,8 +491,8 @@ pub async fn smart_search(
|
|||||||
start_time: pg.start_time,
|
start_time: pg.start_time,
|
||||||
end_time: pg.end_time,
|
end_time: pg.end_time,
|
||||||
raw_text: None,
|
raw_text: None,
|
||||||
summary: Some(pg.summary),
|
summary: Some(format!("{}{}", source_prefix, display_text)),
|
||||||
text_content: pg.text_content.clone(),
|
text_content: Some(format!("{}{}", source_prefix, pg.text_content.clone().unwrap_or_default())),
|
||||||
metadata: pg.metadata.clone(),
|
metadata: pg.metadata.clone(),
|
||||||
similarity: Some(mr.score),
|
similarity: Some(mr.score),
|
||||||
file_name: None,
|
file_name: None,
|
||||||
|
|||||||
@@ -835,6 +835,7 @@ pub struct SemanticSearchResult {
|
|||||||
pub text_content: Option<String>,
|
pub text_content: Option<String>,
|
||||||
pub metadata: Option<serde_json::Value>,
|
pub metadata: Option<serde_json::Value>,
|
||||||
pub similarity: Option<f64>,
|
pub similarity: Option<f64>,
|
||||||
|
pub content: Option<serde_json::Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Result structure for child chunks
|
/// 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, \
|
(start_time * fps)::bigint as start_frame, (end_time * fps)::bigint as end_frame, \
|
||||||
fps, start_time, end_time, \
|
fps, start_time, end_time, \
|
||||||
COALESCE(summary_text, text_content, '') as summary, \
|
COALESCE(summary_text, text_content, '') as summary, \
|
||||||
|
text_content, \
|
||||||
metadata, \
|
metadata, \
|
||||||
(1 - (embedding <=> $1::vector)) as similarity \
|
(1 - (embedding <=> $1::vector)) as similarity, \
|
||||||
|
content \
|
||||||
FROM {} \
|
FROM {} \
|
||||||
WHERE file_uuid = $2 AND chunk_type IN ('sentence', 'story_parent', 'llm_parent') AND embedding IS NOT NULL \
|
WHERE file_uuid = $2 AND chunk_type IN ('sentence', 'story_parent', 'llm_parent') AND embedding IS NOT NULL \
|
||||||
ORDER BY embedding <=> $1::vector \
|
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, \
|
(start_time * fps)::bigint as start_frame, (end_time * fps)::bigint as end_frame, \
|
||||||
fps, start_time, end_time, \
|
fps, start_time, end_time, \
|
||||||
COALESCE(summary_text, text_content, '') as summary, \
|
COALESCE(summary_text, text_content, '') as summary, \
|
||||||
|
text_content, \
|
||||||
metadata, \
|
metadata, \
|
||||||
(1 - (embedding <=> $1::vector)) as similarity \
|
(1 - (embedding <=> $1::vector)) as similarity, \
|
||||||
|
content \
|
||||||
FROM {} \
|
FROM {} \
|
||||||
WHERE chunk_type IN ('sentence', 'story_parent', 'llm_parent') AND embedding IS NOT NULL \
|
WHERE chunk_type IN ('sentence', 'story_parent', 'llm_parent') AND embedding IS NOT NULL \
|
||||||
ORDER BY embedding <=> $1::vector \
|
ORDER BY embedding <=> $1::vector \
|
||||||
@@ -2584,7 +2589,8 @@ impl PostgresDb {
|
|||||||
COALESCE(summary_text, text_content, '') as summary, \
|
COALESCE(summary_text, text_content, '') as summary, \
|
||||||
text_content as text_content, \
|
text_content as text_content, \
|
||||||
metadata, \
|
metadata, \
|
||||||
1.0::float8 as similarity \
|
1.0::float8 as similarity, \
|
||||||
|
content \
|
||||||
FROM {} \
|
FROM {} \
|
||||||
WHERE file_uuid = $1 AND chunk_id = $2 AND embedding IS NOT NULL \
|
WHERE file_uuid = $1 AND chunk_id = $2 AND embedding IS NOT NULL \
|
||||||
LIMIT 1",
|
LIMIT 1",
|
||||||
@@ -2615,7 +2621,8 @@ impl PostgresDb {
|
|||||||
COALESCE(summary_text, text_content, '') as summary, \
|
COALESCE(summary_text, text_content, '') as summary, \
|
||||||
text_content as text_content, \
|
text_content as text_content, \
|
||||||
metadata, \
|
metadata, \
|
||||||
1.0::float8 as similarity \
|
1.0::float8 as similarity, \
|
||||||
|
content \
|
||||||
FROM {} \
|
FROM {} \
|
||||||
WHERE file_uuid = $1 AND chunk_id = $2 \
|
WHERE file_uuid = $1 AND chunk_id = $2 \
|
||||||
LIMIT 1",
|
LIMIT 1",
|
||||||
|
|||||||
Reference in New Issue
Block a user