feat: add face filter components and integration guide
- useFaceFilter composable: manages face filtering state and API calls
- FaceFilterPanel component: UI for filtering controls
- FaceCard component: text-only face display card
- Integration guide: step-by-step guide for integrating into PeopleView
Implements hybrid filtering mode with:
- Instant mode: client-side filtering (5-10ms)
- Backend mode: server-side complex filtering
- Hybrid mode: backend + instant filtering (AND)
Uses actual Core API endpoints:
- GET /file/{uuid}/faces
- POST /file/{uuid}/faces/filter
- GET /file/{uuid}/face/{faceId}/filter-report
- GET /file/{uuid}/filter-stats
Preferences stored in localStorage.
This commit is contained in:
543
docs/face_filter_integration_guide.md
Normal file
543
docs/face_filter_integration_guide.md
Normal file
@@ -0,0 +1,543 @@
|
||||
# Face Filter Integration Guide for PeopleView
|
||||
|
||||
**版本**: v1.0
|
||||
**日期**: 2026-08-09
|
||||
**狀態**: 可整合
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
本指南說明如何將 Face Filter 功能整合到 PeopleView 中。
|
||||
|
||||
---
|
||||
|
||||
## 已創建的檔案
|
||||
|
||||
### 1. Composable
|
||||
**位置**: `src/composables/useFaceFilter.ts`
|
||||
|
||||
**功能**:
|
||||
- 管理臉部過濾狀態
|
||||
- 提供三種過濾模式(instant, backend, hybrid)
|
||||
- 處理 API 呼叫
|
||||
- 儲存使用者偏好到 localStorage
|
||||
|
||||
**主要 exports**:
|
||||
```typescript
|
||||
export function useFaceFilter() {
|
||||
return {
|
||||
mode, // 'instant' | 'backend' | 'hybrid'
|
||||
rules, // QuickFilterRules
|
||||
allFaces, // FaceMetadata[]
|
||||
filteredFaces, // FaceMetadata[]
|
||||
filterResult, // FilterResult | null
|
||||
filterStats, // FilterStats | null
|
||||
loading, // boolean
|
||||
error, // string | null
|
||||
stats, // computed stats
|
||||
|
||||
// Actions
|
||||
initialize,
|
||||
updateRules,
|
||||
setMode,
|
||||
applyBackendFilter,
|
||||
getFaceReportData,
|
||||
refreshStats,
|
||||
resetToDefault,
|
||||
applyLocalFilter
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Components
|
||||
|
||||
#### FaceFilterPanel.vue
|
||||
**位置**: `src/components/FaceFilterPanel.vue`
|
||||
|
||||
**功能**:
|
||||
- 過濾面板 UI
|
||||
- 模式選擇(instant/backend/hybrid)
|
||||
- 過濾規則控制(confidence, bbox_size, source, backend_status)
|
||||
- 統計顯示
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
{
|
||||
fileUuid: string // 必需的檔案 UUID
|
||||
}
|
||||
```
|
||||
|
||||
**使用範例**:
|
||||
```vue
|
||||
<FaceFilterPanel :fileUuid="selectedFileUuid" />
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### FaceCard.vue
|
||||
**位置**: `src/components/FaceCard.vue`
|
||||
|
||||
**功能**:
|
||||
- 臉部卡片顯示(純文字,無縮圖)
|
||||
- 狀態標示(accept/reject/review)
|
||||
- 來源徽章(AV/MP)
|
||||
- 選擇功能
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
{
|
||||
face: FaceMetadata,
|
||||
selected?: boolean,
|
||||
showCheckbox?: boolean
|
||||
}
|
||||
```
|
||||
|
||||
**Events**:
|
||||
```typescript
|
||||
{
|
||||
click: [face: FaceMetadata],
|
||||
select: [face: FaceMetadata]
|
||||
}
|
||||
```
|
||||
|
||||
**使用範例**:
|
||||
```vue
|
||||
<FaceCard
|
||||
:face="face"
|
||||
:selected="selectedFaces.includes(face)"
|
||||
@click="openFaceDetail(face)"
|
||||
@select="toggleFaceSelection(face)"
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 整合步驟
|
||||
|
||||
### Step 1: 導入 Composable
|
||||
|
||||
在 PeopleView.vue 中導入 useFaceFilter:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useFaceFilter } from '../composables/useFaceFilter';
|
||||
|
||||
const {
|
||||
mode,
|
||||
rules,
|
||||
allFaces,
|
||||
filteredFaces,
|
||||
filterStats,
|
||||
loading: filterLoading,
|
||||
error: filterError,
|
||||
stats: filterStats,
|
||||
initialize,
|
||||
updateRules,
|
||||
setMode,
|
||||
applyBackendFilter,
|
||||
resetToDefault
|
||||
} = useFaceFilter();
|
||||
|
||||
// ... existing code ...
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 2: 添加 FaceFilterPanel 組件
|
||||
|
||||
在 toolbar 區域添加 FaceFilterPanel:
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<div class="people-view">
|
||||
<div class="ms-ppl-control-area">
|
||||
<!-- Existing toolbar -->
|
||||
<div class="ms-ppl-toolbar">
|
||||
<!-- ... existing controls ... -->
|
||||
</div>
|
||||
|
||||
<!-- NEW: Face Filter Panel -->
|
||||
<FaceFilterPanel
|
||||
v-if="selectedFileUuid"
|
||||
:fileUuid="selectedFileUuid"
|
||||
ref="faceFilterPanel"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ... rest of the view ... -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import FaceFilterPanel from '../components/FaceFilterPanel.vue';
|
||||
|
||||
const faceFilterPanel = ref();
|
||||
|
||||
// ... existing code ...
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 3: 初始化過濾
|
||||
|
||||
當選擇檔案時初始化過濾:
|
||||
|
||||
```typescript
|
||||
// 在 onFileFilterChange 或 watch(selectedFileUuid) 中
|
||||
watch(selectedFileUuid, async (newUuid) => {
|
||||
if (newUuid) {
|
||||
await initialize(newUuid);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 4: 使用過濾後的臉部
|
||||
|
||||
使用 `filteredFaces` 替代原始的臉部列表:
|
||||
|
||||
```vue
|
||||
<!-- 原本的臉部列表 -->
|
||||
<div v-for="face in allFaces" :key="face.face_id">
|
||||
<!-- ... -->
|
||||
</div>
|
||||
|
||||
<!-- 改為使用過濾後的臉部 -->
|
||||
<div v-for="face in filteredFaces" :key="face.face_id">
|
||||
<FaceCard
|
||||
:face="face"
|
||||
@click="openFaceDetail(face)"
|
||||
/>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 5: 整合到現有 UI
|
||||
|
||||
#### 選項 A: 獨立面板(推薦)
|
||||
|
||||
FaceFilterPanel 作為獨立面板,放在 toolbar 下方:
|
||||
|
||||
```vue
|
||||
<div class="ms-ppl-control-area">
|
||||
<div class="ms-ppl-toolbar">
|
||||
<!-- Existing controls -->
|
||||
</div>
|
||||
|
||||
<!-- Face Filter Panel -->
|
||||
<FaceFilterPanel :fileUuid="selectedFileUuid" />
|
||||
|
||||
<!-- Undo Banner -->
|
||||
<div v-if="showUndoBanner" class="ms-undo-banner">
|
||||
<!-- ... -->
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### 選項 B: 整合到現有面板
|
||||
|
||||
將過濾控制整合到現有的 Pending Faces 面板:
|
||||
|
||||
```vue
|
||||
<div v-if="showUface" class="ms-ppl-section">
|
||||
<div class="ms-ppl-section-toolbar">
|
||||
<div class="ms-ppl-section-title">Pending Faces</div>
|
||||
|
||||
<!-- Add filter controls -->
|
||||
<div class="filter-controls-inline">
|
||||
<select v-model="rules.source">
|
||||
<option value="all">All</option>
|
||||
<option value="apple_vision">AV</option>
|
||||
<option value="mediapipe">MP</option>
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
v-model.number="rules.min_confidence"
|
||||
placeholder="Min confidence"
|
||||
/>
|
||||
<button @click="applyBackendFilter(selectedFileUuid)">
|
||||
Apply Backend
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Face cards -->
|
||||
<div class="ms-ppl-face-grid">
|
||||
<FaceCard
|
||||
v-for="face in filteredFaces"
|
||||
:key="face.face_id"
|
||||
:face="face"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 端點
|
||||
|
||||
所有 API 呼叫都通過 composable 處理,使用 proxy 到 `http://localhost:8888/api/v1/...`
|
||||
|
||||
**實際端點**:
|
||||
1. `GET /api/v1/file/{uuid}/faces` - 獲取臉部元數據
|
||||
2. `POST /api/v1/file/{uuid}/faces/filter` - 套用後端過濾
|
||||
3. `GET /api/v1/file/{uuid}/face/{faceId}/filter-report` - 獲取單臉報告
|
||||
4. `GET /api/v1/file/{uuid}/filter-stats` - 獲取統計
|
||||
5. `GET/POST /api/v1/file/{uuid}/filter-rules` - 規則 CRUD
|
||||
|
||||
---
|
||||
|
||||
## 資料結構
|
||||
|
||||
### FaceMetadata
|
||||
|
||||
```typescript
|
||||
interface FaceMetadata {
|
||||
face_id: string;
|
||||
frame: number;
|
||||
primary_source: 'apple_vision' | 'mediapipe';
|
||||
primary_confidence: number;
|
||||
detection_sources: Array<{
|
||||
source_type: string;
|
||||
confidence: number;
|
||||
}>;
|
||||
filter_features: {
|
||||
face_id: string;
|
||||
frame: number;
|
||||
bbox_size: number; // min(width, height)
|
||||
apple_vision_confidence: number;
|
||||
mediapipe_confidence?: number;
|
||||
eye_ratio?: number;
|
||||
nose_ratio?: number;
|
||||
pose_score?: number;
|
||||
mediapipe_detected?: boolean;
|
||||
mediapipe_iou?: number;
|
||||
embedding_similarity?: number;
|
||||
consecutive_frames?: number;
|
||||
total_frames_detected?: number;
|
||||
backend_filter_status?: 'accept' | 'reject' | 'review';
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 過濾模式
|
||||
|
||||
### Instant Mode
|
||||
- **效能**: 5-10ms
|
||||
- **邏輯**: Client 端即時過濾
|
||||
- **適用**: 快速調整規則
|
||||
|
||||
### Backend Mode
|
||||
- **效能**: 100-200ms(API 呼叫)
|
||||
- **邏輯**: 後端複雜過濾(landmark, embedding, temporal)
|
||||
- **適用**: 高品質過濾
|
||||
|
||||
### Hybrid Mode
|
||||
- **效能**: 5-10ms(前端)+ 後端預先計算
|
||||
- **邏輯**: 後端標示 + 前端即時過濾(AND)
|
||||
- **適用**: 最佳使用者體驗
|
||||
|
||||
---
|
||||
|
||||
## 使用者偏好
|
||||
|
||||
偏好儲存在 `localStorage`:
|
||||
|
||||
**Key**: `momentry_face_filter_preferences`
|
||||
|
||||
**結構**:
|
||||
```json
|
||||
{
|
||||
"mode": "instant",
|
||||
"rules": {
|
||||
"min_confidence": 0.5,
|
||||
"min_bbox_size": 80,
|
||||
"source": "all",
|
||||
"backend_status": "accept"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 效能優化
|
||||
|
||||
### 分頁
|
||||
- `page_size ≤ 50`(防 hang)
|
||||
- 虛擬滾動(可選)
|
||||
|
||||
### 快取
|
||||
- 臉部元數據快取在 composable
|
||||
- 偏好儲存在 localStorage
|
||||
|
||||
### 懶載入
|
||||
- JPG 縮圖懶載入(未來擴展)
|
||||
- 當前使用純文字卡片
|
||||
|
||||
---
|
||||
|
||||
## 錯誤處理
|
||||
|
||||
Composable 提供 error 狀態:
|
||||
|
||||
```typescript
|
||||
if (error.value) {
|
||||
console.error('Face filter error:', error.value);
|
||||
// 顯示錯誤訊息給使用者
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 測試
|
||||
|
||||
### 單元測試
|
||||
```typescript
|
||||
import { useFaceFilter } from '../composables/useFaceFilter';
|
||||
|
||||
describe('useFaceFilter', () => {
|
||||
it('should filter faces by confidence', () => {
|
||||
const { updateRules, filteredFaces } = useFaceFilter();
|
||||
|
||||
// Setup test data
|
||||
// ...
|
||||
|
||||
updateRules({ min_confidence: 0.7 });
|
||||
expect(filteredFaces.value.length).toBeLessThan(allFaces.value.length);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### 整合測試
|
||||
1. 選擇檔案
|
||||
2. 驗證臉部載入
|
||||
3. 調整過濾規則
|
||||
4. 驗證過濾結果
|
||||
5. 切換模式
|
||||
6. 驗證模式切換
|
||||
|
||||
---
|
||||
|
||||
## 未來擴展
|
||||
|
||||
### 1. 縮圖支援
|
||||
當 Core 實作 `/face/{id}/thumbnail` 端點後:
|
||||
|
||||
```vue
|
||||
<FaceCard :face="face" showThumbnail />
|
||||
```
|
||||
|
||||
### 2. 規則管理 UI
|
||||
當需要規則 CRUD 時:
|
||||
|
||||
```vue
|
||||
<RuleManager
|
||||
:fileUuid="selectedFileUuid"
|
||||
@rule-change="handleRuleChange"
|
||||
/>
|
||||
```
|
||||
|
||||
### 3. 批量操作
|
||||
```typescript
|
||||
const selectedFaces = ref<FaceMetadata[]>([]);
|
||||
|
||||
const batchAction = async (action: 'accept' | 'reject') => {
|
||||
await fetch('/api/v1/file/.../faces/batch', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
face_ids: selectedFaces.value.map(f => f.face_id),
|
||||
action
|
||||
})
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
### 4. 匯出功能
|
||||
```typescript
|
||||
const exportFilteredFaces = () => {
|
||||
const csv = filteredFaces.value.map(face =>
|
||||
`${face.face_id},${face.frame},${face.primary_confidence},${face.filter_features.bbox_size}`
|
||||
).join('\n');
|
||||
|
||||
// Download CSV
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 疑難排解
|
||||
|
||||
### 問題 1: 臉部未載入
|
||||
**檢查**:
|
||||
- `selectedFileUuid` 是否正確
|
||||
- API 端點是否可達
|
||||
- 網路請求是否成功
|
||||
|
||||
**解決**:
|
||||
```typescript
|
||||
console.log('File UUID:', selectedFileUuid);
|
||||
console.log('API Response:', await fetchFaces(selectedFileUuid));
|
||||
```
|
||||
|
||||
### 問題 2: 過濾未生效
|
||||
**檢查**:
|
||||
- `mode` 是否正確
|
||||
- `rules` 是否正確
|
||||
- `filteredFaces` 是否更新
|
||||
|
||||
**解決**:
|
||||
```typescript
|
||||
console.log('Mode:', mode.value);
|
||||
console.log('Rules:', rules.value);
|
||||
console.log('Filtered:', filteredFaces.value.length);
|
||||
```
|
||||
|
||||
### 問題 3: 偏好未儲存
|
||||
**檢查**:
|
||||
- `localStorage` 是否可寫
|
||||
- Key 是否正確
|
||||
|
||||
**解決**:
|
||||
```typescript
|
||||
console.log('LocalStorage:', localStorage.getItem('momentry_face_filter_preferences'));
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 總結
|
||||
|
||||
**整合步驟**:
|
||||
1. ✅ 導入 useFaceFilter composable
|
||||
2. ✅ 添加 FaceFilterPanel 組件
|
||||
3. ✅ 初始化過濾(watch selectedFileUuid)
|
||||
4. ✅ 使用 filteredFaces 顯示
|
||||
5. ✅ 處理錯誤和載入狀態
|
||||
|
||||
**預期效果**:
|
||||
- 即時過濾(5-10ms)
|
||||
- 三種模式切換
|
||||
- 使用者偏好儲存
|
||||
- 純文字卡片(無縮圖)
|
||||
|
||||
**未來擴展**:
|
||||
- 縮圖支援
|
||||
- 規則管理 UI
|
||||
- 批量操作
|
||||
- 匯出功能
|
||||
|
||||
---
|
||||
|
||||
**文件版本**: v1.0
|
||||
**最後更新**: 2026-08-09
|
||||
**相關文件**:
|
||||
- `/Users/accusys/momentry_core/docs_v1.0/STUDIO_TEAM/face_filter_usage_guide.md`
|
||||
- `/Users/accusys/momentry_core/docs_v1.0/STUDIO_TEAM/face_filter_hybrid_mode_guide.md`
|
||||
Reference in New Issue
Block a user