# 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, } #[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` 可直接用於 `` 顯示 --- ## 7. 風險與注意事項 - **首次請求延遲**:對有大量 trace 的 identity(如主角),首次請求可能需 5-15 秒。建議前端顯示 loading state - **ffmpeg 資源**:同時多個請求可能導致高 CPU 使用。可考慮加入 per-identity lock 避免重複計算 - **邊界案例**:trace 內的 faces 全部 confidence ≤ 0.7 或 qc_ok=false,則該 trace 被跳過,可能導致 `best: null`