# Core API Face Groups Endpoint - 技術建議書 **日期**: 2026-07-19 **提出團隊**: Momentry Studio **狀態**: 建議中 --- ## 一、背景 ### 1.1 問題描述 Momentry Studio Proxy 當前實現以下 endpoints: | Endpoint | 當前行為 | 用途 | |----------|---------|------| | `/api/v1/processor-json` | 讀取本地 `{file_hash}.{processor}.json` | 返回 processor 輸出(asrx, ocr, face 等)| | `/api/v1/cluster-results` | 讀取本地 `{file_hash}.cluster_result.json` | 返回 face 分組結果 | **問題**: - 遠端瀏覽器訪問 `studio.momentry.ddns.net` 時,無法獲取本地文件數據 - Proxy 應該作為 forward 層,將請求轉發到 Core API ### 1.2 現有 Core API Endpoints | Endpoint | 狀態 | 說明 | |----------|------|------| | `POST /api/v1/file/:file_uuid/json/:processor` | ✅ 已存在 | 返回 processor JSON 數據 | | `POST /api/v1/file/:file_uuid/tkg/nodes` | ✅ 已存在 | 查詢 TKG nodes | | `GET /api/v1/trace-profile` | ✅ 已存在 | 查詢單一 trace profile | | `PUT /api/v1/trace-profile` | ✅ 已存在 | 更新 trace profile(含名稱)| | `PUT /api/v1/trace-profile/group` | ✅ 已存在 | 批量更新 trace 名稱 | | `GET /api/v1/file/:file_uuid/cluster-results` | ❌ 不存在 | 需要新增 | --- ## 二、架構理解 ### 2.1 Face Group 概念 **定義**: - Face Group = 多個 face traces 共享同一個名稱(label) - 名稱來自 TKG `face_trace` node 的 `label` 欄位 - 名稱可被用戶覆蓋(overridable) **技術實現**: ```json { "node_type": "face_trace", "label": "Cary Grant", // ← 可被用戶覆蓋 "properties": { "trace_id": 9, "face_count": 142, "avg_confidence": 0.87 } } ``` ### 2.2 Cluster 與 Face Group 的關係 **核心觀點**: - **Cluster 是技術手段**:用於將多個 face traces 分組 - **Face Group 是語義概念**:多個 traces 共享同一個名稱 - **Cluster ID 是技術編號**:無業務語義,僅用於識別分組 **命名流程**: ``` 用戶修改名稱 → Studio Frontend → Studio Proxy → Core API: PUT /api/v1/trace-profile → 更新 TKG node 的 label ``` --- ## 三、建議方案 ### 3.1 Processor JSON Endpoint **建議**:使用 Core API 現有 endpoint **Studio Proxy 實現**: ``` GET /api/v1/processor-json?file_hash={uuid}&processor={name} → Forward to Core API: POST /api/v1/file/{uuid}/json/{name}?api_key={API_KEY} ``` **Core API 需要做的**:無需修改 --- ### 3.2 Face Groups Endpoint(建議新增) #### 選項 A:以 Cluster 格式返回 **Endpoint**: ``` GET /api/v1/file/:file_uuid/cluster-results ``` **響應格式**: ```json { "success": true, "file_uuid": "d3f9ae8e471a1fc4d47022c66091b920", "clusters": [ { "cluster_id": 1, "name": "Cary Grant", "trace_ids": [9, 6, 8], "trace_count": 3, "representative_trace": 9 }, { "cluster_id": 2, "name": "Audrey Hepburn", "trace_ids": [1, 11, 4, 10], "trace_count": 4, "representative_trace": 1 } ], "total_clusters": 2 } ``` --- #### 選項 B:以 Face Groups 格式返回(推薦) **Endpoint**: ``` GET /api/v1/file/:file_uuid/face-groups ``` **響應格式**: ```json { "success": true, "file_uuid": "d3f9ae8e471a1fc4d47022c66091b920", "face_groups": [ { "group_id": 1, "name": "Cary Grant", "trace_ids": [9, 6, 8], "trace_count": 3, "representative_trace": 9, "editable": true, "total_face_count": 425 }, { "group_id": 2, "name": "Audrey Hepburn", "trace_ids": [1, 11, 4, 10], "trace_count": 4, "representative_trace": 1, "editable": true, "total_face_count": 380 } ], "total_groups": 2, "unassigned_traces": [5, 7] } ``` **推薦理由**: - 語義更清晰(Face Group vs Cluster) - 與命名機制對應 - 擴展性更好(可加入 `editable`, `total_face_count` 等屬性) - 可區分已命名和未命名的 traces --- ### 3.3 實現邏輯(建議) ```python # Pseudocode def get_face_groups(file_uuid: str): # 1. 查詢 TKG nodes response = post( f"{CORE_API}/api/v1/file/{file_uuid}/tkg/nodes", json={"node_type": "face_trace", "page_size": 500} ) nodes = response.json()["nodes"] # 2. 按 label 分組 groups = {} unassigned = [] for node in nodes: trace_id = node["properties"]["trace_id"] label = node["label"] face_count = node["properties"].get("face_count", 0) # 判斷是否為預設名稱(未命名) if label.startswith("Face Trace ") or label.startswith("Trace "): unassigned.append(trace_id) continue if label not in groups: groups[label] = { "trace_ids": [], "total_face_count": 0 } groups[label]["trace_ids"].append(trace_id) groups[label]["total_face_count"] += face_count # 3. 轉換為輸出格式 result = [] for idx, (name, data) in enumerate(sorted(groups.items()), 1): result.append({ "group_id": idx, "name": name, "trace_ids": data["trace_ids"], "trace_count": len(data["trace_ids"]), "representative_trace": data["trace_ids"][0], "editable": True, "total_face_count": data["total_face_count"] }) return { "success": True, "file_uuid": file_uuid, "face_groups": result, "total_groups": len(result), "unassigned_traces": unassigned } ``` --- ## 四、設計決策問題 ### 4.1 Endpoint 命名 **問題**: - 使用 `/cluster-results`(現有概念)? - 使用 `/face-groups`(語義更清晰)? **建議**:使用 `/face-groups`,理由: - 語義更準確(名稱可被覆蓋,不是固定的 cluster) - 與 `trace-profile` endpoint 命名一致 - 未來可以擴展(如 `/face-groups/:name` 查詢單一 group) --- ### 4.2 未命名 Traces 處理 **場景**: - 新創建的 face_trace,`label` 為預設值(如 `"Face Trace 9"`) - 用戶尚未命名 **建議選項**: | 選項 | 處理方式 | 優點 | 缺點 | |------|---------|------|------| | A | 歸類為 `unassigned_traces` 列表 | 清晰區分已命名/未命名 | 需要額外欄位 | | B | 每個未命名 trace 作為獨立 group | 格式統一 | 可能產生大量 group | | C | 不返回未命名 traces | 響應簡潔 | 遺失數據 | **建議**:選項 A,返回 `unassigned_traces` 列表 --- ### 4.3 Cluster ID / Group ID 的必要性 **問題**: - `cluster_id` 或 `group_id` 是否需要? - 如果只是順序編號,每次查詢可能不同 **建議選項**: | 選項 | 實現方式 | 優點 | 缺點 | |------|---------|------|------| | A | 使用 `name` 作為唯一標識 | 語義清晰,無需額外 ID | 名稱變更時引用會失效 | | B | 生成固定的 `group_id`(如 UUID) | ID 固定,不受名稱變更影響 | 需要額外存儲 | | C | 每次查詢時動態生成順序編號 | 實現簡單 | 編號可能變化 | **建議**:選項 C,動態生成順序編號(因為 Cluster ID 只是技術手段) --- ### 4.4 分頁支持 **問題**: - 如果一個文件有大量 face traces(如超過 500),如何處理? **建議**: - 支持分頁參數 `page` 和 `page_size` - 默認 `page_size=100`,最大 `500` --- ### 4.5 性能考慮 **問題**: - TKG nodes 查詢是否需要緩存? **建議**: - Core API 可以在內部緩存 TKG nodes 查詢結果 - 緩存時效:5-10 分鐘(或直到有 trace-profile 更新) --- ## 五、Studio Proxy 職責 ### 5.1 Processor JSON Handler **修改前**: ```rust // 讀取本地文件 let path = format!("{}/{}.{}.json", output_dir, file_hash, processor); let content = std::fs::read_to_string(&path)?; ``` **修改後**: ```rust // Forward 到 Core API let url = format!("{}/api/v1/file/{}/json/{}?api_key={}", CORE_API, file_hash, processor, API_KEY); let response = client.post(&url).send().await?; ``` --- ### 5.2 Cluster Results / Face Groups Handler **修改前**: ```rust // 讀取本地文件 let path = format!("{}/{}/{}.cluster_result.json", base, file_hash, file_hash); let content = std::fs::read_to_string(&path)?; ``` **修改後**: ```rust // Forward 到 Core API let url = format!("{}/api/v1/file/{}/face-groups?api_key={}", CORE_API, file_hash, API_KEY); let response = client.get(&url).send().await?; ``` --- ### 5.3 共享邏輯 **建議抽取共享函數**: ```rust async fn call_core_api(url: &str, method: Method, body: Option) -> Response { let client = reqwest::Client::new(); // 統一的錯誤處理 // 統一的響應轉換 } ``` --- ## 六、數據流向 ### 6.1 當前流程(本地文件) ``` Studio Frontend ↓ Studio Proxy ↓ 本地文件系統 → {file_hash}.{processor}.json ↓ 返回數據 問題:遠端瀏覽器無法訪問本地文件 ``` --- ### 6.2 建議流程(Forward 到 Core API) ``` Studio Frontend ↓ Studio Proxy (forward) ↓ Core API ├── POST /file/{uuid}/json/{processor} (已有) └── GET /file/{uuid}/face-groups (新增) ↓ 返回數據 優點:遠端瀏覽器可正常訪問 ``` --- ## 七、請求反饋 ### 7.1 請 Core API 團隊確認 1. **Endpoint 選擇**: - `/cluster-results` 或 `/face-groups`? 2. **未命名 traces**: - 是否同意返回 `unassigned_traces` 列表? 3. **Group ID**: - 是否同意使用動態生成的順序編號? - 或需要固定的 UUID? 4. **分頁**: - 是否需要支持? 5. **性能**: - 是否需要內部緩存? --- ### 7.2 Studio 團隊負責 1. 修改 Proxy handlers: - `get_processor_json_handler` → Forward 到 Core API - `get_cluster_results_handler` → Forward 到 Core API 2. 抽取共享邏輯: - 統一 Core API 客戶端 - 統一錯誤處理 --- ## 八、附錄 ### A. 相關 Core API Endpoints | Endpoint | 文檔位置 | |----------|---------| | Processor JSON | `docs_v1.0/doc_wasm/modules/04_lookup.md` | | TKG Nodes | `docs_v1.0/doc_wasm/modules/15_tkg.md` | | Trace Profile | `docs_v1.0/doc_wasm/modules/18_profile.md` | | Progress | `docs_v1.0/doc_wasm/modules/17_progress.md` | ### B. Studio 相關代碼 | 文件 | 說明 | |------|------| | `src-tauri/src/proxy.rs` | Proxy handlers | | `src/views/PeopleView.vue` | Face 頁面 | | `src/store.ts` | 數據加載邏輯 | --- **以上為 Studio 團隊提出的技術建議,歡迎 Core API 團隊討論與反饋。**