docs: add Identity Best-Face API requirement document for frontend team
This commit is contained in:
277
IDENTITY_BEST_FACE_API.md
Normal file
277
IDENTITY_BEST_FACE_API.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# Identity Best-Face API
|
||||||
|
|
||||||
|
**狀態:** 規劃中
|
||||||
|
**提出日期:** 2026-06-01
|
||||||
|
**提出者:** WordPress Portal 前端團隊
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 背景
|
||||||
|
|
||||||
|
WordPress Portal 的 People 頁面需要在 identity detail view 與 grid card 中顯示代表臉部縮圖。目前前端作法:
|
||||||
|
|
||||||
|
1. `GET /identity/{uuid}/traces` → 取得所有 trace 列表(含 `avg_confidence`)
|
||||||
|
2. 對每個 trace 載入第一幀 thumbnail → `GET /file/{uuid}/trace/{tid}/thumbnail`
|
||||||
|
3. 從有 thumbnail 的 trace 中,選 `avg_confidence` 最高者作為代表圖
|
||||||
|
|
||||||
|
### 現有問題
|
||||||
|
|
||||||
|
- **品質不佳**:trace thumbnail 固定取第一幀,不一定是該 trace 內最清晰或正面的臉部畫面
|
||||||
|
- **浪費頻寬**:前端需發送大量並行請求(最多 20 trace × thumbnail),多數 thumbnail 最終不會被使用
|
||||||
|
- **無快取**:每次進入 detail view 都要重複載入所有 thumbnail
|
||||||
|
- **不一致**:同樣 identity 在 grid card 與 detail view 可能顯示不同代表圖
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 目標
|
||||||
|
|
||||||
|
後端新增一個 endpoint,對指定 identity **跨所有 trace** 選出品質最佳(最清晰)的臉部畫面,並提供可直接使用的縮圖 URL,支援 disk cache。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. API 規格
|
||||||
|
|
||||||
|
### `GET /api/v1/identity/:identity_uuid/best-face`
|
||||||
|
|
||||||
|
無 query parameter。
|
||||||
|
|
||||||
|
#### 成功回應 `200`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"identity_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||||
|
"name": "Audrey Hepburn",
|
||||||
|
"source": "fresh",
|
||||||
|
"best": {
|
||||||
|
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||||
|
"trace_id": 42,
|
||||||
|
"frame_number": 3120,
|
||||||
|
"timestamp_secs": 124.8,
|
||||||
|
"bbox": {
|
||||||
|
"x": 240,
|
||||||
|
"y": 180,
|
||||||
|
"width": 120,
|
||||||
|
"height": 160
|
||||||
|
},
|
||||||
|
"confidence": 0.97,
|
||||||
|
"quality_score": 18624.0,
|
||||||
|
"blur_score": 2.1,
|
||||||
|
"thumbnail_url": "/api/v1/file/a6fb22eebefaef17e62af874997c5944/trace/42/thumbnail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 無可用臉部 `200`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"identity_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||||
|
"name": "Audrey Hepburn",
|
||||||
|
"source": "fresh",
|
||||||
|
"best": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 欄位說明
|
||||||
|
|
||||||
|
| 欄位 | 型態 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `success` | boolean | 請求是否成功 |
|
||||||
|
| `identity_uuid` | string | identity UUID(32字元無連字號) |
|
||||||
|
| `name` | string | identity 名稱 |
|
||||||
|
| `source` | string | `"fresh"`(即時計算)或 `"cache"`(來自 disk cache) |
|
||||||
|
| `best` | object/null | 最佳臉部資訊,無可用臉部時為 `null` |
|
||||||
|
| `best.file_uuid` | string | 該臉部所屬檔案 UUID |
|
||||||
|
| `best.trace_id` | int | 該臉部所屬 trace ID |
|
||||||
|
| `best.frame_number` | int | 代表臉的影格編號 |
|
||||||
|
| `best.timestamp_secs` | float | 代表臉的時間戳(秒) |
|
||||||
|
| `best.bbox` | object | 臉部 bounding box `{x, y, width, height}` |
|
||||||
|
| `best.confidence` | float | 該臉部的 detection confidence |
|
||||||
|
| `best.quality_score` | float | 品質分數 = `(width * height) * confidence` |
|
||||||
|
| `best.blur_score` | float | 模糊度分數(ffmpeg blurdetect),越低越清晰 |
|
||||||
|
| `best.thumbnail_url` | string | 縮圖 URL(相對路徑,可直接用於瀏覽器) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 實作建議
|
||||||
|
|
||||||
|
### 4.1 建議放置位置
|
||||||
|
|
||||||
|
**選項 A(建議):** `src/api/trace_agent_api.rs`
|
||||||
|
|
||||||
|
- 原因:核心邏輯重用 `select_rep_face()`(目前為 `pub(crate)`,位於同一檔案),無需修改既有的 function visibility
|
||||||
|
- 在 `trace_agent_routes()` 中新增路由
|
||||||
|
|
||||||
|
**選項 B:** `src/api/identity_binding.rs`
|
||||||
|
|
||||||
|
- 需將 `select_rep_face` 改為 `pub` 才能跨檔案呼叫
|
||||||
|
- 路由語意上更接近 identity 操作
|
||||||
|
|
||||||
|
### 4.2 演算法
|
||||||
|
|
||||||
|
```
|
||||||
|
1. DISK CACHE CHECK
|
||||||
|
路徑:{OUTPUT_DIR}/identities/{uuid}/best_face.json
|
||||||
|
讀取 identity.json 的 updated_at,與 cache 中記錄的版本比較
|
||||||
|
若 cache 未過期 → 直接回傳(source: "cache")
|
||||||
|
若無 cache 或已過期 → 繼續計算
|
||||||
|
|
||||||
|
2. QUERY IDENTITY
|
||||||
|
SELECT id, name FROM identities
|
||||||
|
WHERE REPLACE(uuid::text, '-', '') = $1
|
||||||
|
|
||||||
|
3. QUERY TOP N TRACES
|
||||||
|
SELECT fd.file_uuid, fd.trace_id,
|
||||||
|
AVG(fd.confidence)::float8 AS avg_conf
|
||||||
|
FROM {schema}.face_detections fd
|
||||||
|
WHERE fd.identity_id = $1
|
||||||
|
AND fd.confidence > 0.7
|
||||||
|
AND (fd.metadata->>'qc_ok' IS NULL
|
||||||
|
OR (fd.metadata->>'qc_ok')::boolean = true)
|
||||||
|
GROUP BY fd.file_uuid, fd.trace_id
|
||||||
|
ORDER BY avg_conf DESC
|
||||||
|
LIMIT 5
|
||||||
|
|
||||||
|
4. FOR EACH TRACE (並行)
|
||||||
|
select_rep_face(pool, file_uuid, trace_id, err_fn)
|
||||||
|
→ 回傳該 trace 內 blur_score 最低(最清晰)的臉
|
||||||
|
失敗則 skip(log warning)
|
||||||
|
|
||||||
|
5. SELECT BEST AMONG RESULTS
|
||||||
|
主排序:blur_score ASC(越低越清晰)
|
||||||
|
次排序:quality_score DESC(blur_score 差距 < 0.5 時)
|
||||||
|
全部失敗 → best = null
|
||||||
|
|
||||||
|
6. WRITE DISK CACHE
|
||||||
|
路徑:{OUTPUT_DIR}/identities/{uuid}/best_face.json
|
||||||
|
內容:best 欄位 + 計算時間 + identity updated_at
|
||||||
|
|
||||||
|
7. RESPONSE
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 效能參數
|
||||||
|
|
||||||
|
| 參數 | 值 | 說明 |
|
||||||
|
|------|----|------|
|
||||||
|
| TOP N | 5 | 只對 confidence 最高的 5 個 trace 做 blurdetect |
|
||||||
|
| confidence 門檻 | > 0.7 | 同既有的 `select_rep_face` 邏輯 |
|
||||||
|
| QC 過濾 | qc_ok = true/null | 同既有邏輯 |
|
||||||
|
| ffmpeg timeout | inherit from Command | 每個 trace 約 1-3s |
|
||||||
|
| cache TTL | 直到下一次 bind/unbind/merge | 事件驅動失效 |
|
||||||
|
|
||||||
|
### 4.4 快取策略
|
||||||
|
|
||||||
|
**寫入時機:** `get_identity_best_face` 計算完成後
|
||||||
|
|
||||||
|
**失效時機(刪除 `best_face.json`):**
|
||||||
|
|
||||||
|
| 觸發 operation | 所在檔案 | 備註 |
|
||||||
|
|---------------|---------|------|
|
||||||
|
| `bind_trace` (POST) | `identity_binding.rs` | 新增 face 關聯 |
|
||||||
|
| `unbind` (POST) | `identity_binding.rs` | 移除 face 關聯 |
|
||||||
|
| `mergeinto` (POST) | `identity_binding.rs` | source + target 雙雙清除 |
|
||||||
|
| `profile-image` (POST) | `identity_api.rs` | 使用者上傳新大頭照 |
|
||||||
|
|
||||||
|
**Cache 驗證機制:** 儲存計算時的 `identity.updated_at`,每次請求時比對:
|
||||||
|
- 若 identity 的 `updated_at` 未變 → cache 有效
|
||||||
|
- 若已變 → 重新計算
|
||||||
|
|
||||||
|
### 4.5 建議的新增/修改檔案
|
||||||
|
|
||||||
|
| 檔案 | 動作 | 說明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `src/api/trace_agent_api.rs` | **新增** handler + struct + route | ~+130 行 |
|
||||||
|
| `src/api/identity_binding.rs` | **修改** 3 處 + cache invalidation helper | ~+25 行 |
|
||||||
|
| `src/api/identity_api.rs` | **修改** 1 處(profile-image POST) | ~+5 行 |
|
||||||
|
|
||||||
|
### 4.6 需要的新 struct
|
||||||
|
|
||||||
|
**`src/api/trace_agent_api.rs`**(或獨立檔案 `src/core/identity_best_face.rs`):
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct BestFaceResponse {
|
||||||
|
pub success: bool,
|
||||||
|
pub identity_uuid: String,
|
||||||
|
pub name: String,
|
||||||
|
pub source: String,
|
||||||
|
pub best: Option<BestFaceResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
|
pub struct BestFaceResult {
|
||||||
|
pub file_uuid: String,
|
||||||
|
pub trace_id: i32,
|
||||||
|
pub frame_number: i64,
|
||||||
|
pub timestamp_secs: f64,
|
||||||
|
pub bbox: RepFaceBbox,
|
||||||
|
pub confidence: f64,
|
||||||
|
pub quality_score: f64,
|
||||||
|
pub blur_score: f64,
|
||||||
|
pub thumbnail_url: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.7 Cache Invalidation Helper Function
|
||||||
|
|
||||||
|
```rust
|
||||||
|
async fn invalidate_best_face_cache(output_dir: &str, uuid_clean: &str) {
|
||||||
|
let path = format!("{}/identities/{}/best_face.json", output_dir, uuid_clean);
|
||||||
|
let _ = tokio::fs::remove_file(path).await;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 前端整合參考(供後端團隊理解使用情境)
|
||||||
|
|
||||||
|
WP snippet 72 (`ms-people.js`) 的 `loadPersonDetail` 中,優先使用新 endpoint:
|
||||||
|
|
||||||
|
```js
|
||||||
|
async function loadPersonDetail(person) {
|
||||||
|
if (person.thumb && person._hasProfileImage) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await apiFetch('/identity/' + person.id + '/best-face');
|
||||||
|
if (res?.success && res?.best) {
|
||||||
|
const b = res.best;
|
||||||
|
person.thumb = `${API_BASE}/file/${b.file_uuid}/trace/${b.trace_id}/thumbnail?api_key=${API_KEY}`;
|
||||||
|
person._hasProfileImage = true;
|
||||||
|
updateDetailAvatar(person);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) { /* fallback to legacy */ }
|
||||||
|
|
||||||
|
// 原邏輯:traces → thumbnails → confidence sort
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
同樣可用於 grid card 的代表圖載入(`loadGridThumbnails`):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// 一次性載入所有 pending identity 的 best-face
|
||||||
|
const results = await Promise.allSettled(
|
||||||
|
persons.map(p => apiFetch('/identity/' + p.id + '/best-face'))
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 驗收標準
|
||||||
|
|
||||||
|
1. `GET /api/v1/identity/{uuid}/best-face` → `200` + valid JSON
|
||||||
|
2. 有 trace 的 identity → `best` 不為 null,且 `blur_score` 為該 identity 所有 trace 中最低
|
||||||
|
3. 無 trace 的 identity → `best: null`
|
||||||
|
4. 短時間內重複請求同一 identity → `source: "cache"`,回應時間 < 10ms
|
||||||
|
5. 綁定新 trace 後再次請求 → `source: "fresh"`(cache 已正確失效)
|
||||||
|
6. `thumbnail_url` 可直接用於 `<img>` 顯示
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 風險與注意事項
|
||||||
|
|
||||||
|
- **首次請求延遲**:對有大量 trace 的 identity(如主角),首次請求可能需 5-15 秒。建議前端顯示 loading state
|
||||||
|
- **ffmpeg 資源**:同時多個請求可能導致高 CPU 使用。可考慮加入 per-identity lock 避免重複計算
|
||||||
|
- **邊界案例**:trace 內的 faces 全部 confidence ≤ 0.7 或 qc_ok=false,則該 trace 被跳過,可能導致 `best: null`
|
||||||
Reference in New Issue
Block a user