feat: backup architecture docs, source code, and scripts

This commit is contained in:
Warren
2026-04-25 17:15:45 +08:00
parent 59809dae1f
commit 1f84e5469f
368 changed files with 146329 additions and 261 deletions

View File

@@ -0,0 +1,248 @@
# Momentry Face / Speaker / Person API 開發指南
> **版本**: 3.5 | **更新日期**: 2026-04-17
> **適用對象**: n8n 自動化流程開發者、Portal 前端開發者
---
## 快速開始
### 環境
| 環境 | URL | 說明 |
|------|-----|------|
| **正式版** | `https://api.momentry.ddns.net` | 外部存取 (HTTPS/TLSv1.3) |
| **本機版** | `http://localhost:3002` | 同一台機器使用 (延遲更低) |
### 認證
所有 API 請求需在 Header 加入 API Key
```bash
curl https://api.momentry.ddns.net/api/v1/person/list \
-H "X-API-Key: YOUR_API_KEY"
```
**API Key**marcom 團隊使用):
```
muser_68600856036340bcafc01930eb4bd839
```
---
## ⚠️ 鐵律:所有 Face/Speaker/Person API 都必須提供 video_uuid
**沒有例外。** 所有端點都需要 `video_uuid`
```
錯誤: GET /api/v1/person/list → 400 missing field `video_uuid`
錯誤: GET /api/v1/person/Person_0 → 400 missing field `video_uuid`
正確: GET /api/v1/person/list?video_uuid=xxx → 200 OK
```
| 識別碼 | 全域唯一 | 說明 |
|--------|:---:|------|
| `chunk_id` | ❌ | 每部影片重新編號 |
| `person_id` | ❌ | 每部影片有自己的 Person_0, Person_1... |
| `speaker_id` | ❌ | 每部影片有自己的 SPEAKER_0, SPEAKER_1... |
| **`video_uuid + person_id`** | ✅ | 唯一組合 |
| **`video_uuid + chunk_id`** | ✅ | 唯一組合 |
| `face_id` | ✅ | UUID 格式,全域唯一 |
| `merge_id` | ✅ | UUID 格式,全域唯一 |
---
## API 端點總覽(全部需要 video_uuid
| 端點 | 方法 | video_uuid 位置 | 說明 |
|------|:---:|:---:|------|
| `/api/v1/person/list` | GET | query | 列出人物 |
| `/api/v1/person/auto-identify` | POST | body | 自動識別人 |
| `/api/v1/person/suggest` | POST | body | AI 建議 |
| `/api/v1/person/:id` | GET | query | 人物詳情 |
| `/api/v1/person/:id` | PATCH | query | 更新人物 |
| `/api/v1/person/:id/thumbnail` | GET | query | 臉部截圖 |
| `/api/v1/person/:id/timeline` | GET | query | 出場時間軸 |
| `/api/v1/person/:id/similar` | GET | query | 相似人物 |
| `/api/v1/person/:id/appearances` | GET | query | 出場紀錄 |
| `/api/v1/person/:id/unbind-speaker` | POST | body | 解除 Speaker |
| `/api/v1/person/:id/reassign-speaker` | POST | body | 重新綁定 Speaker |
| `/api/v1/person/:id/remove-appearance` | POST | body | 刪除出場紀錄 |
| `/api/v1/person/:id/reassign-appearance` | POST | body | 轉移出場紀錄 |
| `/api/v1/person/:id/split` | POST | body | 分割人物 |
| `/api/v1/person/merge` | POST | body | 合併人物 |
| `/api/v1/person/merge/undo` | POST | body | 撤銷合併 |
| `/api/v1/person/merge/history` | GET | query | 合併歷史 |
| `/api/v1/search/universal` | POST | body | 統一搜尋 |
| `/api/v1/search/persons` | GET | query | 搜尋人物 |
| `/api/v1/chunks/:id/persons` | GET | query | chunk 內人物 |
| `/api/v1/face/register` | POST | body | 註冊臉孔 |
| `/api/v1/face/list` | GET | query | 已註冊臉孔列表 |
---
## 詳細 API 說明
### 1. GET /api/v1/person/list
列出指定影片的人物。
**Query Parameters:**
| 參數 | 類型 | 必填 | 說明 |
|------|:---:|:---:|------|
| `video_uuid` | string | **是** | 影片 UUID |
| `limit` | int | 否 | 每頁筆數 (預設 50) |
| `offset` | int | 否 | 偏移量 (預設 0) |
| `min_appearances` | int | 否 | 最低出場次數 |
| `has_speaker` | bool | 否 | 僅顯示有 Speaker 的人物 |
**Request:**
```
GET /api/v1/person/list?video_uuid=384b0ff44aaaa1f1&limit=10&min_appearances=100
```
**Response:**
```json
{
"success": true,
"persons": [
{
"person_id": "Person_0",
"name": null,
"speaker_id": "SPEAKER_0",
"appearance_count": 17832,
"total_appearance_duration": 3600.5,
"first_appearance_time": 79.56,
"last_appearance_time": 6863.34,
"is_confirmed": false,
"speaker_confidence": 0.504
}
],
"total": 303
}
```
### 2. GET /api/v1/person/:id
取得人物詳情。
**Query Parameters:**
| 參數 | 類型 | 必填 |
|------|:---:|:---:|
| `video_uuid` | string | **是** |
### 3. POST /api/v1/person/merge
合併多個人物為一人。
**Request:**
```json
{
"video_uuid": "384b0ff44aaaa1f1",
"target_person_id": "Person_0",
"source_person_ids": ["Person_4", "Person_25"]
}
```
**Response:**
```json
{
"success": true,
"message": "Merged 2 persons into Person_0",
"target_person_id": "Person_0",
"merge_id": "5b12e3ac-12fa-45c0-88e1-5cff67604a7d"
}
```
> ⚠️ **請儲存 `merge_id`**,以便日後撤銷合併。
### 4. POST /api/v1/search/universal
統一搜尋。
**Request:**
```json
{
"query": "stamp",
"uuid": "384b0ff44aaaa1f1",
"types": ["chunk", "person"],
"limit": 20
}
```
---
## 影片定位Frame 為主
**重要**: 所有影片位置都以 **frame (幀號)** 為唯一準確單位time 僅供參考。
```json
{
"start_frame": 29795,
"end_frame": 29963,
"fps": 59.94,
"start_time": 497.08,
"end_time": 499.88
}
```
**轉換公式**: `time = frame / fps`
> ⚠️ **注意**: 所有搜尋 API (`/api/v1/search`, `/api/v1/n8n/search`, `/api/v1/search/universal`) 現在都統一回傳 `start_frame`, `end_frame`, `fps` 欄位,確保前端可以精確定位影片幀號。
---
## n8n 工作流範例
```
[Webhook: video_processed]
body: { "uuid": "384b0ff44aaaa1f1" }
[HTTP: POST /api/v1/person/auto-identify]
body: { "video_uuid": "{{ $json.uuid }}" }
[HTTP: POST /api/v1/person/suggest]
body: { "video_uuid": "{{ $json.uuid }}" }
[IF: confidence >= 0.7]
├─ YES → [HTTP: PATCH /api/v1/person/{{person_id}}?video_uuid={{uuid}}]
└─ NO → [等待人工確認]
```
---
## 錯誤碼
| HTTP | 說明 |
|:---:|------|
| 200 | 成功 |
| 400 | 缺少 video_uuid 或參數錯誤 |
| 401 | API Key 無效 |
| 404 | 資源不存在 |
| 422 | 請求體缺少 video_uuid |
| 500 | 伺服器錯誤 |
---
## 資料庫結構
### person_identities
| 欄位 | 類型 | 說明 |
|------|------|------|
| `person_id` | VARCHAR | 識別碼 (每部影片獨立) |
| `video_uuid` | VARCHAR | **所屬影片 (必填)** |
| `name` | VARCHAR | 人物名稱 |
| `speaker_id` | VARCHAR | 對應說話者 ID (每部影片獨立) |
| `appearance_count` | INT | 出場次數 |
| `is_confirmed` | BOOLEAN | 是否已確認 |
### 唯一性約束
```sql
UNIQUE (video_uuid, person_id)
```
每部影片可以有自己的 `Person_0`,但同一部影片內 `person_id` 必須唯一。

