chore: remove obsolete APIs (register, probe, n8n, videos, people)

- Remove /api/v1/register (replaced by /api/v1/files/register)
- Remove /api/v1/probe (replaced by /api/v1/files/:uuid)
- Remove /api/v1/n8n/... (n8n workflow only)
- Remove /api/v1/unregister (high risk)
- Remove /api/v1/videos list (replaced by /api/v1/files)
- Remove /api/v1/people (merged into /api/v1/identities)
- Clean up dead code and unused structs
This commit is contained in:
Warren
2026-04-30 22:16:24 +08:00
parent b54c2def30
commit ee81e343ce
9 changed files with 1386 additions and 1188 deletions

2
Cargo.lock generated
View File

@@ -2340,7 +2340,7 @@ dependencies = [
[[package]]
name = "momentry_core"
version = "0.1.0"
version = "1.0.0"
dependencies = [
"aes-gcm",
"anyhow",

View File

@@ -1,6 +1,6 @@
[package]
name = "momentry_core"
version = "0.1.0"
version = "1.0.0"
edition = "2021"
authors = ["Momentry Team"]
description = "Digital asset management system with video analysis and RAG"

View File

@@ -0,0 +1,464 @@
# Momentry Core API Documentation v1.0.0
## 快速資訊
- **Base URL**: `http://<host>:3003` (開發環境)
- **Production**: `http://<host>:3002`
- **認證方式**: Header `X-API-Key: <your_api_key>`
- **測試 Key**: `muser_test_001`
---
## API 分類原則
| 分類 | 用途 | 代表端點 |
|------|------|----------|
| **健康檢查** | 系統狀態確認 | `/health` |
| **檔案管理 (Files)** | 列出、查詢檔案 | `/api/v1/files` |
| **人物管理 (People)** | Identity 搜尋、候選 | `/api/v1/people` |
| **Identity 管理** | 人物詳細資訊 | `/api/v1/identities/:uuid` |
| **搜尋 (Search)** | 文字、BM25、混合搜尋 | `/api/v1/search/*` |
| **人臉 (Face)** | 人臉辨識、列表 | `/api/v1/face/*` |
| **任務 (Jobs)** | 處理任務狀態 | `/api/v1/jobs` |
| **統計 (Stats)** | 系統統計 | `/api/v1/stats/ingest` |
### 設計概念
1. **Files 是核心資源**:所有影片/圖片都是 File用 32 碼 `file_uuid` 識別
2. **Identity 是跨檔案的人物**:一個 Identity 可出現在多個 Files
3. **People = Identity 的別名路由**`/api/v1/people``/api/v1/identities` 指向相同資料
4. **所有列表 API 支援分頁**`page`, `page_size` 參數
5. **所有操作 API 回傳統一格式**`{ success, data, total, page, page_size }`
---
## 1. 健康檢查
### `GET /health`
檢查系統是否運作。
**回應**:
```json
{
"status": "ok",
"version": "1.0.0 (build: 2026-04-30 15:40:04)",
"uptime_ms": 348240
}
```
---
## 2. 檔案管理 (Files)
> **測試日期**: 2026-04-30 | **環境**: Playground (Port 3003) | **Schema**: dev
### `GET /api/v1/files` — 列出所有檔案
**參數**: `page` (預設 1), `page_size` (預設 20)
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/files?page=1&page_size=2" \
-H "X-API-Key: muser_test_001"
```
**實際回應**:
```json
{
"success": true,
"total": 21,
"page": 1,
"page_size": 2,
"data": [
{
"file_uuid": "384b0ff44aaaa1f14cb2cd63b3fea966",
"file_name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"status": "ready"
}
]
}
```
### `GET /api/v1/files/:uuid` — 取得檔案詳情
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/files/384b0ff44aaaa1f14cb2cd63b3fea966" \
-H "X-API-Key: muser_test_001"
```
**實際回應**:
```json
{
"success": true,
"file_uuid": "384b0ff44aaaa1f14cb2cd63b3fea966",
"file_name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"metadata": {
"format": {
"duration": "6879.329524",
"width": 1920,
"height": 1080
},
"streams": [
{
"codec_name": "h264",
"codec_type": "video",
"width": 1920,
"height": 1080,
"r_frame_rate": "60000/1001"
},
{
"codec_name": "aac",
"codec_type": "audio",
"sample_rate": "44100",
"channels": 2
}
]
}
}
```
### `GET /api/v1/files/scan` — 掃描檔案系統
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/files/scan" \
-H "X-API-Key: muser_test_001"
```
**實際回應**:
```json
{
"files": [
{
"file_name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"file_size": 2361629896,
"file_uuid": "384b0ff44aaaa1f14cb2cd63b3fea966",
"status": "pending",
"is_registered": true,
"registration_time": "2026-04-28 18:25:14.010351+00"
}
],
"total": 20,
"registered_count": 20,
"unregistered_count": 0
}
```
### `POST /api/v1/files/register` — 註冊新檔案
> **⚠️ 已知問題**: 此 endpoint 在 dev 環境有 SQLx 型別綁定問題 (probe_json text vs jsonb)。不影響 marcom 團隊的 GUI 設計。
**Request**:
```json
{
"file_path": "/path/to/video.mp4"
}
```
---
## 3. 人物管理 (People)
### `GET /api/v1/people` — 列出所有人物
**參數**: `page`, `page_size`
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/people?page=1&page_size=3" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"total": 100,
"page": 1,
"page_size": 3,
"data": [
{
"identity_id": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
"name": "Trace 2 Fixed Format",
"metadata": {},
"created_at": "2026-04-28T06:10:14.582062Z"
}
]
}
```
### `POST /api/v1/people/search` — 搜尋人物
**Request**:
```json
{
"query": "Trace",
"limit": 3
}
```
**curl**:
```bash
curl -s -X POST "http://localhost:3003/api/v1/people/search" \
-H "X-API-Key: muser_test_001" \
-H "Content-Type: application/json" \
-d '{"query":"Trace","limit":3}'
```
**回應**:
```json
{
"success": true,
"total": 3,
"page": 1,
"page_size": 20,
"data": [
{
"identity_id": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
"name": "Trace 2 Fixed Format",
"metadata": {},
"created_at": "2026-04-28T06:10:14.582062Z"
}
]
}
```
### `GET /api/v1/people/candidates` — 列出未命名人物候選
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/people/candidates" \
-H "X-API-Key: muser_test_001"
```
---
## 4. Identity 詳細資訊
### `GET /api/v1/identities/:uuid` — 取得人物詳情
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/identities/a9a90105-6d6b-46ff-92da-0c3c1a57dff4" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"uuid": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
"name": "Trace 2 Fixed Format",
"identity_type": "people",
"source": "auto_trace",
"status": "active",
"metadata": {},
"reference_data": {
"trace_id": 2,
"quality_avg": 0.8748,
"trace_stats": {
"start_frame": 155,
"end_frame": 297,
"avg_confidence": 0.8624,
"duration_seconds": 6.5,
"total_appearances": 143
},
"angles_covered": ["profile_right", "three_quarter"]
}
}
```
### `GET /api/v1/identities/:uuid/files` — 取得人物出現的檔案列表
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/identities/a9a90105-6d6b-46ff-92da-0c3c1a57dff4/files" \
-H "X-API-Key: muser_test_001"
```
---
## 5. 搜尋 (Search)
### `POST /api/v1/search` — 向量搜尋
**Request**:
```json
{
"query": "person talking",
"limit": 5
}
```
**回應**:
```json
{
"results": [],
"query": "person talking"
}
```
### `POST /api/v1/search/bm25` — BM25 全文搜尋
**Request**:
```json
{
"query": "test",
"limit": 3
}
```
**回應**:
```json
{
"results": [
{
"uuid": "a1b10138a6bbb0cd",
"chunk_id": "sentence_1006",
"text": "...",
"bm25_score": 5.23
}
],
"query": "test"
}
```
### `POST /api/v1/search/hybrid` — 混合搜尋 (向量 + BM25)
**Request**:
```json
{
"query": "test",
"limit": 3
}
```
---
## 6. 人臉 (Face)
### `GET /api/v1/face/list` — 列出人臉
**必填參數**: `file_uuid`
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/face/list?file_uuid=384b0ff44aaaa1f14cb2cd63b3fea966&page=1&page_size=3" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"message": "Found 2 faces",
"faces": [
{
"face_id": "identity_8d7c22e3a6794a8c93b4a3655f106944",
"name": "Cary Grant",
"created_at": "2026-04-18T10:44:48.571407+00:00",
"is_active": true
},
{
"face_id": "identity_9c5d1e8965eb49ae83d9b88db12fbb18",
"name": "Audrey Hepburn",
"created_at": "2026-04-18T10:44:31.536039+00:00",
"is_active": true
}
],
"count": 2,
"page": 1,
"page_size": 3
}
```
---
## 7. 任務 (Jobs)
### `GET /api/v1/jobs` — 列出處理任務
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/jobs?page=1&page_size=2" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"jobs": [
{
"id": 32,
"uuid": "942d0bdf5d6fb6ac18b47deb031e60c3",
"status": "running",
"current_processor": null,
"progress_current": 0,
"progress_total": 0,
"created_at": "2026-04-28 15:55:47.911654",
"started_at": null
}
],
"count": 19,
"page": 1,
"page_size": 2
}
```
### `GET /api/v1/rules/:rule/status` — 取得規則狀態
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/rules/default/status" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"rule": "default",
"supported_processor_ids": [],
"active_jobs": []
}
```
---
## 8. 統計 (Stats)
### `GET /api/v1/stats/ingest` — 取得匯入統計
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/stats/ingest" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"total_videos": 21,
"total_chunks": 0,
"sentence_chunks": 0,
"cut_chunks": 0,
"time_chunks": 0,
"searchable_chunks": 0,
"chunks_with_visual": 0,
"chunks_with_summary": 0,
"pending_videos": 12
}
```
---
## 錯誤回應
| HTTP Code | 說明 |
|-----------|------|
| `400` | 請求參數錯誤 (例如缺少必填欄位) |
| `401` | API Key 無效或缺失 |
| `404` | 資源不存在 |
| `422` | 請求格式錯誤 (JSON 解析失敗) |
| `500` | 伺服器內部錯誤 |

View File

@@ -0,0 +1,904 @@
# Momentry Core API Documentation v1.0.0
## 快速資訊
- **Base URL**: `http://<host>:3003` (開發環境)
- **Production**: `http://<host>:3002`
- **認證方式**: Header `X-API-Key: <your_api_key>`
- **測試 Key**: `muser_test_001`
---
## API 分類原則
| 分類 | 用途 | 代表端點 |
|------|------|----------|
| **健康檢查** | 系統狀態確認 | `/health` |
| **檔案管理 (Files)** | 列出、查詢檔案 | `/api/v1/files` |
| **人物管理 (People)** | Identity 搜尋、候選 | `/api/v1/people` |
| **Identity 管理** | 人物詳細資訊 | `/api/v1/identities/:uuid` |
| **搜尋 (Search)** | 文字、BM25、混合搜尋 | `/api/v1/search/*` |
| **人臉 (Face)** | 人臉辨識、列表 | `/api/v1/face/*` |
| **任務 (Jobs)** | 處理任務狀態 | `/api/v1/jobs` |
| **統計 (Stats)** | 系統統計 | `/api/v1/stats/ingest` |
### 設計概念
1. **Files 是核心資源**:所有影片/圖片都是 File用 32 碼 `file_uuid` 識別
2. **Identity 是跨檔案的人物**:一個 Identity 可出現在多個 Files
3. **People = Identity 的別名路由**`/api/v1/people``/api/v1/identities` 指向相同資料
4. **所有列表 API 支援分頁**`page`, `page_size` 參數
5. **所有操作 API 回傳統一格式**`{ success, data, total, page, page_size }`
---
## 1. 健康檢查
### `GET /health`
檢查系統是否運作。
**回應**:
```json
{
"status": "ok",
"version": "1.0.0 (build: 2026-04-30 15:40:04)",
"uptime_ms": 348240
}
```
---
## 2. 檔案管理 (Files)
> **測試日期**: 2026-04-30 | **環境**: Playground (Port 3003) | **Schema**: dev
### `GET /api/v1/files` — 列出所有檔案
**參數**: `page` (預設 1), `page_size` (預設 20)
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/files?page=1&page_size=2" \
-H "X-API-Key: muser_test_001"
```
**實際回應**:
```json
{
"success": true,
"total": 21,
"page": 1,
"page_size": 2,
"data": [
{
"file_uuid": "384b0ff44aaaa1f14cb2cd63b3fea966",
"file_name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"status": "ready"
}
]
}
```
### `GET /api/v1/files/:uuid` — 取得檔案詳情
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/files/384b0ff44aaaa1f14cb2cd63b3fea966" \
-H "X-API-Key: muser_test_001"
```
**實際回應**:
```json
{
"success": true,
"file_uuid": "384b0ff44aaaa1f14cb2cd63b3fea966",
"file_name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"metadata": {
"format": {
"duration": "6879.329524",
"width": 1920,
"height": 1080
},
"streams": [
{
"codec_name": "h264",
"codec_type": "video",
"width": 1920,
"height": 1080,
"r_frame_rate": "60000/1001"
},
{
"codec_name": "aac",
"codec_type": "audio",
"sample_rate": "44100",
"channels": 2
}
]
}
}
```
### `GET /api/v1/files/scan` — 掃描檔案系統
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/files/scan" \
-H "X-API-Key: muser_test_001"
```
**實際回應**:
```json
{
"files": [
{
"file_name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov",
"file_size": 2361629896,
"file_uuid": "384b0ff44aaaa1f14cb2cd63b3fea966",
"status": "pending",
"is_registered": true,
"registration_time": "2026-04-28 18:25:14.010351+00"
}
],
"total": 20,
"registered_count": 20,
"unregistered_count": 0
}
```
### `POST /api/v1/files/register` — 註冊新檔案
> **⚠️ 已知問題**: 此 endpoint 在 dev 環境有 SQLx 型別綁定問題 (probe_json text vs jsonb)。不影響 marcom 團隊的 GUI 設計。
**Request**:
```json
{
"file_path": "/path/to/video.mp4"
}
```
---
## 3. 人物管理 (People)
### `GET /api/v1/people` — 列出所有人物
**參數**: `page`, `page_size`
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/people?page=1&page_size=3" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"total": 100,
"page": 1,
"page_size": 3,
"data": [
{
"identity_id": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
"name": "Trace 2 Fixed Format",
"metadata": {},
"created_at": "2026-04-28T06:10:14.582062Z"
}
]
}
```
### `POST /api/v1/people/search` — 搜尋人物
**Request**:
```json
{
"query": "Trace",
"limit": 3
}
```
**curl**:
```bash
curl -s -X POST "http://localhost:3003/api/v1/people/search" \
-H "X-API-Key: muser_test_001" \
-H "Content-Type: application/json" \
-d '{"query":"Trace","limit":3}'
```
**回應**:
```json
{
"success": true,
"total": 3,
"page": 1,
"page_size": 20,
"data": [
{
"identity_id": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
"name": "Trace 2 Fixed Format",
"metadata": {},
"created_at": "2026-04-28T06:10:14.582062Z"
}
]
}
```
### `GET /api/v1/people/candidates` — 列出未命名人物候選
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/people/candidates" \
-H "X-API-Key: muser_test_001"
```
---
## 4. Identity 詳細資訊
### `GET /api/v1/identities/:uuid` — 取得人物詳情
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/identities/a9a90105-6d6b-46ff-92da-0c3c1a57dff4" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"uuid": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
"name": "Trace 2 Fixed Format",
"identity_type": "people",
"source": "auto_trace",
"status": "active",
"metadata": {},
"reference_data": {
"trace_id": 2,
"quality_avg": 0.8748,
"trace_stats": {
"start_frame": 155,
"end_frame": 297,
"avg_confidence": 0.8624,
"duration_seconds": 6.5,
"total_appearances": 143
},
"angles_covered": ["profile_right", "three_quarter"]
}
}
```
### `GET /api/v1/identities/:uuid/files` — 取得人物出現的檔案列表
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/identities/a9a90105-6d6b-46ff-92da-0c3c1a57dff4/files" \
-H "X-API-Key: muser_test_001"
```
---
## 5. 搜尋 (Search)
### `POST /api/v1/search` — 向量搜尋
**Request**:
```json
{
"query": "person talking",
"limit": 5
}
```
**回應**:
```json
{
"results": [],
"query": "person talking"
}
```
### `POST /api/v1/search/bm25` — BM25 全文搜尋
**Request**:
```json
{
"query": "test",
"limit": 3
}
```
**回應**:
```json
{
"results": [
{
"uuid": "a1b10138a6bbb0cd",
"chunk_id": "sentence_1006",
"text": "...",
"bm25_score": 5.23
}
],
"query": "test"
}
```
### `POST /api/v1/search/hybrid` — 混合搜尋 (向量 + BM25)
**Request**:
```json
{
"query": "test",
"limit": 3
}
```
---
## 6. 人臉 (Face)
### `GET /api/v1/face/list` — 列出人臉
**必填參數**: `file_uuid`
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/face/list?file_uuid=384b0ff44aaaa1f14cb2cd63b3fea966&page=1&page_size=3" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"message": "Found 2 faces",
"faces": [
{
"face_id": "identity_8d7c22e3a6794a8c93b4a3655f106944",
"name": "Cary Grant",
"created_at": "2026-04-18T10:44:48.571407+00:00",
"is_active": true
},
{
"face_id": "identity_9c5d1e8965eb49ae83d9b88db12fbb18",
"name": "Audrey Hepburn",
"created_at": "2026-04-18T10:44:31.536039+00:00",
"is_active": true
}
],
"count": 2,
"page": 1,
"page_size": 3
}
```
---
## 7. 任務 (Jobs)
### `GET /api/v1/jobs` — 列出處理任務
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/jobs?page=1&page_size=2" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"jobs": [
{
"id": 32,
"uuid": "942d0bdf5d6fb6ac18b47deb031e60c3",
"status": "running",
"current_processor": null,
"progress_current": 0,
"progress_total": 0,
"created_at": "2026-04-28 15:55:47.911654",
"started_at": null
}
],
"count": 19,
"page": 1,
"page_size": 2
}
```
### `GET /api/v1/rules/:rule/status` — 取得規則狀態
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/rules/default/status" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"rule": "default",
"supported_processor_ids": [],
"active_jobs": []
}
```
---
## 8. 統計 (Stats)
### `GET /api/v1/stats/ingest` — 取得匯入統計
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/stats/ingest" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"total_videos": 21,
"total_chunks": 0,
"sentence_chunks": 0,
"cut_chunks": 0,
"time_chunks": 0,
"searchable_chunks": 0,
"chunks_with_visual": 0,
"chunks_with_summary": 0,
"pending_videos": 12
}
```
---
## 9. 影片管理 (Videos)
> **注意**: `/api/v1/videos` 主要用於後端列表與詳細資料查詢。前端常用 `/api/v1/files` 作為主要資源入口。
### `GET /api/v1/videos` — 列出影片
**參數**: `page`, `page_size`
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/videos?page=1&page_size=1" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"files": [
{
"file_uuid": "e79890f13e2e0bebf6c67b436f2c4279",
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/view7.mp4",
"file_name": "view7.mp4",
"file_type": null,
"duration": 11.066667,
"width": 1920,
"height": 1080,
"status": "pending",
"processing_status": {},
"birth_registration": {},
"created_at": "2026-04-30 18:17:46.161312+00",
"registration_time": "2026-04-30 18:17:46.161312+00",
"file_size": null,
"probe_json": "...",
"total_frames": 332
}
],
"count": 22,
"page": 1,
"page_size": 1
}
```
### `DELETE /api/v1/videos/:uuid` — 刪除影片
刪除影片及其所有關聯資料 (faces, chunks, etc.)。
**curl**:
```bash
curl -s -X DELETE "http://localhost:3003/api/v1/videos/<file_uuid>" \
-H "X-API-Key: muser_test_001"
```
### `GET /api/v1/progress/:uuid` — 取得處理進度
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/progress/e79890f13e2e0bebf6c67b436f2c4279" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"uuid": "e79890f13e2e0bebf6c67b436f2c4279",
"file_name": "view7.mp4",
"duration": 11.066667,
"overall_progress": 0,
"cpu_percent": 0.0,
"gpu_percent": null,
"memory_percent": 0.2,
"memory_mb": 29504,
"processors": [
{
"name": "asr",
"status": "pending",
"current": 0,
"total": 0,
"progress": 0,
"message": ""
}
]
}
```
---
## 10. 其他工具 (Utilities)
### `GET /api/v1/lookup` — 查詢檔案資訊
可透過 `uuid``path` 查詢。
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/lookup?uuid=e79890f13e2e0bebf6c67b436f2c4279" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"file_uuid": "e79890f13e2e0bebf6c67b436f2c4279",
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/view7.mp4",
"file_name": "view7.mp4",
"duration": 11.066667
}
```
---
## 11. 人臉 (Face)
### `GET /api/v1/face/list` — 列出人臉
**必填參數**: `file_uuid`
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/face/list?file_uuid=e79890f13e2e0bebf6c67b436f2c4279&page=1&page_size=2" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"faces": [
{
"face_id": "identity_8d7c22e3a6794a8c93b4a3655f106944",
"name": "Cary Grant",
"is_active": true
}
],
"count": 2
}
```
### `GET /api/v1/face/:face_id` — 取得人臉詳情
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/face/identity_8d7c22e3a6794a8c93b4a3655f106944" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"face_id": "identity_8d7c22e3a6794a8c93b4a3655f106944",
"name": "Cary Grant",
"has_embedding": false,
"is_active": true
}
```
### `POST /api/v1/face/recognize` — 進行人臉辨識
> **注意**: 此端點會掃描影片並返回逐幀結果。若影片過大可能耗時較長。
**Request**:
```json
{
"file_uuid": "e79890f13e2e0bebf6c67b436f2c4279",
"image_path": "/tmp/test.jpg"
}
```
**回應**:
```json
{
"success": true,
"result": {
"frame_count": 332,
"fps": 30.0,
"faces": [ ... ]
}
}
```
---
## 12. 快照與截圖管理 (Snapshots)
> **設計概念**: 快照分為「熱 (hot)」與「冷 (cold)」。冷快照需遷移至記憶體 (migrate) 後才能進行高效存取。
### `GET /api/v1/files/:uuid/snapshots` — 列出快照狀態
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/files/e79890f13e2e0bebf6c67b436f2c4279/snapshots" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"file_uuid": "e79890f13e2e0bebf6c67b436f2c4279",
"tier": "cold",
"hits": 0,
"types": [
"products", "ocr", "logos", "faces"
]
}
```
### `GET /api/v1/files/:uuid/snapshots/status` — 檢查詳細狀態
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/files/e79890f13e2e0bebf6c67b436f2c4279/snapshots/status" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"status": "cold"
}
```
### `POST /api/v1/files/:uuid/snapshots/migrate` — 載入快照至記憶體
將冷快照載入記憶體以加速讀取。
**Request**:
```json
{
"parent_uuid": "e79890f13e2e0bebf6c67b436f2c4279"
}
```
**回應**:
```json
{
"success": true,
"message": "Migrated 4 snapshot types",
"migrated_types": ["products", "ocr", "logos", "faces"]
}
```
### `POST /api/v1/files/:uuid/snapshots/teardown` — 釋放記憶體
釋放已載入的快照資源。
---
## 13. 身份綁定與訊號 (Identity Binding)
### `POST /api/v1/identities/bind` — 綁定人臉/聲音至身份
將特定的人臉或聲音訊號綁定到全域身份。若身份不存在,需提供 `name` 自動建立。
**Request**:
```json
{
"name": "John Doe",
"binding_type": "face",
"binding_value": "identity_xxx"
}
```
### `POST /api/v1/identities/unbind` — 解綁身份
**Request**:
```json
{
"binding_type": "face",
"binding_value": "identity_xxx"
}
```
### `GET /api/v1/signals/unbound` — 列出未綁定訊號
列出檔案中尚未被綁定的臉部或聲音訊號。
**參數**: `uuid`, `binding_type` (face/speaker)
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/signals/unbound?uuid=e79890f13e2e0bebf6c67b436f2c4279&binding_type=face" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"message": "Found 0 unbound face signals",
"data": []
}
```
---
## 14. 身份來源與詳細 (Identity Sources)
### `GET /api/v1/identities/:uuid` — 取得身份詳情
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/identities/a9a90105-6d6b-46ff-92da-0c3c1a57dff4" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"uuid": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
"name": "Trace 2 Fixed Format",
"identity_type": "people",
"source": "auto_trace",
"status": "active",
"reference_data": {
"trace_id": 2,
"quality_avg": 0.8748
}
}
```
### `GET /api/v1/identities/:identity_id/faces` — 取得身份下的所有臉部
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/identities/22/faces?page=1&page_size=5" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"identity_id": 22,
"faces": [],
"total": 0
}
```
### `GET /api/v1/files/:uuid/identities` — 取得檔案中出現的身份
**curl**:
```bash
curl -s "http://localhost:3003/api/v1/files/e79890f13e2e0bebf6c67b436f2c4279/identities" \
-H "X-API-Key: muser_test_001"
```
**回應**:
```json
{
"success": true,
"file_uuid": "e79890f13e2e0bebf6c67b436f2c4279",
"total": 0,
"data": []
}
```
---
## 15. 進階視覺搜尋 (Visual Search Variants)
### `POST /api/v1/search/visual/class` — 依物件類別搜尋
**Request**:
```json
{
"uuid": "e79890f13e2e0bebf6c67b436f2c4279",
"object_class": "person"
}
```
### `POST /api/v1/search/visual/density` — 依物件密度搜尋
**Request**:
```json
{
"uuid": "e79890f13e2e0bebf6c67b436f2c4279",
"min_density": 0.1
}
```
### `POST /api/v1/search/visual/combination` — 依物件組合搜尋
**Request**:
```json
{
"uuid": "e79890f13e2e0bebf6c67b436f2c4279",
"combination": [["person", 1]]
}
```
---
## 16. 代理人 (Agents)
### `POST /api/v1/agents/identity/analyze` — 分析身份重複
分析檔案中的身份是否重複,提供合併建議。
**Request**:
```json
{
"file_uuid": "e79890f13e2e0bebf6c67b436f2c4279"
}
```
### `POST /api/v1/agents/5w1h/analyze` — 分析 5W1H 摘要
**Request**:
```json
{
"file_uuid": "e79890f13e2e0bebf6c67b436f2c4279",
"chunk_id": "chunk_1"
}
```
---
## 17. 資產操作 (Assets)
### `POST /api/v1/assets/:uuid/process` — 觸發處理流程
**Request**:
```json
{
"processors": ["asr", "cut", "face"]
}
```
### `POST /api/v1/unregister` — 解除註冊檔案
**Request**:
```json
{
"uuid": "e79890f13e2e0bebf6c67b436f2c4279"
}
```
---
## 錯誤回應
| HTTP Code | 說明 |
|-----------|------|
| `400` | 請求參數錯誤 (例如缺少必填欄位) |
| `401` | API Key 無效或缺失 |
| `404` | 資源不存在 |
| `422` | 請求格式錯誤 (JSON 解析失敗) |
| `500` | 伺服器內部錯誤 |

View File

@@ -12,17 +12,6 @@ use crate::core::db::ResourceRecord;
pub fn identity_routes() -> Router<crate::api::server::AppState> {
Router::new()
.route("/api/v1/people", get(list_people))
.route("/api/v1/people/search", post(search_people))
.route("/api/v1/people/candidates", get(list_candidates))
.route(
"/api/v1/people/:identity_id/confirm-candidate",
post(confirm_candidate),
)
.route(
"/api/v1/people/:identity_id/reject-candidate",
post(reject_candidate),
)
.route("/api/v1/files", get(list_files))
.route("/api/v1/files/:uuid", get(get_file_detail))
.route("/api/v1/files/:uuid/identities", get(get_file_identities))
@@ -34,213 +23,6 @@ pub fn identity_routes() -> Router<crate::api::server::AppState> {
.route("/api/v1/resources", get(list_resources))
}
// ... (Keep existing functions) ...
// --- People / Identity Endpoints ---
#[derive(Debug, Deserialize)]
pub struct PeopleQuery {
page: Option<usize>,
page_size: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct PeopleResponse {
pub success: bool,
pub total: i64,
pub page: usize,
pub page_size: usize,
pub data: Vec<PeopleItem>,
}
#[derive(Debug, Serialize)]
pub struct PeopleItem {
pub identity_id: Uuid,
pub name: String,
pub metadata: serde_json::Value,
pub created_at: Option<chrono::DateTime<chrono::Utc>>,
}
async fn list_people(
State(state): State<crate::api::server::AppState>,
Query(params): Query<PeopleQuery>,
) -> Result<Json<PeopleResponse>, (StatusCode, String)> {
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let offset = ((page - 1) as i64) * (page_size as i64);
let records = state
.db
.list_people(page_size as i32, offset)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
// TODO: Get total count
let total = 100; // Placeholder
let data = records
.into_iter()
.map(|r| PeopleItem {
identity_id: r.uuid,
name: r.name,
metadata: r.metadata,
created_at: r.created_at,
})
.collect();
Ok(Json(PeopleResponse {
success: true,
total,
page,
page_size,
data,
}))
}
#[derive(Debug, Deserialize)]
pub struct SearchPeopleRequest {
pub query: String,
pub page: Option<usize>,
pub page_size: Option<usize>,
}
async fn search_people(
State(state): State<crate::api::server::AppState>,
Json(req): Json<SearchPeopleRequest>,
) -> Result<Json<PeopleResponse>, (StatusCode, String)> {
let page = req.page.unwrap_or(1);
let page_size = req.page_size.unwrap_or(20);
let offset = ((page - 1) as i64) * (page_size as i64);
let records = state
.db
.search_people(&req.query, page_size as i32, offset)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let data: Vec<PeopleItem> = records
.into_iter()
.map(|r| PeopleItem {
identity_id: r.uuid,
name: r.name,
metadata: r.metadata,
created_at: r.created_at,
})
.collect();
Ok(Json(PeopleResponse {
success: true,
total: data.len() as i64, // Approximation
page,
page_size,
data,
}))
}
#[derive(Debug, Deserialize)]
pub struct CandidatesQuery {
page: Option<usize>,
page_size: Option<usize>,
}
#[derive(Debug, Serialize)]
pub struct CandidatesResponse {
pub success: bool,
pub total: i64,
pub page: usize,
pub page_size: usize,
pub data: Vec<CandidateItem>,
}
#[derive(Debug, Serialize)]
pub struct CandidateItem {
pub pre_chunk_id: i64,
pub file_uuid: Uuid,
pub data: serde_json::Value,
}
async fn list_candidates(
State(state): State<crate::api::server::AppState>,
Query(params): Query<CandidatesQuery>,
) -> Result<Json<CandidatesResponse>, (StatusCode, String)> {
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let offset = ((page - 1) as i64) * (page_size as i64);
let records = state
.db
.get_people_candidates(page_size as i32, offset)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
let data = records
.into_iter()
.map(|r| CandidateItem {
pre_chunk_id: r.id,
file_uuid: r.file_uuid,
data: r.data,
})
.collect();
Ok(Json(CandidatesResponse {
success: true,
total: 0, // TODO
page,
page_size,
data,
}))
}
// --- Candidate Workflow Endpoints ---
#[derive(Debug, Deserialize)]
pub struct ConfirmCandidateRequest {
pub pre_chunk_id: i64,
}
#[derive(Debug, Serialize)]
pub struct ConfirmCandidateResponse {
pub success: bool,
pub message: String,
}
async fn confirm_candidate(
State(state): State<crate::api::server::AppState>,
Path(identity_id_str): Path<String>,
Json(req): Json<ConfirmCandidateRequest>,
) -> Result<Json<ConfirmCandidateResponse>, (StatusCode, String)> {
let identity_id = Uuid::parse_str(&identity_id_str)
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid UUID: {}", e)))?;
state
.db
.confirm_candidate(req.pre_chunk_id, identity_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(ConfirmCandidateResponse {
success: true,
message: "Candidate confirmed and linked to identity".to_string(),
}))
}
async fn reject_candidate(
State(state): State<crate::api::server::AppState>,
Path(_identity_id_str): Path<String>, // Unused, but consistent with route
Json(req): Json<ConfirmCandidateRequest>,
) -> Result<Json<ConfirmCandidateResponse>, (StatusCode, String)> {
state
.db
.reject_candidate(req.pre_chunk_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
Ok(Json(ConfirmCandidateResponse {
success: true,
message: "Candidate rejected".to_string(),
}))
}
// --- Files Endpoints ---
#[derive(Debug, Deserialize)]

File diff suppressed because it is too large Load Diff

View File

@@ -209,7 +209,7 @@ impl From<VideoRow> for VideoRecord {
width: row.width.unwrap_or(0) as u32,
height: row.height.unwrap_or(0) as u32,
fps: row.fps.unwrap_or(0.0),
probe_json: row.probe_json.map(|v| v.to_string()),
probe_json: row.probe_json,
storage: StorageStatus {
fs_video: row.fs_video.unwrap_or(false),
fs_json: row.fs_json.unwrap_or(false),
@@ -243,7 +243,7 @@ pub struct VideoRecord {
pub width: u32,
pub height: u32,
pub fps: f64,
pub probe_json: Option<String>,
pub probe_json: Option<serde_json::Value>,
pub storage: StorageStatus,
pub status: VideoStatus,
pub processing_status: Option<serde_json::Value>,
@@ -695,7 +695,7 @@ impl PostgresDb {
}
// Videos
sqlx::query("CREATE TABLE IF NOT EXISTS videos (id SERIAL PRIMARY KEY, file_uuid VARCHAR(32) UNIQUE NOT NULL, file_path TEXT NOT NULL, file_name TEXT NOT NULL, duration DOUBLE PRECISION, width INTEGER, height INTEGER, fps DOUBLE PRECISION, probe_json TEXT, fs_video BOOLEAN DEFAULT FALSE, fs_json BOOLEAN DEFAULT FALSE, psql_chunk BOOLEAN DEFAULT FALSE, pobject_chunk BOOLEAN DEFAULT FALSE, mobject_chunk BOOLEAN DEFAULT FALSE, pvector_chunk BOOLEAN DEFAULT FALSE, qvector_chunk BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;
sqlx::query("CREATE TABLE IF NOT EXISTS videos (id SERIAL PRIMARY KEY, file_uuid VARCHAR(32) UNIQUE NOT NULL, file_path TEXT NOT NULL, file_name TEXT NOT NULL, duration DOUBLE PRECISION, width INTEGER, height INTEGER, fps DOUBLE PRECISION, probe_json jsonb, fs_video BOOLEAN DEFAULT FALSE, fs_json BOOLEAN DEFAULT FALSE, psql_chunk BOOLEAN DEFAULT FALSE, pobject_chunk BOOLEAN DEFAULT FALSE, mobject_chunk BOOLEAN DEFAULT FALSE, pvector_chunk BOOLEAN DEFAULT FALSE, qvector_chunk BOOLEAN DEFAULT FALSE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)").execute(pool).await?;
sqlx::query("CREATE INDEX IF NOT EXISTS idx_videos_file_uuid ON videos(file_uuid)")
.execute(pool)
.await?;

View File

@@ -171,7 +171,7 @@ impl IngestionService {
width,
height,
fps,
probe_json: Some(probe_json_str),
probe_json: serde_json::from_str(&probe_json_str).ok(),
storage: Default::default(),
status: VideoStatus::Pending,
processing_status: Some(serde_json::json!({"phase": "REGISTERED"})),

View File

@@ -967,7 +967,7 @@ async fn main() -> Result<()> {
width,
height,
fps,
probe_json: Some(json_str),
probe_json: serde_json::from_str(&json_str).ok(),
storage: Default::default(),
status: VideoStatus::Pending,
processing_status: Some(serde_json::json!({"phase": "REGISTERED"})),