feat: frame-based positioning and mark system foundation
Core Changes: - Fix SearchView to use start_frame/end_frame directly (no time*fps conversion) - Add hard_delete support to delete_trace API - VideoPlayer: Main timeline + Mark system foundation - Proxy: Add local routes for auth, media, identity-matches, cluster-results - Add .gitignore to exclude build artifacts and dependencies Design Documents: - Multi-track Mark system design (.opencode/plans/) - Video editing positioning standards research Files Modified: - src/views/SearchView.vue: Frame positioning, ensureMinDuration (240 frames) - src/views/PeopleView.vue: batchDeleteGroups with hard_delete - src/api/index.ts: delete_trace with hard_delete body - src/components/VideoPlayer.vue: Timeline + Mark UI - src-tauri/src/proxy.rs: New local routes - AGENTS.md: Update documentation
This commit is contained in:
68
docs/FACE_CLUSTERING_AGENT_REQUEST.md
Normal file
68
docs/FACE_CLUSTERING_AGENT_REQUEST.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Face Clustering Agent Endpoint Request
|
||||
|
||||
## 需求
|
||||
新增一個 on-demand face clustering endpoint,讓前端可以手動觸發 face trace 分組。
|
||||
|
||||
## 背景
|
||||
Momentry Studio 的 People 頁面有「Face Deduplication」按鈕,目的是讓使用者可以對已處理 face 的影片執行 face 分組(clustering)。目前這個功能無法使用,因為對應的 API endpoint 不存在。
|
||||
|
||||
## 需要的 Endpoint
|
||||
|
||||
### `POST /api/v1/file/:file_uuid/cluster-agent`
|
||||
|
||||
**用途**:對指定檔案觸發 face clustering,將相似的 face traces 分組。
|
||||
|
||||
#### Request
|
||||
```
|
||||
POST /api/v1/file/{file_uuid}/cluster-agent
|
||||
Content-Type: application/json
|
||||
X-API-Key: {key}
|
||||
|
||||
{}
|
||||
```
|
||||
|
||||
| 參數 | 類型 | 必填 | 說明 |
|
||||
|------|------|------|------|
|
||||
| `file_uuid` | string | ✅ (URL path) | 檔案 UUID |
|
||||
|
||||
#### Response (200)
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"file_uuid": "477b24d3...",
|
||||
"message": "Clustering started for file",
|
||||
"clusters": 0,
|
||||
"total_traces": 2
|
||||
}
|
||||
```
|
||||
|
||||
| 欄位 | 類型 | 說明 |
|
||||
|------|------|------|
|
||||
| `success` | boolean | 是否成功觸發 |
|
||||
| `file_uuid` | string | 檔案 UUID |
|
||||
| `message` | string | 狀態訊息 |
|
||||
| `clusters` | integer | 已分組數量(剛觸發時為 0) |
|
||||
| `total_traces` | integer | 該檔案總 face trace 數 |
|
||||
|
||||
#### 實作邏輯(建議)
|
||||
1. 檢查檔案是否存在且 face 已處理完成
|
||||
2. 讀取 face detections / face embeddings
|
||||
3. 執行 face clustering(可參考 pipeline 中的 clustering 邏輯)
|
||||
4. 將分組結果寫入 TKG(更新 `face_trace` node 的 `label`)
|
||||
5. 回傳觸發結果(clustering 可非同步執行)
|
||||
|
||||
### 前端消費流程
|
||||
```
|
||||
1. POST /api/v1/file/{uuid}/cluster-agent → 觸發 clustering
|
||||
2. GET /api/v1/file/{uuid}/face-groups → 輪詢直到 face_groups 有資料
|
||||
3. 顯示 face groups 在 People 頁面
|
||||
```
|
||||
|
||||
## 相關檔案
|
||||
- **前端按鈕**: `src/views/PeopleView.vue` line 14-18 (`runClusterAgent()`)
|
||||
- **API 映射**: `src/api/index.ts` line 369-371 (`run_cluster_agent`)
|
||||
- **現有 face-groups**: `GET /api/v1/file/:file_uuid/face-groups` (已存在,用於讀取結果)
|
||||
- **Pipeline 處理**: `docs_v1.0/doc_wasm/modules/05_process.md` (face 處理流程)
|
||||
|
||||
## 優先級
|
||||
中優先級 — 目前有 2 個 unassigned traces 但無法分組,使用者無法有效的進行 face deduplication。
|
||||
358
docs/SEARCH_IMPROVEMENTS_FOR_CORE_TEAM.md
Normal file
358
docs/SEARCH_IMPROVEMENTS_FOR_CORE_TEAM.md
Normal file
@@ -0,0 +1,358 @@
|
||||
# Search Improvements for Core Team
|
||||
|
||||
## Overview
|
||||
|
||||
This document outlines the search improvements implemented in Studio that may require Core API support or coordination.
|
||||
|
||||
---
|
||||
|
||||
## 1. Keyword Search - Result Merging
|
||||
|
||||
### Current Implementation (Studio-side)
|
||||
|
||||
```typescript
|
||||
function mergeResults(results: any[]): any[] {
|
||||
// Sort by start_frame
|
||||
const sorted = [...results].sort((a, b) => (a.start_frame || 0) - (b.start_frame || 0))
|
||||
|
||||
// Merge overlapping results (gap <= 30 frames)
|
||||
for (const r of sorted) {
|
||||
const last = merged[merged.length - 1]
|
||||
if (last && last.file_uuid === r.file_uuid) {
|
||||
const gap = r.start_frame - last.end_frame
|
||||
if (gap <= 30) {
|
||||
// Merge: extend end_frame, keep best summary
|
||||
last.end_frame = Math.max(last.end_frame, r.end_frame)
|
||||
if (r.similarity > last.similarity) {
|
||||
last.summary = r.summary
|
||||
last.similarity = r.similarity
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
merged.push({ ...r })
|
||||
}
|
||||
return merged.slice(0, 10) // Top 10
|
||||
}
|
||||
```
|
||||
|
||||
### Behavior
|
||||
|
||||
| Before | After |
|
||||
|--------|-------|
|
||||
| 20 results, overlapping segments | 10 merged results |
|
||||
| Same content appears multiple times | Deduplicated by frame proximity |
|
||||
| Short segments (1-2 seconds) | Merged into longer segments |
|
||||
|
||||
### Core API Considerations
|
||||
|
||||
**Option A: Server-side Merging**
|
||||
- Add `merge=true` parameter to `/api/v1/search/smart`
|
||||
- Server returns pre-merged results
|
||||
- More efficient, less data transfer
|
||||
|
||||
**Option B: Client-side (Current)**
|
||||
- Studio fetches 30 results, merges locally
|
||||
- More flexible, no Core API changes needed
|
||||
|
||||
---
|
||||
|
||||
## 2. Minimum Duration Enforcement
|
||||
|
||||
### Problem
|
||||
|
||||
Some search results are too short (1-2 seconds) to provide meaningful context.
|
||||
|
||||
### Proposed Solution
|
||||
|
||||
Add `min_duration` parameter to search endpoints:
|
||||
|
||||
```bash
|
||||
POST /api/v1/search/smart
|
||||
{
|
||||
"query": "Audrey Hepburn",
|
||||
"min_duration": 10, // Minimum 10 seconds
|
||||
"limit": 10
|
||||
}
|
||||
```
|
||||
|
||||
### Core API Impact
|
||||
|
||||
| Endpoint | Change |
|
||||
|----------|--------|
|
||||
| `/api/v1/search/smart` | Add `min_duration` param |
|
||||
| `/api/v1/search/llm-smart` | Add `min_duration` param |
|
||||
|
||||
**Implementation:**
|
||||
1. Filter results where `end_time - start_time < min_duration`
|
||||
2. Or expand short results by including adjacent chunks
|
||||
|
||||
---
|
||||
|
||||
## 3. Frame-based Range Adjustment
|
||||
|
||||
### Current Studio Implementation
|
||||
|
||||
Users can adjust playback range by frame number:
|
||||
|
||||
```vue
|
||||
<div class="card-range">
|
||||
<input type="number" v-model.number="r._startFrame">
|
||||
<input type="number" v-model.number="r._endFrame">
|
||||
</div>
|
||||
```
|
||||
|
||||
### Core API Requirements
|
||||
|
||||
No changes needed - Studio uses existing `start_frame`/`end_frame` fields.
|
||||
|
||||
**Display format:**
|
||||
```
|
||||
F100–F350 (4:10–14:35)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Mark & Export Feature
|
||||
|
||||
### Studio Implementation
|
||||
|
||||
```typescript
|
||||
const markedResults = ref<any[]>([])
|
||||
|
||||
function toggleMark(r: any) {
|
||||
const key = `${r.file_uuid}:${r.start_frame}-${r.end_frame}`
|
||||
const idx = markedResults.value.findIndex(m =>
|
||||
`${m.file_uuid}:${m.start_frame}-${m.end_frame}` === key
|
||||
)
|
||||
if (idx >= 0) {
|
||||
markedResults.value.splice(idx, 1)
|
||||
} else {
|
||||
markedResults.value.push({...r})
|
||||
}
|
||||
}
|
||||
|
||||
function exportMarked() {
|
||||
const data = {
|
||||
exportedAt: new Date().toISOString(),
|
||||
count: markedResults.value.length,
|
||||
results: markedResults.value
|
||||
}
|
||||
// Download as JSON
|
||||
}
|
||||
```
|
||||
|
||||
### Export Format
|
||||
|
||||
```json
|
||||
{
|
||||
"exportedAt": "2026-07-20T12:00:00.000Z",
|
||||
"count": 3,
|
||||
"results": [
|
||||
{
|
||||
"file_uuid": "abc123",
|
||||
"file_name": "Roman Holiday.mp4",
|
||||
"start_frame": 100,
|
||||
"end_frame": 350,
|
||||
"start_time": 4.16,
|
||||
"end_time": 14.58,
|
||||
"summary": "Audrey Hepburn speaking...",
|
||||
"similarity": 0.85
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Future Core API Integration
|
||||
|
||||
**Possible endpoints:**
|
||||
|
||||
```
|
||||
POST /api/v1/marks # Save marked segment
|
||||
GET /api/v1/marks # List saved marks
|
||||
POST /api/v1/marks/export # Export marks as EDL/JSON
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Vector/Keyword Weight Ratio
|
||||
|
||||
### Current State
|
||||
|
||||
`/api/v1/search/llm-smart` uses RRF (Reciprocal Rank Fusion):
|
||||
- Vector search: 50%
|
||||
- Keyword (BM25): 50%
|
||||
|
||||
### Proposed Change
|
||||
|
||||
Increase vector weight for semantic relevance:
|
||||
|
||||
```python
|
||||
# Current
|
||||
score = 0.5 / (k + rank_vector) + 0.5 / (k + rank_keyword)
|
||||
|
||||
# Proposed
|
||||
VECTOR_WEIGHT = 0.7
|
||||
KEYWORD_WEIGHT = 0.3
|
||||
score = VECTOR_WEIGHT / (k + rank_vector) + KEYWORD_WEIGHT / (k + rank_keyword)
|
||||
```
|
||||
|
||||
### Core API Change Required
|
||||
|
||||
Add `vector_weight` parameter:
|
||||
|
||||
```bash
|
||||
POST /api/v1/search/llm-smart
|
||||
{
|
||||
"query": "two people talking",
|
||||
"vector_weight": 0.7, # Default: 0.5
|
||||
"limit": 10
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Agent Search - Question Templates
|
||||
|
||||
### Studio Implementation
|
||||
|
||||
Pre-defined question templates shown below search input:
|
||||
|
||||
```typescript
|
||||
const QUESTION_TEMPLATES = [
|
||||
{
|
||||
category: '找檔案',
|
||||
examples: [
|
||||
'找出包含 {人物} 的影片',
|
||||
'找出 {年份} 年的影片'
|
||||
]
|
||||
},
|
||||
{
|
||||
category: '找人物',
|
||||
examples: [
|
||||
'{人物} 出現在哪些影片?',
|
||||
'{人物} 和 {人物} 第一次同框'
|
||||
]
|
||||
},
|
||||
{
|
||||
category: '找內容',
|
||||
examples: [
|
||||
'找出討論 {主題} 的片段',
|
||||
'找出 {時間點} 發生什麼事'
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Core API Requirements
|
||||
|
||||
No changes needed - templates are Studio-side UI.
|
||||
|
||||
---
|
||||
|
||||
## 7. VLM Search Integration (Future)
|
||||
|
||||
### Proposal
|
||||
|
||||
Add visual-language model search capability:
|
||||
|
||||
```bash
|
||||
POST /api/v1/search/vlm
|
||||
{
|
||||
"query": "person wearing red dress",
|
||||
"file_uuid": "optional"
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"file_uuid": "abc123",
|
||||
"start_frame": 1000,
|
||||
"end_frame": 1050,
|
||||
"description": "Woman in red dress walking",
|
||||
"confidence": 0.92
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Core Team Considerations
|
||||
|
||||
1. Model selection (CLIP, BLIP, etc.)
|
||||
2. GPU requirements
|
||||
3. Indexing strategy
|
||||
4. Latency expectations
|
||||
|
||||
---
|
||||
|
||||
## Summary of Core API Changes
|
||||
|
||||
| Feature | Priority | Core API Changes |
|
||||
|---------|----------|------------------|
|
||||
| Result merging | Low | Optional: `merge=true` param |
|
||||
| Min duration | Medium | Add `min_duration` param |
|
||||
| Frame adjustment | None | No changes needed |
|
||||
| Mark/Export | Low | Future: marks endpoints |
|
||||
| Vector weight | Medium | Add `vector_weight` param |
|
||||
| Question templates | None | Studio-side only |
|
||||
| VLM search | Future | New endpoint |
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist for Core Team
|
||||
|
||||
### 1. Search Endpoint Testing
|
||||
|
||||
```bash
|
||||
# Test basic search
|
||||
curl -X POST http://localhost:3002/api/v1/search/smart \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "test", "limit": 30}'
|
||||
|
||||
# Test with min_duration (if implemented)
|
||||
curl -X POST http://localhost:3002/api/v1/search/smart \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "test", "min_duration": 10}'
|
||||
|
||||
# Test vector weight (if implemented)
|
||||
curl -X POST http://localhost:3002/api/v1/search/llm-smart \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"query": "test", "vector_weight": 0.7}'
|
||||
```
|
||||
|
||||
### 2. Performance Testing
|
||||
|
||||
| Metric | Target |
|
||||
|--------|--------|
|
||||
| Search latency | < 500ms |
|
||||
| Result count | Stable at limit=30 |
|
||||
| Memory usage | No significant increase |
|
||||
|
||||
### 3. Integration Testing
|
||||
|
||||
- [ ] Search with file_uuid filter
|
||||
- [ ] Search without file_uuid (global)
|
||||
- [ ] Verify start_frame/end_frame accuracy
|
||||
- [ ] Verify time/frame consistency (fps)
|
||||
|
||||
---
|
||||
|
||||
## Questions for Core Team
|
||||
|
||||
1. Should result merging be server-side or client-side?
|
||||
2. What's the acceptable latency for search?
|
||||
3. Is `min_duration` parameter feasible?
|
||||
4. Can `vector_weight` be made configurable?
|
||||
5. Timeline for VLM search integration?
|
||||
|
||||
---
|
||||
|
||||
*Document created: 2026-07-20*
|
||||
*Author: Studio Team*
|
||||
117
docs/SEARCH_SOURCE_TYPE_REQUEST.md
Normal file
117
docs/SEARCH_SOURCE_TYPE_REQUEST.md
Normal file
@@ -0,0 +1,117 @@
|
||||
# Search API Source Type Request
|
||||
|
||||
## Problem
|
||||
|
||||
Currently, the Search API (`/api/v1/search/keyword` and `/api/v1/search/semantic`) returns results without indicating the source type:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 0,
|
||||
"file_uuid": "...",
|
||||
"start_frame": 0,
|
||||
"end_frame": 128,
|
||||
"start_time": 0.0,
|
||||
"end_time": 5.33,
|
||||
"summary": "And speaking of storage and workflow...",
|
||||
"text_content": "And speaking of storage and workflow...",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
There's no field to distinguish between:
|
||||
- **OCR**: Text extracted from video frames (on-screen text, titles, credits)
|
||||
- **ASRX**: Speech-to-text transcription
|
||||
|
||||
---
|
||||
|
||||
## Request
|
||||
|
||||
Add a `source_type` field to the `SearchResult` struct:
|
||||
|
||||
```rust
|
||||
pub struct SearchResult {
|
||||
pub id: i32,
|
||||
pub file_uuid: Option<String>,
|
||||
pub parent_id: i32,
|
||||
pub scene_order: Option<i32>,
|
||||
pub start_frame: i64,
|
||||
pub end_frame: i64,
|
||||
pub fps: f64,
|
||||
pub start_time: f64,
|
||||
pub end_time: f64,
|
||||
pub raw_text: Option<String>,
|
||||
pub summary: Option<String>,
|
||||
pub text_content: Option<String>,
|
||||
pub metadata: Option<serde_json::Value>,
|
||||
pub similarity: Option<f64>,
|
||||
pub file_name: Option<String>,
|
||||
pub serve_url: Option<String>,
|
||||
pub thumbnail_url: Option<String>,
|
||||
pub source_type: Option<String>, // NEW: "ocr" or "asrx"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Expected Response
|
||||
|
||||
```json
|
||||
{
|
||||
"id": 0,
|
||||
"file_uuid": "...",
|
||||
"start_frame": 0,
|
||||
"end_frame": 128,
|
||||
"start_time": 0.0,
|
||||
"end_time": 5.33,
|
||||
"summary": "And speaking of storage and workflow...",
|
||||
"text_content": "And speaking of storage and workflow...",
|
||||
"source_type": "ocr",
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Use Case
|
||||
|
||||
Studio frontend needs to display source type tags on search result cards:
|
||||
|
||||
```
|
||||
[OCR] And speaking of storage and workflow...
|
||||
F0–128 (0:00–0:05)
|
||||
```
|
||||
|
||||
```
|
||||
[ASRX] Today we're talking about storage solutions...
|
||||
(0:10–0:20)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
1. The database already has `chunk_type` field with values like `'sentence'`, `'cut'`, etc.
|
||||
2. Need to map chunk types to source types:
|
||||
- OCR chunks → `source_type: "ocr"`
|
||||
- ASRX/sentence chunks → `source_type: "asrx"`
|
||||
3. Update `enrich_from_pg()` function to include source_type
|
||||
4. Update `SearchResult` struct serialization
|
||||
|
||||
---
|
||||
|
||||
## Affected Files
|
||||
|
||||
- `/Users/accusys/momentry_core/src/api/search.rs`: `SearchResult` struct and enrichment
|
||||
- `/Users/accusys/momentry_core/src/core/db/postgres_db.rs`: `get_chunk_by_file_and_chunk_id()` query
|
||||
|
||||
---
|
||||
|
||||
## Priority
|
||||
|
||||
Medium - Required for proper search result categorization and display
|
||||
|
||||
---
|
||||
|
||||
## Contact
|
||||
|
||||
Studio Team
|
||||
90
docs/VLM_AGENT_TOOL_REQUEST.md
Normal file
90
docs/VLM_AGENT_TOOL_REQUEST.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# VLM (Vision Language Model) Tool for Agent Search
|
||||
|
||||
## Request
|
||||
Add a `vlm_describe` tool to the Agent system that can analyze video keyframes via VLM.
|
||||
|
||||
## Background
|
||||
We have Ollama with LLaVA running locally (`http://localhost:11434`). The VLM can describe image content (people, objects, scenes, clothing, text, etc.). We want the Agent to be able to use this as a tool when users ask questions about visual content.
|
||||
|
||||
## Use Cases
|
||||
- "What is the person wearing in this scene?"
|
||||
- "Find scenes where someone is wearing a red shirt"
|
||||
- "Describe the background of this video"
|
||||
- "What objects are visible in frame 1000?"
|
||||
|
||||
## API Design
|
||||
|
||||
### Agent Tool Registration
|
||||
Add a `vlm_describe` tool to the Agent's tool registry in Core API.
|
||||
|
||||
### Tool Definition
|
||||
```json
|
||||
{
|
||||
"name": "vlm_describe",
|
||||
"description": "Analyze a video frame using Vision Language Model. Returns a description of the image content.",
|
||||
"parameters": {
|
||||
"file_uuid": "string - UUID of the video file",
|
||||
"frame": "integer - Frame number to analyze",
|
||||
"prompt": "string (optional) - Specific question about the image (default: 'Describe this image in 1-2 sentences.')"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Internal Implementation
|
||||
The Core API should call:
|
||||
```
|
||||
POST http://localhost:11434/api/generate
|
||||
{
|
||||
"model": "llava",
|
||||
"prompt": "<user prompt or default>",
|
||||
"images": ["<base64 of frame thumbnail>"],
|
||||
"stream": false,
|
||||
"options": { "num_predict": 80 }
|
||||
}
|
||||
```
|
||||
|
||||
To get the frame image, use the existing thumbnail API:
|
||||
```
|
||||
GET /api/v1/file/{file_uuid}/thumbnail?api_key={key}&frame={frame}
|
||||
```
|
||||
→ Returns JPEG bytes → resize to 480x270 → base64 encode → send to Ollama.
|
||||
|
||||
### Result Format
|
||||
```json
|
||||
{
|
||||
"tool": "vlm_describe",
|
||||
"result": {
|
||||
"file_uuid": "...",
|
||||
"frame": 1234,
|
||||
"description": "A man wearing a blue suit and red tie standing at a podium.",
|
||||
"time_sec": 41.13
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Performance (Benchmarked on M5 Max)
|
||||
|
||||
**Model**: LLaVA 7B Q4_0 (Ollama), 1280x720 → 480x270 resize
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Avg per frame | **1.02s** |
|
||||
| Min | 0.81s |
|
||||
| Max | 1.60s |
|
||||
| Throughput | ~1 fps (sequential) |
|
||||
| Concurrent (4 workers) | No speedup — Ollama queues single-GPU |
|
||||
|
||||
**Recommendations**:
|
||||
- Resize to 480x270 before sending reduces token count without quality loss
|
||||
- Batch processing doesn't help (Ollama queues requests to single model instance)
|
||||
- For large video indexing, process frames asynchronously in background
|
||||
- Consider a faster model if throughput becomes critical (e.g., LLaVA-Next 8B, Moondream 1.6B)
|
||||
|
||||
## Frontend Changes (for reference)
|
||||
Once Core API returns `vlm_describe` in agent sources, the frontend will display:
|
||||
- Tool badge: "VLM"
|
||||
- Description text paired with the relevant video frame
|
||||
- Click to jump to that frame in the video player
|
||||
|
||||
## Timeline
|
||||
Not urgent - this is an enhancement for visual search capabilities.
|
||||
186
docs/core-api-fix-video-range.md
Normal file
186
docs/core-api-fix-video-range.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# Core API Fix Request: Video Range Requests Support
|
||||
|
||||
## Issue
|
||||
|
||||
Video player in Studio cannot seek (跳轉) because Core API video endpoint does not support HTTP Range requests.
|
||||
|
||||
## Current Behavior
|
||||
|
||||
```bash
|
||||
$ curl -I "http://localhost:3002/api/v1/file/{uuid}/video?start_time=0&end_time=5"
|
||||
HTTP/1.1 200 OK
|
||||
content-type: video/mp4
|
||||
content-length: 1918410
|
||||
# Missing: Accept-Ranges header
|
||||
# Missing: 206 Partial Content support
|
||||
```
|
||||
|
||||
## Expected Behavior
|
||||
|
||||
```bash
|
||||
$ curl -I "http://localhost:3002/api/v1/file/{uuid}/video" -H "Range: bytes=0-1000"
|
||||
HTTP/1.1 206 Partial Content
|
||||
content-type: video/mp4
|
||||
content-range: bytes 0-1000/1918410
|
||||
accept-ranges: bytes
|
||||
content-length: 1001
|
||||
```
|
||||
|
||||
## Why This Matters
|
||||
|
||||
1. **Browser seeking requires Range support** - HTML5 `<video>` element uses Range requests to seek to different timestamps
|
||||
2. **Current workaround is inefficient** - Users must download entire video or use `start_time` param (re-downloads video)
|
||||
3. **High bitrate videos suffer most** - 4K/10Mbps videos cannot be quickly navigated
|
||||
|
||||
## Implementation Requirements
|
||||
|
||||
### 1. Add `Accept-Ranges: bytes` header to video endpoint
|
||||
|
||||
```
|
||||
GET /api/v1/file/{uuid}/video
|
||||
Response Headers:
|
||||
Accept-Ranges: bytes
|
||||
```
|
||||
|
||||
### 2. Handle Range requests with 206 Partial Content
|
||||
|
||||
```rust
|
||||
// Pseudo-code for Rust implementation
|
||||
if let Some(range_header) = request.headers().get("Range") {
|
||||
let (start, end) = parse_range(range_header)?;
|
||||
let file_chunk = read_file_range(file_path, start, end)?;
|
||||
|
||||
Response::builder()
|
||||
.status(206)
|
||||
.header("Content-Range", format!("bytes {}-{}/{}", start, end, file_size))
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.header("Content-Length", end - start + 1)
|
||||
.body(file_chunk)
|
||||
} else {
|
||||
// Full file response
|
||||
Response::builder()
|
||||
.status(200)
|
||||
.header("Accept-Ranges", "bytes")
|
||||
.body(full_file)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Support multiple Range formats
|
||||
|
||||
- `Range: bytes=0-999` (first 1000 bytes)
|
||||
- `Range: bytes=1000-` (from byte 1000 to end)
|
||||
- `Range: bytes=-500` (last 500 bytes)
|
||||
|
||||
## Affected Files (Core API)
|
||||
|
||||
- `src/api/files.rs` - Video streaming endpoint
|
||||
- Possibly `src/api/video.rs` if exists
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Test 1: Initial request should include Accept-Ranges
|
||||
curl -I "http://localhost:3002/api/v1/file/{uuid}/video"
|
||||
# Expect: Accept-Ranges: bytes
|
||||
|
||||
# Test 2: Range request should return 206
|
||||
curl -I "http://localhost:3002/api/v1/file/{uuid}/video" -H "Range: bytes=0-1000"
|
||||
# Expect: HTTP/1.1 206 Partial Content
|
||||
# Expect: Content-Range: bytes 0-1000/{total}
|
||||
|
||||
# Test 3: Browser seeking should work
|
||||
# Open Studio, play video, click on timeline
|
||||
# Video should seek without re-downloading
|
||||
```
|
||||
|
||||
## Priority
|
||||
|
||||
**High** - Critical for user experience when navigating long videos
|
||||
|
||||
## Related Studio Files
|
||||
|
||||
- `src/components/VideoPlayer.vue` - Uses `<video>` element with native controls
|
||||
- `src-tauri/src/proxy.rs` - Already streams video, will pass through Range headers
|
||||
|
||||
## Notes
|
||||
|
||||
- Studio proxy already supports streaming (lines 325-367 in proxy.rs)
|
||||
- Just need Core API to handle Range requests properly
|
||||
- This is standard HTTP behavior for video streaming
|
||||
|
||||
---
|
||||
|
||||
## Additional Request: Adaptive Bitrate / Resolution Selection
|
||||
|
||||
### Problem
|
||||
|
||||
High bitrate videos (4K, 10+ Mbps) cause playback lag in browser due to software decoding limitations.
|
||||
|
||||
### Solution Options
|
||||
|
||||
#### Option A: Resolution Parameter
|
||||
|
||||
```bash
|
||||
GET /api/v1/file/{uuid}/video?start_time=0&end_time=60&resolution=720p
|
||||
GET /api/v1/file/{uuid}/video?start_time=0&end_time=60&resolution=480p
|
||||
```
|
||||
|
||||
**Implementation:**
|
||||
- Use FFmpeg to transcode on-the-fly or pre-generate proxy versions
|
||||
- Store proxy videos at lower bitrates (720p @ 2Mbps, 480p @ 1Mbps)
|
||||
|
||||
#### Option B: Quality Parameter (1-100)
|
||||
|
||||
```bash
|
||||
GET /api/v1/file/{uuid}//video?start_time=0&end_time=60&quality=50
|
||||
```
|
||||
|
||||
#### Option C: Max Bitrate Parameter
|
||||
|
||||
```bash
|
||||
GET /api/v1/file/{uuid}/video?start_time=0&end_time=60&max_bitrate=2000
|
||||
```
|
||||
|
||||
### Recommended Approach
|
||||
|
||||
**Pre-generate proxy videos during processing:**
|
||||
|
||||
```rust
|
||||
// During file processing, generate multiple versions:
|
||||
// - Original: {uuid}_original.mp4 (preserve original quality)
|
||||
// - 720p: {uuid}_720p.mp4 (2 Mbps)
|
||||
// - 480p: {uuid}_480p.mp4 (1 Mbps)
|
||||
```
|
||||
|
||||
**Storage Impact:**
|
||||
- Original 100MB video → ~105MB total (5% overhead for proxies)
|
||||
|
||||
**API Changes:**
|
||||
|
||||
```rust
|
||||
pub fn get_video_stream(
|
||||
file_uuid: String,
|
||||
start_time: f64,
|
||||
end_time: f64,
|
||||
resolution: Option<String>, // "original" | "720p" | "480p"
|
||||
) -> Result<VideoStream, Error>
|
||||
```
|
||||
|
||||
### Frontend Integration
|
||||
|
||||
Studio VideoPlayer can add quality selector:
|
||||
|
||||
```vue
|
||||
<select v-model="selectedResolution">
|
||||
<option value="original">Original (1080p)</option>
|
||||
<option value="720p">HD (720p)</option>
|
||||
<option value="480p">SD (480p)</option>
|
||||
</select>
|
||||
```
|
||||
|
||||
### Benefits
|
||||
|
||||
1. **Eliminates lag** on high bitrate videos
|
||||
2. **Faster loading** with smaller file sizes
|
||||
3. **Bandwidth savings** for remote access
|
||||
4. **Better UX** - smooth playback on all devices
|
||||
290
docs/core-api-pose-appearance-endpoint.md
Normal file
290
docs/core-api-pose-appearance-endpoint.md
Normal file
@@ -0,0 +1,290 @@
|
||||
# Pose & Appearance API Endpoints - Technical Specification
|
||||
|
||||
## Overview
|
||||
|
||||
This document specifies two new Core API endpoints needed for displaying pose skeleton and appearance colors in Momentry Studio's Face Detail Modal.
|
||||
|
||||
---
|
||||
|
||||
## Endpoints
|
||||
|
||||
### 1. Get Pose
|
||||
|
||||
```
|
||||
GET /api/v1/file/:file_uuid/pose?frame=:frame_no
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"frame": 100,
|
||||
"keypoints": [
|
||||
{ "name": "nose", "x": 993, "y": 372, "confidence": 0.84 },
|
||||
{ "name": "left_eye", "x": 950, "y": 350, "confidence": 0.91 },
|
||||
{ "name": "right_eye", "x": 1030, "y": 352, "confidence": 0.89 },
|
||||
{ "name": "left_ear", "x": 920, "y": 360, "confidence": 0.75 },
|
||||
{ "name": "right_ear", "x": 1060, "y": 358, "confidence": 0.77 },
|
||||
{ "name": "left_shoulder", "x": 850, "y": 480, "confidence": 0.88 },
|
||||
{ "name": "right_shoulder", "x": 1100, "y": 475, "confidence": 0.90 },
|
||||
{ "name": "left_elbow", "x": 780, "y": 620, "confidence": 0.82 },
|
||||
{ "name": "right_elbow", "x": 1180, "y": 610, "confidence": 0.85 },
|
||||
{ "name": "left_wrist", "x": 720, "y": 750, "confidence": 0.78 },
|
||||
{ "name": "right_wrist", "x": 1240, "y": 740, "confidence": 0.80 },
|
||||
{ "name": "left_hip", "x": 900, "y": 720, "confidence": 0.86 },
|
||||
{ "name": "right_hip", "x": 1050, "y": 715, "confidence": 0.87 },
|
||||
{ "name": "left_knee", "x": 870, "y": 950, "confidence": 0.83 },
|
||||
{ "name": "right_knee", "x": 1080, "y": 945, "confidence": 0.84 },
|
||||
{ "name": "left_ankle", "x": 850, "y": 1150, "confidence": 0.79 },
|
||||
{ "name": "right_ankle", "x": 1100, "y": 1145, "confidence": 0.81 }
|
||||
],
|
||||
"pose_class": "standing",
|
||||
"confidence": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `frame` (int): Frame number
|
||||
- `keypoints` (array): 17 COCO keypoints
|
||||
- `name` (string): Keypoint name (see list below)
|
||||
- `x` (float): X coordinate in pixels
|
||||
- `y` (float): Y coordinate in pixels
|
||||
- `confidence` (float, optional): Detection confidence 0-1
|
||||
- `pose_class` (string): One of `standing`, `sitting`, `kneeling`, `lying`, `prone`, `unknown`
|
||||
- `confidence` (float, optional): Overall pose classification confidence
|
||||
|
||||
**COCO-17 Keypoint Names:**
|
||||
```
|
||||
nose, left_eye, right_eye, left_ear, right_ear,
|
||||
left_shoulder, right_shoulder, left_elbow, right_elbow,
|
||||
left_wrist, right_wrist, left_hip, right_hip,
|
||||
left_knee, right_knee, left_ankle, right_ankle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Get Appearance
|
||||
|
||||
```
|
||||
GET /api/v1/file/:file_uuid/appearance?frame=:frame_no
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"frame": 100,
|
||||
"dominant_colors": [
|
||||
{ "rgb": [255, 100, 50], "percentage": 0.35 },
|
||||
{ "rgb": [50, 150, 200], "percentage": 0.25 },
|
||||
{ "rgb": [100, 200, 100], "percentage": 0.15 }
|
||||
],
|
||||
"hsv_histogram": [
|
||||
[/* 30 bins for H */],
|
||||
[/* 30 bins for S */],
|
||||
[/* 30 bins for V */]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
- `frame` (int): Frame number
|
||||
- `dominant_colors` (array, optional): Top 3-5 dominant colors
|
||||
- `rgb` (array): [R, G, B] values 0-255
|
||||
- `percentage` (float, optional): Proportion 0-1
|
||||
- `hsv_histogram` (array, optional): Raw HSV histogram for custom analysis
|
||||
|
||||
---
|
||||
|
||||
## Data Source
|
||||
|
||||
### Option A: Read from JSON files
|
||||
|
||||
**Pose file location:**
|
||||
```
|
||||
/momentry/output/{file_hash}/{file_hash}.pose.json
|
||||
```
|
||||
|
||||
**Appearance file location:**
|
||||
```
|
||||
/momentry/output/{file_hash}/{file_hash}.appearance.json
|
||||
```
|
||||
|
||||
**Expected JSON structure:**
|
||||
```json
|
||||
{
|
||||
"frames": [
|
||||
{
|
||||
"frame": 0,
|
||||
"keypoints": [...],
|
||||
"pose_class": "standing"
|
||||
},
|
||||
{
|
||||
"frame": 1,
|
||||
"keypoints": [...],
|
||||
"pose_class": "standing"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Option B: Query from TKG
|
||||
|
||||
If pose/appearance data is stored in TKG, implement Neo4j query:
|
||||
|
||||
```cypher
|
||||
MATCH (p:Pose {file_uuid: $file_uuid, frame: $frame})
|
||||
RETURN p.keypoints, p.pose_class, p.confidence
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Example (FastAPI)
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI, HTTPException
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
OUTPUT_BASE = "/momentry/output"
|
||||
|
||||
@app.get("/api/v1/file/{file_uuid}/pose")
|
||||
async def get_pose(file_uuid: str, frame: int):
|
||||
pose_file = Path(f"{OUTPUT_BASE}/{file_uuid}/{file_uuid}.pose.json")
|
||||
|
||||
if not pose_file.exists():
|
||||
raise HTTPException(status_code=404, detail="Pose data not found")
|
||||
|
||||
with open(pose_file) as f:
|
||||
data = json.load(f)
|
||||
|
||||
# Find frame
|
||||
for frame_data in data.get("frames", []):
|
||||
if frame_data.get("frame") == frame:
|
||||
return {
|
||||
"frame": frame,
|
||||
"keypoints": frame_data.get("keypoints", []),
|
||||
"pose_class": frame_data.get("pose_class", "unknown"),
|
||||
"confidence": frame_data.get("confidence")
|
||||
}
|
||||
|
||||
raise HTTPException(status_code=404, detail="Frame not found")
|
||||
|
||||
@app.get("/api/v1/file/{file_uuid}/appearance")
|
||||
async def get_appearance(file_uuid: str, frame: int):
|
||||
appearance_file = Path(f"{OUTPUT_BASE}/{file_uuid}/{file_uuid}.appearance.json")
|
||||
|
||||
if not appearance_file.exists():
|
||||
raise HTTPException(status_code=404, detail="Appearance data not found")
|
||||
|
||||
with open(appearance_file) as f:
|
||||
data = json.load(f)
|
||||
|
||||
for frame_data in data.get("frames", []):
|
||||
if frame_data.get("frame") == frame:
|
||||
return {
|
||||
"frame": frame,
|
||||
"dominant_colors": frame_data.get("dominant_colors", []),
|
||||
"hsv_histogram": frame_data.get("hsv_histogram")
|
||||
}
|
||||
|
||||
raise HTTPException(status_code=404, detail="Frame not found")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mock Data for Testing
|
||||
|
||||
If real data is not yet available, use mock response:
|
||||
|
||||
```python
|
||||
@app.get("/api/v1/file/{file_uuid}/pose")
|
||||
async def get_pose(file_uuid: str, frame: int):
|
||||
return {
|
||||
"frame": frame,
|
||||
"keypoints": [
|
||||
{"name": "nose", "x": 100, "y": 50},
|
||||
{"name": "left_eye", "x": 90, "y": 45},
|
||||
{"name": "right_eye", "x": 110, "y": 45},
|
||||
{"name": "left_ear", "x": 80, "y": 50},
|
||||
{"name": "right_ear", "x": 120, "y": 50},
|
||||
{"name": "left_shoulder", "x": 60, "y": 100},
|
||||
{"name": "right_shoulder", "x": 140, "y": 100},
|
||||
{"name": "left_elbow", "x": 50, "y": 150},
|
||||
{"name": "right_elbow", "x": 150, "y": 150},
|
||||
{"name": "left_wrist", "x": 45, "y": 190},
|
||||
{"name": "right_wrist", "x": 155, "y": 190},
|
||||
{"name": "left_hip", "x": 70, "y": 200},
|
||||
{"name": "right_hip", "x": 130, "y": 200},
|
||||
{"name": "left_knee", "x": 65, "y": 280},
|
||||
{"name": "right_knee", "x": 135, "y": 280},
|
||||
{"name": "left_ankle", "x": 60, "y": 350},
|
||||
{"name": "right_ankle", "x": 140, "y": 350}
|
||||
],
|
||||
"pose_class": "standing"
|
||||
}
|
||||
|
||||
@app.get("/api/v1/file/{file_uuid}/appearance")
|
||||
async def get_appearance(file_uuid: str, frame: int):
|
||||
return {
|
||||
"frame": frame,
|
||||
"dominant_colors": [
|
||||
{"rgb": [255, 100, 50]},
|
||||
{"rgb": [50, 150, 200]}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Studio Integration Status
|
||||
|
||||
**Phase 1 (Completed):**
|
||||
- ✅ TypeScript interfaces (`src/api/types.ts`)
|
||||
- ✅ API builder cases (`src/api/index.ts`)
|
||||
- ✅ Store functions (`src/store.ts`)
|
||||
- ✅ Canvas rendering utilities (`src/utils/poseRenderer.ts`)
|
||||
- ✅ Mock data for testing (`src/utils/mockPoseData.ts`)
|
||||
|
||||
**Phase 2 (Pending - Core Team):**
|
||||
- ⏳ Implement pose endpoint
|
||||
- ⏳ Implement appearance endpoint
|
||||
|
||||
**Phase 3-4 (After Phase 2):**
|
||||
- Studio Proxy handlers
|
||||
- UI integration in Face Detail Modal
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Manual Test
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3002/api/v1/file/{file_uuid}/pose?frame=100"
|
||||
curl "http://localhost:3002/api/v1/file/{file_uuid}/appearance?frame=100"
|
||||
```
|
||||
|
||||
### Expected Response Time
|
||||
|
||||
- Target: < 100ms per request
|
||||
- Cacheable: Yes (pose/appearance data doesn't change)
|
||||
|
||||
---
|
||||
|
||||
## Questions
|
||||
|
||||
1. **Data location**: Confirm if `.pose.json` and `.appearance.json` files exist, or if data is in TKG?
|
||||
|
||||
2. **Frame alignment**: Confirm that pose/appearance frame numbers align with face `best_face_frame`?
|
||||
|
||||
3. **Pose classification**: Do you have pose classification (`standing`, `sitting`, etc.), or just keypoints?
|
||||
|
||||
4. **Dominant colors**: Is color extraction already done, or need to implement?
|
||||
|
||||
---
|
||||
|
||||
## Contact
|
||||
|
||||
Studio Team: Ready to integrate once endpoints are available.
|
||||
Expected Phase 3-4 completion: 1-2 hours after Phase 2 delivery.
|
||||
434
docs/core-api-usage.md
Normal file
434
docs/core-api-usage.md
Normal file
@@ -0,0 +1,434 @@
|
||||
# Momentry Studio Core API 使用說明
|
||||
|
||||
本文檔整理 Momentry Studio 使用的 Core API 端點及其使用時機。
|
||||
|
||||
**Core API 地址**: `http://localhost:3002`
|
||||
**API Key**: 透過 `api_key` query parameter 注入(由 Rust proxy 自動處理)
|
||||
|
||||
---
|
||||
|
||||
## 一、搜尋相關 API
|
||||
|
||||
### 1.1 智能搜尋
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/search/llm-smart` | POST | LLM 智能搜尋,結合關鍵字和語意搜尋 |
|
||||
| `/api/v1/search/keyword` | POST | 純關鍵字搜尋 |
|
||||
| `/api/v1/search/semantic` | POST | 純語意搜尋(向量相似度) |
|
||||
| `/api/v1/agents/search` | POST | Agent 搜尋(支援多輪對話) |
|
||||
|
||||
**使用時機**:
|
||||
- `llm-smart`: 搜尋頁面預設搜尋
|
||||
- `keyword`: 快速關鍵字匹配
|
||||
- `semantic`: 概念搜尋
|
||||
- `agents/search`: 搜尋頁面對話模式(支援 `conversation_id` 多輪對話)
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"query": "搜尋詞",
|
||||
"limit": 20
|
||||
}
|
||||
```
|
||||
|
||||
**Response**: 搜尋結果陣列,每個結果包含 `file_uuid`, `start_time`, `end_time`, `start_frame`, `end_frame`, `summary`, `similarity`
|
||||
|
||||
---
|
||||
|
||||
### 1.2 人物搜尋
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/identities/search` | GET | 搜尋人物名稱 |
|
||||
|
||||
**使用時機**: 在人物詳情頁面搜尋要合併的目標人物
|
||||
|
||||
**Query Params**: `q` (搜尋詞), `limit`
|
||||
|
||||
---
|
||||
|
||||
## 二、檔案管理 API
|
||||
|
||||
### 2.1 檔案列表與詳情
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/files/scan` | GET | 掃描目錄,取得檔案列表 |
|
||||
| `/api/v1/file/:uuid` | GET | 取得檔案詳情(fps, duration, width, height 等) |
|
||||
| `/api/v1/file/:uuid/processor-counts` | GET | 取得檔案的處理器計數統計 |
|
||||
|
||||
**使用時機**:
|
||||
- `files/scan`: 檔案庫頁面載入檔案列表
|
||||
- `file/:uuid`: 播放器需要影片參數、縮圖需要原始尺寸
|
||||
- `processor-counts`: 檔案詳情頁面顯示處理狀態
|
||||
|
||||
---
|
||||
|
||||
### 2.2 檔案註冊與處理
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/files/register` | POST | 註冊檔案到系統 |
|
||||
| `/api/v1/file/:uuid/process` | POST | 執行處理器(transcribe, ocr, face 等) |
|
||||
| `/api/v1/file/:uuid/checkin` | POST | 檔案入庫(ingest) |
|
||||
| `/api/v1/file/:uuid/checkout` | POST | 檔案出庫 |
|
||||
| `/api/v1/unregister` | POST | 取消註冊檔案 |
|
||||
|
||||
**使用時機**:
|
||||
- `register`: 檔案庫頁面註冊新檔案
|
||||
- `process`: 檔案庫頁面觸發處理流程
|
||||
- `checkin/checkout`: 檔案版本控制
|
||||
- `unregister`: 檔案庫頁面刪除檔案
|
||||
|
||||
---
|
||||
|
||||
### 2.3 檔案狀態同步
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/file/:uuid/sync-status` | POST | 從資料庫同步檔案處理狀態 |
|
||||
|
||||
**使用時機**: 前端 polling 檢查處理中的檔案狀態(每 10 秒)
|
||||
|
||||
---
|
||||
|
||||
## 三、媒體 API
|
||||
|
||||
### 3.1 縮圖
|
||||
|
||||
| 端點 | 方法 | 說明 | 實作位置 |
|
||||
|------|------|------|----------|
|
||||
| `/api/v1/file/:uuid/thumbnail` | GET | 取得檔案縮圖 | Core API |
|
||||
| `/api/v1/face-thumbnail` | GET | 取得人臉縮圖(含 bbox crop) | **Studio 本地** |
|
||||
| `/api/v1/file/thumbnail` | GET | 依路徑取得縮圖(未註冊檔案) | **Studio 本地** |
|
||||
|
||||
**使用時機**:
|
||||
- `file/:uuid/thumbnail`: 搜尋結果縮圖、影片時間軸縮圖
|
||||
- `face-thumbnail`: 人物詳情頁面人臉縮圖(含 bbox crop)
|
||||
- `file/thumbnail`: 檔案庫頁面未註冊檔案的縮圖
|
||||
|
||||
**注意**: `face-thumbnail` 由 Studio 本地處理,因為 Core API 不支援 bbox crop
|
||||
|
||||
---
|
||||
|
||||
### 3.2 影片串流
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/file/:uuid/video` | GET | 影片串流 |
|
||||
|
||||
**使用時機**: 搜尋結果播放影片
|
||||
|
||||
**注意**: Core API 已支援 HTTP Range requests,瀏覽器可透過 `<video>` 元素跳轉播放
|
||||
|
||||
---
|
||||
|
||||
### 3.3 影格提取
|
||||
|
||||
| 端點 | 方法 | 說明 | 實作位置 |
|
||||
|------|------|------|----------|
|
||||
| `/api/v1/media/frame` | GET | 提取指定影格 | **Studio 本地** |
|
||||
|
||||
**使用時機**: 人物詳情頁面顯示特定幀的圖片
|
||||
|
||||
---
|
||||
|
||||
## 四、人物(Identity)API
|
||||
|
||||
### 4.1 人物列表
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/identities` | GET | 取得所有人物(分頁) |
|
||||
|
||||
**使用時機**: 人物頁面載入人物列表
|
||||
|
||||
**注意**: Studio 使用特殊邏輯(最多 10 頁 × 100 筆)避免 Core API timeout
|
||||
|
||||
---
|
||||
|
||||
### 4.2 人物詳情
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/identity/:uuid` | GET | 取得單一人物詳情 |
|
||||
| `/api/v1/identity/:uuid/faces` | GET | 取得人物的人臉列表 |
|
||||
| `/api/v1/identity/:uuid/traces` | GET | 取得人物的軌跡列表 |
|
||||
| `/api/v1/identity/:uuid/files` | GET | 取得人物出現的檔案列表 |
|
||||
|
||||
**使用時機**:
|
||||
- `identity/:uuid`: 人物詳情頁面
|
||||
- `faces`: 人物詳情頁面人臉列表
|
||||
- `traces`: 人物詳情頁面軌跡列表
|
||||
- `files`: 人物詳情頁面檔案列表
|
||||
|
||||
---
|
||||
|
||||
### 4.3 人物更新
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/identity/:uuid` | PATCH | 更新人物名稱或 metadata |
|
||||
|
||||
**使用時機**: 人物詳情頁面編輯名稱、狀態、星號
|
||||
|
||||
**Request Body**:
|
||||
```json
|
||||
{
|
||||
"name": "新名稱",
|
||||
"metadata": {
|
||||
"status": "confirmed",
|
||||
"starred": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4.4 人物大頭貼
|
||||
|
||||
| 端點 | 方法 | 說明 | 實作位置 |
|
||||
|------|------|------|----------|
|
||||
| `/api/v1/identity/:uuid/profile` | GET | 取得人物大頭貼 | **Studio 本地** |
|
||||
| `/api/v1/identity/:uuid/profile-image` | POST | 上傳大頭貼 | Core API |
|
||||
| `/api/v1/identity/:uuid/profile-image/from-face` | POST | 從人臉設定大頭貼 | Core API |
|
||||
|
||||
**使用時機**:
|
||||
- `profile`: 人物列表、人物詳情頁面顯示大頭貼(本地檔案系統)
|
||||
- `profile-image`: 上傳自訂大頭貼
|
||||
- `profile-image/from-face`: 從人臉截圖設定大頭貼
|
||||
|
||||
**注意**: `profile` 由 Studio 本地處理,讀取 `output/identities/{uuid}/profile.jpg`
|
||||
|
||||
---
|
||||
|
||||
### 4.5 人物操作
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/identity/:uuid/bind` | POST | 綁定人臉到人物 |
|
||||
| `/api/v1/identity/:uuid/unbind` | POST | 解綁人臉 |
|
||||
| `/api/v1/identity/:uuid/mergeinto` | POST | 合併人物到另一個人物 |
|
||||
| `/api/v1/identity/:uuid` | DELETE | 刪除人物 |
|
||||
|
||||
**使用時機**:
|
||||
- `bind`: 人物詳情頁面新增人臉
|
||||
- `unbind`: 人物詳情頁面移除人臉
|
||||
- `mergeinto`: 人物詳情頁面合併兩個人物
|
||||
- `delete`: 人物詳情頁面刪除人物
|
||||
|
||||
---
|
||||
|
||||
### 4.6 人物建立
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/file/:uuid/pending-person` | POST | 從檔案建立待確認人物 |
|
||||
| `/api/v1/identities/pending` | POST | 建立待確認身份 |
|
||||
|
||||
**使用時機**:
|
||||
- `pending-person`: 從軌跡建立新人物
|
||||
- `pending`: 從軌跡/人臉建立待確認身份
|
||||
|
||||
---
|
||||
|
||||
### 4.7 Undo/Redo
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/identity/:uuid/undo` | POST | 復原人物操作 |
|
||||
| `/api/v1/identity/:uuid/redo` | POST | 重做人物操作 |
|
||||
| `/api/v1/identity/:uuid/history` | GET | 取得操作歷史 |
|
||||
| `/api/v1/identity/:uuid/bind/undo` | POST | 復原綁定操作 |
|
||||
| `/api/v1/identity/:uuid/bind/redo` | POST | 重做綁定操作 |
|
||||
| `/api/v1/identity/:uuid/bind/history` | GET | 取得綁定操作歷史 |
|
||||
| `/api/v1/identity/merge/:mergeId/undo` | POST | 復原合併 |
|
||||
| `/api/v1/identity/merge/:mergeId/redo` | POST | 重做合併 |
|
||||
| `/api/v1/identity/merge/history` | GET | 取得合併歷史 |
|
||||
|
||||
**使用時機**: 人物詳情頁面 Undo/Redo 功能
|
||||
|
||||
---
|
||||
|
||||
## 五、人臉與軌跡 API
|
||||
|
||||
### 5.1 人臉候選
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/faces/candidates` | GET | 取得未綁定的人臉候選 |
|
||||
|
||||
**使用時機**: 人物詳情頁面顯示可綁定的人臉
|
||||
|
||||
---
|
||||
|
||||
### 5.2 軌跡管理
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/traces/unassigned` | GET | 取得未分配的軌跡 |
|
||||
| `/api/v1/file/:uuid/traces` | POST | 列出檔案的所有軌跡 |
|
||||
| `/api/v1/file/:uuid/trace/:traceId` | DELETE | 刪除軌跡 |
|
||||
| `/api/v1/file/:uuid/trace/:traceId/restore` | POST | 還原刪除的軌跡 |
|
||||
| `/api/v1/file/:uuid/trace/:sourceId/merge/:targetId` | POST | 合併軌跡 |
|
||||
|
||||
**使用時機**:
|
||||
- `unassigned`: 人物詳情頁面顯示未分配軌跡
|
||||
- 其他: 軌跡管理功能
|
||||
|
||||
---
|
||||
|
||||
### 5.3 軌跡 Profile
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/trace-profile` | GET | 取得軌跡 profile |
|
||||
| `/api/v1/trace-profile` | PUT | 更新軌跡 profile |
|
||||
| `/api/v1/trace-profile/group` | PUT | 批次更新軌跡 profile |
|
||||
|
||||
**使用時機**: 軌跡標記、命名
|
||||
|
||||
---
|
||||
|
||||
### 5.4 檔案 Identity 列表
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/file/:uuid/identities` | GET | 取得檔案中出現的人物 |
|
||||
| `/api/v1/file/:uuid/pending-persons` | GET | 取得檔案的待確認人物 |
|
||||
|
||||
**使用時機**: 檔案詳情頁面顯示人物列表
|
||||
|
||||
---
|
||||
|
||||
## 六、處理器 API
|
||||
|
||||
### 6.1 執行 Agent
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/file/:uuid/identity-agent` | POST | 執行人物識別 Agent |
|
||||
| `/api/v1/file/:uuid/cluster-agent` | POST | 執行軌跡聚類 Agent |
|
||||
| `/api/v1/agents/identity/run-for-seed` | POST | 為種子人物執行識別 |
|
||||
| `/api/v1/agents/identity/match-from-photo` | POST | 從照片匹配人物 |
|
||||
|
||||
**使用時機**:
|
||||
- `identity-agent`: 檔案庫頁面執行人物識別
|
||||
- `cluster-agent`: 檔案庫頁面執行軌跡聚類
|
||||
- `run-for-seed`: 為已知人物擴展識別
|
||||
- `match-from-photo`: 上傳照片比對人物
|
||||
|
||||
---
|
||||
|
||||
### 6.2 處理器結果
|
||||
|
||||
| 端點 | 方法 | 說明 | 實作位置 |
|
||||
|------|------|------|----------|
|
||||
| `/api/v1/file/:uuid/face-groups` | GET | 取得人臉分組結果 | Core API |
|
||||
| `/api/v1/file/:uuid/json/:processor` | POST | 取得處理器 JSON 輸出 | Core API |
|
||||
| `/api/v1/cluster-results` | GET | 取得聚類結果 | Studio proxy |
|
||||
| `/api/v1/processor-json` | GET | 取得處理器 JSON | Studio proxy |
|
||||
| `/api/v1/identity-matches` | GET | 取得人物匹配結果 | Studio 本地檔案 |
|
||||
|
||||
**使用時機**:
|
||||
- `face-groups`: 取得人臉分組
|
||||
- `json/:processor`: 取得特定處理器的輸出(如 ocr, asr)
|
||||
- `cluster-results`, `processor-json`, `identity-matches`: QC 頁面
|
||||
|
||||
---
|
||||
|
||||
## 七、Profile API
|
||||
|
||||
### 7.1 檔案 Profile
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/file-profile` | GET | 取得檔案 profile |
|
||||
| `/api/v1/file-profile` | PUT | 更新檔案 profile |
|
||||
|
||||
**使用時機**: 檔案元數據管理
|
||||
|
||||
---
|
||||
|
||||
## 八、統計 API
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/progress/:uuid` | POST | 取得處理進度 |
|
||||
| `/api/v1/stats/pipeline/:uuid` | GET | 取得 pipeline 統計 |
|
||||
| `/api/v1/stats/file/:uuid` | GET | 取得檔案統計 |
|
||||
|
||||
**使用時機**: 檔案處理進度顯示
|
||||
|
||||
---
|
||||
|
||||
## 九、姿勢與外觀 API
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/file/:uuid/pose` | GET | 取得指定幀的姿勢估計 |
|
||||
| `/api/v1/file/:uuid/appearance` | GET | 取得指定幀的外觀特徵 |
|
||||
|
||||
**Query Params**: `frame`, `bbox_x`, `bbox_y`, `bbox_w`, `bbox_h`
|
||||
|
||||
**使用時機**: 人物詳情頁面顯示姿勢/外觀分析
|
||||
|
||||
---
|
||||
|
||||
## 十、其他 API
|
||||
|
||||
### 10.1 說話者綁定
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/file/:uuid/bind-speakers` | POST | 綁定說話者到人物 |
|
||||
|
||||
**使用時機**: 將語音與人物關聯
|
||||
|
||||
---
|
||||
|
||||
### 10.2 本地 API(不經過 Core API)
|
||||
|
||||
以下 API 由 Studio 本地處理,**不會**轉發到 Core API:
|
||||
|
||||
| 端點 | 說明 | 資料來源 |
|
||||
|------|------|----------|
|
||||
| `/api/v1/auth/login` | 本地登入驗證 | SQLite `app_users` 表 |
|
||||
| `/api/v1/search-history` | 搜尋歷史 CRUD | SQLite `search_history` 表 |
|
||||
| `/api/v1/bookmarks` | 書籤 CRUD | SQLite `bookmarks` 表 |
|
||||
| `/api/v1/identity/:uuid/profile` | 人物大頭貼 | 本地檔案系統 |
|
||||
| `/api/v1/face-thumbnail` | 人臉縮圖(bbox crop) | Core API + 本地裁切 |
|
||||
| `/api/v1/media/frame` | 影格提取 | ffmpeg 本地執行 |
|
||||
| `/api/v1/file/thumbnail` | 依路徑縮圖 | ffmpeg 本地執行 |
|
||||
| `/api/v1/identity-matches` | 人物匹配結果 | 本地 JSON 檔案 |
|
||||
| `/api/v1/cluster-results` | 聚類結果 | Core API `/face-groups` |
|
||||
| `/api/v1/processor-json` | 處理器 JSON | Core API `/json/:processor` |
|
||||
|
||||
---
|
||||
|
||||
## 十一、已知問題與改進建議
|
||||
|
||||
### 11.1 高畫質影片卡頓
|
||||
|
||||
**問題**: 4K、10+ Mbps 影片在瀏覽器播放卡頓
|
||||
|
||||
**建議**: Core API 實作 adaptive bitrate streaming(HLS/DASH)
|
||||
|
||||
### 11.2 分頁限制
|
||||
|
||||
**問題**: Core API 在 `per_page >= 100` 時可能 timeout
|
||||
|
||||
**現行解法**: 前端限制 `perPage <= 20`,`get_people` 使用特殊分頁邏輯
|
||||
|
||||
---
|
||||
|
||||
## 十二、API Key 注入
|
||||
|
||||
所有經由 Rust proxy 轉發的請求都會自動注入 `api_key` query parameter:
|
||||
|
||||
```
|
||||
http://localhost:3002/api/v1/identities?api_key=muser_xxx&page=1&per_page=100
|
||||
```
|
||||
|
||||
前端無需手動處理 API Key,由 proxy.rs 統一管理。
|
||||
147
docs/issues/corrupted-video-seek-issue.md
Normal file
147
docs/issues/corrupted-video-seek-issue.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# 影片 Seek 異常問題
|
||||
|
||||
## 問題描述
|
||||
|
||||
**影片檔案**: `Gamma 8-Director Chih-Lin Yang Shares His Experience:楊智麟導演經驗分享.mp4`
|
||||
**UUID**: `d3f9ae8e471a1fc4d47022c66091b920`
|
||||
**大小**: 219.0 MB
|
||||
**時長**: 298.67 秒(約 5 分鐘)
|
||||
|
||||
**症狀**:
|
||||
- 3 分鐘後 seek 異常
|
||||
- 異常行為:
|
||||
1. 回到影片開頭
|
||||
2. 跳到影片結尾
|
||||
|
||||
## 診斷結果
|
||||
|
||||
### ffprobe 檢測錯誤
|
||||
|
||||
```
|
||||
[h264 @ 0x7ad048e00] Invalid NAL unit size (49912 > 22668).
|
||||
[h264 @ 0x7ad048a80] missing picture in access unit with size 22672
|
||||
[h264 @ 0x7ad048a80] Error splitting the input into NAL units.
|
||||
```
|
||||
|
||||
### 影片結構分析
|
||||
|
||||
| 項目 | 數值 | 狀態 |
|
||||
|------|------|------|
|
||||
| Codec | H.264 High Profile | ✅ |
|
||||
| 解析度 | 1920x1080 | ✅ |
|
||||
| 幀率 | 29.97 fps (30000/1001) | ✅ |
|
||||
| Keyframe 間距 | ~28 幀 (~1 秒) | ✅ |
|
||||
| moov atom 位置 | 28 bytes | ✅ 正確(在開頭) |
|
||||
| stss seek table | 存在 | ✅ |
|
||||
| NAL units | **損壞** | ❌ |
|
||||
|
||||
### Range Requests 測試
|
||||
|
||||
Core API Range requests 運作正常:
|
||||
|
||||
```
|
||||
curl -I -H "Range: bytes=150000000-150001000" ...
|
||||
HTTP/1.1 206 Partial Content
|
||||
content-range: bytes 150000000-150001000/229638144
|
||||
```
|
||||
|
||||
## 根本原因
|
||||
|
||||
**影片檔案本身有損壞的 NAL units**,非 Studio 或 Core API 問題。
|
||||
|
||||
瀏覽器的 H.264 解碼器在遇到損壞的 NAL units 時會:
|
||||
1. 無法正確解碼目標幀
|
||||
2. 嘔試從最近的 keyframe 恢復
|
||||
3. 若恢復失敗,跳回開頭或結尾
|
||||
|
||||
## 解決方案
|
||||
|
||||
### 方案 1: 重新封裝(Remux)
|
||||
|
||||
不重新編碼,僅重新打包容器:
|
||||
|
||||
```bash
|
||||
ffmpeg -i "/Users/accusys/momentry/var/sftpgo/data/demo/Gamma 8-Director Chih-Lin Yang Shares His Experience:楊智麟導演經驗分享.mp4" \
|
||||
-c copy \
|
||||
"/Users/accusys/momentry/var/sftpgo/data/demo/Gamma 8-Director Chih-Lin Yang Shares His Experience_fixed.mp4"
|
||||
```
|
||||
|
||||
優點:快速、不損失畫質
|
||||
缺點:可能無法修復損壞的幀
|
||||
|
||||
### 方案 2: 重新編碼(Transcode)
|
||||
|
||||
完整重新編碼:
|
||||
|
||||
```bash
|
||||
ffmpeg -i "/Users/accusys/momentry/var/sftpgo/data/demo/Gamma 8-Director Chih-Lin Yang Shares His Experience:楊智麟導演經驗分享.mp4" \
|
||||
-c:v libx264 -crf 18 \
|
||||
-c:a aac -b:a 128k \
|
||||
"/Users/accusys/momentry/var/sftpgo/data/demo/Gamma 8-Director Chih-Lin Yang Shares His Experience_reencoded.mp4"
|
||||
```
|
||||
|
||||
優點:完整修復損壞的幀
|
||||
缺點:耗時、些微畫質損失
|
||||
|
||||
### 方案 3: 重新註冊
|
||||
|
||||
修復後需重新註冊到系統:
|
||||
|
||||
```bash
|
||||
# 取消註冊舊檔案
|
||||
curl -X POST http://localhost:8888/api/v1/unregister \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"file_uuid": "d3f9ae8e471a1fc4d47022c66091b920"}'
|
||||
|
||||
# 註冊新檔案
|
||||
curl -X POST http://localhost:8888/api/v1/files/register \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"file_path": "/path/to/fixed_video.mp4"}'
|
||||
```
|
||||
|
||||
## 預防措施
|
||||
|
||||
### 上傳時驗證
|
||||
|
||||
可在檔案註冊時加入驗證:
|
||||
|
||||
```bash
|
||||
ffprobe -v error -count_frames -select_streams v:0 \
|
||||
-show_entries stream=codec_name \
|
||||
-of default=noprint_wrappers=1 input.mp4
|
||||
```
|
||||
|
||||
若有錯誤輸出,標記檔案為「需要檢查」或自動嘗試修復。
|
||||
|
||||
### 定期檢查腳本
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# check_videos.sh
|
||||
for file in /path/to/videos/*.mp4; do
|
||||
errors=$(ffprobe -v error -count_frames "$file" 2>&1 | grep -i "invalid\|error\|corrupt")
|
||||
if [ -n "$errors" ]; then
|
||||
echo "ISSUE: $file"
|
||||
echo "$errors"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
## 其他影片測試
|
||||
|
||||
測試其他影片均正常,確認問題僅限於此特定檔案。
|
||||
|
||||
## 狀態
|
||||
|
||||
- [x] 問題診斷完成
|
||||
- [ ] 嘗試 remux 修復
|
||||
- [ ] 嘗試 transcode 修復(若 remux 失敗)
|
||||
- [ ] 重新註冊檔案
|
||||
- [ ] 驗證 seek 功能正常
|
||||
|
||||
---
|
||||
|
||||
**建立日期**: 2026-07-22
|
||||
**相關文件**:
|
||||
- `/Users/accusys/momentry_studio/docs/core-api-usage.md`
|
||||
- `/Users/accusys/momentry_studio/docs/core-api-fix-video-range.md`
|
||||
451
docs/proposals/core-api-face-groups-endpoint.md
Normal file
451
docs/proposals/core-api-face-groups-endpoint.md
Normal file
@@ -0,0 +1,451 @@
|
||||
# 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<Value>) -> 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 團隊討論與反饋。**
|
||||
285
docs/proposals/face-group-naming-issue.md
Normal file
285
docs/proposals/face-group-naming-issue.md
Normal file
@@ -0,0 +1,285 @@
|
||||
# Face Group 命名持久化問題 - 技術分析報告
|
||||
|
||||
**日期**: 2026-07-19
|
||||
**提出團隊**: Momentry Studio
|
||||
**狀態**: 待 Core Team 討論
|
||||
|
||||
---
|
||||
|
||||
## 一、問題現象
|
||||
|
||||
### 1.1 用戶操作流程
|
||||
|
||||
1. 選擇影片檔案
|
||||
2. 建立 face group 並命名(如 "test group")
|
||||
3. 將 trace 移入該 group
|
||||
4. 成功顯示新 group,可播放影片
|
||||
5. 跳轉到其他頁面
|
||||
6. 返回 Face 頁面
|
||||
7. **"test group" 消失**
|
||||
|
||||
### 1.2 影響範圍
|
||||
|
||||
| 功能 | 影響 |
|
||||
|------|------|
|
||||
| 新建 face group | ❌ 無法持久化 |
|
||||
| 重新命名 face group | ❌ 無法持久化 |
|
||||
| 移動 trace 到 group | ❌ 無法持久化 |
|
||||
| 系統自動生成的 group | ✅ 正常 |
|
||||
|
||||
---
|
||||
|
||||
## 二、技術分析
|
||||
|
||||
### 2.1 數據流追蹤
|
||||
|
||||
```
|
||||
Frontend Core API TKG
|
||||
│ │ │
|
||||
│ updateTraceProfileGroup( │ │
|
||||
│ fileUuid, traceIds, │ │
|
||||
│ { name: "test group" } │ │
|
||||
│ ) │ │
|
||||
├─────────────────────────────────►│ │
|
||||
│ │ PUT /trace-profile/group │
|
||||
│ │ { name: "test group" } │
|
||||
│ ├─────────────────────────►│
|
||||
│ │ │
|
||||
│ │ 存入 face_trace.label
|
||||
│ │ │
|
||||
│ loadClusterResults() │ │
|
||||
├─────────────────────────────────►│ │
|
||||
│ │ GET /file/{uuid}/ │
|
||||
│ │ face-groups │
|
||||
│ ├─────────────────────────►│
|
||||
│ │ │
|
||||
│ │ 從 TKG 讀取 label
|
||||
│ │◄─────────────────────────┤
|
||||
│ │ │
|
||||
│ ◄───────────────────────────────┤ { face_groups: [...] } │
|
||||
│ │ │
|
||||
```
|
||||
|
||||
### 2.2 Bug 定位
|
||||
|
||||
**文件位置**: `/Users/accusys/momentry_studio/src/api/index.ts`
|
||||
**行號**: 379
|
||||
|
||||
**問題代碼**:
|
||||
```javascript
|
||||
case 'update_trace_profile_group': {
|
||||
return {
|
||||
url: '/api/v1/trace-profile/group',
|
||||
method: 'PUT',
|
||||
body: {
|
||||
file_uuid: a.fileUuid,
|
||||
trace_ids: a.traceIds,
|
||||
name: a.group_name || a.label, // ← BUG: 忽略 a.name
|
||||
key_frame: a.keyFrame,
|
||||
key_face: a.keyFace
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Frontend 調用分析**:
|
||||
|
||||
| 行號 | 文件 | 調用方式 | 參數 |
|
||||
|------|------|---------|------|
|
||||
| 430 | PeopleView.vue | `updateTraceProfileGroup(..., { name })` | `name` ✅ |
|
||||
| 864 | PeopleView.vue | `updateTraceProfileGroup(..., { label: name })` | `label` ⚠️ |
|
||||
| 928 | PeopleView.vue | `updateTraceProfileGroup(..., { name: ... })` | `name` ✅ |
|
||||
| 971 | PeopleView.vue | `updateTraceProfileGroup(..., { name })` | `name` ✅ |
|
||||
|
||||
**結果**:
|
||||
- API Builder 期望 `group_name` 或 `label`
|
||||
- Frontend 大部分傳入 `name`
|
||||
- 導致 Core API 收到 `name: undefined`
|
||||
|
||||
---
|
||||
|
||||
## 三、命名不一致問題
|
||||
|
||||
### 3.1 各層級欄位對照
|
||||
|
||||
| 層級 | 欄位名稱 | 說明 |
|
||||
|------|---------|------|
|
||||
| TKG `face_trace` node | `label` | 根本存儲位置 |
|
||||
| Core API `/trace-profile` (GET) | `name` | 返回給前端 |
|
||||
| Core API `/trace-profile/group` (PUT) | `name` | 接受前端參數 |
|
||||
| Frontend 大部分調用 | `name` | ✅ 正確 |
|
||||
| Frontend line 864 | `label` | ⚠️ 不一致 |
|
||||
| API Builder 期望 | `group_name \|\| label` | ❌ 不匹配 |
|
||||
|
||||
### 3.2 Core API 文檔定義
|
||||
|
||||
**`PUT /api/v1/trace-profile/group`**
|
||||
|
||||
| 參數 | 類型 | 必填 | 說明 |
|
||||
|------|------|------|------|
|
||||
| `file_uuid` | string | Yes | File UUID |
|
||||
| `trace_ids` | integer[] | Yes | Trace IDs |
|
||||
| `name` | string | Yes | 新群組名稱 |
|
||||
|
||||
**Core API 明確接受 `name` 參數。**
|
||||
|
||||
---
|
||||
|
||||
## 四、架構設計問題
|
||||
|
||||
### 4.1 TKG Label 類型混淆
|
||||
|
||||
**現狀**: TKG `label` 欄位可能代表不同意圖:
|
||||
|
||||
| 來源 | 值範例 | 說明 |
|
||||
|------|-------|------|
|
||||
| 用戶命名 | "Peter", "Cary Grant" | 用戶主動指定 |
|
||||
| 系統生成 | "Trace_1", "Face Trace 9" | 自動生成 |
|
||||
| 其他 | 可能還有其他類型 | 待確認 |
|
||||
|
||||
**問題**: 無法區分 label 的來源/類型
|
||||
|
||||
### 4.2 建議方案
|
||||
|
||||
#### 選項 A:增加 `label_type` 欄位
|
||||
|
||||
```javascript
|
||||
// TKG face_trace node
|
||||
{
|
||||
"node_type": "face_trace",
|
||||
"label": "Peter",
|
||||
"label_type": "face_name", // 新增欄位
|
||||
"properties": {
|
||||
"trace_id": 9,
|
||||
"face_count": 142,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`label_type` 可能值**:
|
||||
|
||||
| 值 | 說明 |
|
||||
|----|------|
|
||||
| `face_name` | 用戶定義的名稱 |
|
||||
| `system` | 系統自動生成 |
|
||||
| `tmdb` | 來自 TMDB 識別 |
|
||||
|
||||
#### 選項 B:使用 `labels` 陣列(支援多標籤)
|
||||
|
||||
```javascript
|
||||
{
|
||||
"node_type": "face_trace",
|
||||
"labels": [
|
||||
{ "value": "Peter", "type": "face_name", "source": "user" },
|
||||
{ "value": "Trace_9", "type": "system", "source": "auto" }
|
||||
],
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
#### 選項 C:維持現狀
|
||||
|
||||
- TKG 保持 `label` 單一欄位
|
||||
- Core API 翻譯:`label` ↔ `name`
|
||||
- Frontend 統一使用 `name`
|
||||
|
||||
---
|
||||
|
||||
## 五、修復方案
|
||||
|
||||
### 5.1 方案 A:前端統一使用 `name`(最小改動)
|
||||
|
||||
**修改點**:
|
||||
|
||||
| 文件 | 行號 | 原代碼 | 新代碼 |
|
||||
|------|------|--------|--------|
|
||||
| `src/api/index.ts` | 379 | `name: a.group_name \|\| a.label` | `name: a.name` |
|
||||
| `src/views/PeopleView.vue` | 864 | `{ label: name }` | `{ name }` |
|
||||
|
||||
**優點**:
|
||||
- 改動少
|
||||
- 符合 Core API 文檔
|
||||
- 立即解決問題
|
||||
|
||||
**缺點**:
|
||||
- 未處理 TKG label 類型問題
|
||||
- 未來可能有類似問題
|
||||
|
||||
**預計工作量**: 0.5 小時
|
||||
|
||||
### 5.2 方案 B:TKG 增加 `label_type`(完整方案)
|
||||
|
||||
**需要修改**:
|
||||
|
||||
1. **TKG Schema**
|
||||
- 增加 `label_type` 欄位
|
||||
|
||||
2. **Core API**
|
||||
- `/trace-profile/group` 增加 `label_type` 參數(默認 `face_name`)
|
||||
- 更新 TKG node 時設定 `label_type`
|
||||
|
||||
3. **Frontend**
|
||||
- 傳遞 `{ name: "...", label_type: "face_name" }`
|
||||
|
||||
**優點**:
|
||||
- 完整解決命名類型問題
|
||||
- 支援未來擴展
|
||||
|
||||
**缺點**:
|
||||
- 需要改動 TKG 和 Core API
|
||||
- 工作量較大
|
||||
|
||||
**預計工作量**: 2-4 小時(跨團隊)
|
||||
|
||||
---
|
||||
|
||||
## 六、討論議題
|
||||
|
||||
### 6.1 短期決策
|
||||
|
||||
1. **是否先執行方案 A**,讓功能正常運作?
|
||||
2. **方案 A 的風險評估**?
|
||||
|
||||
### 6.2 長期設計
|
||||
|
||||
1. **TKG 是否需要 `label_type` 欄位**?
|
||||
2. **如果需要,`label_type` 應該有哪些值**?
|
||||
3. **Core API 是否需要同時支援 `name` 和 `label_type` 參數**?
|
||||
|
||||
### 6.3 命名規範
|
||||
|
||||
**建議建立正式對照表**:
|
||||
|
||||
| 層級 | 欄位 | 類型 | 說明 |
|
||||
|------|------|------|------|
|
||||
| TKG | `label` | string | 根本存儲 |
|
||||
| TKG | `label_type` | string | 類型標識(新增?) |
|
||||
| Core API | `name` | string | API 參數/返回 |
|
||||
| Frontend | `name` | string | 統一使用 |
|
||||
|
||||
---
|
||||
|
||||
## 七、附錄
|
||||
|
||||
### A. 相關代碼位置
|
||||
|
||||
| 文件 | 說明 |
|
||||
|------|------|
|
||||
| `/Users/accusys/momentry_studio/src/api/index.ts:379` | API Builder |
|
||||
| `/Users/accusys/momentry_studio/src/store.ts:252` | updateTraceProfileGroup |
|
||||
| `/Users/accusys/momentry_studio/src/views/PeopleView.vue:430,864,928,971` | 調用點 |
|
||||
| `/Users/accusys/momentry_core/docs_v1.0/doc_wasm/modules/18_profile.md` | Core API 文檔 |
|
||||
|
||||
### B. Core API 端點
|
||||
|
||||
| 端點 | 方法 | 說明 |
|
||||
|------|------|------|
|
||||
| `/api/v1/trace-profile` | GET | 獲取單一 trace profile |
|
||||
| `/api/v1/trace-profile` | PUT | 更新單一 trace profile |
|
||||
| `/api/v1/trace-profile/group` | PUT | 批量更新 trace profile |
|
||||
| `/api/v1/file/{uuid}/face-groups` | GET | 獲取 face groups |
|
||||
|
||||
---
|
||||
|
||||
**以上問題請 Core Team 確認後,再決定修復方向。**
|
||||
Reference in New Issue
Block a user