View File

@@ -0,0 +1,183 @@
# Face, Speaker, Person, Identity API 教學示範
本文件將以 1963 年電影《Charade》謎中謎為例示範如何使用 API 管理 **Face** (臉孔)、**Person** (影片中的角色實體) 與 **Identity** (真實身份)。
## 核心概念定義
在開始之前,請區分以下名詞:
1. **Face (臉孔)**: 影像中偵測到的具體臉部特徵數據(向量)。
2. **Person (角色實體)**: 在特定影片中出現的角色。他是 Face + Speaker (說話者) 的集合體。
* *例如:影片 `384b0ff44aaaa1f1` 中的 `Person_17`。*
3. **Identity (真實身份)**: 跨越所有影片的全域實體(如真實演員或新聞人物)。
* *例如Cary Grant, Audrey Hepburn。*
---
## 前置準備
* **API URL**: `http://localhost:3003`
* **API Key**: `/`
* **目標影片 (Video UUID)**: `384b0ff44aaaa1f1` (Charade)
---
## 情境設定
我們要在影片中識別兩位主角:
1. **Audrey Hepburn** (飾演 Reggie Lampert)
2. **Cary Grant** (飾演 Peter Joshua)
---
## 步驟一:查看影片中的現有角色 (Person List)
首先,我們查詢系統在影片中偵測到了哪些人物 (Person)。
```bash
curl -s "http://localhost:3003/api/v1/person/list?video_uuid=384b0ff44aaaa1f1&limit=5" \
-H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
| python3 -m json.tool
```
**預期回應**
你會看到類似如下的列表,其中包含系統自動分配的 `person_id` (例如 `Person_17`, `Person_4` 等)。
```json
{
"persons": [
{
"person_id": "Person_17",
"name": null,
"speaker_id": "SPEAKER_1",
"appearance_count": 1636
},
{
"person_id": "Person_4",
"name": null,
"speaker_id": "SPEAKER_0",
"appearance_count": 936
}
]
}
```
---
## 步驟二:建立身份並綁定角色 (Register Identity from Person)
假設經過人工確認,我們知道 `Person_17` 是 Audrey Hepburn。我們可以使用單一 API 同時完成 **「建立 Identity」** 與 **「綁定 Person」** 兩個動作。
### 範例 1: 註冊 Audrey Hepburn
我們指定 `Person_17` 為 "Audrey Hepburn"。系統會檢查此 Identity 是否存在;若不存在則建立,若已存在則直接綁定。
```bash
curl -s -X POST "http://localhost:3003/api/v1/identities/from-person" \
-H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
-H "Content-Type: application/json" \
-d '{
"video_uuid": "384b0ff44aaaa1f1",
"person_id": "Person_17",
"identity_name": "Audrey Hepburn",
"metadata": { "role": "Reggie Lampert" }
}' | python3 -m json.tool
```
**預期回應**
```json
{
"success": true,
"message": "Successfully registered identity 'Audrey Hepburn' and linked to person 'Person_17'",
"identity_id": 10,
"identity_name": "Audrey Hepburn",
"person_id": "Person_17"
}
```
*(註:此操作會自動將該影片中 `Person_17` 的名稱更新為 "Audrey Hepburn")*
### 範例 2: 註冊 Cary Grant
假設 `Person_4` 是 Cary Grant我們進行同樣的操作。
```bash
curl -s -X POST "http://localhost:3003/api/v1/identities/from-person" \
-H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
-H "Content-Type: application/json" \
-d '{
"video_uuid": "384b0ff44aaaa1f1",
"person_id": "Person_4",
"identity_name": "Cary Grant",
"metadata": { "role": "Peter Joshua" }
}' | python3 -m json.tool
```
**預期回應**
```json
{
"success": true,
"message": "Successfully registered identity 'Cary Grant' and linked to person 'Person_4'",
"identity_id": 11,
"identity_name": "Cary Grant",
"person_id": "Person_4"
}
```
---
## 步驟三:查看全域身份庫 (List Identities)
現在我們可以查看所有已建立的「真實身份」,這些身份是跨影片通用的。
```bash
curl -s "http://localhost:3003/api/v1/identities?limit=10" \
-H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
| python3 -m json.tool
```
**預期回應**
你應該能看到剛剛建立的 "Audrey Hepburn" 和 "Cary Grant"。
```json
[
{
"id": 11,
"name": "Cary Grant",
"metadata": { "role": "Peter Joshua" }
},
{
"id": 10,
"name": "Audrey Hepburn",
"metadata": { "role": "Reggie Lampert" }
}
]
```
---
## 步驟四:驗證綁定結果
再次查詢影片中的 `Person` 列表,確認名稱是否已自動更新。
```bash
curl -s "http://localhost:3003/api/v1/person/list?video_uuid=384b0ff44aaaa1f1&limit=5" \
-H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
| python3 -m json.tool
```
**預期結果**
原本的 `Person_17` 現在應該顯示為 `"name": "Audrey Hepburn"`
---
## 常見問題 (FAQ)
**Q: 如果我想把「現有的 Person」綁定到「已經存在的 Identity」要怎麼做**
A: 使用相同的 `POST /api/v1/identities/from-person` API。只要傳入相同的 `identity_name` (例如 "Audrey Hepburn"),系統會自動找到該 Identity 並將新的 Person 連結過去,不會建立重複的 Identity。
**Q: Identity 和 Person 的差別是什麼?**
A: **Identity** 是真實世界的人(例如 "Tom Hanks"),這是全域共享的。
**Person** 是他在某部電影裡的具體出現(例如《阿甘正傳》裡的阿甘)。一個 Identity 可以對應多個影片中的多個 Person。

