# 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` 必須唯一。
