8.8 KiB
8.8 KiB
Identity Best-Face API
狀態: 規劃中 提出日期: 2026-06-01 提出者: WordPress Portal 前端團隊
1. 背景
WordPress Portal 的 People 頁面需要在 identity detail view 與 grid card 中顯示代表臉部縮圖。目前前端作法:
GET /identity/{uuid}/traces→ 取得所有 trace 列表(含avg_confidence)- 對每個 trace 載入第一幀 thumbnail →
GET /file/{uuid}/trace/{tid}/thumbnail - 從有 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
{
"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
{
"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):
#[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
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:
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):
// 一次性載入所有 pending identity 的 best-face
const results = await Promise.allSettled(
persons.map(p => apiFetch('/identity/' + p.id + '/best-face'))
);
6. 驗收標準
GET /api/v1/identity/{uuid}/best-face→200+ valid JSON- 有 trace 的 identity →
best不為 null,且blur_score為該 identity 所有 trace 中最低 - 無 trace 的 identity →
best: null - 短時間內重複請求同一 identity →
source: "cache",回應時間 < 10ms - 綁定新 trace 後再次請求 →
source: "fresh"(cache 已正確失效) 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