View File

@@ -0,0 +1,97 @@
# Face/Speaker/Person 分析完成度
**UUID**: `384b0ff44aaaa1f1`
**视频**: Charade (1963) - ~115 min, 412,343 frames, 59.94 fps
**更新日期**: 2026-04-14
---
## 📊 数据统计
| 模块 | 状态 | 文件 | 数据量 |
|------|------|------|--------|
| **Face Detection** | ✅ 完成 | `384b0ff44aaaa1f1.face.json` | 10,691 frames, 25,174 faces |
| **Face Clustering** | ✅ 完成 | `384b0ff44aaaa1f1.face_clustered.json` | 302 unique Person IDs |
| **ASR (语音识别)** | ✅ 完成 | `384b0ff44aaaa1f1.asr.json` | 1,011 segments |
| **ASRX (增强语音)** | ✅ 完成 | `384b0ff44aaaa1f1.asrx.json` | - |
| **Pose (姿态)** | ✅ 完成 | `384b0ff44aaaa1f1.pose.json` | - |
| **Speaker Diarization** | ⚠️ 未集成 | - | ASR segments 无 speaker 信息 |
---
## 🎯 Top 20 人物 (按帧数)
| Person ID | 帧数 | 说明 |
|-----------|------|------|
| Person_0 | 17,832 | 主角 (Cary Grant/Audrey Hepburn) |
| Person_17 | 1,636 | 主要配角 |
| Person_4 | 936 | 主要配角 |
| Person_25 | 217 | 次要角色 |
| Person_12 | 154 | 次要角色 |
| Person_46 | 122 | - |
| Person_70 | 119 | - |
| Person_8 | 109 | - |
| Person_3 | 109 | - |
| Person_124 | 97 | - |
| Person_37 | 95 | - |
| Person_176 | 90 | - |
| Person_34 | 85 | - |
| Person_80 | 78 | - |
| Person_50 | 73 | - |
| Person_94 | 73 | - |
| Person_33 | 63 | - |
| Person_21 | 58 | - |
| Person_14 | 57 | - |
| Person_7 | 57 | - |
**总计**: 302 个独立 Person ID其中 282 个出现少于 57 帧。
---
## ⚠️ 未完成的整合
### 1. Speaker Diarization (说话者识别)
- **问题**: ASR 的 `segments` 中没有 `speaker` 字段
- **影响**: 无法将语音片段关联到具体说话者
- **待办**:
- 运行 speaker diarization 模型
- 或使用 ASRX 输出中的 speaker_id
### 2. Face ↔ Speaker 关联
- **脚本存在**: `scripts/sync_face_speaker_to_chunks.py`
- **状态**: 需要数据库支持 (chunks 表)
- **功能**: 将 face_ids 和 speaker_ids 写入 chunks 表
### 3. Face ↔ ASR 验证
- **文档存在**: `scripts/ASR_FACE_POSE_INTEGRATION.md`
- **状态**: 方案设计完成,但未执行
- **功能**: 使用 Face + Pose 验证 ASR 语句的置信度
### 4. 人物命名/识别
- **当前**: 只有机器生成的 Person_0, Person_1...
- **待办**:
- 将主要人物与演员名字关联 (Cary Grant, Audrey Hepburn 等)
- 使用 face_registration 功能注册已知演员
---
## 📁 相关脚本
| 脚本 | 用途 | 状态 |
|------|------|------|
| `face_clustering_processor.py` | 人脸聚类 | ✅ 已执行 |
| `fast_face_clustering_processor.py` | 快速人脸聚类 | 备选 |
| `sync_face_speaker_to_chunks.py` | 同步到数据库 | 待执行 |
| `match_speakers_to_chunks.py` | 匹配说话者 | 待执行 |
| `export_person_thumbnails.py` | 导出人物缩略图 | 可用 |
| `face_registration.py` | 人脸注册 | 可用 |
| `register_sample_faces.py` | 注册样本 | 可用 |
---
## 🔧 建议下一步
1. **检查 ASRX 输出** 是否有 speaker diarization 信息
2. **导出 Top 20 人物缩略图** 供人工识别
3. **关联主要演员名字** 到 Person_0, Person_17, Person_4 等
4. **执行 Face ↔ ASR 验证** 提升语音识别置信度

