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:
2026-07-24 20:19:47 +08:00
parent 299f58e883
commit 5951aca086
38 changed files with 12107 additions and 1666 deletions

View 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。

View 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:**
```
F100F350 (4:1014: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*

View 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...
F0128 (0:000:05)
```
```
[ASRX] Today we're talking about storage solutions...
(0:100: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

View 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.

View 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

View 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
View 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 本地** |
**使用時機**: 人物詳情頁面顯示特定幀的圖片
---
## 四、人物IdentityAPI
### 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 streamingHLS/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 統一管理。

View 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`

View 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 團隊討論與反饋。**

View 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 方案 BTKG 增加 `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 確認後,再決定修復方向。**