View File

@@ -0,0 +1,421 @@
# Face / Speaker / Person API 簡易指南
> **版本**: 1.1 | **適用**: 前端開發團隊
> **更新日期**: 2026-04-17
>
> **⚠️ 重要**: 3002 (正式版) 和 3003 (開發版) 使用**完全獨立的資料空間** (public vs dev schema),絕非共用。開發版測試不會影響正式版資料。
---
## 快速開始
```bash
export BASE="http://localhost:3002"
export KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
export UUID="384b0ff44aaaa1f1"
```
---
## 1. 用 uuid + chunk_id 查看 face / speaker / person
### 取得 chunk 內的人物
```bash
curl "$BASE/api/v1/chunks/sentence_0093/persons" \
-H "X-API-Key: $KEY"
```
```json
{
"success": true,
"chunk_id": "sentence_0093",
"persons": [
{
"person_id": "Person_0",
"name": "Person_0",
"confidence": 0.85,
"overlap_duration": 3.2
}
]
}
```
### 取得 chunk 的 speaker從 content 欄位)
```bash
curl -X POST "$BASE/api/v1/search/universal" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"query": "", "uuid": "'$UUID'", "types": ["chunk"], "filters": {"speaker_id": "SPEAKER_0"}, "limit": 10}'
```
```json
{
"results": [
{
"type": "chunk",
"chunk_id": "sentence_0093",
"chunk_type": "sentence",
"start_frame": 29795,
"end_frame": 29963,
"fps": 59.94,
"start_time": 497.08,
"end_time": 499.88,
"text": "You could have the stamps.",
"speaker_id": "SPEAKER_0"
}
]
}
```
### 統一搜尋 chunk + face + person
```bash
curl -X POST "$BASE/api/v1/search/universal" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"query": "stamp", "uuid": "'$UUID'", "types": ["chunk", "person"], "limit": 10}'
```
```json
{
"query": "stamp",
"results": [
{
"type": "chunk",
"chunk_id": "sentence_1566",
"chunk_type": "sentence",
"start_frame": 329980,
"end_frame": 330040,
"fps": 59.94,
"start_time": 5506.84,
"end_time": 5507.84,
"text": "The envelope, but the stamps on it",
"speaker_id": "SPEAKER_0"
},
{
"type": "person",
"person_id": "Person_0",
"name": "Person_0",
"speaker_id": "SPEAKER_0",
"appearance_count": 17832
}
],
"total": 10,
"took_ms": 27
}
```
---
## 2. 選擇 face 並綁定 person
### 步驟 1: 列出所有人物
```bash
curl "$BASE/api/v1/person/list?min_appearances=100&has_speaker=true&limit=20" \
-H "X-API-Key: $KEY"
```
```json
{
"persons": [
{
"person_id": "Person_0",
"name": "Person_0",
"speaker_id": "SPEAKER_0",
"appearance_count": 17832
},
{
"person_id": "Person_17",
"name": "Person_17",
"speaker_id": "SPEAKER_1",
"appearance_count": 1636
}
],
"total": 9
}
```
### 步驟 2: 查看人物詳情 + 取得截圖
```bash
# 查看詳情
curl "$BASE/api/v1/person/Person_0" -H "X-API-Key: $KEY"
# 取得臉部截圖
curl "$BASE/api/v1/person/Person_0/thumbnail?video_uuid=$UUID" \
-H "X-API-Key: $KEY" -o person0_face.jpg
# 取得第 5 次出現的臉部截圖
curl "$BASE/api/v1/person/Person_0/thumbnail?video_uuid=$UUID&index=4" \
-H "X-API-Key: $KEY" -o person0_face_5.jpg
```
### 步驟 3: 綁定名稱(將 face 關聯到 person
```bash
curl -X PATCH "$BASE/api/v1/person/Person_0" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Cary Grant", "is_confirmed": true}'
```
```json
{
"success": true,
"message": "Person 'Cary Grant' updated successfully",
"person_id": "Person_0"
}
```
### 步驟 4: 註冊新臉孔(建立參考樣本)
```bash
curl -X POST "$BASE/api/v1/face/register" \
-H "X-API-Key: $KEY" \
-F "image=@known_face.jpg" \
-F "name=Cary Grant" \
-F 'metadata={"imdb_id": "nm0000001"}'
```
---
## 3. 合併前檢視:取得臉部截圖
### 取得單張截圖
```bash
# 預設:第一次出現的臉部
curl "$BASE/api/v1/person/Person_0/thumbnail?video_uuid=$UUID" \
-H "X-API-Key: $KEY" -o face.jpg
# 指定第 N 次出現
curl "$BASE/api/v1/person/Person_0/thumbnail?video_uuid=$UUID&index=10" \
-H "X-API-Key: $KEY" -o face_10.jpg
```
### 找出相似人物(可能為同一人)
```bash
curl "$BASE/api/v1/person/Person_0/similar?threshold=0.5&limit=10" \
-H "X-API-Key: $KEY"
```
```json
{
"person_id": "Person_0",
"similar_persons": [
{
"person_id": "Person_4",
"name": "Person_4",
"speaker_id": "SPEAKER_0",
"similarity": 0.7
},
{
"person_id": "Person_25",
"name": "Person_25",
"speaker_id": "SPEAKER_0",
"similarity": 0.7
}
]
}
```
### 取得 AI 合併建議
```bash
curl -X POST "$BASE/api/v1/person/suggest" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"video_uuid": "'$UUID'"}'
```
```json
{
"merge_suggestions": [
{
"person_id": "Person_0",
"merge_with": ["Person_4", "Person_25"],
"confidence": 0.65,
"reasons": [
"All share speaker_id: SPEAKER_0",
"Primary Person_0 has 17832 appearances (89% of group)"
],
"action": "needs_review"
}
]
}
```
---
## 統一搜尋
### ⚠️ 重要:搜尋 chunks 時 uuid 為必填
**只有 `uuid + chunk_id` 組合才是唯一識別碼。** 單獨 `chunk_id` 在不同影片中可能重複。
```bash
# ✅ 正確:包含 uuid
curl -X POST "$BASE/api/v1/search/universal" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"query": "stamp", "uuid": "'$UUID'", "types": ["chunk"]}'
# ❌ 錯誤:缺少 uuid
curl -X POST "$BASE/api/v1/search/universal" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"query": "stamp", "types": ["chunk"]}'
# 回傳: {"error": "uuid is required for chunk search"}
```
---
## 4. 使用 API 合併 face / speaker / person
### ⚠️ 重要:合併撤銷限制
**合併撤銷完全依賴 `merge_history` 記錄。**
| 情況 | 可否撤銷 |
|------|:---:|
| 使用 `POST /api/v1/person/merge` API 合併 | ✅ 可以(自動記錄歷史) |
| 手動修改資料庫合併 | ❌ 不可以(無歷史記錄) |
| 舊版程式碼合併(無 merge_history 表) | ❌ 不可以 |
| 已撤銷過的合併 | ❌ 不可以(防止重複撤銷) |
**每次合併 API 都會回傳 `merge_id`,請務必儲存以便日後撤銷。**
### 執行合併
```bash
curl -X POST "$BASE/api/v1/person/merge" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{
"target_person_id": "Person_0",
"source_person_ids": ["Person_4", "Person_25"]
}'
```
```json
{
"success": true,
"message": "Merged 2 persons into Person_0",
"target_person_id": "Person_0",
"merge_id": "5b12e3ac-12fa-45c0-88e1-5cff67604a7d"
}
```
### 合併做了什麼?
```
合併前:
Person_0 (17832 幀, SPEAKER_0)
Person_4 (936 幀, SPEAKER_0)
Person_25 (217 幀, SPEAKER_0)
合併後:
Person_0 (17832+936+217=18985 幀, SPEAKER_0) ← 保留
Person_4 ← 刪除
Person_25 ← 刪除
```
### 撤銷合併
```bash
# 使用合併時回傳的 merge_id
curl -X POST "$BASE/api/v1/person/merge/undo" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"merge_id": "5b12e3ac-12fa-45c0-88e1-5cff67604a7d"}'
```
```json
{
"success": true,
"message": "Undo merge completed. Restored 2 source persons",
"merge_id": "5b12e3ac-12fa-45c0-88e1-5cff67604a7d",
"target_person_id": "Person_0",
"restored_persons": ["Person_4", "Person_25"]
}
```
**⚠️ 如果沒有 merge_id手動合併/舊版合併),無法撤銷。**
### 查看合併歷史
```bash
curl "$BASE/api/v1/person/merge/history" -H "X-API-Key: $KEY"
```
### 完整合併流程
```
1. 取得建議 → POST /api/v1/person/suggest
2. 檢視截圖 → GET /api/v1/person/:id/thumbnail
3. 檢視相似 → GET /api/v1/person/:id/similar
4. 執行合併 → POST /api/v1/person/merge ← 儲存 merge_id!
5. 確認結果 → GET /api/v1/person/list
6. 如需撤銷 → POST /api/v1/person/merge/undo ← 需要 merge_id
```
---
## API 速查表
| 用途 | 方法 | 端點 |
|------|:---:|------|
| **查看 chunk 內人物** | GET | `/api/v1/chunks/:chunk_id/persons` |
| **搜尋人物** | GET | `/api/v1/search/persons?query=Person` |
| **列出人物** | GET | `/api/v1/person/list?limit=20` |
| **人物詳情** | GET | `/api/v1/person/:id` |
| **人物截圖** | GET | `/api/v1/person/:id/thumbnail?video_uuid=...` |
| **相似人物** | GET | `/api/v1/person/:id/similar` |
| **AI 建議** | POST | `/api/v1/person/suggest` |
| **綁定名稱** | PATCH | `/api/v1/person/:id` |
| **合併人物** | POST | `/api/v1/person/merge` |
| **撤銷合併** | POST | `/api/v1/person/merge/undo` |
| **合併歷史** | GET | `/api/v1/person/merge/history` |
| **統一搜尋** | POST | `/api/v1/search/universal` |
| **註冊臉孔** | POST | `/api/v1/face/register` |
---
## 錯誤處理
```bash
# 錯誤回應
curl -X POST "$BASE/api/v1/person/merge" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"target_person_id": "Person_0", "source_person_ids": []}'
# → "source_person_ids cannot be empty"
```
| 狀態碼 | 說明 |
|:---:|------|
| 200 | 成功 |
| 400 | 參數錯誤 |
| 401 | API Key 無效 |
| 404 | 找不到 |
| 500 | 伺服器錯誤 |
---
## 資料修正
發現綁定錯誤時,參考 [人物資料修正機制指南](./PERSON_CORRECTION_GUIDE.md)
| 錯誤類型 | 修正方式 |
|---------|---------|
| Speaker 綁錯 | `POST /person/:id/reassign-speaker` |
| 不該綁 Speaker | `POST /person/:id/unbind-speaker` |
| Appearance 分錯人 | `POST /person/:id/reassign-appearance` |
| 錯誤 Appearance | `POST /person/:id/remove-appearance` |
| 兩人被合併為一 | `POST /person/:id/split` |
| 錯誤合併 | `POST /person/merge/undo` |
| 錯誤命名 | `PATCH /person/:id` |

View File

@@ -0,0 +1,167 @@
# Face / Speaker / Person / Identity Workflow Guide
This document describes the end-to-end workflow for managing characters in Momentry Core, from raw detection to a clean, aggregated identity database.
## 📊 1. Workflow Visualization
```mermaid
graph TD
%% Nodes
Start((Start Analysis))
ListPersons[List Persons]
subgraph "Phase 1: Registration"
CheckIdentity{Identity Exists?}
Register[Register Identity]
Link[Link Person to Identity]
end
subgraph "Phase 2: Aggregation"
Suggest[Get AI Suggestions]
Review[Review Suggestions]
Merge[Execute Merge]
Confirm[Confirm Result]
end
End((Database Clean))
%% Flow
Start --> ListPersons
ListPersons --> CheckIdentity
CheckIdentity -- No --> Register
Register --> Link
Link --> Suggest
CheckIdentity -- Yes --> Suggest
Suggest --> Review
Review -- Merge Recommended --> Merge
Review -- Naming Recommended --> Rename[Update Name]
Rename --> Confirm
Merge --> Confirm
Confirm --> End
style Start fill:#f9f,stroke:#333
style End fill:#bbf,stroke:#333
style Register fill:#dfd,stroke:#333
style Merge fill:#dfd,stroke:#333
```
---
## 🛠️ 2. Step-by-Step API Operations
### Phase 1: Registration (Creating Identities)
**Scenario**: You see `Person_17` is Audrey Hepburn. You want to create a global record for her.
1. **Find the Person**:
```bash
curl -s "http://localhost:3003/api/v1/person/list?video_uuid=...&limit=5" ...
# Output: Person_17 (1636 frames, null name)
```
2. **Register Identity**:
```bash
curl -X POST "http://localhost:3003/api/v1/identities/from-person" ... \
-d '{
"video_uuid": "...",
"person_id": "Person_17",
"identity_name": "Audrey Hepburn"
}'
```
*Result: `Person_17` is now named "Audrey Hepburn". A global `identity_id` is created.*
---
### Phase 2: Suggestion (AI Analysis)
**Scenario**: You suspect `Person_25` might also be Audrey Hepburn, or you just want to clean up the data.
1. **Ask for Suggestions**:
```bash
curl -X POST "http://localhost:3003/api/v1/person/suggest" ... \
-d '{"video_uuid": "..."}'
```
*Response*:
```json
{
"merge_suggestions": [
{
"person_id": "Person_17",
"merge_with": ["Person_25"],
"reasons": ["All share speaker_id: SPEAKER_1", "Person_17 has 88% of frames"],
"action": "auto_apply"
}
]
}
```
---
### Phase 3: Review & Execution
**Scenario**: You verify the suggestion. The AI logic (Shared Speaker + Frame dominance) seems correct.
1. **Execute the Merge**:
```bash
curl -X POST "http://localhost:3003/api/v1/person/merge" ... \
-d '{
"video_uuid": "...",
"target_person_id": "Person_17",
"source_person_ids": ["Person_25"]
}'
```
*Result*: `Person_25` is deleted. All 217 frames of `Person_25` are added to `Person_17`.
---
## 🚀 3. Automated Demo Script
Run the following script to see the entire process in action automatically.
```bash
#!/bin/bash
# scripts/demo_identity_workflow.sh
# Usage: chmod +x scripts/demo_identity_workflow.sh && ./scripts/demo_identity_workflow.sh
API_URL="http://localhost:3002"
API_KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
UUID="384b0ff44aaaa1f1"
echo "🎬 === MOMENTRY IDENTITY WORKFLOW DEMO ==="
# 1. Registration
echo "👉 STEP 1: Registering Person_17 as Audrey Hepburn..."
curl -s -X POST "$API_URL/api/v1/identities/from-person" \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d "{\"video_uuid\":\"$UUID\", \"person_id\":\"Person_17\", \"identity_name\":\"Audrey Hepburn\"}" \
| python3 -m json.tool
# 2. Suggestion
echo ""
echo "👉 STEP 2: Asking AI for cleaning suggestions..."
curl -s -X POST "$API_URL/api/v1/person/suggest" \
-H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d "{\"video_uuid\":\"$UUID\"}" \
| python3 -c "
import sys, json
d = json.load(sys.stdin)
sugs = d.get('naming_suggestions', []) + d.get('merge_suggestions', [])
if sugs:
print(f' Found {len(sugs)} suggestions.')
for s in sugs:
print(f' - {s}')
else:
print(' No suggestions (Data is already clean!).')
"
# 3. Execution (Example Merge if Person_25 existed)
echo ""
echo "👉 STEP 3: Simulating a merge (Merging hypothetical Person_25 -> Person_17)..."
# Note: In a real scenario, Person_25 would exist.
# Here we just show the command structure.
echo " Command: POST /api/v1/person/merge { target: 'Person_17', sources: ['Person_25'] }"
echo " Result: Person_25 frames added to Person_17. Person_25 deleted."
echo ""
echo "✅ Demo Complete."

View File

@@ -0,0 +1,214 @@
# 📘 Momentry 身份管理 (Identity Management) API 實作指南
本文件示範如何透過 API 完成「從影片選擇 → 臉部分析 → 全域身份註冊」的完整流程。
## 1. 選擇目標影片
**目標**: 獲取系統中已註冊的影片列表,選擇要進行管理的影片。
**API**: `GET /api/v1/videos`
```bash
curl -s "http://127.0.0.1:3002/api/v1/videos" \
-H "x-api-key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" | jq .
```
**回應範例**:
```json
{
"videos": [
{
"uuid": "384b0ff44aaaa1f1",
"file_name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"duration": 6879.33
},
{
"uuid": "9760d0820f0cf9a7",
"file_name": "ExaSAN PCIe series - Director Ou.mp4",
"duration": 159.64
}
]
}
```
> **決策**: 我們選擇 `Charade 1963` (UUID: `384b0ff44aaaa1f1`) 進行管理。
---
## 2. 分析影片內的所有人物 (Faces / Persons / Speakers)
**目標**: 查看該影片內所有偵測到的「臉群 (Clusters)」。區分**已命名 (Named)**、**待命名 (Unregistered)** 與 **AI 建議**
**API**: `GET /api/v1/videos/{uuid}/faces`
```bash
curl -s "http://127.0.0.1:3002/api/v1/videos/384b0ff44aaaa1f1/faces" \
-H "x-api-key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" | jq .
```
**回應範例**:
```json
{
"success": true,
"video_uuid": "384b0ff44aaaa1f1",
"total_faces": 6,
"registered_count": 0,
"unregistered_count": 6,
"clusters": [
{
"cluster_id": "Person_4",
"face_count": 45,
"status": "unregistered",
"identity": {
"name": "Cary Grant",
"is_confirmed": true
}
},
{
"cluster_id": "Person_17",
"face_count": 32,
"status": "unregistered",
"identity": {
"name": "Audrey Hepburn",
"is_confirmed": true
}
},
{
"cluster_id": "Person_12",
"face_count": 10,
"status": "unregistered",
"identity": { "name": "Person_12" }
},
{
"cluster_id": "Person_124",
"face_count": 5,
"status": "unregistered",
"identity": null
}
]
}
```
### 如何解讀結果?
| 欄位 | 說明 | 狀態 |
| :--- | :--- | :--- |
| **`identity.name`** | 若顯示具體人名 (如 "Audrey Hepburn"),代表 **已命名**。 | ✅ 待註冊 |
| **`identity.name`** | 若顯示 `Person_XX` (系統預設名),代表 **待命名**。 | 🔄 等待 AI 或人工命名 |
| **`identity: null`** | 代表完全 **未識別**,通常數量較少。 | ❓ 待處理 |
---
## 3. 註冊全域身份 (Register Identity)
**目標**: 將已命名的人物升級為 **全域身份 (Global Identity)**。這能讓系統在其他影片中自動認出他們。
**API**: `POST /api/v1/person/{person_id}/register?video_uuid={uuid}`
### 3.1 註冊 Audrey Hepburn
```bash
curl -s -X POST "http://127.0.0.1:3002/api/v1/person/Person_17/register?video_uuid=384b0ff44aaaa1f1" \
-H "x-api-key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" | jq .
```
**回應**:
```json
{
"success": true,
"message": "Successfully registered as global identity",
"person_id": "Person_17",
"name": "Audrey Hepburn",
"face_identity_id": 12
}
```
### 3.2 註冊 Cary Grant
```bash
curl -s -X POST "http://127.0.0.1:3002/api/v1/person/Person_4/register?video_uuid=384b0ff44aaaa1f1" \
-H "x-api-key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" | jq .
```
**回應**:
```json
{
"success": true,
"face_identity_id": 13,
"name": "Cary Grant"
}
```
---
## ✅ 驗證成果
現在可以使用全域搜尋 API 確認身份是否註冊成功:
```bash
curl -s -X POST "http://127.0.0.1:3002/api/v1/identities/search" \
-H "Content-Type: application/json" \
-H "x-api-key: muser_..." \
-d '{"query": "Audrey"}' | jq '.identities[] | {name: .profile.name, identity_id: .face_identity_id}'
```
**結果**:
```json
{
"name": "Audrey Hepburn",
"identity_id": 12
}
```
---
## 4. 擷取身份 / 人物 / 臉部 截圖
**目標**: 取得特定人物的臉部特寫截圖。
由於「Identity (全域身份)」是由多個影片中的「Person (區域人物)」組成而「Person」是由多個「Face (臉部偵測點)」聚合而成,因此擷取截圖的核心是取得 **該人物在某部影片中的某幀臉部影像**
**API**: `GET /api/v1/person/{person_id}/thumbnail`
### 參數說明
| 參數 | 類型 | 必填 | 說明 |
| :--- | :--- | :--- | :--- |
| `person_id` | Path | ✅ | 人物 ID (例如: `Person_17`) |
| `video_uuid` | Query | ✅ | 影片 UUID (用來定位影像源) |
| `index` | Query | ❌ | 指定第幾張臉 (預設 `0`) |
### 4.1 擷取 Audrey Hepburn 的臉部截圖 (預設第一張)
此指令會自動從 `Charade 1963` 影片中擷取 Audrey Hepburn 最清晰的一張臉,並儲存為 `audrey.jpg`
```bash
curl -s -o audrey.jpg \
"http://127.0.0.1:3002/api/v1/person/Person_17/thumbnail?video_uuid=384b0ff44aaaa1f1" \
-H "x-api-key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
```
> **注意**: 回應是 **圖片二進位資料 (JPG)**,請使用 `-o filename.jpg` 儲存,**不要**使用 `| jq`。
### 4.2 擷取 Cary Grant 的其他臉部截圖 (指定 Index)
若你想看同一人物的其他角度,可以調整 `index` 參數。
假設 Cary Grant (`Person_4`) 在影片中出現了 45 次:
```bash
# 擷取第 5 次出現的臉部截圖 (index 從 0 開始)
curl -s -o cary_face_5.jpg \
"http://127.0.0.1:3002/api/v1/person/Person_4/thumbnail?video_uuid=384b0ff44aaaa1f1&index=4" \
-H "x-api-key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
```
### 4.3 Identity (全域身份) 的截圖策略
由於全域 Identity (`face_identity_id: 12`) 跨越多部影片,要取得它的截圖,請先查詢它所屬的影片:
1. **查詢 Identity 所在的影片**:
```bash
curl -s "http://127.0.0.1:3002/api/v1/identities/12/videos" \
-H "x-api-key: muser_..." | jq '.videos[0].video_uuid'
```
2. **取得該影片中的對應 Person ID**: 從上一步結果中找到 `person_id` (例如 `Person_17`)。
3. **呼叫截圖 API**: 使用該 `video_uuid` 和 `person_id` 呼叫上述截圖 API。