Compare commits
20 Commits
de88fd4e44
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1572907ae | ||
|
|
e3066c3f49 | ||
|
|
3731a1230f | ||
|
|
874d688987 | ||
|
|
0d58a738a1 | ||
|
|
08167d73b2 | ||
|
|
3d13d1390e | ||
|
|
04cbb71ca0 | ||
|
|
e96cc8c8de | ||
|
|
f5cf12409b | ||
|
|
ea20e27a4d | ||
|
|
a036d985b7 | ||
|
|
c85794292a | ||
|
|
955282e587 | ||
|
|
127d646ef1 | ||
|
|
87dead7f65 | ||
|
|
20dae387ee | ||
|
|
b9e93c6293 | ||
|
|
6967b99142 | ||
|
|
4cd5d63e64 |
@@ -41,8 +41,8 @@ MOMENTRY_PYTHON_PATH=/Users/accusys/momentry_core/venv/bin/python
|
||||
MOMENTRY_SCRIPTS_DIR=/Users/accusys/momentry_core/scripts
|
||||
|
||||
# Logging
|
||||
RUST_LOG=debug
|
||||
MOMENTRY_LOG_LEVEL=debug
|
||||
RUST_LOG=info
|
||||
MOMENTRY_LOG_LEVEL=info
|
||||
|
||||
# Media
|
||||
MOMENTRY_MEDIA_BASE_URL=https://wp.momentry.ddns.net
|
||||
@@ -77,5 +77,27 @@ MOMENTRY_LLM_SUMMARY_URL=http://127.0.0.1:8082/v1/chat/completions
|
||||
MOMENTRY_LLM_SUMMARY_MODEL=google_gemma-4-26B-A4B-it-Q5_K_M.gguf
|
||||
MOMENTRY_LLM_SUMMARY_ENABLED=true
|
||||
|
||||
# LLM Chat (A4B on port 8082)
|
||||
MOMENTRY_LLM_CHAT_URL=http://127.0.0.1:8082/v1/chat/completions
|
||||
MOMENTRY_LLM_CHAT_MODEL=google_gemma-4-26B-A4B-it-Q5_K_M.gguf
|
||||
|
||||
# LLM Vision (E4B on port 8083)
|
||||
MOMENTRY_LLM_VISION_URL=http://127.0.0.1:8083/v1/chat/completions
|
||||
MOMENTRY_LLM_VISION_MODEL=gemma-4-E4B-it-Q4_K_M.gguf
|
||||
|
||||
# Embedding (ANE CoreML server)
|
||||
MOMENTRY_EMBED_URL=http://localhost:11436
|
||||
|
||||
# === Binary & Data Paths (for start_momentry.sh) ===
|
||||
MOMENTRY_LOG_DIR=/Users/accusys/momentry/logs
|
||||
MOMENTRY_PG_BIN_DIR=/Users/accusys/pgsql/18.3/bin
|
||||
MOMENTRY_PG_DATA_DIR=/Users/accusys/pgsql/data
|
||||
MOMENTRY_QDRANT_BIN=/Users/accusys/.cargo/bin/qdrant
|
||||
MOMENTRY_QDRANT_STORAGE_DIR=/Users/accusys/momentry/qdrant_storage
|
||||
MOMENTRY_LLAMACPP_BIN=/Users/accusys/llama/bin/llama-server
|
||||
MOMENTRY_LLM_A4B_MODEL_PATH=/Users/accusys/models/google_gemma-4-26B-A4B-it-Q5_K_M.gguf
|
||||
MOMENTRY_LLM_A4B_MMPROJ_PATH=/Users/accusys/models/gemma-4-26B-A4B-it.mmproj-f16.gguf
|
||||
MOMENTRY_LLM_E4B_MODEL_PATH=/Users/accusys/models/gemma-4-E4B-it-Q4_K_M.gguf
|
||||
MOMENTRY_LLM_E4B_MMPROJ_PATH=/Users/accusys/models/mmproj-gemma-4-E4B-it-BF16.gguf
|
||||
MOMENTRY_OLLAMA_BIN=/Users/accusys/bin/ollama
|
||||
MOMENTRY_PLAYGROUND_BIN=target/debug/momentry_playground
|
||||
|
||||
10
.env.example
10
.env.example
@@ -32,6 +32,16 @@ MOMENTRY_LLM_SUMMARY_URL=http://127.0.0.1:8082/v1/chat/completions
|
||||
MOMENTRY_LLM_SUMMARY_MODEL=google_gemma-4-26B-A4B-it-Q5_K_M.gguf
|
||||
MOMENTRY_LLM_SUMMARY_TIMEOUT=120
|
||||
|
||||
# LLM Chat (A4B)
|
||||
MOMENTRY_LLM_CHAT_URL=http://127.0.0.1:8082/v1/chat/completions
|
||||
MOMENTRY_LLM_CHAT_MODEL=google_gemma-4-26B-A4B-it-Q5_K_M.gguf
|
||||
MOMENTRY_LLM_CHAT_TIMEOUT=120
|
||||
|
||||
# LLM Vision (E4B)
|
||||
MOMENTRY_LLM_VISION_URL=http://127.0.0.1:8083/v1/chat/completions
|
||||
MOMENTRY_LLM_VISION_MODEL=gemma-4-E4B-it-Q4_K_M.gguf
|
||||
MOMENTRY_LLM_VISION_TIMEOUT=120
|
||||
|
||||
# === Paths ===
|
||||
MOMENTRY_OUTPUT_DIR=/Users/accusys/momentry/output_dev
|
||||
MOMENTRY_BACKUP_DIR=/Users/accusys/momentry/backup
|
||||
|
||||
31
.gitignore
vendored
31
.gitignore
vendored
@@ -15,6 +15,35 @@ __pycache__/
|
||||
node_modules/
|
||||
*.log
|
||||
/tmp/
|
||||
*.log
|
||||
*.diff
|
||||
*.bundle
|
||||
*.probe.json
|
||||
*.cut.json
|
||||
.qdrant-initialized
|
||||
dump.rdb
|
||||
fix55.js
|
||||
checksums.sha256
|
||||
|
||||
scripts/swift_processors/.build/
|
||||
.opencode/
|
||||
.vscode/
|
||||
backups/
|
||||
logs/
|
||||
output/
|
||||
models/
|
||||
data/
|
||||
storage/
|
||||
thumbnails/
|
||||
services/
|
||||
model_checkpoints/
|
||||
release/delivery/
|
||||
release/system/
|
||||
release/phase*/
|
||||
release/dev_*.sql
|
||||
release/migrate_*.sql
|
||||
release/files/
|
||||
package-lock.json
|
||||
package.json
|
||||
portal/dist/
|
||||
portal/src-tauri/icons/
|
||||
momentry_runtime/logs/
|
||||
|
||||
@@ -14,6 +14,7 @@ Rust-based digital asset management system with video analysis and RAG capabilit
|
||||
- **🔴 DELETE / REMOVE / DROP / CLEAR 任何資料前必須先問使用者「要刪嗎?」獲得明確同意後才能執行**
|
||||
- **🔴 Qdrant collection 刪除、DB truncate、檔案刪除、資料清空 — 一律要先問**
|
||||
- **🔴 不確定是否該刪 → 先問,不要自己決定**
|
||||
- **🔴 改變議題前必須先存檔紀錄**:使用 `todowrite` 工具或建立紀錄文件(如 `docs_v1.0/M4_workspace/YYYY-MM-DD_topic_handoff.md`),確保上下文不丟失
|
||||
|
||||
### 開發範圍界定
|
||||
| 範圍 | 狀態 | 說明 |
|
||||
|
||||
@@ -134,6 +134,14 @@ path = "src/bin/integrated_player.rs"
|
||||
name = "release"
|
||||
path = "src/bin/release.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "vectorize_missing"
|
||||
path = "src/bin/vectorize_missing.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "sync_qdrant_from_pg"
|
||||
path = "src/bin/sync_qdrant_from_pg.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "service"
|
||||
path = "src/bin/service.rs"
|
||||
|
||||
277
IDENTITY_BEST_FACE_API.md
Normal file
277
IDENTITY_BEST_FACE_API.md
Normal file
@@ -0,0 +1,277 @@
|
||||
# Identity Best-Face API
|
||||
|
||||
**狀態:** 規劃中
|
||||
**提出日期:** 2026-06-01
|
||||
**提出者:** WordPress Portal 前端團隊
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景
|
||||
|
||||
WordPress Portal 的 People 頁面需要在 identity detail view 與 grid card 中顯示代表臉部縮圖。目前前端作法:
|
||||
|
||||
1. `GET /identity/{uuid}/traces` → 取得所有 trace 列表(含 `avg_confidence`)
|
||||
2. 對每個 trace 載入第一幀 thumbnail → `GET /file/{uuid}/trace/{tid}/thumbnail`
|
||||
3. 從有 thumbnail 的 trace 中,選 `avg_confidence` 最高者作為代表圖
|
||||
|
||||
### 現有問題
|
||||
|
||||
- **品質不佳**:trace thumbnail 固定取第一幀,不一定是該 trace 內最清晰或正面的臉部畫面
|
||||
- **浪費頻寬**:前端需發送大量並行請求(最多 20 trace × thumbnail),多數 thumbnail 最終不會被使用
|
||||
- **無快取**:每次進入 detail view 都要重複載入所有 thumbnail
|
||||
- **不一致**:同樣 identity 在 grid card 與 detail view 可能顯示不同代表圖
|
||||
|
||||
---
|
||||
|
||||
## 2. 目標
|
||||
|
||||
後端新增一個 endpoint,對指定 identity **跨所有 trace** 選出品質最佳(最清晰)的臉部畫面,並提供可直接使用的縮圖 URL,支援 disk cache。
|
||||
|
||||
---
|
||||
|
||||
## 3. API 規格
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/best-face`
|
||||
|
||||
無 query parameter。
|
||||
|
||||
#### 成功回應 `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"name": "Audrey Hepburn",
|
||||
"source": "fresh",
|
||||
"best": {
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"trace_id": 42,
|
||||
"frame_number": 3120,
|
||||
"timestamp_secs": 124.8,
|
||||
"bbox": {
|
||||
"x": 240,
|
||||
"y": 180,
|
||||
"width": 120,
|
||||
"height": 160
|
||||
},
|
||||
"confidence": 0.97,
|
||||
"quality_score": 18624.0,
|
||||
"blur_score": 2.1,
|
||||
"thumbnail_url": "/api/v1/file/a6fb22eebefaef17e62af874997c5944/trace/42/thumbnail"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 無可用臉部 `200`
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"name": "Audrey Hepburn",
|
||||
"source": "fresh",
|
||||
"best": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 欄位說明
|
||||
|
||||
| 欄位 | 型態 | 說明 |
|
||||
|------|------|------|
|
||||
| `success` | boolean | 請求是否成功 |
|
||||
| `identity_uuid` | string | identity UUID(32字元無連字號) |
|
||||
| `name` | string | identity 名稱 |
|
||||
| `source` | string | `"fresh"`(即時計算)或 `"cache"`(來自 disk cache) |
|
||||
| `best` | object/null | 最佳臉部資訊,無可用臉部時為 `null` |
|
||||
| `best.file_uuid` | string | 該臉部所屬檔案 UUID |
|
||||
| `best.trace_id` | int | 該臉部所屬 trace ID |
|
||||
| `best.frame_number` | int | 代表臉的影格編號 |
|
||||
| `best.timestamp_secs` | float | 代表臉的時間戳(秒) |
|
||||
| `best.bbox` | object | 臉部 bounding box `{x, y, width, height}` |
|
||||
| `best.confidence` | float | 該臉部的 detection confidence |
|
||||
| `best.quality_score` | float | 品質分數 = `(width * height) * confidence` |
|
||||
| `best.blur_score` | float | 模糊度分數(ffmpeg blurdetect),越低越清晰 |
|
||||
| `best.thumbnail_url` | string | 縮圖 URL(相對路徑,可直接用於瀏覽器) |
|
||||
|
||||
---
|
||||
|
||||
## 4. 實作建議
|
||||
|
||||
### 4.1 建議放置位置
|
||||
|
||||
**選項 A(建議):** `src/api/trace_agent_api.rs`
|
||||
|
||||
- 原因:核心邏輯重用 `select_rep_face()`(目前為 `pub(crate)`,位於同一檔案),無需修改既有的 function visibility
|
||||
- 在 `trace_agent_routes()` 中新增路由
|
||||
|
||||
**選項 B:** `src/api/identity_binding.rs`
|
||||
|
||||
- 需將 `select_rep_face` 改為 `pub` 才能跨檔案呼叫
|
||||
- 路由語意上更接近 identity 操作
|
||||
|
||||
### 4.2 演算法
|
||||
|
||||
```
|
||||
1. DISK CACHE CHECK
|
||||
路徑:{OUTPUT_DIR}/identities/{uuid}/best_face.json
|
||||
讀取 identity.json 的 updated_at,與 cache 中記錄的版本比較
|
||||
若 cache 未過期 → 直接回傳(source: "cache")
|
||||
若無 cache 或已過期 → 繼續計算
|
||||
|
||||
2. QUERY IDENTITY
|
||||
SELECT id, name FROM identities
|
||||
WHERE REPLACE(uuid::text, '-', '') = $1
|
||||
|
||||
3. QUERY TOP N TRACES
|
||||
SELECT fd.file_uuid, fd.trace_id,
|
||||
AVG(fd.confidence)::float8 AS avg_conf
|
||||
FROM {schema}.face_detections fd
|
||||
WHERE fd.identity_id = $1
|
||||
AND fd.confidence > 0.7
|
||||
AND (fd.metadata->>'qc_ok' IS NULL
|
||||
OR (fd.metadata->>'qc_ok')::boolean = true)
|
||||
GROUP BY fd.file_uuid, fd.trace_id
|
||||
ORDER BY avg_conf DESC
|
||||
LIMIT 5
|
||||
|
||||
4. FOR EACH TRACE (並行)
|
||||
select_rep_face(pool, file_uuid, trace_id, err_fn)
|
||||
→ 回傳該 trace 內 blur_score 最低(最清晰)的臉
|
||||
失敗則 skip(log warning)
|
||||
|
||||
5. SELECT BEST AMONG RESULTS
|
||||
主排序:blur_score ASC(越低越清晰)
|
||||
次排序:quality_score DESC(blur_score 差距 < 0.5 時)
|
||||
全部失敗 → best = null
|
||||
|
||||
6. WRITE DISK CACHE
|
||||
路徑:{OUTPUT_DIR}/identities/{uuid}/best_face.json
|
||||
內容:best 欄位 + 計算時間 + identity updated_at
|
||||
|
||||
7. RESPONSE
|
||||
```
|
||||
|
||||
### 4.3 效能參數
|
||||
|
||||
| 參數 | 值 | 說明 |
|
||||
|------|----|------|
|
||||
| TOP N | 5 | 只對 confidence 最高的 5 個 trace 做 blurdetect |
|
||||
| confidence 門檻 | > 0.7 | 同既有的 `select_rep_face` 邏輯 |
|
||||
| QC 過濾 | qc_ok = true/null | 同既有邏輯 |
|
||||
| ffmpeg timeout | inherit from Command | 每個 trace 約 1-3s |
|
||||
| cache TTL | 直到下一次 bind/unbind/merge | 事件驅動失效 |
|
||||
|
||||
### 4.4 快取策略
|
||||
|
||||
**寫入時機:** `get_identity_best_face` 計算完成後
|
||||
|
||||
**失效時機(刪除 `best_face.json`):**
|
||||
|
||||
| 觸發 operation | 所在檔案 | 備註 |
|
||||
|---------------|---------|------|
|
||||
| `bind_trace` (POST) | `identity_binding.rs` | 新增 face 關聯 |
|
||||
| `unbind` (POST) | `identity_binding.rs` | 移除 face 關聯 |
|
||||
| `mergeinto` (POST) | `identity_binding.rs` | source + target 雙雙清除 |
|
||||
| `profile-image` (POST) | `identity_api.rs` | 使用者上傳新大頭照 |
|
||||
|
||||
**Cache 驗證機制:** 儲存計算時的 `identity.updated_at`,每次請求時比對:
|
||||
- 若 identity 的 `updated_at` 未變 → cache 有效
|
||||
- 若已變 → 重新計算
|
||||
|
||||
### 4.5 建議的新增/修改檔案
|
||||
|
||||
| 檔案 | 動作 | 說明 |
|
||||
|------|------|------|
|
||||
| `src/api/trace_agent_api.rs` | **新增** handler + struct + route | ~+130 行 |
|
||||
| `src/api/identity_binding.rs` | **修改** 3 處 + cache invalidation helper | ~+25 行 |
|
||||
| `src/api/identity_api.rs` | **修改** 1 處(profile-image POST) | ~+5 行 |
|
||||
|
||||
### 4.6 需要的新 struct
|
||||
|
||||
**`src/api/trace_agent_api.rs`**(或獨立檔案 `src/core/identity_best_face.rs`):
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BestFaceResponse {
|
||||
pub success: bool,
|
||||
pub identity_uuid: String,
|
||||
pub name: String,
|
||||
pub source: String,
|
||||
pub best: Option<BestFaceResult>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct BestFaceResult {
|
||||
pub file_uuid: String,
|
||||
pub trace_id: i32,
|
||||
pub frame_number: i64,
|
||||
pub timestamp_secs: f64,
|
||||
pub bbox: RepFaceBbox,
|
||||
pub confidence: f64,
|
||||
pub quality_score: f64,
|
||||
pub blur_score: f64,
|
||||
pub thumbnail_url: String,
|
||||
}
|
||||
```
|
||||
|
||||
### 4.7 Cache Invalidation Helper Function
|
||||
|
||||
```rust
|
||||
async fn invalidate_best_face_cache(output_dir: &str, uuid_clean: &str) {
|
||||
let path = format!("{}/identities/{}/best_face.json", output_dir, uuid_clean);
|
||||
let _ = tokio::fs::remove_file(path).await;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 前端整合參考(供後端團隊理解使用情境)
|
||||
|
||||
WP snippet 72 (`ms-people.js`) 的 `loadPersonDetail` 中,優先使用新 endpoint:
|
||||
|
||||
```js
|
||||
async function loadPersonDetail(person) {
|
||||
if (person.thumb && person._hasProfileImage) return;
|
||||
|
||||
try {
|
||||
const res = await apiFetch('/identity/' + person.id + '/best-face');
|
||||
if (res?.success && res?.best) {
|
||||
const b = res.best;
|
||||
person.thumb = `${API_BASE}/file/${b.file_uuid}/trace/${b.trace_id}/thumbnail?api_key=${API_KEY}`;
|
||||
person._hasProfileImage = true;
|
||||
updateDetailAvatar(person);
|
||||
return;
|
||||
}
|
||||
} catch (e) { /* fallback to legacy */ }
|
||||
|
||||
// 原邏輯:traces → thumbnails → confidence sort
|
||||
}
|
||||
```
|
||||
|
||||
同樣可用於 grid card 的代表圖載入(`loadGridThumbnails`):
|
||||
|
||||
```js
|
||||
// 一次性載入所有 pending identity 的 best-face
|
||||
const results = await Promise.allSettled(
|
||||
persons.map(p => apiFetch('/identity/' + p.id + '/best-face'))
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 驗收標準
|
||||
|
||||
1. `GET /api/v1/identity/{uuid}/best-face` → `200` + valid JSON
|
||||
2. 有 trace 的 identity → `best` 不為 null,且 `blur_score` 為該 identity 所有 trace 中最低
|
||||
3. 無 trace 的 identity → `best: null`
|
||||
4. 短時間內重複請求同一 identity → `source: "cache"`,回應時間 < 10ms
|
||||
5. 綁定新 trace 後再次請求 → `source: "fresh"`(cache 已正確失效)
|
||||
6. `thumbnail_url` 可直接用於 `<img>` 顯示
|
||||
|
||||
---
|
||||
|
||||
## 7. 風險與注意事項
|
||||
|
||||
- **首次請求延遲**:對有大量 trace 的 identity(如主角),首次請求可能需 5-15 秒。建議前端顯示 loading state
|
||||
- **ffmpeg 資源**:同時多個請求可能導致高 CPU 使用。可考慮加入 per-identity lock 避免重複計算
|
||||
- **邊界案例**:trace 內的 faces 全部 confidence ≤ 0.7 或 qc_ok=false,則該 trace 被跳過,可能導致 `best: null`
|
||||
227
config/README.md
227
config/README.md
@@ -1,105 +1,178 @@
|
||||
# Momentry Core 配置管理
|
||||
# Momentry Core Config Management
|
||||
|
||||
## 目錄結構
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
momentry_core_0.1/
|
||||
├── .env.example # 配置模板(已納入版本控制)
|
||||
├── .env # 本地配置(已從版本控制排除)
|
||||
├── .env.local # 本地覆蓋配置(已從版本控制排除)
|
||||
├── .env.example # Template (version controlled)
|
||||
├── .env # Local config (gitignored)
|
||||
├── .env.development # Playground dev overrides (gitignored)
|
||||
├── .env.local # Local overrides (gitignored)
|
||||
├── config/
|
||||
│ └── README.md # 本文件
|
||||
└── src/core/config.rs # 配置代碼
|
||||
│ ├── README.md # This file
|
||||
│ └── port_registry.tsv # Central port registry
|
||||
└── src/core/config.rs # Config code with lazy_static env reading
|
||||
```
|
||||
|
||||
## 配置加載順序
|
||||
## Load Order
|
||||
|
||||
1. `.env` - 默認本地配置
|
||||
2. `.env.local` - 本地覆蓋(最高優先級)
|
||||
For `momentry_playground` (development):
|
||||
1. `.env` — shared defaults
|
||||
2. `.env.development` — dev-specific overrides (loaded by playground binary)
|
||||
|
||||
## 環境變數列表
|
||||
For `momentry` (production):
|
||||
1. `.env` — production config
|
||||
|
||||
### 數據庫配置
|
||||
In Rust: `config.rs` reads env vars with lazy_static, falling back to hardcoded defaults.
|
||||
|
||||
| 變數 | 說明 | 默認值 |
|
||||
|------|------|--------|
|
||||
| `DATABASE_URL` | PostgreSQL 連接字串 | `postgres://accusys@localhost:5432/momentry` |
|
||||
## Environment Variables
|
||||
|
||||
### Redis 配置
|
||||
### Server
|
||||
|
||||
| 變數 | 說明 | 默認值 |
|
||||
|------|------|--------|
|
||||
| `REDIS_URL` | Redis 連接字串 | `redis://:accusys@localhost:6379` |
|
||||
| `REDIS_PASSWORD` | Redis 密碼 | `accusys` |
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `MOMENTRY_SERVER_PORT` | Server port (3002=prod, 3003=dev) | `3002` |
|
||||
| `MOMENTRY_REDIS_PREFIX` | Redis key prefix | `momentry:` (prod), `momentry_dev:` (dev) |
|
||||
|
||||
### 存儲路徑
|
||||
### Database
|
||||
|
||||
| 變數 | 說明 | 默認值 |
|
||||
|------|------|--------|
|
||||
| `MOMENTRY_OUTPUT_DIR` | 輸出目錄 | `/Users/accusys/momentry/output` |
|
||||
| `MOMENTRY_BACKUP_DIR` | 備份目錄 | `/Users/accusys/momentry/backup/momentry` |
|
||||
| `MOMENTRY_SCRIPTS_DIR` | 腳本目錄 | `/Users/accusys/momentry_core_0.1/scripts` |
|
||||
| `MOMENTRY_PYTHON_PATH` | Python 路徑 | `/opt/homebrew/bin/python3.11` |
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `DATABASE_URL` | PostgreSQL connection string | `postgres://accusys@localhost:5432/momentry` |
|
||||
| `DATABASE_SCHEMA` | Schema for dev isolation | `dev` |
|
||||
| `MONGODB_URL` | MongoDB connection string | `mongodb://localhost:27017` |
|
||||
| `MONGODB_DATABASE` | MongoDB database name | `momentry` (prod), `momentry_dev` (dev) |
|
||||
| `MONGODB_CACHE_ENABLED` | MongoDB cache toggle | `true` |
|
||||
| `MONGODB_CACHE_TTL_VIDEOS` | Cache TTL for videos | `300` |
|
||||
| `MONGODB_CACHE_TTL_SEARCH` | Cache TTL for search | `300` |
|
||||
| `MONGODB_CACHE_TTL_HYBRID_SEARCH` | Cache TTL for hybrid search | `600` |
|
||||
| `MONGODB_CACHE_TTL_VIDEO_META` | Cache TTL for video metadata | `3600` |
|
||||
|
||||
### 處理器超時(秒)
|
||||
### Redis
|
||||
|
||||
| 變數 | 說明 | 默認值 |
|
||||
|------|------|--------|
|
||||
| `MOMENTRY_ASR_TIMEOUT` | ASR 處理超時 | `3600` |
|
||||
| `MOMENTRY_CUT_TIMEOUT` | CUT 處理超時 | `3600` |
|
||||
| `MOMENTRY_DEFAULT_TIMEOUT` | 默認超時 | `7200` |
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `REDIS_URL` | Redis connection string | `redis://:accusys@localhost:6379` |
|
||||
| `REDIS_PASSWORD` | Redis password | `accusys` |
|
||||
| `REDIS_CACHE_TTL_HEALTH` | Health check cache TTL | `30` |
|
||||
| `REDIS_CACHE_TTL_VIDEO_META` | Video metadata cache TTL | `3600` |
|
||||
|
||||
### 日誌
|
||||
### Qdrant
|
||||
|
||||
| 變數 | 說明 | 默認值 |
|
||||
|------|------|--------|
|
||||
| `RUST_LOG` | 日誌級別 | `info` |
|
||||
| `MOMENTRY_LOG_LEVEL` | 日誌級別(備選) | `info` |
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `QDRANT_URL` | Qdrant server URL | `http://localhost:6333` |
|
||||
| `QDRANT_API_KEY` | Qdrant API key | `Test3200Test3200Test3200` |
|
||||
| `QDRANT_COLLECTION` | Collection name | `momentry_rule1` (prod), `momentry_dev_rule1_v2` (dev) |
|
||||
|
||||
## 使用方式
|
||||
### LLM
|
||||
|
||||
### 1. 首次設置
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `MOMENTRY_LLM_CHAT_URL` | Chat/function-calling endpoint | `http://127.0.0.1:8082/v1/chat/completions` |
|
||||
| `MOMENTRY_LLM_CHAT_MODEL` | Chat model name | `google_gemma-4-26B-A4B-it-Q5_K_M.gguf` |
|
||||
| `MOMENTRY_LLM_VISION_URL` | Vision LLM endpoint (E4B) | falls back to CHAT_URL |
|
||||
| `MOMENTRY_LLM_VISION_MODEL` | Vision model name (E4B) | falls back to CHAT_MODEL |
|
||||
| `MOMENTRY_LLM_SUMMARY_URL` | Summary LLM endpoint (5W1H) | falls back to CHAT_URL |
|
||||
| `MOMENTRY_LLM_SUMMARY_MODEL` | Summary model name | falls back to CHAT_MODEL |
|
||||
| `MOMENTRY_LLM_SUMMARY_ENABLED` | Toggle 5W1H summary generation | `true` |
|
||||
| `MOMENTRY_LLM_SUMMARY_TIMEOUT` | 5W1H timeout in seconds | `120` |
|
||||
| `MOMENTRY_LLM_CHAT_TIMEOUT` | Chat LLM timeout in seconds | `120` |
|
||||
| `MOMENTRY_LLM_VISION_TIMEOUT` | Vision LLM timeout in seconds | `120` |
|
||||
|
||||
### Embedding
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `MOMENTRY_EMBED_URL` | Embedding server URL | `http://localhost:11436` |
|
||||
|
||||
### TMDb Integration
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `TMDB_API_KEY` | TMDb API key (required for probe) | (none) |
|
||||
| `MOMENTRY_TMDB_PROBE_ENABLED` | Enable TMDb probe during register | `false` |
|
||||
|
||||
### Paths
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `MOMENTRY_OUTPUT_DIR` | Output directory for processing | `/Users/accusys/momentry/output` |
|
||||
| `MOMENTRY_BACKUP_DIR` | Backup directory | `/Users/accusys/momentry/backup/momentry` |
|
||||
| `MOMENTRY_SCRIPTS_DIR` | Python scripts directory | `/Users/accusys/momentry_core_0.1/scripts` |
|
||||
| `MOMENTRY_PYTHON_PATH` | Python interpreter path | `/opt/homebrew/bin/python3.11` |
|
||||
| `MOMENTRY_MEDIA_BASE_URL` | Base URL for media serving | (none) |
|
||||
|
||||
### Processor Timeouts
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `MOMENTRY_ASR_TIMEOUT` | ASR timeout in seconds | `3600` |
|
||||
| `MOMENTRY_CUT_TIMEOUT` | CUT timeout in seconds | `3600` |
|
||||
| `MOMENTRY_DEFAULT_TIMEOUT` | Default timeout in seconds | `7200` |
|
||||
|
||||
### Logging
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `RUST_LOG` | Rust log level (tracing) | `info` |
|
||||
| `MOMENTRY_LOG_LEVEL` | Fallback log level | `info` |
|
||||
|
||||
### Worker
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `MOMENTRY_WORKER_ENABLED` | Enable background worker | `true` |
|
||||
| `MOMENTRY_MAX_CONCURRENT` | Max concurrent jobs | `6` |
|
||||
| `MOMENTRY_POLL_INTERVAL` | Poll interval in seconds | `10` |
|
||||
| `MOMENTRY_WORKER_BATCH_SIZE` | Batch size | `5` |
|
||||
|
||||
### Synonym Expansion
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `MOMENTRY_SYNONYM_FILES` | Comma-separated paths to synonym JSON files | (none) |
|
||||
| `MOMENTRY_SYNONYM_FILE` | Single synonym file (deprecated) | (none) |
|
||||
|
||||
### Encryption
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `AUDIT_ENCRYPTION_KEY` | 32-byte hex encryption key (64 hex chars) | (none) |
|
||||
|
||||
## Port Registry
|
||||
|
||||
See `config/port_registry.tsv` for the authoritative list of all ports and their owners.
|
||||
|
||||
| Port | Service | Owner | Config Key |
|
||||
|------|---------|-------|------------|
|
||||
| 5432 | PostgreSQL | postgres | `DATABASE_URL` |
|
||||
| 6379 | Redis | redis-server | `REDIS_URL` |
|
||||
| 6333 | Qdrant | qdrant | `QDRANT_URL` |
|
||||
| 8082 | LLM Chat (A4B) | llama-server | `MOMENTRY_LLM_CHAT_URL` |
|
||||
| 8083 | LLM Vision (E4B) | llama-server | `MOMENTRY_LLM_VISION_URL` |
|
||||
| 11434 | Ollama | ollama | `MOMENTRY_OLLAMA_URL` |
|
||||
| 11436 | Embedding | embeddinggemma_server.py | `MOMENTRY_EMBED_URL` |
|
||||
| 27017 | MongoDB | mongod | `MONGODB_URL` |
|
||||
| 3002 | Production API | momentry | `MOMENTRY_SERVER_PORT` |
|
||||
| 3003 | Playground API | momentry_playground | `MOMENTRY_SERVER_PORT` |
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# 複製模板
|
||||
# 1. Copy template
|
||||
cp .env.example .env
|
||||
|
||||
# 編輯配置
|
||||
nano .env
|
||||
# 2. Edit .env for production or use .env.development for playground
|
||||
# 3. Start all services
|
||||
./scripts/start_momentry.sh
|
||||
```
|
||||
|
||||
### 2. 本地覆蓋
|
||||
## Version Control
|
||||
|
||||
創建 `.env.local` 設置僅本地適用的配置:
|
||||
|
||||
```bash
|
||||
# .env.local 示例
|
||||
DATABASE_URL=postgres://local:password@localhost:5432/momentry_dev
|
||||
MOMENTRY_LOG_LEVEL=debug
|
||||
```
|
||||
|
||||
### 3. 運行應用
|
||||
|
||||
```bash
|
||||
# 加載配置並運行
|
||||
source .env && cargo run
|
||||
|
||||
# 或使用 direnv
|
||||
direnv allow
|
||||
```
|
||||
|
||||
## 版本控制策略
|
||||
|
||||
| 文件 | 版本控制 | 說明 |
|
||||
|------|---------|------|
|
||||
| `.env.example` | ✅ 追蹤 | 模板,包含所有選項 |
|
||||
| `.env` | ❌ 忽略 | 本地敏感配置 |
|
||||
| `.env.local` | ❌ 忽略 | 本地覆蓋配置 |
|
||||
|
||||
## 部署檢查清單
|
||||
|
||||
- [ ] 複製 `.env.example` 到 `.env`
|
||||
- [ ] 設置數據庫連接
|
||||
- [ ] 設置 Redis 密碼
|
||||
- [ ] 配置目錄路徑
|
||||
- [ ] 確認日誌級別
|
||||
| File | Tracked | Purpose |
|
||||
|------|---------|---------|
|
||||
| `.env.example` | ✅ Yes | Template with all options documented |
|
||||
| `.env` | ❌ No | Local sensitive config |
|
||||
| `.env.development` | ❌ No | Dev-specific overrides |
|
||||
| `.env.local` | ❌ No | Local overrides (highest priority) |
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
6379 redis redis-server REDIS_URL redis://...:6379 start_momentry.sh
|
||||
6333 qdrant qdrant QDRANT_URL http://...:6333 start_momentry.sh
|
||||
8081 wordpress Caddy - - Caddyfile
|
||||
8082 llm llama-server MOMENTRY_LLM_CHAT_URL http://...:8082 start_momentry.sh
|
||||
8082 llm-chat llama-server MOMENTRY_LLM_CHAT_URL http://...:8082 start_momentry.sh
|
||||
8083 llm-vision llama-server MOMENTRY_LLM_VISION_URL http://...:8083 start_momentry.sh
|
||||
9000 php-fpm php-fpm - 9000 brew services
|
||||
11434 ollama ollama MOMENTRY_OLLAMA_URL http://...:11434 start_momentry.sh
|
||||
11436 embedding embeddinggemma MOMENTRY_EMBED_URL http://...:11436 start_momentry.sh
|
||||
27017 mongodb mongod MONGODB_URL mongodb://...:27017 start_momentry.sh
|
||||
|
||||
|
761
deliverable_v1.1.0/AGENTS.md
Normal file
761
deliverable_v1.1.0/AGENTS.md
Normal file
@@ -0,0 +1,761 @@
|
||||
# AGENTS.md - Momentry Core
|
||||
|
||||
Rust-based digital asset management system with video analysis and RAG capabilities.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ CRITICAL: 開發隔離原則
|
||||
|
||||
### 絕對禁止事項
|
||||
- **絕對不可修改 `/Users/accusys/wordpress/` 目錄下的任何檔案**
|
||||
- **絕對不可修改 n8n 工作流或設定**
|
||||
- **絕對不可修改 WordPress 或 n8n 的資料庫 table**
|
||||
- **除非是 release 作業,絕對不可動 port 3002 (production)**
|
||||
- **🔴 DELETE / REMOVE / DROP / CLEAR 任何資料前必須先問使用者「要刪嗎?」獲得明確同意後才能執行**
|
||||
- **🔴 Qdrant collection 刪除、DB truncate、檔案刪除、資料清空 — 一律要先問**
|
||||
- **🔴 不確定是否該刪 → 先問,不要自己決定**
|
||||
|
||||
### 開發範圍界定
|
||||
| 範圍 | 狀態 | 說明 |
|
||||
|------|------|------|
|
||||
| `momentry_core_0.1/` | ✅ **可開發** | Momentry Core 主要開發目錄 |
|
||||
| `momentry_core_0.1/portal/` | ✅ **可開發** | Tauri Portal 前端 |
|
||||
| `momentry_core_0.1/src/` | ✅ **可開發** | Rust 後端程式碼 |
|
||||
| `/Users/accusys/wordpress/` | ❌ **禁止修改** | WordPress/Marcom 團隊負責 |
|
||||
| n8n 工作流 | ❌ **禁止修改** | 自動化流程,與 dev 無關 |
|
||||
| WordPress/n8n 資料庫 table | ❌ **禁止修改** | Marcom 團隊管理,與 dev 無關 |
|
||||
|
||||
### 開發環境
|
||||
| 服務 | Port | 用途 | 命令 |
|
||||
|------|------|------|------|
|
||||
| Playground | 3003 | **唯一開發環境** | `cargo run --bin momentry_playground -- server` |
|
||||
| Production | 3002 | ❌ 禁止修改 | `cargo run -- server` (僅 release 時) |
|
||||
| Portal (Tauri) | 1420 | 前端開發 | `npm run tauri dev` |
|
||||
|
||||
## ⚠️ 交叉污染防制 (Cross-Contamination Prevention)
|
||||
|
||||
**每個執行前必須評估是否會汙染其他獨立作業。**
|
||||
|
||||
### Scope Isolation Matrix
|
||||
|
||||
| 執行內容 | 允許的 Scope | 禁止影響 | 檢查事項 |
|
||||
|----------|-------------|----------|----------|
|
||||
| M4 delivery binary | `target/release/momentry` | Playground (3003), Production (3002) | 確認舊 process 未被誤殺 |
|
||||
| Playground server | `localhost:3003`, `dev.*` schema | Production (3002), `public.*` schema | `DATABASE_SCHEMA=dev` |
|
||||
| Production deploy | `localhost:3002`, `public.*` schema | Playground (3003), `dev.*` schema | 先停 production,不影響 playground |
|
||||
| Git commit | 只包含意圖修改的檔案 | 無關的 untracked files | `git status` 確認 stage 內容正確 |
|
||||
| CI / packaged tests | 測試環境 | 正式資料 | 測試用 DB 不能連到 production |
|
||||
| Doc changes | 指定文件 | 其他文件、程式碼 | `git diff --stat` 檢查 scope |
|
||||
| SQL migration | 目標 schema | 其他 schema、無關 table | `WHERE` clause 要精準 |
|
||||
| `sed` / `grep` / mass edit | 目標檔案集 | 非目標檔案 | 先用 `grep -c` 確認只有目標檔案匹配 |
|
||||
|
||||
### Recent Violations / Near-Misses
|
||||
|
||||
| 事件 | 問題 | 防止方式 |
|
||||
|------|------|----------|
|
||||
| `sed` API doc 編號 | `sed -i '' 's/.../.../g'` 改到所有行 | 先 `grep -c` 確認匹配,`git diff` 再提交 |
|
||||
| 亂加 `/api/v1/register` route | 不必要的 API 別名,汙染路由表 | 角色切換:路由設計不該由實作方決定 |
|
||||
| `API_WORKSPACE/` vs `GUIDES/` vs `REFERENCE/` vs `DESIGN/` vs `OPERATIONS/` vs `INTEGRATIONS/` | 文件放到錯誤分類 | API 文件改在 API_WORKSPACE/modules/ 編輯,`make deploy` 生成到 GUIDES/ |
|
||||
| Build release binary in plan mode | 浪費時間,無意義 | 嚴格遵守 plan/build mode 規定 |
|
||||
|
||||
### ⛔ 嚴格測試隔離規則 (Strict Test Isolation)
|
||||
- **所有測試 (Test) 必須在 Dev (3003) 進行**。
|
||||
- **絕對禁止 (ABSOLUTELY FORBIDDEN)** 在任何測試指令、Demo 流程或 API 檢查中使用 `localhost:3002`。
|
||||
- 即使是「測試 Unregister」或「檢查版本」,若未明確標示為 "Production Deployment",一律視為違規。
|
||||
- **預設行為**: 所有 curl, CLI, 或程式碼測試指令,預設 URL 必須為 `http://localhost:3003`。
|
||||
|
||||
### 違反後果
|
||||
- 修改 WordPress/n8n 可能影響 marcom 團隊工作與生產環境
|
||||
- 修改 WordPress/n8n 資料庫 table 可能破壞自動化流程與資料完整性
|
||||
- 修改 port 3002 可能中斷正在使用的服務 (這是非常嚴重的錯誤)
|
||||
- 所有 dev 測試必須在 playground (3003) 進行
|
||||
|
||||
---
|
||||
|
||||
## AI Coding Principles (Karpathy-Inspired)
|
||||
|
||||
Behavioral guidelines to reduce common LLM coding mistakes.
|
||||
Source: [andrej-karpathy-skills](https://github.com/forrestchang/andrej-karpathy-skills) (94K stars)
|
||||
|
||||
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
|
||||
|
||||
### 1. Think Before Coding
|
||||
|
||||
**Don't assume. Don't hide confusion. Surface tradeoffs.**
|
||||
|
||||
- State your assumptions explicitly. If uncertain, ask.
|
||||
- If multiple interpretations exist, present them - don't pick silently.
|
||||
- If a simpler approach exists, say so. Push back when warranted.
|
||||
- If something is unclear, stop. Name what's confusing. Ask.
|
||||
|
||||
### 2. Simplicity First
|
||||
|
||||
**Minimum code that solves the problem. Nothing speculative.**
|
||||
|
||||
- No features beyond what was asked.
|
||||
- No abstractions for single-use code.
|
||||
- No "flexibility" or "configurability" that wasn't requested.
|
||||
- No error handling for impossible scenarios.
|
||||
- If you write 200 lines and it could be 50, rewrite it.
|
||||
|
||||
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
|
||||
|
||||
### 3. Surgical Changes
|
||||
|
||||
**Touch only what you must. Clean up only your own mess.**
|
||||
|
||||
When editing existing code:
|
||||
- Don't "improve" adjacent code, comments, or formatting.
|
||||
- Don't refactor things that aren't broken.
|
||||
- Match existing style, even if you'd do it differently.
|
||||
- If you notice unrelated dead code, mention it - don't delete it.
|
||||
|
||||
When your changes create orphans:
|
||||
- Remove imports/variables/functions that YOUR changes made unused.
|
||||
- Don't remove pre-existing dead code unless asked.
|
||||
|
||||
The test: Every changed line should trace directly to the user's request.
|
||||
|
||||
### 4. Goal-Driven Execution
|
||||
|
||||
**Define success criteria. Loop until verified.**
|
||||
|
||||
Transform tasks into verifiable goals:
|
||||
- "Add validation" -> "Write tests for invalid inputs, then make them pass"
|
||||
- "Fix the bug" -> "Write a test that reproduces it, then make it pass"
|
||||
- "Refactor X" -> "Ensure tests pass before and after"
|
||||
|
||||
For multi-step tasks, state a brief plan:
|
||||
```
|
||||
1. [Step] -> verify: [check]
|
||||
2. [Step] -> verify: [check]
|
||||
3. [Step] -> verify: [check]
|
||||
```
|
||||
|
||||
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
|
||||
|
||||
---
|
||||
|
||||
These guidelines are working if: fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
|
||||
|
||||
---
|
||||
|
||||
## Terminology (V4.0)
|
||||
|
||||
| Term | Scope | Description | Example |
|
||||
|------|-------|-------------|---------|
|
||||
| **file_uuid** | Video file | Video file identifier (renamed from `video_uuid`) | `384b0ff44aaaa1f1` |
|
||||
| **identity_uuid** | Global identity | Global person identity (cross-file) | `a9a90105-6d6b-46ff-92da-0c3c1a57dff4` |
|
||||
| **face_id** | Single detection | Single face detection (frame-level) | `face_100` |
|
||||
| **trace_id** | Face tracking | Face tracking ID (Face Tracker output) | `2` |
|
||||
| **chunk_id** | Sentence chunk | Sentence chunk (from pre_chunks via rules) | `chunk_1` |
|
||||
| **speaker_id** | Speaker segment | Speaker ID (from ASRX) | `SPEAKER_0` |
|
||||
| **person_id** | ❌ **Deprecated** | Video-local person ID (removed in V4.0) | - |
|
||||
|
||||
### Architecture (V4.0)
|
||||
|
||||
```
|
||||
Face → Identity (Two-layer, direct binding)
|
||||
↓
|
||||
person_identities table: REMOVED
|
||||
file_identities table: ADDED (N:N relationship)
|
||||
```
|
||||
|
||||
### Key Changes (V3.x → V4.0)
|
||||
|
||||
| Change | V3.x | V4.0 |
|
||||
|--------|------|------|
|
||||
| **video_uuid** | Used everywhere | **file_uuid** |
|
||||
| **person_identities** | Required (303 records) | **Removed** |
|
||||
| **person_id APIs** | 28 endpoints | **Removed** (except register/bind) |
|
||||
| **Face binding** | Person → Identity | **Face → Identity** (direct) |
|
||||
| **Chunk binding** | Manual | **Auto** (time alignment) |
|
||||
|
||||
---
|
||||
|
||||
## Build & Run Commands
|
||||
|
||||
```bash
|
||||
# Build project (use debug builds for development/testing)
|
||||
cargo build
|
||||
cargo build --bin momentry
|
||||
cargo build --bin momentry_playground
|
||||
|
||||
# Build all binaries
|
||||
cargo build --bins
|
||||
|
||||
# Run CLI
|
||||
cargo run -- --help
|
||||
cargo run -- register /path/to/video.mp4
|
||||
cargo run -- server --host 0.0.0.0 --port 3002
|
||||
|
||||
# Run playground (development binary)
|
||||
cargo run --bin momentry_playground -- server
|
||||
cargo run --bin momentry_playground -- --help
|
||||
```
|
||||
|
||||
### ⚠️ CRITICAL: `cargo build --release` PROHIBITION
|
||||
- **NEVER run `cargo build --release` unless the user explicitly says "release the binary" or "正式 release"**
|
||||
- `cargo build --release` is SLOW and only needed when producing a production binary for deployment
|
||||
- For all development, testing, debugging, and linting: use `cargo build` or `cargo check`
|
||||
- If uncertain, ALWAYS ask the user first
|
||||
|
||||
## Binaries
|
||||
|
||||
| Binary | Purpose | Port | Redis Prefix | Environment |
|
||||
|--------|---------|------|--------------|-------------|
|
||||
| `momentry` | Production | 3002 | `momentry:` | `.env` |
|
||||
| `momentry_playground` | Development | 3003 | `momentry_dev:` | `.env.development` |
|
||||
| `momentry_player` | Video player | - | - | - |
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run single test by name
|
||||
cargo test test_name
|
||||
|
||||
# Run with output
|
||||
cargo test -- --nocapture
|
||||
|
||||
# Doc tests
|
||||
cargo test --doc
|
||||
```
|
||||
|
||||
## Linting & Formatting
|
||||
|
||||
```bash
|
||||
# Format code (edition=2021, max_width=100, tab_spaces=4)
|
||||
cargo fmt
|
||||
cargo fmt -- --check
|
||||
|
||||
# Lint
|
||||
cargo clippy
|
||||
cargo clippy --all-features
|
||||
|
||||
# Check for errors
|
||||
cargo check
|
||||
cargo check --all-features
|
||||
```
|
||||
|
||||
## Code Style
|
||||
|
||||
### General
|
||||
- Use Rust 2021 edition
|
||||
- Use tracing for logging (not println!)
|
||||
- Keep lines under 100 characters
|
||||
|
||||
### Imports (order: std → external → local)
|
||||
```rust
|
||||
use std::path::Path;
|
||||
use anyhow::{Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::core::chunk::Chunk;
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
- Use `anyhow::Result<T>` for application code
|
||||
- Use `thiserror` for library code
|
||||
- Use `.context()` for error context
|
||||
- Use `anyhow::bail!()` for early returns
|
||||
|
||||
```rust
|
||||
fn example() -> Result<SomeType> {
|
||||
let output = Command::new("ffprobe")
|
||||
.args([...])
|
||||
.output()
|
||||
.context("Failed to run ffprobe")?;
|
||||
|
||||
if !output.status.success() {
|
||||
anyhow::bail!("Command failed");
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
```
|
||||
|
||||
### Naming
|
||||
- Types/Enums: PascalCase (`VideoRecord`, `ChunkType`)
|
||||
- Functions/Variables: snake_case (`get_video_by_uuid`)
|
||||
- Traits: PascalCase with -er suffix (`Database`, `ChunkStore`)
|
||||
- Files: snake_case (`postgres_db.rs`)
|
||||
|
||||
### Types
|
||||
- Use `serde::{Deserialize, Serialize}` for serializable types
|
||||
- Use `#[serde(rename_all = "snake_case")]` for enum variants
|
||||
- Use explicit numeric types (i64, u32, f64)
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct VideoRecord {
|
||||
pub id: i64,
|
||||
pub uuid: String,
|
||||
pub duration: f64,
|
||||
pub width: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ChunkType {
|
||||
TimeBased,
|
||||
Sentence,
|
||||
Cut,
|
||||
}
|
||||
```
|
||||
|
||||
### Async Programming
|
||||
- Use `tokio` runtime with full features
|
||||
- Use `#[async_trait]` for async trait methods
|
||||
|
||||
```rust
|
||||
#[async_trait]
|
||||
pub trait Database: Send + Sync {
|
||||
async fn init() -> Result<Self>
|
||||
where Self: Sized;
|
||||
}
|
||||
```
|
||||
|
||||
## Code Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.rs # CLI entry point
|
||||
├── lib.rs # Library exports
|
||||
├── core/
|
||||
│ ├── api_key/ # API key management (anomaly, blacklist, encryption, etc.)
|
||||
│ ├── chunk/ # Chunking logic
|
||||
│ ├── config.rs # Centralized configuration (env vars)
|
||||
│ ├── db/ # Database (PostgreSQL, MongoDB, Redis, Qdrant)
|
||||
│ ├── embedding/ # Vector embeddings
|
||||
│ ├── overlay/ # Video overlay
|
||||
│ ├── probe/ # ffprobe integration
|
||||
│ ├── processor/ # ASR, OCR, YOLO, Face, Pose, CUT, ASRX
|
||||
│ │ └── executor.rs # Unified Python script executor
|
||||
│ ├── storage/ # File management
|
||||
│ └── thumbnail/ # Thumbnail extraction
|
||||
├── api/ # HTTP API (axum)
|
||||
├── player/ # Video player
|
||||
├── ui/ # TUI components
|
||||
└── watcher/ # File system watcher
|
||||
```
|
||||
|
||||
## Key Dependencies
|
||||
|
||||
- **Error handling**: `anyhow`, `thiserror`
|
||||
- **Async**: `tokio` (full features), `async-trait`
|
||||
- **CLI**: `clap` (derive)
|
||||
- **Serialization**: `serde`, `serde_json`, `chrono`
|
||||
- **Database**: `sqlx`, `mongodb`, `redis` (1.0), `qdrant-client`
|
||||
- **HTTP**: `axum`, `tower`
|
||||
- **Logging**: `tracing`, `tracing-subscriber`
|
||||
- **Config**: `once_cell` (lazy static config)
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Server
|
||||
- `MOMENTRY_SERVER_PORT` - API server port (default: `3002` for production, `3003` for playground)
|
||||
- `MOMENTRY_REDIS_PREFIX` - Redis key prefix (default: `momentry:` for production, `momentry_dev:` for playground)
|
||||
- `MOMENTRY_API_KEY` - API key for Player online mode testing
|
||||
|
||||
### Testing API Key
|
||||
```bash
|
||||
export MOMENTRY_API_KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
|
||||
|
||||
# Test Player online mode
|
||||
cargo run --features player --bin momentry_player -- -o
|
||||
```
|
||||
|
||||
### Database
|
||||
- `DATABASE_URL` - PostgreSQL (default: `postgres://accusys@localhost:5432/momentry`)
|
||||
|
||||
### Redis
|
||||
- `REDIS_URL` - Redis URL (default: `redis://:accusys@localhost:6379`)
|
||||
- `REDIS_PASSWORD` - Redis password (default: `accusys`)
|
||||
|
||||
### Paths
|
||||
- `MOMENTRY_OUTPUT_DIR` - Output directory (default: `/Users/accusys/momentry/output`)
|
||||
- `MOMENTRY_BACKUP_DIR` - Backup directory
|
||||
- `MOMENTRY_PYTHON_PATH` - Python path (default: `/opt/homebrew/bin/python3.11`)
|
||||
- `MOMENTRY_SCRIPTS_DIR` - Scripts directory
|
||||
|
||||
### Processor Timeouts
|
||||
- `MOMENTRY_ASR_TIMEOUT` - ASR timeout in seconds (default: 3600)
|
||||
- `MOMENTRY_CUT_TIMEOUT` - CUT timeout in seconds (default: 3600)
|
||||
- `MOMENTRY_DEFAULT_TIMEOUT` - Default timeout (default: 7200)
|
||||
|
||||
### TMDb Integration (Face Clustering)
|
||||
- `TMDB_API_KEY` - TMDb API key for movie metadata lookup (required for `MOMENTRY_TMDB_PROBE_ENABLED=true`)
|
||||
- `MOMENTRY_TMDB_PROBE_ENABLED` - Enable TMDb probe during registration (default: `false`)
|
||||
- Register phase: searches TMDb by filename, creates identities with tmdb_id/tmdb_profile
|
||||
- Post-process phase: matches detected faces against TMDb identities via cosine similarity
|
||||
|
||||
### Synonym Expansion
|
||||
- `MOMENTRY_SYNONYM_FILES` - Comma-separated paths to synonym JSON files (e.g., `data/english_synonyms.json,data/llm_synonyms.json`)
|
||||
- `MOMENTRY_SYNONYM_FILE` - Single synonym JSON file path (deprecated, use above)
|
||||
|
||||
### Logging
|
||||
- `RUST_LOG` or `MOMENTRY_LOG_LEVEL` - Log level (default: `info`)
|
||||
|
||||
## Notes
|
||||
|
||||
- Unit tests exist (86 library tests)
|
||||
- Video processing uses external tools (ffprobe, Python scripts)
|
||||
- Multi-database architecture (PostgreSQL, MongoDB, Redis, Qdrant)
|
||||
- Monitor directory is a separate system (not Rust)
|
||||
- PythonExecutor provides unified script execution with timeout support
|
||||
- Redis 1.0.x for improved performance
|
||||
- FaceNet CoreML model (`models/facenet512.mlpackage`) replaces InsightFace for embedding extraction (MIT license, ANE-accelerated)
|
||||
|
||||
### LLM Synonym Generation
|
||||
|
||||
Generate synonym database using llama.cpp (Gemma4):
|
||||
|
||||
```bash
|
||||
# Generate full database (162 entries, ~5 minutes)
|
||||
python3 scripts/generate_synonyms_llamacpp.py
|
||||
|
||||
# Quick test
|
||||
python3 scripts/generate_synonyms_llamacpp.py --test
|
||||
|
||||
# Resume from existing file
|
||||
python3 scripts/generate_synonyms_llamacpp.py --resume
|
||||
|
||||
# Output: data/llm_synonyms.json (27 Chinese + 135 English words)
|
||||
```
|
||||
|
||||
## Task Management
|
||||
|
||||
### 使用 todowrite 追蹤任務
|
||||
```bash
|
||||
# 創建任務清單
|
||||
/todo 建立配置模組 [in_progress]
|
||||
/todo 添加單元測試 [pending]
|
||||
|
||||
# 更新狀態
|
||||
/todo 完成標記 [completed]
|
||||
```
|
||||
|
||||
### 任務批次建議
|
||||
- 一次處理 1-2 個功能
|
||||
- 每個功能完成後驗證 (clippy + test)
|
||||
- 驗證通過後再繼續下一個
|
||||
|
||||
## Code Review Checklist
|
||||
|
||||
完成任務後檢查:
|
||||
- [ ] `cargo clippy --lib` 通過
|
||||
- [ ] `cargo test --lib` 通過
|
||||
- [ ] `cargo fmt -- --check` 通過
|
||||
- [ ] 文檔已更新 (如需要)
|
||||
- [ ] 新功能有單元測試
|
||||
|
||||
## Commit Guidelines
|
||||
|
||||
```bash
|
||||
# feat: 新功能
|
||||
git commit -m "feat: add monitor_jobs table"
|
||||
|
||||
# fix: 錯誤修復
|
||||
git commit -m "fix: resolve SQL injection in store_vector"
|
||||
|
||||
# refactor: 重構
|
||||
git commit -m "refactor: use parameterized queries"
|
||||
|
||||
# docs: 文檔更新
|
||||
git commit -m "docs: update AGENTS.md with new modules"
|
||||
```
|
||||
|
||||
## Pre-commit Hook
|
||||
|
||||
專案已配置 `.git/hooks/pre-commit`,提交前自動檢查:
|
||||
|
||||
```bash
|
||||
# 檢查內容
|
||||
1. cargo fmt --check # Rust 格式化檢查
|
||||
2. cargo clippy --lib # Rust Lint 檢查
|
||||
3. cargo test --lib # Rust 單元測試
|
||||
4. ruff check # Python Lint 檢查
|
||||
5. ruff format --check # Python 格式化檢查
|
||||
6. markdownlint # Markdown 格式檢查
|
||||
7. shellcheck # Shell 腳本檢查
|
||||
|
||||
# 跳過檢查(不建議)
|
||||
git commit --no-verify
|
||||
|
||||
# 跳過特定檢查
|
||||
git commit --skip-checks
|
||||
```
|
||||
|
||||
**注意**: Hook 僅檢查已暫存的 Rust/Python/Markdown 文件。
|
||||
|
||||
### Python 環境設置
|
||||
```bash
|
||||
# 安裝 ruff
|
||||
pip install ruff==0.11.2
|
||||
|
||||
# 格式化 Python 文件
|
||||
ruff format scripts/
|
||||
|
||||
# Lint Python 文件
|
||||
ruff check scripts/
|
||||
```
|
||||
|
||||
### Markdown 環境設置
|
||||
```bash
|
||||
# 安裝 markdownlint-cli (使用系統 Node.js)
|
||||
npm install -g markdownlint-cli
|
||||
|
||||
# 檢查 Markdown 文件
|
||||
markdownlint docs/
|
||||
|
||||
# 配置檔案
|
||||
.markdownlint.json
|
||||
```
|
||||
|
||||
### Shell 環境設置
|
||||
```bash
|
||||
# 安裝 shellcheck
|
||||
brew install shellcheck
|
||||
|
||||
# 檢查 Shell 腳本
|
||||
shellcheck scripts/*.sh monitor/**/*.sh
|
||||
```
|
||||
|
||||
**注意**: Hook 只檢查 error 等級的 shellcheck 問題,style 警告會顯示但不阻擋提交。
|
||||
|
||||
## Release Workflow
|
||||
|
||||
### Release 前準備
|
||||
每次 release production binary 前,必須:
|
||||
|
||||
1. **建立 Release Tag**
|
||||
```bash
|
||||
git tag -a v0.X.X -m "Release vX.X.X - YYYY-MM-DD"
|
||||
git push origin v0.X.X
|
||||
```
|
||||
|
||||
2. **備份獨立 Source Code**
|
||||
```bash
|
||||
# 建立 release 獨立目錄
|
||||
RELEASE_DIR="/Users/accusys/momentry_core_releases/v0.X.X"
|
||||
mkdir -p "$RELEASE_DIR"
|
||||
|
||||
# 複製完整原始碼(排除不必要的檔案)
|
||||
rsync -av --exclude='.git' --exclude='target' --exclude='node_modules' \
|
||||
/Users/accusys/momentry_core_0.1/ "$RELEASE_DIR/"
|
||||
|
||||
# 記錄 release 資訊
|
||||
echo "Release: v0.X.X" > "$RELEASE_DIR/RELEASE_INFO.txt"
|
||||
echo "Date: $(date)" >> "$RELEASE_DIR/RELEASE_INFO.txt"
|
||||
echo "Git Commit: $(git rev-parse HEAD)" >> "$RELEASE_DIR/RELEASE_INFO.txt"
|
||||
echo "Binary: $(ls -la target/release/momentry)" >> "$RELEASE_DIR/RELEASE_INFO.txt"
|
||||
```
|
||||
|
||||
3. **備份 Binary**
|
||||
```bash
|
||||
cp target/release/momentry "$RELEASE_DIR/momentry_v0.X.X"
|
||||
cp target/release/momentry_playground "$RELEASE_DIR/momentry_playground_v0.X.X" 2>/dev/null
|
||||
```
|
||||
|
||||
4. **記錄資料庫 Schema**
|
||||
```bash
|
||||
pg_dump -U accusys -d momentry --schema-only > "$RELEASE_DIR/schema_v0.X.X.sql"
|
||||
```
|
||||
|
||||
### 重要性
|
||||
- 避免 release binary 與 current source code 不一致
|
||||
- 方便追蹤特定 release 的程式碼狀態
|
||||
- 必要時可快速復原或比對差異
|
||||
- 確保資料庫 schema 與程式碼版本對應
|
||||
|
||||
## Reference Documents
|
||||
|
||||
| 文件 | 用途 |
|
||||
|------|------|
|
||||
| `docs/OPENCODE_GUIDE.md` | OpenCode 使用規範 |
|
||||
| `docs/ARCHITECTURE_EVALUATION.md` | 架構優化待評估項目 (含 GraphRAG) |
|
||||
| `docs/PENDING_ISSUES.md` | 待解決問題追蹤 |
|
||||
| `docs/MOMENTRY_CORE_MONITORING.md` | 監控系統規範 |
|
||||
| `docs/MOMENTRY_CORE_REDIS_KEYS.md` | Redis Key 設計規範 |
|
||||
| `docs/PYTHON.md` | Python 腳本規範 |
|
||||
| `docs/FILE_CHANGE_MANAGEMENT.md` | 文件修改管理規範 |
|
||||
| `docs/YOLO_RESUME_INTEGRATION.md` | YOLO Resume 功能整合記錄 |
|
||||
| `docs/DOCUMENT_EMBEDDING_STRATEGY.md` | Parent-Child 嵌入策略 |
|
||||
| `docs/PROCESSING_PIPELINE.md` | 處理流程文檔 |
|
||||
| `docs/N8N_DEMO_WORKFLOW.md` | n8n 工作流文檔 |
|
||||
| `docs/FRESH_MAC_INSTALLATION.md` | 全新 Mac 安裝指南 |
|
||||
| `docs/SERVICES.md` | 服務總覽與管理 |
|
||||
| `docs/SFTPGO_DEMO_USER.md` | SFTPGo 用戶指南 |
|
||||
|
||||
## Document Change Workflow
|
||||
|
||||
修改文件前請參考 `docs/FILE_CHANGE_MANAGEMENT.md`,確保:
|
||||
|
||||
1. **修改前**:完整閱讀文件、執行預檢清單
|
||||
2. **修改中**:提供變更計畫、取得確認
|
||||
3. **修改後**:展示 diff、更新版本歷史
|
||||
4. **驗證**:執行 lint/test、提交前審查
|
||||
|
||||
### AI 工具修改規範
|
||||
|
||||
AI 工具修改文件時:
|
||||
- 必須先完整閱讀文件(不可只讀取部分章節)
|
||||
- 修改前先提出變更計畫供確認
|
||||
- 修改後展示 diff 內容
|
||||
- 更新版本歷史表
|
||||
|
||||
## PHP Development
|
||||
|
||||
WordPress 作為 Momentry Portal,負責 n8n 自動化與 sftpgo 檔案服務的頁面整合。
|
||||
|
||||
### 編輯器設定
|
||||
|
||||
| 編輯器 | LSP 方案 | 安裝方式 |
|
||||
|--------|----------|----------|
|
||||
| VS Code | Intelephense | Extension Marketplace (推薦) |
|
||||
| Cursor | Intelephense | Extension Marketplace (推薦) |
|
||||
| CLI | phpactor | `~/bin/phpactor` |
|
||||
|
||||
### Intelephense (VS Code/Cursor)
|
||||
|
||||
1. 安裝 Extension: 搜尋 "Intelephense"
|
||||
2. 設定:
|
||||
```json
|
||||
{
|
||||
"intelephense.stubs": ["wordpress"]
|
||||
}
|
||||
```
|
||||
|
||||
### phpactor (CLI)
|
||||
|
||||
```bash
|
||||
# 安裝方式
|
||||
brew install composer
|
||||
curl -sSL https://github.com/phpactor/phpactor/releases/latest/download/phpactor.phar -o ~/bin/phpactor
|
||||
chmod +x ~/bin/phpactor
|
||||
|
||||
# 安裝 WordPress Stubs
|
||||
cd /Users/accusys/wordpress/web
|
||||
composer require --dev php-stubs/wordpress-stubs
|
||||
|
||||
# 建立 WordPress 索引
|
||||
cd /Users/accusys/wordpress/web
|
||||
~/bin/phpactor index:build --reset
|
||||
|
||||
# 常用指令
|
||||
~/bin/phpactor class:search "WP_User" # 搜尋類別
|
||||
~/bin/phpactor index:query WP_User # 查看類別資訊
|
||||
~/bin/phpactor navigate /path/to/file.php # 導航到定義
|
||||
```
|
||||
|
||||
### WordPress 程式碼位置
|
||||
| 類型 | 路徑 |
|
||||
|------|------|
|
||||
| 主題 | `/Users/accusys/wordpress/web/wp-content/themes/` |
|
||||
| 插件 | `/Users/accusys/wordpress/web/wp-content/plugins/` |
|
||||
|
||||
### 與 marcom 團隊協作
|
||||
| 角色 | 負責 |
|
||||
|------|------|
|
||||
| marcom 團隊 | Figma 設計 / Elementor 建構 |
|
||||
| OpenCode | 程式碼實作 / 重構 |
|
||||
|
||||
### 開發時程
|
||||
```
|
||||
Phase 1: marcom 建構 (現在) → Elementor 頁面建構
|
||||
Phase 2: 交付審視 (TBD) → 功能確認 / 重構評估
|
||||
Phase 3: OpenCode 重構 → 純程式碼實作,交付無 Elementor 依賴版本
|
||||
```
|
||||
|
||||
## M4 通知規範
|
||||
|
||||
### 固定通知方式
|
||||
|
||||
通知 M4 的唯一管道:**`M4_workspace/` 下建立回覆文件 + `git commit`**。不需口頭、即時訊息、郵件。
|
||||
|
||||
### 命名規則
|
||||
|
||||
```
|
||||
docs_v1.0/M4_workspace/YYYY-MM-DD_<topic>_response.md (回覆 M4 問題)
|
||||
docs_v1.0/M4_workspace/YYYY-MM-DD_<topic>.md (主動通報)
|
||||
docs_v1.0/M4_workspace/YYYY-MM-DD_<topic>_test_report.md (測試報告)
|
||||
```
|
||||
|
||||
### 觸發時機
|
||||
|
||||
| 情境 | 動作 |
|
||||
|------|------|
|
||||
| M4 提交問題報告到 `M4_workspace/` | 修復後,回覆 `*_response.md` |
|
||||
| 完成 M4 要求的任務 | 回覆 `*_response.md` |
|
||||
| 重大變更(模型替換、架構變更) | 主動通知 `*.md` |
|
||||
| 新測試包產出 | `*_test_report.md` |
|
||||
|
||||
### 交付檢查
|
||||
|
||||
1. 文件寫入 `docs_v1.0/M4_workspace/`
|
||||
2. `git add` 包含該文件
|
||||
3. `git commit` 含相關變更
|
||||
4. M4 透過 git log 查看
|
||||
|
||||
詳細規範見 `docs_v1.0/M4_workspace/M4_NOTIFICATION_PROTOCOL.md`。
|
||||
|
||||
## UUID Naming Rule
|
||||
|
||||
**Never use bare `uuid` in API route paths, query params, JSON keys, or code variable names. Always qualify:**
|
||||
|
||||
| Context | Must use | Never |
|
||||
|---------|----------|-------|
|
||||
| Video/file resource | `file_uuid` | `uuid` |
|
||||
| Identity resource | `identity_uuid` | `uuid` |
|
||||
| Query parameter | `file_uuid=`, `identity_uuid=` | `uuid=` |
|
||||
| Route path | `:file_uuid`, `:identity_uuid` | `:uuid` |
|
||||
| JSON key | `"file_uuid"`, `"identity_uuid"` | `"uuid"` |
|
||||
|
||||
This applies to docs, code, API responses, and curl examples. Exceptions: internal database primary key names (e.g. `identities.uuid` column).
|
||||
|
||||
## Document Compliance Checklist
|
||||
|
||||
Before creating any file in `docs_v1.0/` (API_WORKSPACE, GUIDES, REFERENCE, DESIGN, OPERATIONS, INTEGRATIONS), verify all items below.
|
||||
**IMPORTANT**: API functional documents are generated from `API_WORKSPACE/modules/`. Edit modules there, then run `make deploy` in `API_WORKSPACE/` to update `GUIDES/`. Never edit generated files in `GUIDES/` directly. See `DESIGN/Modular_Doc_System_V1.0.md` for the full system design.
|
||||
|
||||
### P0 — Mandatory (7 items)
|
||||
|
||||
| # | Check | Rule |
|
||||
|---|-------|------|
|
||||
| 1 | YAML frontmatter | `title`, `version`, `date`, `author`, `status` present |
|
||||
| 2 | Version history | Table at bottom of file tracking changes |
|
||||
| 3 | Top info table | scope, status, applicable to, etc. |
|
||||
| 4 | PascalCase filename | e.g. `DetectorRegistry.md`, not `detector_registry.md` |
|
||||
| 5 | `_` separator | Within filenames use `_`, never spaces or other chars |
|
||||
| 6 | English content | Entire file in English |
|
||||
| 7 | Correct directory | File must reside in appropriate directory: `API_WORKSPACE/modules/` (API endpoint modules), `GUIDES/` (user docs, generated), `REFERENCE/` (data models), `DESIGN/` (architecture), `OPERATIONS/` (infra/release), `INTEGRATIONS/` (n8n/tests) |
|
||||
|
||||
### P0b — UUID Naming
|
||||
|
||||
| # | Check | Rule |
|
||||
|---|-------|------|
|
||||
| 8 | `file_uuid` not bare `uuid` | All file references use `file_uuid` (see UUID Naming Rule above) |
|
||||
| 9 | `identity_uuid` not bare `uuid` | All identity references use `identity_uuid` |
|
||||
|
||||
### P1 — Suggested (3 items)
|
||||
|
||||
| # | Check | Note |
|
||||
|---|-------|------|
|
||||
| 1 | Cross-references | Link to related docs in API_WORKSPACE/, GUIDES/, REFERENCE/, DESIGN/, OPERATIONS/ |
|
||||
| 2 | Glossary terms | Define non-obvious terms inline or link glossary |
|
||||
| 3 | Diagrams | Include Mermaid/ASCII diagram for complex topics |
|
||||
|
||||
### Exception
|
||||
|
||||
`M4_workspace/` files are exempt from this checklist (free-format reply documents).
|
||||
|
||||
---
|
||||
|
||||
## Delivery Procedure
|
||||
|
||||
完整交付程序(M4_workspace → M5 → Release → Deploy → Public)見:
|
||||
|
||||
`docs_v1.0/OPERATIONS/DELIVERY_PROCEDURE.md`
|
||||
71
deliverable_v1.1.0/SYSTEM_AUDIT_2026-05-17.md
Normal file
71
deliverable_v1.1.0/SYSTEM_AUDIT_2026-05-17.md
Normal file
@@ -0,0 +1,71 @@
|
||||
# System Audit — 2026-05-17
|
||||
|
||||
## Current State
|
||||
|
||||
### Embedding Storage (三重冗余,無主)
|
||||
|
||||
| 資料類型 | PG pgvector | Qdrant | JSON 檔案 |
|
||||
|---------|------------|--------|-----------|
|
||||
| Sentence 向量 | `chunk.embedding` ✅ | `dev_v1` / `rule1_v2` / `sentence_*` ✅ | ❌ 無 |
|
||||
| Story 向量 | `chunk.embedding` ✅ | `dev_v1` / `dev_stories` ✅ | `.story_llm.json` ✅ |
|
||||
| Face 向量 | ❌ 已清除(依使用者指示) | `dev_faces` ✅ (97K) | `.face.json` ✅ |
|
||||
| Voice 向量 | ❌ 無 | `dev_voice` ✅ (4K) | ❌ 無 |
|
||||
|
||||
### Pipeline 問題
|
||||
|
||||
| 問題 | 影響 |
|
||||
|------|------|
|
||||
| `processor_results.duration_secs` 全為 0 | 無法查各步驟耗時 |
|
||||
| `processor_results.started_at/completed_at` 全 NULL | 時間線遺失 |
|
||||
| Redis timing 在 job 完成後被清掉 | 唯一 timing 來源消失 |
|
||||
| `get_chunk_by_chunk_id_and_uuid` 原本是 stub(已修) | Smart search 找不到 PG chunk |
|
||||
| `server.rs::search()` 未 mount 但仍編譯 | Dead code,混淆 Qdrant 用途 |
|
||||
| Face embedding 只寫 Qdrant 不寫 PG | 已刪除則全失 |
|
||||
|
||||
### Qdrant Collections 現況
|
||||
|
||||
| Collection | Points | 來源 | UUID |
|
||||
|-----------|--------|------|------|
|
||||
| `dev_v1` | 9,936 | PG rebuild | ✅ bd80fec... |
|
||||
| `dev_faces` | 97,000 | face.json rebuild | ✅ bd80fec... |
|
||||
| `dev_stories` | 560 | Snapshot | ✅ bd80fec... |
|
||||
| `dev_voice` | 4,188 | Snapshot | ✅ bd80fec... |
|
||||
| `dev_rule1_v2` | 3,417 | Snapshot | ✅ bd80fec... |
|
||||
| `sentence_story` | 4,188 | Snapshot | ✅ bd80fec... |
|
||||
| `sentence_summary` | 4,188 | Snapshot | ✅ bd80fec... |
|
||||
|
||||
## Safeguards & Fixes
|
||||
|
||||
### P0 — 必須修
|
||||
|
||||
| # | Fix | 做法 |
|
||||
|---|-----|------|
|
||||
| 1 | **Pipeline timing 寫入 DB** | `update_processor_result()` 加入 `started_at`、`completed_at`、`duration_secs` |
|
||||
| 2 | **Qdrant 不當主要儲存** | Embedding 以 PG `chunk.embedding` 為 source of truth,Qdrant 唯讀 cache |
|
||||
| 3 | **Smart search 只走 PG pgvector** | `search_parent_chunks_semantic` 已正確,無需 Qdrant |
|
||||
| 4 | **移除 `server.rs::search()` dead code** | 或 mount 到正式 route 並確認可用 |
|
||||
|
||||
### P1 — 建議修
|
||||
|
||||
| # | Fix | 做法 |
|
||||
|---|-----|------|
|
||||
| 5 | **刪除 Qdrant 前先 snapshot** | 自動 snapshot script |
|
||||
| 6 | **清理多餘 Qdrant collections** | `dev_voice` / `dev_stories` / `dev_rule1_v2` / `sentence_*` 無 server reader,可移除 |
|
||||
| 7 | **Face embedding 寫入 PG 或移除 dead code** | 目前 face Qdrant write 無人讀取,可移除 `sync_face_embeddings` |
|
||||
| 8 | **UUID 一致性檢查** | 同一 content 不應產生不同 UUID |
|
||||
|
||||
### P2 — 可選
|
||||
|
||||
| # | Fix | 做法 |
|
||||
|---|-----|------|
|
||||
| 9 | `chunk_selector.rs` (player binary)hardcode `momentry_rule1` | 改讀 env var 或 PG |
|
||||
| 10 | AGENTS.md 已加入 delete 安全規則 | ✅ Done |
|
||||
|
||||
## Data Recovery Path
|
||||
|
||||
| 資料來源 | 可恢復到 | 方法 |
|
||||
|---------|---------|------|
|
||||
| `chunk.embedding` (PG) | Qdrant `dev_v1` | SQL → Qdrant upsert |
|
||||
| `face.json` (磁碟) | Qdrant `dev_faces` | Python script |
|
||||
| `story_llm.json` (磁碟) | Qdrant `dev_stories` | Python script |
|
||||
| Qdrant snapshots (phase1) | Qdrant collections | Snapshot upload API |
|
||||
388
deliverable_v1.1.0/html_docs/doc/01_auth.html
Normal file
388
deliverable_v1.1.0/html_docs/doc/01_auth.html
Normal file
@@ -0,0 +1,388 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>01 Auth - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: auth -->
|
||||
<!-- description: Authentication — login, logout, JWT, session cookie, API key -->
|
||||
<!-- depends: -->
|
||||
|
||||
<h2>Base URL</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment</th>
|
||||
<th>URL</th>
|
||||
<th>Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Production</td>
|
||||
<td><code>http://localhost:3002</code></td>
|
||||
<td>Production deployment</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>External (M5)</td>
|
||||
<td><code>https://m5api.momentry.ddns.net</code></td>
|
||||
<td>Remote access</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Variables</h2>
|
||||
<p>All examples in this documentation use these environment variables:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nv">API</span><span class="o">=</span><span class="s2">"http://localhost:3002"</span>
|
||||
<span class="nv">KEY</span><span class="o">=</span><span class="s2">"your-api-key-here"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h2>Authentication</h2>
|
||||
<p>All endpoints under <code>/api/v1/*</code> require authentication.
|
||||
The following endpoints are public (no auth needed):</p>
|
||||
<ul>
|
||||
<li><code>GET /health</code></li>
|
||||
<li><code>POST /api/v1/auth/login</code></li>
|
||||
<li><code>POST /api/v1/auth/logout</code></li>
|
||||
</ul>
|
||||
<h3>Three Authentication Modes</h3>
|
||||
<p>The system supports three authentication methods, checked in <strong>priority order</strong> by the middleware:</p>
|
||||
<div class="codehilite"><pre><span></span><code>Middleware priority:
|
||||
1. Session Cookie (Portal/browser)
|
||||
2. JWT Bearer (API clients, CLI)
|
||||
3. API Key Header (legacy compatibility)
|
||||
4. API Key Query Param (?api_key=)
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mode</th>
|
||||
<th>Transport</th>
|
||||
<th>Expiry</th>
|
||||
<th>Scope</th>
|
||||
<th>Best for</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Session Cookie</strong></td>
|
||||
<td><code>Cookie: session_id=<session_id></code></td>
|
||||
<td>24h</td>
|
||||
<td>per-browser session</td>
|
||||
<td>Portal (browser)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>JWT</strong></td>
|
||||
<td><code>Authorization: Bearer <token></code></td>
|
||||
<td>1h</td>
|
||||
<td>per-login token</td>
|
||||
<td>API clients, CLI, scripts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>API Key</strong></td>
|
||||
<td><code>X-API-Key: <key></code></td>
|
||||
<td>90d</td>
|
||||
<td>fixed key for automation</td>
|
||||
<td>Legacy scripts, WordPress</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3>Login</h3>
|
||||
<p><strong>Default accounts & API keys:</strong></p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Password</th>
|
||||
<th>API Key</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>admin</code></td>
|
||||
<td><code>admin</code></td>
|
||||
<td>—</td>
|
||||
<td>admin</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>demo</code></td>
|
||||
<td><code>demo</code></td>
|
||||
<td><code>muser_demo_key_32chars_abcdef1234567890</code></td>
|
||||
<td>user</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>The demo API key is set via <code>MOMENTRY_DEMO_API_KEY</code> env var and can be used in place of JWT for marcom integrations:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Using API key instead of JWT</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: muser_demo_key_32chars_abcdef1234567890"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Login as admin</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username": "admin", "password": "admin"}'</span>
|
||||
|
||||
<span class="c1"># Login as demo user</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username": "demo", "password": "demo"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Success Response</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"jwt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJIUzI1NiIs..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"api_key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"muser_..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"user"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"role"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin"</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"expires_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-18T13:00:00Z"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>jwt</code></td>
|
||||
<td>string</td>
|
||||
<td>JWT access token. Use as <code>Authorization: Bearer <jwt></code>. Expires in 1 hour.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>api_key</code></td>
|
||||
<td>string</td>
|
||||
<td>Legacy API key. Use as <code>X-API-Key: <key></code>. Good for 90 days.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user.username</code></td>
|
||||
<td>string</td>
|
||||
<td>Username</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user.role</code></td>
|
||||
<td>string</td>
|
||||
<td>Role: <code>admin</code>, <code>user</code>, or <code>readonly</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>expires_at</code></td>
|
||||
<td>string</td>
|
||||
<td>ISO8601 timestamp of JWT expiration</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>The login endpoint also sets a <code>Set-Cookie</code> header for browser-based clients:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nt">Set-Cookie</span><span class="o">:</span><span class="w"> </span><span class="nt">session_id</span><span class="o">=<</span><span class="nt">session_id</span><span class="o">>;</span><span class="w"> </span><span class="nt">Path</span><span class="o">=/;</span><span class="w"> </span><span class="nt">HttpOnly</span><span class="o">;</span><span class="w"> </span><span class="nt">SameSite</span><span class="o">=</span><span class="nt">Strict</span><span class="o">;</span><span class="w"> </span><span class="nt">Max-Age</span><span class="o">=</span><span class="nt">86400</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Error Response (401)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Invalid username or password"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Using JWT</h3>
|
||||
<p>JWT is preferred for API clients (CLI scripts, WordPress). It is validated by the middleware without a database lookup (stateless).</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Login and capture JWT</span>
|
||||
<span class="nv">JWT</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username":"admin","password":"admin"}'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>python3<span class="w"> </span>-c<span class="w"> </span><span class="s2">"import json,sys;print(json.load(sys.stdin)['jwt'])"</span><span class="k">)</span>
|
||||
|
||||
<span class="c1"># Use JWT for all subsequent requests</span>
|
||||
curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span>
|
||||
curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>JWT is short-lived (1 hour). When it expires, request a new one via login.</p>
|
||||
<hr />
|
||||
<h3>Using Session Cookie (Browser)</h3>
|
||||
<p>Browser-based clients (Portal) get a session cookie automatically after login. The browser sends the cookie with every request—no manual header needed.</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Login captures the session cookie from Set-Cookie header</span>
|
||||
curl<span class="w"> </span>-v<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username":"admin","password":"admin"}'</span><span class="w"> </span><span class="m">2</span>><span class="p">&</span><span class="m">1</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span><span class="s2">"Set-Cookie"</span>
|
||||
|
||||
<span class="c1"># Browser automatically sends: Cookie: session_id=<session_id></span>
|
||||
<span class="c1"># No manual header needed for subsequent requests</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>The session cookie is HttpOnly (not accessible from JavaScript) and SameSite=Strict (protected against CSRF).</p>
|
||||
<hr />
|
||||
<h3>Using Legacy API Key</h3>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span>
|
||||
|
||||
<span class="c1"># Also accepted via Bearer header (non-JWT format) or query parameter:</span>
|
||||
curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span>
|
||||
curl<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?api_key=</span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>API keys are validated via SHA256 hash lookup in the database. They are long-lived (90 days) and intended for automation.</p>
|
||||
<h3>Obtaining an API Key (CLI)</h3>
|
||||
<div class="codehilite"><pre><span></span><code>momentry<span class="w"> </span>api-key<span class="w"> </span>create<span class="w"> </span><span class="s2">"My API Key"</span><span class="w"> </span>--key-type<span class="w"> </span>user
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Logout</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Logout using the session cookie (browser)</span>
|
||||
curl<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/logout"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=<uuid>"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>What logout does</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Auth mode</th>
|
||||
<th>Effect</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Session Cookie</strong></td>
|
||||
<td>Session deleted from database. Same cookie returns 401 on subsequent requests.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>JWT</strong></td>
|
||||
<td>JWT remains valid until expiry. (JWT is stateless — logout adds JWT to a blacklist only if API key mode is used.)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>API Key</strong></td>
|
||||
<td>API key remains valid. (Legacy keys are shared across sessions — revoking would break other clients.)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example: full session lifecycle</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># 1. Login</span>
|
||||
<span class="nv">SESSION_ID</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-s<span class="w"> </span>-D<span class="w"> </span>-<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username":"admin","password":"admin"}'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span><span class="s2">"Set-Cookie"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span><span class="s1">'s/.*session_id=\([^;]*\).*/\1/'</span><span class="k">)</span>
|
||||
|
||||
<span class="c1"># 2. Use session (works)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-o<span class="w"> </span>/dev/null<span class="w"> </span>-w<span class="w"> </span><span class="s2">"HTTP %{http_code}\n"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=</span><span class="nv">$SESSION_ID</span><span class="s2">"</span>
|
||||
<span class="c1"># → HTTP 200</span>
|
||||
|
||||
<span class="c1"># 3. Logout</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/logout"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=</span><span class="nv">$SESSION_ID</span><span class="s2">"</span>
|
||||
<span class="c1"># → {"success": true}</span>
|
||||
|
||||
<span class="c1"># 4. Use session again (rejected)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-o<span class="w"> </span>/dev/null<span class="w"> </span>-w<span class="w"> </span><span class="s2">"HTTP %{http_code}\n"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=</span><span class="nv">$SESSION_ID</span><span class="s2">"</span>
|
||||
<span class="c1"># → HTTP 401</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Authentication Flow Summary</h3>
|
||||
<div class="codehilite"><pre><span></span><code>Login Request
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 1. Check users │ ← users table (argon2 password verify)
|
||||
│ table │
|
||||
└──────┬───────────┘
|
||||
│
|
||||
┌───┴───┐
|
||||
│ match │
|
||||
└───┬───┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 2. Create JWT │ ← 1h expiry, signed with JWT_SECRET
|
||||
├──────────────────┤
|
||||
│ 3. Create │ ← 24h expiry, stored in sessions table
|
||||
│ session │
|
||||
├──────────────────┤
|
||||
│ 4. Set-Cookie │ ← HttpOnly, SameSite=Strict, Path=/
|
||||
├──────────────────┤
|
||||
│ 5. Return │ ← JWT + api_key + user info to client
|
||||
└──────────────────┘
|
||||
</code></pre></div>
|
||||
|
||||
<div class="codehilite"><pre><span></span><code>Protected Request
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Middleware checks: │
|
||||
│ │
|
||||
│ 1. Cookie session? │ → DB lookup session → get api_key → verify
|
||||
│ │
|
||||
│ 2. JWT Bearer? │ → verify JWT signature → decode claims
|
||||
│ │
|
||||
│ 3. X-API-Key? │ → SHA256 hash → DB lookup → verify
|
||||
│ │
|
||||
│ 4. ?api_key=? │ → same as #3
|
||||
│ │
|
||||
│ 5. None → 401 │
|
||||
└──────────────────────┘
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Error Responses</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>Missing or invalid authentication</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>Session expired or logged out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>JWT expired</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>API key revoked or inactive</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3>Related</h3>
|
||||
<ul>
|
||||
<li><code>POST /api/v1/resource/tmdb/check</code> — test authentication + TMDb API connectivity</li>
|
||||
<li><code>GET /health/detailed</code> — view auth status (integrations section)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
277
deliverable_v1.1.0/html_docs/doc/02_health.html
Normal file
277
deliverable_v1.1.0/html_docs/doc/02_health.html
Normal file
@@ -0,0 +1,277 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>02 Health - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: health -->
|
||||
<!-- description: Health check endpoints -->
|
||||
<!-- depends: 01_auth -->
|
||||
|
||||
<h2>Health Check</h2>
|
||||
<h3><code>GET /health</code></h3>
|
||||
<p><strong>Auth</strong>: Public
|
||||
<strong>Scope</strong>: system-level</p>
|
||||
<p>Returns basic server health status — used by load balancers and monitoring.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/health"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{status, version}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"build_git_hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"build_timestamp"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-16T13:38:15Z"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"uptime_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">3015</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>status</code></td>
|
||||
<td>string</td>
|
||||
<td><code>ok</code> or <code>degraded</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>version</code></td>
|
||||
<td>string</td>
|
||||
<td>Semver version</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>build_git_hash</code></td>
|
||||
<td>string</td>
|
||||
<td>Git commit hash</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>build_timestamp</code></td>
|
||||
<td>string</td>
|
||||
<td>Binary build time</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>uptime_ms</code></td>
|
||||
<td>integer</td>
|
||||
<td>Milliseconds since server start</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /health/detailed</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: system-level</p>
|
||||
<p>Returns full system health including each service status, resource utilization, pipeline readiness, schema migration status, identity file sync status, and external integrations.</p>
|
||||
<blockquote>
|
||||
<p>Requires authentication (JWT, session cookie, or API key). The basic <code>/health</code> endpoint remains public for load balancer checks.</p>
|
||||
</blockquote>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/health/detailed"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{status, services, resources: {cpu: .resources.cpu_used_percent, memory: .resources.memory_used_percent}}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"services"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"postgres"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">,</span><span class="w"> </span><span class="nt">"latency_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"redis"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">,</span><span class="w"> </span><span class="nt">"latency_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"qdrant"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">,</span><span class="w"> </span><span class="nt">"latency_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"resources"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"cpu_used_percent"</span><span class="p">:</span><span class="w"> </span><span class="mf">12.5</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"memory_available_mb"</span><span class="p">:</span><span class="w"> </span><span class="mi">32768</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"memory_used_percent"</span><span class="p">:</span><span class="w"> </span><span class="mf">31.7</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"pipeline"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"scripts_ready"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"scripts_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">345</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"processors"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"asr"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"yolo"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"face"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"pose"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"ocr"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"cut"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"scene"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"asrx"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"visual_chunk"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"models_ready"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"models_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"scripts_integrity"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"matched"</span><span class="p">:</span><span class="w"> </span><span class="mi">332</span><span class="p">,</span><span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">345</span><span class="p">,</span><span class="w"> </span><span class="nt">"ok"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"ffmpeg"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"schema"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"table_exists"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"applied"</span><span class="p">:</span><span class="w"> </span><span class="p">[{</span><span class="nt">"filename"</span><span class="p">:</span><span class="w"> </span><span class="s2">"migrate_add_users_table.sql"</span><span class="p">}],</span>
|
||||
<span class="w"> </span><span class="nt">"required"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span>
|
||||
<span class="w"> </span><span class="nt">"ok"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"identities"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"directory_exists"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"files_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">3481</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"index_ok"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"db_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">3481</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"synced"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"integrations"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"tmdb"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"api_key_configured"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"api_reachable"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response Fields</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>status</code></td>
|
||||
<td>string</td>
|
||||
<td><code>ok</code> if all essential services healthy</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>services</code></td>
|
||||
<td>object</td>
|
||||
<td>Per-service status (postgres, redis, qdrant)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>services.*.status</code></td>
|
||||
<td>string</td>
|
||||
<td><code>ok</code>, <code>error</code>, or <code>degraded</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>services.*.latency_ms</code></td>
|
||||
<td>int</td>
|
||||
<td>Response time in milliseconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>resources</code></td>
|
||||
<td>object</td>
|
||||
<td>CPU, memory usage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pipeline.scripts_ready</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Scripts directory accessible</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pipeline.scripts_count</code></td>
|
||||
<td>int</td>
|
||||
<td>Number of Python processor scripts</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pipeline.processors</code></td>
|
||||
<td>object</td>
|
||||
<td>Per-processor availability</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pipeline.models_ready</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Models directory accessible</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pipeline.scripts_integrity</code></td>
|
||||
<td>object</td>
|
||||
<td>SHA256 checksum verification results</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>schema.ok</code></td>
|
||||
<td>boolean</td>
|
||||
<td>All required migrations applied</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>identities.synced</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Identity file count matches DB count</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>integrations.tmdb</code></td>
|
||||
<td>object</td>
|
||||
<td>TMDB API key config and reachability</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Health status rules</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Condition</th>
|
||||
<th>status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>All services ok</td>
|
||||
<td><code>ok</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Any service error</td>
|
||||
<td><code>degraded</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Postgres or Redis error</td>
|
||||
<td><code>degraded</code> (server still responds)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3>Stats Endpoints</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Auth</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>GET</td>
|
||||
<td><code>/api/v1/stats/sftpgo</code></td>
|
||||
<td>No</td>
|
||||
<td>SFTPGo service status</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
444
deliverable_v1.1.0/html_docs/doc/03_register.html
Normal file
444
deliverable_v1.1.0/html_docs/doc/03_register.html
Normal file
@@ -0,0 +1,444 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>03 Register - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: register -->
|
||||
<!-- description: File registration — register, scan -->
|
||||
<!-- depends: 01_auth -->
|
||||
|
||||
<h2>File Registration</h2>
|
||||
<h3><code>POST /api/v1/files/register</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Register a video file for processing. Returns the file's metadata and UUID.</p>
|
||||
<p><strong>New in v0.1.2</strong>: Registration now <strong>automatically triggers the processing pipeline</strong> — no need to call <code>POST /api/v1/file/:file_uuid/process</code> separately. The system will:
|
||||
1. Register the file and run ffprobe
|
||||
2. Auto-run offline TMDb probe (reads local identity files, no API calls)
|
||||
3. Create a monitor job for the worker
|
||||
4. Worker starts all 10 processors (Cut → ASR → ASRX → YOLO → OCR → Face → Pose → VisualChunk → Story → 5W1H)</p>
|
||||
<p>If the file already exists (same content hash), returns the existing record with <code>already_exists: true</code>.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>Path to video file on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pattern</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Regex pattern for batch register (requires <code>file_path</code> to be a directory)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>User ID to associate with registration</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>content_hash</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Pre-computed SHA-256 hash (skips computation)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Register a single file</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/register"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_path": "/path/to/video.mp4"}'</span>
|
||||
|
||||
<span class="c1"># Batch register files matching a pattern in a directory</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/register"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_path": "/path/to/dir", "pattern": ".*\\.mp4$"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/path/to/video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</span><span class="p">:</span><span class="w"> </span><span class="mf">120.5</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"width"</span><span class="p">:</span><span class="w"> </span><span class="mi">1920</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"height"</span><span class="p">:</span><span class="w"> </span><span class="mi">1080</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">24.0</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"total_frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">2892</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"already_exists"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"File registered successfully"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>success</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Always true on 200</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>32-char hex UUID of the registered file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_name</code></td>
|
||||
<td>string</td>
|
||||
<td>File name (auto-renamed if name conflict)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Canonical path on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_type</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"video"</code>, <code>"audio"</code>, or <code>"unknown"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>duration</code></td>
|
||||
<td>float</td>
|
||||
<td>Duration in seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>width</code></td>
|
||||
<td>integer</td>
|
||||
<td>Video width in pixels</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>height</code></td>
|
||||
<td>integer</td>
|
||||
<td>Video height in pixels</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fps</code></td>
|
||||
<td>float</td>
|
||||
<td>Frames per second</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>total_frames</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total frame count</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>already_exists</code></td>
|
||||
<td>boolean</td>
|
||||
<td>True if same content was already registered</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>message</code></td>
|
||||
<td>string</td>
|
||||
<td>Human-readable status</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Error Responses</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>Missing or invalid API key</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>400</code></td>
|
||||
<td>Invalid request body</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>404</code></td>
|
||||
<td>File path does not exist</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/files/scan</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Scan the filesystem directory and list all media files, showing which are registered, processing, or unregistered.</p>
|
||||
<h4>Query Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>1</td>
|
||||
<td>Page number (1-based)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>all</td>
|
||||
<td>Items per page (alias: <code>limit</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>all</td>
|
||||
<td>Max items (alias for <code>page_size</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pattern</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Regex filter on file name (e.g., <code>.*\\.mp4$</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sort_by</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>name</code></td>
|
||||
<td>Sort field: <code>name</code>, <code>size</code>, <code>modified</code>, <code>status</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sort_order</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>asc</code></td>
|
||||
<td>Sort direction: <code>asc</code> or <code>desc</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Full scan</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, registered_count, unregistered_count}'</span>
|
||||
|
||||
<span class="c1"># Paginated (page 1, 5 per page)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?page=1&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{page, total_pages, files: [.files[].file_name]}'</span>
|
||||
|
||||
<span class="c1"># Regex filter: only mp4 files</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?pattern=.*\\.mp4</span>$<span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{filtered_total, files: [.files[].file_name]}'</span>
|
||||
|
||||
<span class="c1"># Sort by file size (largest first)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?sort_by=size&sort_order=desc&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.files[] | {file_name, file_size}]'</span>
|
||||
|
||||
<span class="c1"># Sort by modified time (most recent first)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?sort_by=modified&sort_order=desc&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.files[] | {file_name, modified_time}]'</span>
|
||||
|
||||
<span class="c1"># Sort by status</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?sort_by=status&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.files[] | {file_name, status}]'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"files"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">12345678</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"is_registered"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"registration_time"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-16T12:00:00Z"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"job_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">107</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"filtered_total"</span><span class="p">:</span><span class="w"> </span><span class="mi">80</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"page"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"page_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"total_pages"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"registered_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">26</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"unregistered_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">81</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>files</code></td>
|
||||
<td>array</td>
|
||||
<td>Array of file info objects (paginated)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_name</code></td>
|
||||
<td>string</td>
|
||||
<td>File name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].relative_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Path relative to scan root</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Absolute path on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>File size in bytes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].modified_time</code></td>
|
||||
<td>string</td>
|
||||
<td>Last modified timestamp (ISO8601)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].is_registered</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Whether file is registered in DB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>32-char hex UUID (only if registered)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].status</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"completed"</code>, <code>"processing"</code>, <code>"registered"</code>, <code>"unregistered"</code>, or <code>null</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].registration_time</code></td>
|
||||
<td>string</td>
|
||||
<td>DB registration timestamp (only if registered)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].job_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Processing job ID (only if a job exists)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>total</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total files found on disk (unfiltered)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>filtered_total</code></td>
|
||||
<td>integer</td>
|
||||
<td>Files matching regex filter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>Current page number</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>Items per page</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>total_pages</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total pages</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>registered_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Files registered in DB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>unregistered_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Files not yet registered</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Notes</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Behavior</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Regex</strong></td>
|
||||
<td>Case-insensitive (<code>(?i)</code> prefix auto-applied). Applied to <code>file_name</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Sort order</strong></td>
|
||||
<td>Default (<code>sort_by=name</code>): registered files first, then alphabetically. <code>sort_by=status</code>: alphabetical by status string.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Pagination</strong></td>
|
||||
<td><code>page_size</code> and <code>limit</code> are aliases. Default: show all results.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Processing order</strong></td>
|
||||
<td><code>pattern</code> regex filter → <code>sort_by</code>/<code>sort_order</code> → <code>page</code>/<code>page_size</code> slice.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
291
deliverable_v1.1.0/html_docs/doc/04_lookup.html
Normal file
291
deliverable_v1.1.0/html_docs/doc/04_lookup.html
Normal file
@@ -0,0 +1,291 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>04 Lookup - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: lookup -->
|
||||
<!-- description: File lookup by name and unregistration -->
|
||||
<!-- depends: 01_auth, 03_register -->
|
||||
|
||||
<h2>File Lookup</h2>
|
||||
<h3><code>GET /api/v1/files/lookup</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Search registered files by file name. Performs a case-insensitive LIKE search on the file name column. Returns basic info about matching files.</p>
|
||||
<h4>Query Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_name</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>File name to search for (partial matches supported)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Look up a specific file</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/lookup?file_name=video.mp4"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
|
||||
<span class="c1"># Partial name search</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/lookup?file_name=charade"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'.matches[].file_name'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"exists"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"matches"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a03485a40b2df2d3"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"next_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video (2).mp4"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_name</code></td>
|
||||
<td>string</td>
|
||||
<td>Searched name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>exists</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Exact name match exists</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>matches</code></td>
|
||||
<td>array</td>
|
||||
<td>Array of matching registered files</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>matches[].file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>32-char hex UUID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>matches[].file_name</code></td>
|
||||
<td>string</td>
|
||||
<td>Registered file name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>matches[].file_type</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"video"</code>, <code>"audio"</code>, or <code>null</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>matches[].status</code></td>
|
||||
<td>string</td>
|
||||
<td>Registration/processing status</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>next_name</code></td>
|
||||
<td>string</td>
|
||||
<td>Suggested name for avoiding conflicts</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>Unregister</h2>
|
||||
<h3><code>POST /api/v1/unregister</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Delete a registered file from the system. Supports single file by UUID, or batch by directory + regex pattern.</p>
|
||||
<h4>What gets deleted</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Removed (default)</th>
|
||||
<th>Not removed</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Database records (videos, chunks, embeddings, processor_results, pre_chunks)</td>
|
||||
<td>The original source video file on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Processor output JSON files (<code>{uuid}.*.json</code>) — unless <code>delete_output_files: false</code></td>
|
||||
<td>Temp/working directories</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>In-memory cache entries</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>MongoDB cached lists</td>
|
||||
<td></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<blockquote>
|
||||
<p>⚠️ Database deletion is <strong>irreversible</strong>. To keep output files, set <code>"delete_output_files": false</code>.</p>
|
||||
</blockquote>
|
||||
<h4>Request Parameters</h4>
|
||||
<p>At least one mode must be specified: either <code>file_uuid</code> alone, or <code>file_path</code> + <code>pattern</code> together.</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>*</td>
|
||||
<td>—</td>
|
||||
<td>Single file UUID to delete</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_path</code></td>
|
||||
<td>string</td>
|
||||
<td>*</td>
|
||||
<td>—</td>
|
||||
<td>Directory path (for batch delete)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pattern</code></td>
|
||||
<td>string</td>
|
||||
<td>*</td>
|
||||
<td>—</td>
|
||||
<td>Regex pattern (requires <code>file_path</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>delete_output_files</code></td>
|
||||
<td>boolean</td>
|
||||
<td>No</td>
|
||||
<td><code>true</code></td>
|
||||
<td>If <code>true</code>, also delete processor output JSON files (<code>{uuid}.*.json</code>). Set to <code>false</code> to keep them.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Delete a single file by UUID (default: also deletes output JSON files)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/unregister"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_uuid": "'</span><span class="s2">"</span><span class="nv">$FILE_UUID</span><span class="s2">"</span><span class="s1">'"}'</span>
|
||||
|
||||
<span class="c1"># Keep output JSON files, only delete DB records</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/unregister"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_uuid": "'</span><span class="s2">"</span><span class="nv">$FILE_UUID</span><span class="s2">"</span><span class="s1">'", "delete_output_files": false}'</span>
|
||||
|
||||
<span class="c1"># Batch delete all mp4 files in a directory</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/unregister"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_path": "/path/to/dir", "pattern": ".*\\.mp4$"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a03485a40b2df2d3"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Video unregistered successfully"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>success</code></td>
|
||||
<td>boolean</td>
|
||||
<td>True if deletion succeeded</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>UUID of the deleted file (single mode)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>message</code></td>
|
||||
<td>string</td>
|
||||
<td>Human-readable status</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Error Responses</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>400</code></td>
|
||||
<td>Neither <code>file_uuid</code> nor <code>file_path</code>+<code>pattern</code> provided</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>404</code></td>
|
||||
<td>File UUID not found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>Missing or invalid API key</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
505
deliverable_v1.1.0/html_docs/doc/05_process.html
Normal file
505
deliverable_v1.1.0/html_docs/doc/05_process.html
Normal file
@@ -0,0 +1,505 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>05 Process - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: process -->
|
||||
<!-- description: Processing pipeline — trigger, probe, progress, jobs -->
|
||||
<!-- depends: 01_auth, 03_register -->
|
||||
|
||||
<h2>Processing Pipeline</h2>
|
||||
<h3><code>POST /api/v1/file/:file_uuid/process</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Trigger the processing pipeline for a registered file. Creates a monitor job that the worker picks up and processes sequentially. Returns immediately with the job info—processing runs asynchronously in the background.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>processors</code></td>
|
||||
<td>string[]</td>
|
||||
<td>No</td>
|
||||
<td>all</td>
|
||||
<td>Specific processors to run: <code>["cut","asr","asrx","yolo","ocr","face","pose","visual_chunk","story","5w1h"]</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>rules</code></td>
|
||||
<td>string[]</td>
|
||||
<td>No</td>
|
||||
<td>all</td>
|
||||
<td>Rule names to apply (currently unused)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Run all processors</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/process"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-d<span class="w"> </span><span class="s1">'{}'</span>
|
||||
|
||||
<span class="c1"># Run specific processors only</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/process"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"processors": ["asr", "face", "yolo"]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"job_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"processing"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"pids"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="mi">12345</span><span class="p">,</span><span class="w"> </span><span class="mi">12346</span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Processing triggered for video.mp4"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>success</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Always true on 200</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>job_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Monitor job ID (for job tracking)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>32-char hex UUID of the file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>status</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"processing"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pids</code></td>
|
||||
<td>integer[]</td>
|
||||
<td>Process IDs of started processors</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>message</code></td>
|
||||
<td>string</td>
|
||||
<td>Human-readable status</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Error Responses</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>404</code></td>
|
||||
<td>File UUID not found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>Missing or invalid API key</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/file/:file_uuid/probe</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Get ffprobe metadata for a registered file. Returns video/audio stream info, codec details, duration, resolution, and frame rate.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/probe"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">794863677</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</span><span class="p">:</span><span class="w"> </span><span class="mf">120.5</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"width"</span><span class="p">:</span><span class="w"> </span><span class="mi">1920</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"height"</span><span class="p">:</span><span class="w"> </span><span class="mi">1080</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">24.0</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"total_frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">2892</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"cached"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"format"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"filename"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/path/to/video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"format_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mov,mp4,m4a,3gp"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</span><span class="p">:</span><span class="w"> </span><span class="s2">"120.5"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"size"</span><span class="p">:</span><span class="w"> </span><span class="s2">"12345678"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"bit_rate"</span><span class="p">:</span><span class="w"> </span><span class="s2">"819200"</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"streams"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"index"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"codec_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"h264"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"codec_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"width"</span><span class="p">:</span><span class="w"> </span><span class="mi">1920</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"height"</span><span class="p">:</span><span class="w"> </span><span class="mi">1080</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"r_frame_rate"</span><span class="p">:</span><span class="w"> </span><span class="s2">"24/1"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</span><span class="p">:</span><span class="w"> </span><span class="s2">"120.5"</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>32-char hex UUID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_name</code></td>
|
||||
<td>string</td>
|
||||
<td>File name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>File size in bytes (from filesystem)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>duration</code></td>
|
||||
<td>float</td>
|
||||
<td>Duration in seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>width</code></td>
|
||||
<td>integer</td>
|
||||
<td>Video width in pixels</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>height</code></td>
|
||||
<td>integer</td>
|
||||
<td>Video height in pixels</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fps</code></td>
|
||||
<td>float</td>
|
||||
<td>Frames per second</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>total_frames</code></td>
|
||||
<td>integer</td>
|
||||
<td>Estimated total frames</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>cached</code></td>
|
||||
<td>boolean</td>
|
||||
<td>True if result was from cached probe JSON</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>format</code></td>
|
||||
<td>object</td>
|
||||
<td>Container format info (ffprobe format section)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>streams</code></td>
|
||||
<td>array</td>
|
||||
<td>Array of stream info objects</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/progress/:file_uuid</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Get real-time processing progress for a file via Redis pub/sub. Includes per-processor status, current/total frames, ETA, and system resource stats.</p>
|
||||
<h4>Pipeline Order</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Order</th>
|
||||
<th>Processor</th>
|
||||
<th>Dependencies</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td><code>cut</code></td>
|
||||
<td>—</td>
|
||||
<td>Scene detection</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td><code>asr</code></td>
|
||||
<td>cut</td>
|
||||
<td>Speech-to-text (per scene)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td><code>asrx</code></td>
|
||||
<td>asr</td>
|
||||
<td>Speaker diarization</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td><code>yolo</code></td>
|
||||
<td>—</td>
|
||||
<td>Object detection</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td><code>ocr</code></td>
|
||||
<td>—</td>
|
||||
<td>Text recognition</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td><code>face</code></td>
|
||||
<td>—</td>
|
||||
<td>Face detection & embedding</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td><code>pose</code></td>
|
||||
<td>—</td>
|
||||
<td>Pose estimation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td><code>visual_chunk</code></td>
|
||||
<td>yolo</td>
|
||||
<td>Visual scene chunks</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td><code>story</code></td>
|
||||
<td>asr, asrx, cut, yolo, face</td>
|
||||
<td>Scene summaries (template)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10</td>
|
||||
<td><code>5w1h</code></td>
|
||||
<td>story</td>
|
||||
<td>5W1H analysis (Gemma4 LLM)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>All processors except <code>story</code> and <code>5w1h</code> run concurrently when their dependencies are met. Story and 5W1H run sequentially after their prerequisites.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/progress/</span><span class="nv">$FILE_UUID</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{overall_progress, processors: [.processors[] | {processor_type, status}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"overall_progress"</span><span class="p">:</span><span class="w"> </span><span class="mi">71</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"cpu_percent"</span><span class="p">:</span><span class="w"> </span><span class="mf">45.2</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"gpu_percent"</span><span class="p">:</span><span class="w"> </span><span class="mf">30.1</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"memory_percent"</span><span class="p">:</span><span class="w"> </span><span class="mf">62.4</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"processors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"processor_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"asr"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"complete"</span><span class="p">,</span><span class="w"> </span><span class="nt">"progress"</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"processor_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"yolo"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"running"</span><span class="p">,</span><span class="w"> </span><span class="nt">"progress"</span><span class="p">:</span><span class="w"> </span><span class="mi">65</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"processor_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"face"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="p">,</span><span class="w"> </span><span class="nt">"progress"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>32-char hex UUID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>overall_progress</code></td>
|
||||
<td>integer</td>
|
||||
<td>Overall progress percentage (0–100)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>processors</code></td>
|
||||
<td>array</td>
|
||||
<td>Per-processor status list</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>processors[].processor_type</code></td>
|
||||
<td>string</td>
|
||||
<td>Processor name (<code>asr</code>, <code>cut</code>, <code>yolo</code>, etc.)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>processors[].status</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"pending"</code>, <code>"running"</code>, <code>"complete"</code>, or <code>"failed"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>processors[].progress</code></td>
|
||||
<td>integer</td>
|
||||
<td>Per-processor progress (0–100)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>processors[].eta_seconds</code></td>
|
||||
<td>integer</td>
|
||||
<td>Estimated seconds remaining (running processors)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>processors[].current</code></td>
|
||||
<td>integer</td>
|
||||
<td>Current frame count</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>processors[].total</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total frame count</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>cpu_percent</code></td>
|
||||
<td>float</td>
|
||||
<td>Current CPU usage</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>gpu_percent</code></td>
|
||||
<td>float</td>
|
||||
<td>Current GPU utilization</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>memory_percent</code></td>
|
||||
<td>float</td>
|
||||
<td>Current memory usage</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/jobs</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: system-level</p>
|
||||
<p>List all processing jobs (monitor jobs) in the system. Shows job status, which file each job is processing, and current processor info.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/jobs"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{count, jobs: [.jobs[] | {uuid, status}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"jobs"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"running"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"current_processor"</span><span class="p">:</span><span class="w"> </span><span class="s2">"yolo"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-16T12:00:00Z"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"started_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-16T12:01:00Z"</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"count"</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"page"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"page_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>jobs</code></td>
|
||||
<td>array</td>
|
||||
<td>Array of job info objects</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>jobs[].id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Job ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>jobs[].uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>File UUID being processed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>jobs[].status</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"pending"</code>, <code>"running"</code>, <code>"completed"</code>, <code>"failed"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>jobs[].current_processor</code></td>
|
||||
<td>string</td>
|
||||
<td>Currently active processor, or null</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total job count</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>Current page number</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>Jobs per page</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
280
deliverable_v1.1.0/html_docs/doc/06_search.html
Normal file
280
deliverable_v1.1.0/html_docs/doc/06_search.html
Normal file
@@ -0,0 +1,280 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>06 Search - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: search -->
|
||||
<!-- description: Vector search, BM25, smart search, universal search, visual search -->
|
||||
<!-- depends: 01_auth -->
|
||||
|
||||
<h2>Search APIs</h2>
|
||||
<h3><code>POST /api/v1/search/smart</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Semantic vector search using EmbeddingGemma-300m. Generates a query embedding via EmbeddingGemma (port 11436), then searches pgvector <code>story_parent</code> and <code>llm_parent</code> chunks by cosine similarity.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>File UUID to search within</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>query</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>Search text</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>5</td>
|
||||
<td>Max results to return</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>1</td>
|
||||
<td>Page number</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>5</td>
|
||||
<td>Items per page</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/smart"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_uuid": "'</span><span class="s2">"</span><span class="nv">$FILE_UUID</span><span class="s2">"</span><span class="s1">'", "query": "Audrey Hepburn"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"results"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"parent_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1087822</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"scene_order"</span><span class="p">:</span><span class="w"> </span><span class="mi">1087822</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">104438</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">104538</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">24.0</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">4351.6</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">4355.76</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"summary"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[4352s-4356s, 4s] Cast: Audrey Hepburn. Total: 2 lines, 10 words. Speakers: Audrey Hepburn (2 lines)"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"similarity"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.67</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"page"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"page_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"strategy"</span><span class="p">:</span><span class="w"> </span><span class="s2">"semantic_vector_search"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/search/universal</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Multi-type BM25 full-text search across chunks, frames, and persons. Uses PostgreSQL <code>tsvector</code>.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>query</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>Search text</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Restrict to specific file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>types</code></td>
|
||||
<td>string[]</td>
|
||||
<td>No</td>
|
||||
<td><code>["chunk","frame","person"]</code></td>
|
||||
<td>Search types</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>10</td>
|
||||
<td>Max results per type</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>1</td>
|
||||
<td>Page number</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>20</td>
|
||||
<td>Items per page</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_uuid": "'</span><span class="s2">"</span><span class="nv">$FILE_UUID</span><span class="s2">"</span><span class="s1">'", "query": "Cary Grant"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"results"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"chunk"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bd80fec92b0b6963d177a2c55bf713e2_2"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"story_child"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">5103</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">5127</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">212.64</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">213.64</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[213s-214s] Cary Grant: \"Olá!\""</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"took_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">18</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/search/frames</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Search face detection frames by identity name or trace ID.</p>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/search/identity_text</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Search text chunks spoken by a specific identity.</p>
|
||||
<hr />
|
||||
<h3>Visual Search</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>POST</td>
|
||||
<td><code>/api/v1/search/visual</code></td>
|
||||
<td>Search visual chunks</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>POST</td>
|
||||
<td><code>/api/v1/search/visual/class</code></td>
|
||||
<td>Search by object class</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>POST</td>
|
||||
<td><code>/api/v1/search/visual/density</code></td>
|
||||
<td>Search by object density</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>POST</td>
|
||||
<td><code>/api/v1/search/visual/combination</code></td>
|
||||
<td>Search by object combination</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>POST</td>
|
||||
<td><code>/api/v1/search/visual/stats</code></td>
|
||||
<td>Visual chunk statistics</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Embedding Model</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Detail</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Model</strong></td>
|
||||
<td>EmbeddingGemma-300m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Endpoint</strong></td>
|
||||
<td><code>POST /api/v1/embeddings</code> on port 11436</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Dimension</strong></td>
|
||||
<td>768</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Storage</strong></td>
|
||||
<td>pgvector (<code>chunk.embedding</code> column)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
510
deliverable_v1.1.0/html_docs/doc/07_identity.html
Normal file
510
deliverable_v1.1.0/html_docs/doc/07_identity.html
Normal file
@@ -0,0 +1,510 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>07 Identity - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: identity -->
|
||||
<!-- description: Global identities — CRUD, detail, files, faces, bind, unbind, search -->
|
||||
<!-- depends: 01_auth -->
|
||||
|
||||
<h2>Global Identities</h2>
|
||||
<h3><code>GET /api/v1/identities</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>List all registered identities with pagination.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identities?page=1&page_size=20"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{count, identities: [.identities[] | {name}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/identity/:identity_uuid</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Get detailed information for a specific identity, including metadata and TMDb references.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"people"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tmdb"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"confirmed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"tmdb_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">112</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"tmdb_profile"</span><span class="p">:</span><span class="w"> </span><span class="s2">"{output}/identities/{identity_uuid}/profile.jpg"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span>
|
||||
<span class="w"> </span><span class="nt">"reference_data"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span>
|
||||
<span class="w"> </span><span class="nt">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-16T12:00:00Z"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"updated_at"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>identity_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>Identity identifier</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>name</code></td>
|
||||
<td>string</td>
|
||||
<td>Identity name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>identity_type</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"people"</code> or null</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>source</code></td>
|
||||
<td>string</td>
|
||||
<td><code>.json</code>, <code>auto</code>, <code>tmdb</code>, <code>user_defined</code>, or <code>merged</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>status</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"confirmed"</code>, <code>"pending"</code>, or <code>"inactive"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tmdb_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>TMDb person ID (only if source = tmdb)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tmdb_profile</code></td>
|
||||
<td>string</td>
|
||||
<td>Local profile image path (<code>{output}/identities/{uuid}/profile.jpg</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>metadata</code></td>
|
||||
<td>object</td>
|
||||
<td>Metadata JSON (tmdb_character, cast_order, etc.)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>created_at</code></td>
|
||||
<td>string</td>
|
||||
<td>Creation timestamp</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>DELETE /api/v1/identity/:identity_uuid</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Delete an identity permanently.</p>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/identity/:identity_uuid/files</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Get all files where this identity appears. Returns per-file summary including face count, confidence, and appearance time range.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/files"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/identity/:identity_uuid/faces</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Get all face detection records associated with this identity.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/faces"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>File where face was detected</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>frame_number</code></td>
|
||||
<td>integer</td>
|
||||
<td>Frame number of detection</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>face_id</code></td>
|
||||
<td>string</td>
|
||||
<td>Face ID (format: <code>face_{frame_number}</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>confidence</code></td>
|
||||
<td>float</td>
|
||||
<td>Detection confidence</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/identity/:identity_uuid/chunks</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Get all text chunks (sentences) spoken while this identity's face was on screen. Useful for finding what a person said.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/chunks"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bd80fec92b0b6963d177a2c55bf713e2"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bd80fec92b0b6963d177a2c55bf713e2_2"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sentence"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">5103</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">5127</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">24.0</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">212.64</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">213.64</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text_content"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[213s-214s] Cary Grant: \"Olá!\""</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>File identifier</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>chunk_id</code></td>
|
||||
<td>string</td>
|
||||
<td>Sentence chunk identifier</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>start_frame</code></td>
|
||||
<td>integer</td>
|
||||
<td>Frame-accurate start position</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>end_frame</code></td>
|
||||
<td>integer</td>
|
||||
<td>Frame-accurate end position</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fps</code></td>
|
||||
<td>float</td>
|
||||
<td>Frames per second</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>start_time</code></td>
|
||||
<td>float</td>
|
||||
<td>Start time in seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>end_time</code></td>
|
||||
<td>float</td>
|
||||
<td>End time in seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>text_content</code></td>
|
||||
<td>string</td>
|
||||
<td>Spoken text content</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/identity/:identity_uuid/bind</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Bind a face detection to an identity. Associates the face trace with the identity for future search and recognition.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>File where face is detected</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>face_id</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>Face ID (format: <code>{frame}_{idx}</code>)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/bind"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_uuid": "'</span><span class="s2">"</span><span class="nv">$FILE_UUID</span><span class="s2">"</span><span class="s1">'", "face_id": "1_5"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/identity/:identity_uuid/unbind</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Unbind a face detection from an identity. Removes the identity association from the face record.</p>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/identities/search</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Search identities by name (ILIKE search). Returns matching identity records.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identities/search?q=Cary"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>name</code></td>
|
||||
<td>string</td>
|
||||
<td>Identity name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>source</code></td>
|
||||
<td>string</td>
|
||||
<td>Identity source</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tmdb_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>TMDb ID (if source = tmdb)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>Associated file</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/identity/upload</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Upload an identity.json file to create or update an identity. Accepts the same format as the identity.json files stored on disk.</p>
|
||||
<p>If an identity with the same <code>name</code> already exists, it will be updated with the new values.</p>
|
||||
<h4>Request</h4>
|
||||
<p>The request body is an <code>IdentityFile</code> object:</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>identity_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>Identity identifier</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>name</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>Identity display name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>identity_type</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>"people"</code> or null</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>source</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>.json</code>, <code>auto</code>, <code>tmdb</code>, <code>user_defined</code>, or <code>merged</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>status</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>"confirmed"</code>, <code>"pending"</code>, or <code>"inactive"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tmdb_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>TMDb person ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tmdb_profile</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>TMDb profile image URL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>metadata</code></td>
|
||||
<td>object</td>
|
||||
<td>No</td>
|
||||
<td>Arbitrary metadata JSON</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_bindings</code></td>
|
||||
<td>array</td>
|
||||
<td>No</td>
|
||||
<td>Array of <code>{ file_uuid, trace_ids, face_count }</code> (informational)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/upload"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{</span>
|
||||
<span class="s1"> "version": 1,</span>
|
||||
<span class="s1"> "identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",</span>
|
||||
<span class="s1"> "name": "Cary Grant",</span>
|
||||
<span class="s1"> "identity_type": "people",</span>
|
||||
<span class="s1"> "source": ".json",</span>
|
||||
<span class="s1"> "status": "confirmed",</span>
|
||||
<span class="s1"> "metadata": {},</span>
|
||||
<span class="s1"> "file_bindings": []</span>
|
||||
<span class="s1"> }'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Identity uploaded successfully"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/identity/:identity_uuid/profile-image</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Upload a profile image (JPEG or PNG) for an identity. The image is saved to <code>{output}/identities/{uuid}/profile.{ext}</code>.</p>
|
||||
<p>Uses <code>multipart/form-data</code> with field name <code>image</code>.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/profile-image"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-F<span class="w"> </span><span class="s2">"image=@/path/to/photo.jpg"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/path/to/output/identities/.../profile.jpg"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Profile image saved: profile.jpg"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Error Responses</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>400</code></td>
|
||||
<td>Missing image field or unsupported format</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>404</code></td>
|
||||
<td>Identity not found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>415</code></td>
|
||||
<td>Unsupported image type (use JPEG or PNG)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/identity/:identity_uuid/profile-image</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Retrieve the profile image for an identity. Returns the raw image data with appropriate Content-Type header.</p>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/profile-image"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-o<span class="w"> </span>profile.jpg
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Response Header</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>content-type</code></td>
|
||||
<td><code>image/jpeg</code> or <code>image/png</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
97
deliverable_v1.1.0/html_docs/doc/08_identity_agent.html
Normal file
97
deliverable_v1.1.0/html_docs/doc/08_identity_agent.html
Normal file
@@ -0,0 +1,97 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>08 Identity Agent - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: identity_agent -->
|
||||
<!-- description: Identity agent — match from photo, match from trace -->
|
||||
<!-- depends: 01_auth, 07_identity -->
|
||||
|
||||
<h2>Identity Agent</h2>
|
||||
<h3><code>POST /api/v1/agents/identity/match-from-photo</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Upload a face photo to match against known identities. Detects face via InsightFace, extracts 512D embedding via CoreML FaceNet, then searches pgvector for the closest identity.</p>
|
||||
<h4>Request</h4>
|
||||
<p><code>multipart/form-data</code> with field <code>image</code> (JPEG/PNG) and optional <code>file_uuid</code>.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/agents/identity/match-from-photo"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-F<span class="w"> </span><span class="s2">"image=@/path/to/face.jpg"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-F<span class="w"> </span><span class="s2">"file_uuid=</span><span class="nv">$FILE_UUID</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"matches"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a90105..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"similarity"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.87</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/agents/identity/match-from-trace</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Match a face trace (tracked face across frames) against known identities. Samples 3 angles from the trace, generates embeddings, and searches pgvector.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>File containing the trace</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>trace_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Yes</td>
|
||||
<td>Face trace ID to match</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/agents/identity/match-from-trace"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_uuid": "'</span><span class="s2">"</span><span class="nv">$FILE_UUID</span><span class="s2">"</span><span class="s1">'", "trace_id": 10}'</span>
|
||||
</code></pre></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
303
deliverable_v1.1.0/html_docs/doc/08_media.html
Normal file
303
deliverable_v1.1.0/html_docs/doc/08_media.html
Normal file
@@ -0,0 +1,303 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>08 Media - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: media -->
|
||||
<!-- description: Video streaming & frame extraction -->
|
||||
<!-- depends: 01_auth -->
|
||||
|
||||
<h2>Video Streaming & Frame Extraction</h2>
|
||||
<p>All video streaming endpoints support the following common query parameters:</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>mode</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>normal</code></td>
|
||||
<td><code>normal</code> or <code>debug</code> (draws detection overlays)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>audio</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>on</code></td>
|
||||
<td><code>on</code> or <code>off</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/file/:file_uuid/video</code></h3>
|
||||
<p>Stream the full video file with range support for seeking.</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<h4>Response</h4>
|
||||
<ul>
|
||||
<li><strong>200</strong>: Video stream (<code>Content-Type</code> based on file extension)</li>
|
||||
<li><strong>206</strong>: Partial content (range request)</li>
|
||||
<li>Supports <code>Range</code> header for seeking</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/file/:file_uuid/trace/:trace_id/video</code></h3>
|
||||
<p>Stream video with highlights for a specific face trace (follows a single person across frames with bounding box overlay).</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/file/:file_uuid/video/bbox</code></h3>
|
||||
<p>Stream video with bounding box overlay for all detected objects/faces.</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Uses a built-in 5×7 bitmap font renderer to draw labels directly on video frames via FFmpeg <code>drawtext</code> filter.</p>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/file/:file_uuid/thumbnail</code></h3>
|
||||
<p>Extract a single frame from a video as JPEG image. Uses FFmpeg <code>select</code> filter.</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<h4>Query Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>frame</code></td>
|
||||
<td>integer</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>Zero-based frame number to extract</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>x</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Crop start X (left edge). Requires <code>y</code>, <code>w</code>, <code>h</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>y</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Crop start Y (top edge). Requires <code>x</code>, <code>w</code>, <code>h</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>w</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Crop width in pixels. Requires <code>x</code>, <code>y</code>, <code>h</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>h</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Crop height in pixels. Requires <code>x</code>, <code>y</code>, <code>w</code>.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>All four crop params (<code>x</code>, <code>y</code>, <code>w</code>, <code>h</code>) must be provided together or omitted.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Extract frame 1000 (full frame)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/bd80fec92b0b6963d177a2c55bf713e2/thumbnail?frame=1000"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span>-o<span class="w"> </span>frame_1000.jpg
|
||||
|
||||
<span class="c1"># Extract and crop face region (x=320, y=240, w=160, h=160)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/bd80fec92b0b6963d177a2c55bf713e2/thumbnail?frame=1000&x=320&y=240&w=160&h=160"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span>-o<span class="w"> </span>face_crop.jpg
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response</h4>
|
||||
<ul>
|
||||
<li><strong>200</strong>: <code>image/jpeg</code> binary data</li>
|
||||
<li><strong>404</strong>: File not found</li>
|
||||
<li><strong>500</strong>: FFmpeg error (e.g., frame number exceeds video duration)</li>
|
||||
</ul>
|
||||
<h3><code>GET /api/v1/file/:file_uuid/clip</code></h3>
|
||||
<p>Extract a video clip (time range) as MPEG-TS stream. Uses FFmpeg <code>-ss</code> fast seek.</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<h4>Query Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>start_frame</code></td>
|
||||
<td>integer</td>
|
||||
<td>No*</td>
|
||||
<td>—</td>
|
||||
<td>Start frame (zero-based). <strong>Frame-accurate</strong> — use this for precision.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>end_frame</code></td>
|
||||
<td>integer</td>
|
||||
<td>No*</td>
|
||||
<td>—</td>
|
||||
<td>End frame (zero-based, inclusive). Requires <code>start_frame</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>start_time</code></td>
|
||||
<td>float</td>
|
||||
<td>No*</td>
|
||||
<td>—</td>
|
||||
<td>Start time in seconds. Approximate (FPS-dependent). Fallback if frames not given.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>end_time</code></td>
|
||||
<td>float</td>
|
||||
<td>No*</td>
|
||||
<td>—</td>
|
||||
<td>End time in seconds. Approximate (FPS-dependent). Fallback if frames not given.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fps</code></td>
|
||||
<td>float</td>
|
||||
<td>No</td>
|
||||
<td>video FPS</td>
|
||||
<td>Override frames-per-second for frame↔time calculation. Defaults to video's detected FPS.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>mode</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>normal</code></td>
|
||||
<td><code>normal</code> or <code>debug</code> (draws "CLIP" overlay)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>audio</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>on</code></td>
|
||||
<td><code>on</code> or <code>off</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Either (<code>start_frame</code>+<code>end_frame</code>) OR (<code>start_time</code>+<code>end_time</code>) must be provided.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Clip by frame range (primary)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/bd80fec92b0b6963d177a2c55bf713e2/clip?start_frame=0&end_frame=47"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span>-o<span class="w"> </span>clip.ts
|
||||
|
||||
<span class="c1"># Clip by time range (fallback)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/bd80fec92b0b6963d177a2c55bf713e2/clip?start_time=30&end_time=45"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span>-o<span class="w"> </span>clip.ts
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response</h4>
|
||||
<ul>
|
||||
<li><strong>200</strong>: <code>video/mp2t</code> MPEG-TS stream</li>
|
||||
<li><strong>400</strong>: Missing/invalid range parameters</li>
|
||||
<li><strong>404</strong>: File not found</li>
|
||||
<li><strong>500</strong>: FFmpeg error</li>
|
||||
</ul>
|
||||
<h4>Technical Notes</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Detail</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Backend</strong></td>
|
||||
<td>FFmpeg (<code>ffmpeg-full</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Seek</strong></td>
|
||||
<td><code>-ss</code> before <code>-i</code> (fast keyframe seek)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Format</strong></td>
|
||||
<td>MPEG-TS (<code>mpegts</code> muxer, pipe-safe)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Codec</strong></td>
|
||||
<td>H.264 + AAC</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Cache</strong></td>
|
||||
<td><code>Cache-Control: public, max-age=86400</code> (24h)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Detail</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Backend</strong></td>
|
||||
<td>FFmpeg (<code>ffmpeg-full</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Filter</strong></td>
|
||||
<td><code>select=eq(n\,FRAME)</code> to select frame, optional <code>crop=W:H:X:Y</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Output</strong></td>
|
||||
<td>Single JPEG via pipe (<code>image2pipe</code>, <code>mjpeg</code> codec)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Cache</strong></td>
|
||||
<td><code>Cache-Control: public, max-age=86400</code> (24h)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Frame number</strong></td>
|
||||
<td>Zero-based (<code>frame=0</code> = first frame of video)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
123
deliverable_v1.1.0/html_docs/doc/09_tmdb.html
Normal file
123
deliverable_v1.1.0/html_docs/doc/09_tmdb.html
Normal file
@@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>09 Tmdb - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: tmdb -->
|
||||
<!-- description: TMDb enrichment endpoints — prefetch, probe, resource, check -->
|
||||
<!-- depends: 01_auth, 03_register -->
|
||||
|
||||
<h2>TMDb Enrichment</h2>
|
||||
<blockquote>
|
||||
<p><strong>Offline operation</strong>: TMDb prefetch now checks local identity files first (<code>identities/_index.json</code> + <code>*.tmdb.json</code>).
|
||||
If local files exist, no external API call is made. Internet is only needed for initial data seeding.</p>
|
||||
</blockquote>
|
||||
<h3>Overview</h3>
|
||||
<p>TMDb enrichment is an optional identity enrichment step that can be run after Pipeline face detection completes. The workflow is:</p>
|
||||
<ol>
|
||||
<li><strong>Prefetch</strong> (requires internet): Download movie cast data from TMDb API → cache to <code>{file_uuid}.tmdb.json</code></li>
|
||||
<li><strong>Probe</strong>: Read local cache → create identities for <strong>all</strong> cast members (<code>source='tmdb'</code>) + save <code>identity.json</code> + download profile image to <code>{OUTPUT}/identities/{uuid}/profile.jpg</code></li>
|
||||
<li><strong>Match</strong>: The worker automatically matches video faces against TMDb identities when <code>MOMENTRY_TMDB_PROBE_ENABLED=true</code></li>
|
||||
</ol>
|
||||
<h3><code>POST /api/v1/agents/tmdb/prefetch</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Fetch TMDb cast data for a registered file and cache it locally. This is the only step requiring internet access.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>File UUID to enrich</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/agents/tmdb/prefetch"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_uuid": "'</span><span class="s2">"</span><span class="nv">$FILE_UUID</span><span class="s2">"</span><span class="s1">'"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nt">"cache_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/output/...tmdb.json"</span><span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3><code>POST /api/v1/file/:file_uuid/tmdb-probe</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Read local TMDb cache and create/update identities. Requires prefetch to have been run first.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/tmdb-probe"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{identities_created, movie_title}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200 — identities created)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"> </span><span class="nt">"identities_created"</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="p">,</span><span class="w"> </span><span class="nt">"movie_title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade"</span><span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200 — no cache)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"No TMDb cache found. Run tmdb-prefetch first."</span><span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3><code>GET /api/v1/resource/tmdb</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: system-level</p>
|
||||
<p>View TMDb resource status including configuration, identity counts, and cache file count.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{identities_seeded, cache_files}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3><code>POST /api/v1/resource/tmdb/check</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: system-level</p>
|
||||
<p>Ping the TMDb API to verify connectivity and measure latency.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb/check"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'.status'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"api_key_configured"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"api_reachable"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"api_latency_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">120</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
364
deliverable_v1.1.0/html_docs/doc/10_pipeline.html
Normal file
364
deliverable_v1.1.0/html_docs/doc/10_pipeline.html
Normal file
@@ -0,0 +1,364 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>10 Pipeline - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: pipeline -->
|
||||
<!-- description: Pipeline processors, ingestion status, stats endpoints -->
|
||||
<!-- depends: 01_auth -->
|
||||
|
||||
<h2>Pipeline</h2>
|
||||
<h3>Dependency Graph</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="n">flowchart</span><span class="w"> </span><span class="n">TB</span>
|
||||
<span class="w"> </span><span class="n">subgraph</span><span class="w"> </span><span class="n">Processors</span><span class="p">[</span><span class="s">"10 Processors"</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">Cut</span><span class="p">[</span><span class="n">Cut</span><span class="p">]</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">ASR</span><span class="p">[</span><span class="n">ASR</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">ASR</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">ASRX</span><span class="p">[</span><span class="n">ASRX</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">ASRX</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Story</span><span class="p">[</span><span class="n">Story</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">Cut</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Story</span>
|
||||
<span class="w"> </span><span class="n">YOLO</span><span class="p">[</span><span class="n">YOLO</span><span class="p">]</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">VisualChunk</span><span class="p">[</span><span class="n">VisualChunk</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">VisualChunk</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Story</span>
|
||||
<span class="w"> </span><span class="n">Face</span><span class="p">[</span><span class="n">Face</span><span class="p">]</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Story</span>
|
||||
<span class="w"> </span><span class="n">Story</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">FiveW1H</span><span class="p">[</span><span class="mi">5</span><span class="n">W1H</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">OCR</span><span class="p">[</span><span class="n">OCR</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">Pose</span><span class="p">[</span><span class="n">Pose</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">end</span>
|
||||
|
||||
<span class="w"> </span><span class="n">subgraph</span><span class="w"> </span><span class="n">Ingestion</span><span class="p">[</span><span class="s">"入庫 (Post-Processing)"</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">ASR</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Rule1</span><span class="p">[</span><span class="n">Rule</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="n">Sentence</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">ASRX</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Rule1</span>
|
||||
<span class="w"> </span><span class="n">Rule1</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Vectorize</span><span class="p">[</span><span class="n">Auto</span><span class="o">-</span><span class="n">Vectorize</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">Rule1</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Phase1</span><span class="p">[</span><span class="n">Phase</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="n">Pack</span><span class="p">]</span>
|
||||
|
||||
<span class="w"> </span><span class="n">Cut</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Rule3</span><span class="p">[</span><span class="n">Rule</span><span class="w"> </span><span class="mi">3</span><span class="w"> </span><span class="n">Scene</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">ASR</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Rule3</span>
|
||||
|
||||
<span class="w"> </span><span class="n">Face</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Trace</span><span class="p">[</span><span class="n">Face</span><span class="w"> </span><span class="n">Trace</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">Trace</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Qdrant</span><span class="p">[</span><span class="n">Qdrant</span><span class="w"> </span><span class="n">Sync</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">Trace</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">TraceChunks</span><span class="p">[</span><span class="n">Trace</span><span class="w"> </span><span class="n">Chunks</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">Trace</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">TKG</span><span class="p">[</span><span class="n">TKG</span><span class="w"> </span><span class="n">Builder</span><span class="p">]</span>
|
||||
|
||||
<span class="w"> </span><span class="n">Face</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">TMDbMatch</span><span class="p">[</span><span class="n">TMDb</span><span class="w"> </span><span class="n">Match</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">Face</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">SceneMeta</span><span class="p">[</span><span class="n">Scene</span><span class="w"> </span><span class="n">Metadata</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">YOLO</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">SceneMeta</span>
|
||||
<span class="w"> </span><span class="n">Face</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">IdentityAgent</span><span class="p">[</span><span class="n">Identity</span><span class="w"> </span><span class="n">Agent</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">ASRX</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">IdentityAgent</span>
|
||||
|
||||
<span class="w"> </span><span class="n">Cut</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Agent5W1H</span><span class="p">[</span><span class="mi">5</span><span class="n">W1H</span><span class="w"> </span><span class="n">Agent</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">ASR</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Agent5W1H</span>
|
||||
<span class="w"> </span><span class="n">Agent5W1H</span><span class="w"> </span><span class="o">--></span><span class="w"> </span><span class="n">Phase2</span><span class="p">[</span><span class="n">Phase</span><span class="w"> </span><span class="mi">2</span><span class="w"> </span><span class="n">Pack</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="n">end</span>
|
||||
|
||||
<span class="w"> </span><span class="n">style</span><span class="w"> </span><span class="n">Processors</span><span class="w"> </span><span class="n">fill</span><span class="o">:</span><span class="err">#</span><span class="mi">1</span><span class="n">a1a2e</span><span class="p">,</span><span class="n">stroke</span><span class="o">:</span><span class="err">#</span><span class="n">e94560</span>
|
||||
<span class="w"> </span><span class="n">style</span><span class="w"> </span><span class="n">Ingestion</span><span class="w"> </span><span class="n">fill</span><span class="o">:</span><span class="err">#</span><span class="mi">16213</span><span class="n">e</span><span class="p">,</span><span class="n">stroke</span><span class="o">:</span><span class="err">#</span><span class="mf">0f</span><span class="mi">3460</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3>Pipeline Completion Flow</h3>
|
||||
<p>The pipeline is <strong>not complete</strong> until both the 10 processors AND the 入庫 (ingestion) steps have finished. The worker polls every 3 seconds and only marks the job as <code>completed</code> when all ingestion steps verify OK.</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="mf">10</span><span class="w"> </span><span class="n">processors</span><span class="w"> </span><span class="n">done</span>
|
||||
<span class="w"> </span><span class="err">↓</span><span class="w"> </span><span class="p">(</span><span class="n">job</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="n">stays</span><span class="w"> </span><span class="s">"running"</span><span class="p">)</span>
|
||||
<span class="n">Algorithm</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="n">Trigger</span><span class="p">:</span><span class="w"> </span><span class="n">Rule</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">Vectorize</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">Phase</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="n">Pack</span>
|
||||
<span class="w"> </span><span class="err">↓</span><span class="w"> </span><span class="p">(</span><span class="n">job</span><span class="w"> </span><span class="kr">run</span><span class="n">s</span><span class="w"> </span><span class="n">in</span><span class="w"> </span><span class="n">parallel</span><span class="p">)</span>
|
||||
<span class="n">Algorithm</span><span class="w"> </span><span class="mf">2</span><span class="w"> </span><span class="n">Trigger</span><span class="p">:</span><span class="w"> </span><span class="n">Face</span><span class="w"> </span><span class="n">Trace</span><span class="w"> </span><span class="err">→</span><span class="w"> </span><span class="n">TKG</span><span class="p">,</span><span class="w"> </span><span class="n">Scene</span><span class="w"> </span><span class="n">Metadata</span><span class="p">,</span><span class="w"> </span><span class="n">Identity</span><span class="w"> </span><span class="n">Agent</span><span class="p">,</span><span class="w"> </span><span class="mf">5</span><span class="n">W1H</span><span class="w"> </span><span class="n">Agent</span>
|
||||
<span class="w"> </span><span class="err">↓</span><span class="w"> </span><span class="p">(</span><span class="n">poll</span><span class="w"> </span><span class="n">checks</span><span class="w"> </span><span class="n">every</span><span class="w"> </span><span class="mf">3</span><span class="n">s</span><span class="p">)</span>
|
||||
<span class="n">Ingestion</span><span class="w"> </span><span class="n">verification</span><span class="p">:</span><span class="w"> </span><span class="n">rule1</span><span class="w"> </span><span class="err">✓</span><span class="w"> </span><span class="n">vectorize</span><span class="w"> </span><span class="err">✓</span><span class="w"> </span><span class="n">rule3</span><span class="w"> </span><span class="err">✓</span><span class="w"> </span><span class="n">face_trace</span><span class="w"> </span><span class="err">✓</span><span class="w"> </span><span class="n">tkg</span><span class="w"> </span><span class="err">✓</span><span class="w"> </span><span class="n">scene_meta</span><span class="w"> </span><span class="err">✓</span><span class="w"> </span><span class="mf">5</span><span class="n">w1h</span><span class="w"> </span><span class="err">✓</span>
|
||||
<span class="w"> </span><span class="err">↓</span>
|
||||
<span class="n">job</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"completed"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3>10 Processor Stages</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Processor</th>
|
||||
<th>Depends On</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td><code>Cut</code></td>
|
||||
<td>—</td>
|
||||
<td>Scene boundary detection (PySceneDetect)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td><code>ASR</code></td>
|
||||
<td>Cut</td>
|
||||
<td>Automatic speech recognition (faster-whisper)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td><code>ASRX</code></td>
|
||||
<td>ASR</td>
|
||||
<td>Speaker diarization + ASR refinement</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td><code>YOLO</code></td>
|
||||
<td>—</td>
|
||||
<td>Object detection (YOLOv8)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td><code>OCR</code></td>
|
||||
<td>—</td>
|
||||
<td>Optical character recognition</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td><code>Face</code></td>
|
||||
<td>—</td>
|
||||
<td>Face detection + recognition (InsightFace + CoreML)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td><code>Pose</code></td>
|
||||
<td>—</td>
|
||||
<td>Pose estimation</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td><code>VisualChunk</code></td>
|
||||
<td>YOLO</td>
|
||||
<td>Visual object chunking</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td><code>Story</code></td>
|
||||
<td>ASRX + Cut + YOLO + Face</td>
|
||||
<td>Narrative scene summarization (LLM, with embedding)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10</td>
|
||||
<td><code>5W1H</code></td>
|
||||
<td>Story</td>
|
||||
<td>Who/What/When/Where/Why extraction (LLM, with embedding)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>入庫 (Post-Processing / Ingestion)</h3>
|
||||
<p>These steps run after the 10 processors and are <strong>required for pipeline completion</strong>. The worker checks all of them before marking the job as done.</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Step</th>
|
||||
<th>Triggers When</th>
|
||||
<th>Verification</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>1</td>
|
||||
<td><strong>Rule 1 Sentence Chunking</strong></td>
|
||||
<td>ASR + ASRX done</td>
|
||||
<td><code>chunk</code> table has rows with <code>chunk_type = 'sentence'</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>2</td>
|
||||
<td><strong>Auto-Vectorize</strong></td>
|
||||
<td>Rule 1 done</td>
|
||||
<td><code>chunk.embedding</code> IS NOT NULL for sentence chunks</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>3</td>
|
||||
<td><strong>Phase 1 Pack</strong></td>
|
||||
<td>Rule 1 done</td>
|
||||
<td><code>release_pack.py --phase 1</code> executed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>4</td>
|
||||
<td><strong>Rule 3 Scene Chunking</strong></td>
|
||||
<td>All 10 processors done + Cut + ASR</td>
|
||||
<td><code>chunk</code> table has rows with <code>chunk_type = 'cut'</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>5</td>
|
||||
<td><strong>Face Trace</strong></td>
|
||||
<td>All 10 processors done + Face</td>
|
||||
<td><code>face_detections.trace_id</code> IS NOT NULL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>6</td>
|
||||
<td><strong>Qdrant Face Sync</strong></td>
|
||||
<td>Face Trace done</td>
|
||||
<td>Qdrant face_embedding collection populated</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>7</td>
|
||||
<td><strong>Trace Chunks</strong></td>
|
||||
<td>Face Trace done</td>
|
||||
<td><code>chunk</code> table has rows with <code>chunk_type = 'trace'</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>8</td>
|
||||
<td><strong>TKG Builder</strong></td>
|
||||
<td>Face Trace done</td>
|
||||
<td><code>tkg_nodes</code> + <code>tkg_edges</code> tables have rows</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>9</td>
|
||||
<td><strong>TMDb Face Matching</strong></td>
|
||||
<td>TMDb enabled + Face done</td>
|
||||
<td><code>face_detections.identity_id</code> IS NOT NULL</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>10</td>
|
||||
<td><strong>Heuristic Scene Metadata</strong></td>
|
||||
<td>Face + YOLO done</td>
|
||||
<td><code>{file_uuid}.scene_meta.json</code> exists on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>11</td>
|
||||
<td><strong>Identity Agent</strong></td>
|
||||
<td>Face + ASRX done</td>
|
||||
<td><code>identities</code> with <code>source = 'identity_agent'</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>12</td>
|
||||
<td><strong>5W1H Agent</strong></td>
|
||||
<td>Cut + ASR done</td>
|
||||
<td><code>chunk.summary_text</code> IS NOT NULL for cut chunks</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>13</td>
|
||||
<td><strong>Release Pack</strong></td>
|
||||
<td>5W1H Agent done</td>
|
||||
<td><code>release_pack.py --phase 2</code> executed</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Ingestion Status</h3>
|
||||
<p>Check real-time ingestion status for a file:</p>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/stats/ingestion-status/{file_uuid}"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Returns per-step <code>done</code> / <code>pending</code> status with detail counts.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span><span class="s2">"http://localhost:3003/api/v1/stats/ingestion-status/bd80fec9c42afb0307eb28f22c64c76a"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'.steps[] | {name, status, detail}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bd80fec9c42afb0307eb28f22c64c76a"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"steps"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rule1_sentence"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="p">,</span><span class="w"> </span><span class="nt">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 sentence chunks"</span><span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"auto_vectorize"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="p">,</span><span class="w"> </span><span class="nt">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 embedded"</span><span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"rule3_scene"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="p">,</span><span class="w"> </span><span class="nt">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 scene chunks"</span><span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"face_trace"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="p">,</span><span class="w"> </span><span class="nt">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 traces"</span><span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"trace_chunks"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="p">,</span><span class="w"> </span><span class="nt">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 trace chunks"</span><span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tkg"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="p">,</span><span class="w"> </span><span class="nt">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 nodes, 0 edges"</span><span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"identity_match"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="p">,</span><span class="w"> </span><span class="nt">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 identities"</span><span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"scene_metadata"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="p">,</span><span class="w"> </span><span class="nt">"detail"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"5w1h"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="p">,</span><span class="w"> </span><span class="nt">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0 scenes with 5W1H"</span><span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3>Stats Endpoints</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Method</th>
|
||||
<th>Endpoint</th>
|
||||
<th>Auth</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>GET</td>
|
||||
<td><code>/api/v1/stats/sftpgo</code></td>
|
||||
<td>No</td>
|
||||
<td>SFTPGo service status</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>GET</td>
|
||||
<td><code>/api/v1/stats/ingestion-status/:file_uuid</code></td>
|
||||
<td>No</td>
|
||||
<td>Per-file ingestion checklist</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Configuration</h3>
|
||||
<h3><code>POST /api/v1/config/cache</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: system-level</p>
|
||||
<p>Toggle the Redis cache on or off.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>enabled</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Yes</td>
|
||||
<td><code>true</code> to enable, <code>false</code> to disable</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/config/cache"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"enabled": false}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3>Unmounted Routes</h3>
|
||||
<p>The following routes are defined in source code but are <strong>NOT</strong> currently mounted in the router:</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Endpoint</th>
|
||||
<th>Source file</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>/api/v1/search/persons</code></td>
|
||||
<td><code>universal_search.rs</code> (not mounted)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/api/v1/who</code></td>
|
||||
<td><code>who.rs</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>/api/v1/who/candidates</code></td>
|
||||
<td><code>who.rs</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
207
deliverable_v1.1.0/html_docs/doc/12_agent.html
Normal file
207
deliverable_v1.1.0/html_docs/doc/12_agent.html
Normal file
@@ -0,0 +1,207 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>12 Agent - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<h1>Agent Endpoints</h1>
|
||||
<p>Agent endpoints provide AI-powered capabilities including translation, identity analysis, and 5W1H extraction.</p>
|
||||
<h2>POST /api/v1/agents/translate</h2>
|
||||
<p>Translate text between languages using Gemma4 (llama.cpp, port 8082).</p>
|
||||
<h3>Request</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Hello, welcome to Momentry Core."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"target_language"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Traditional Chinese"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"source_language"</span><span class="p">:</span><span class="w"> </span><span class="s2">"English"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>text</code></td>
|
||||
<td>string</td>
|
||||
<td>✅</td>
|
||||
<td>Text to translate</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>target_language</code></td>
|
||||
<td>string</td>
|
||||
<td>✅</td>
|
||||
<td>Target language name (e.g. "Traditional Chinese", "Japanese")</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>source_language</code></td>
|
||||
<td>string</td>
|
||||
<td>❌</td>
|
||||
<td>Source language (default: "auto")</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Response</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"translated_text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"您好,歡迎使用 Momentry Core。"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"source_language_detected"</span><span class="p">:</span><span class="w"> </span><span class="s2">"English"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"model_used"</span><span class="p">:</span><span class="w"> </span><span class="s2">"google_gemma-4-26B-A4B-it-Q5_K_M.gguf"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3>Supported Language Pairs (tested)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Source</th>
|
||||
<th>Target</th>
|
||||
<th>Quality</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>English</td>
|
||||
<td>Traditional Chinese</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>English</td>
|
||||
<td>Japanese</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Chinese</td>
|
||||
<td>English</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>English</td>
|
||||
<td>French</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Chinese</td>
|
||||
<td>Japanese</td>
|
||||
<td>✅</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Model</h3>
|
||||
<ul>
|
||||
<li><strong>Model</strong>: Gemma4 26B (Q5_K_M)</li>
|
||||
<li><strong>Engine</strong>: llama.cpp at <code>localhost:8082</code></li>
|
||||
<li><strong>Endpoint</strong>: <code>/v1/chat/completions</code> (OpenAI-compatible)</li>
|
||||
<li><strong>Temperature</strong>: 0.1</li>
|
||||
<li><strong>Max tokens</strong>: 1024</li>
|
||||
</ul>
|
||||
<h3>Errors</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<th>Condition</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>500</td>
|
||||
<td>LLM unreachable or response parse failure</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>401</td>
|
||||
<td>Missing/invalid auth</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>POST /api/v1/agents/5w1h/analyze</h2>
|
||||
<p>Extract 5W1H (Who, What, When, Where, Why, How) from a scene. Uses Gemma4 LLM on port 8082.</p>
|
||||
<h3>Request</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"scene_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3>Response</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"5w1h"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"who"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Cary Grant"</span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"what"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"discussing plans"</span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"when"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"1963"</span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"where"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"Paris"</span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"why"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"vacation"</span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"how"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"in person"</span><span class="p">]</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h2>POST /api/v1/agents/5w1h/batch</h2>
|
||||
<p>Batch analyze all scenes in a file for 5W1H extraction. Uses the pipeline's <code>parent_chunk_5w1h.py --mode llm</code>.</p>
|
||||
<h3>Request</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h2>GET /api/v1/agents/5w1h/status</h2>
|
||||
<p>Get status of the 5W1H agent pipeline for a file.</p>
|
||||
<hr />
|
||||
<h2>Embedding Model</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Detail</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Model</strong></td>
|
||||
<td>EmbeddingGemma-300m</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Endpoint</strong></td>
|
||||
<td><code>POST /v1/embeddings</code> on port 11436</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Dimension</strong></td>
|
||||
<td>768</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Used by</strong></td>
|
||||
<td><code>parent_chunk_5w1h.py --embed</code>, story, 5W1H, search</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
29
deliverable_v1.1.0/html_docs/doc/index.html
Normal file
29
deliverable_v1.1.0/html_docs/doc/index.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Momentry API 文件</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 900px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 28px; margin-bottom: 8px; }
|
||||
p.subtitle { color: #666; margin-bottom: 24px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
tr { border-bottom: 1px solid #eee; }
|
||||
tr:last-child { border: none; }
|
||||
td { padding: 10px 0; }
|
||||
td.cn { width: 140px; font-weight: 600; color: #333; }
|
||||
td.en { color: #666; font-size: 14px; }
|
||||
a { color: #0066cc; text-decoration: none; display: block; }
|
||||
a:hover td { background: #f8f8f8; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Momentry API 文件</h1>
|
||||
<p class="subtitle">API 參考手冊 — 登入後可瀏覽各模組文件</p>
|
||||
<table><tr onclick="window.location='01_auth.html'" style="cursor:pointer"><td class="cn">安全認證</td><td class="en">Authentication</td></tr><tr onclick="window.location='02_health.html'" style="cursor:pointer"><td class="cn">健康檢查</td><td class="en">Health</td></tr><tr onclick="window.location='03_register.html'" style="cursor:pointer"><td class="cn">檔案註冊</td><td class="en">File Registration</td></tr><tr onclick="window.location='04_lookup.html'" style="cursor:pointer"><td class="cn">檔案屬性查詢</td><td class="en">File Lookup</td></tr><tr onclick="window.location='05_process.html'" style="cursor:pointer"><td class="cn">處理流程</td><td class="en">Processing</td></tr><tr onclick="window.location='06_search.html'" style="cursor:pointer"><td class="cn">搜尋功能</td><td class="en">Search</td></tr><tr onclick="window.location='07_identity.html'" style="cursor:pointer"><td class="cn">身份識別</td><td class="en">Identity</td></tr><tr onclick="window.location='08_identity_agent.html'" style="cursor:pointer"><td class="cn">智能身份綁定</td><td class="en">Smart Identity Binding</td></tr><tr onclick="window.location='08_media.html'" style="cursor:pointer"><td class="cn">串流與截圖</td><td class="en">Streaming & Thumbnails</td></tr><tr onclick="window.location='09_tmdb.html'" style="cursor:pointer"><td class="cn">TMDb 整合</td><td class="en">TMDb Integration</td></tr><tr onclick="window.location='10_pipeline.html'" style="cursor:pointer"><td class="cn">生產線</td><td class="en">Pipeline</td></tr><tr onclick="window.location='12_agent.html'" style="cursor:pointer"><td class="cn">智慧代理</td><td class="en">AI Agents</td></tr></table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
46
deliverable_v1.1.0/html_docs/doc/login.html
Normal file
46
deliverable_v1.1.0/html_docs/doc/login.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login - Momentry Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
.card { background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; width: 360px; }
|
||||
h1 { font-size: 24px; margin-bottom: 24px; text-align: center; }
|
||||
input { width: 100%; padding: 10px 12px; margin-bottom: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
||||
button { width: 100%; padding: 10px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
|
||||
button:hover { background: #0052a3; }
|
||||
.error { color: #cc0000; font-size: 13px; margin-bottom: 12px; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Momentry Docs</h1>
|
||||
<form id="loginForm">
|
||||
<input type="text" id="username" placeholder="Username" value="demo" required>
|
||||
<input type="password" id="password" placeholder="Password" value="demo" required>
|
||||
<div class="error" id="error">Invalid credentials</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('loginForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
const resp = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.href = '/doc/index.html';
|
||||
} else {
|
||||
document.getElementById('error').style.display = 'block';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
180
deliverable_v1.1.0/html_docs/doc_developer/11_error_codes.html
Normal file
180
deliverable_v1.1.0/html_docs/doc_developer/11_error_codes.html
Normal file
@@ -0,0 +1,180 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>11 Error Codes - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<!-- module: error_codes -->
|
||||
<!-- description: Standard API error codes -->
|
||||
<!-- depends: -->
|
||||
|
||||
<h2>Error Response Format</h2>
|
||||
<p>All API errors follow this JSON structure:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"error"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"E001_NOT_FOUND"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Resource not found"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"details"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"file_uuid"</span><span class="p">,</span><span class="w"> </span><span class="nt">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"abc"</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h2>Error Code List</h2>
|
||||
<h3>Generic Errors (E0xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E001_NOT_FOUND</code></td>
|
||||
<td>404</td>
|
||||
<td>Resource not found (file, identity, chunk)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E002_DUPLICATE</code></td>
|
||||
<td>409</td>
|
||||
<td>Resource already exists</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E003_VALIDATION</code></td>
|
||||
<td>400</td>
|
||||
<td>Request parameter validation failed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E004_UNAUTHORIZED</code></td>
|
||||
<td>401</td>
|
||||
<td>Invalid API key or token</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E005_INTERNAL</code></td>
|
||||
<td>500</td>
|
||||
<td>Internal server error</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Processor Errors (E1xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E101_PROCESSOR_FAIL</code></td>
|
||||
<td>500</td>
|
||||
<td>Python script execution failed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E102_TIMEOUT</code></td>
|
||||
<td>504</td>
|
||||
<td>Processing timeout</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E103_RESUME_FAIL</code></td>
|
||||
<td>500</td>
|
||||
<td>Resume failed (checkpoint not found)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E104_NO_VIDEO</code></td>
|
||||
<td>400</td>
|
||||
<td>Video file path not found</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Identity Errors (E2xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E201_FACE_NOT_FOUND</code></td>
|
||||
<td>404</td>
|
||||
<td>Face detection not found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E202_MERGE_CONFLICT</code></td>
|
||||
<td>409</td>
|
||||
<td>Identity merge conflict</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E203_CANDIDATE_EMPTY</code></td>
|
||||
<td>404</td>
|
||||
<td>No candidates available for confirmation</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>TMDb Errors (E3xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E301_TMDB_NO_KEY</code></td>
|
||||
<td>400</td>
|
||||
<td><code>TMDB_API_KEY</code> environment variable not set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E302_TMDB_UNREACHABLE</code></td>
|
||||
<td>502</td>
|
||||
<td>TMDb API unreachable or timed out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E303_TMDB_CACHE_NOT_FOUND</code></td>
|
||||
<td>200</td>
|
||||
<td>No local TMDb cache; run prefetch first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E304_TMDB_PROBE_FAILED</code></td>
|
||||
<td>500</td>
|
||||
<td>TMDb probe execution failed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E305_TMDB_MOVIE_NOT_FOUND</code></td>
|
||||
<td>404</td>
|
||||
<td>No matching TMDb movie found from filename</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
29
deliverable_v1.1.0/html_docs/doc_developer/index.html
Normal file
29
deliverable_v1.1.0/html_docs/doc_developer/index.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Momentry API 文件</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 900px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 28px; margin-bottom: 8px; }
|
||||
p.subtitle { color: #666; margin-bottom: 24px; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
tr { border-bottom: 1px solid #eee; }
|
||||
tr:last-child { border: none; }
|
||||
td { padding: 10px 0; }
|
||||
td.cn { width: 140px; font-weight: 600; color: #333; }
|
||||
td.en { color: #666; font-size: 14px; }
|
||||
a { color: #0066cc; text-decoration: none; display: block; }
|
||||
a:hover td { background: #f8f8f8; border-radius: 4px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Momentry API 文件</h1>
|
||||
<p class="subtitle">API 參考手冊 — 登入後可瀏覽各模組文件</p>
|
||||
<table><tr onclick="window.location='11_error_codes.html'" style="cursor:pointer"><td class="cn">錯誤碼</td><td class="en">Error Codes</td></tr></table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
46
deliverable_v1.1.0/html_docs/doc_developer/login.html
Normal file
46
deliverable_v1.1.0/html_docs/doc_developer/login.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login - Momentry Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
.card { background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; width: 360px; }
|
||||
h1 { font-size: 24px; margin-bottom: 24px; text-align: center; }
|
||||
input { width: 100%; padding: 10px 12px; margin-bottom: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
||||
button { width: 100%; padding: 10px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
|
||||
button:hover { background: #0052a3; }
|
||||
.error { color: #cc0000; font-size: 13px; margin-bottom: 12px; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Momentry Docs</h1>
|
||||
<form id="loginForm">
|
||||
<input type="text" id="username" placeholder="Username" value="demo" required>
|
||||
<input type="password" id="password" placeholder="Password" value="demo" required>
|
||||
<div class="error" id="error">Invalid credentials</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('loginForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
const resp = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.href = '/doc/index.html';
|
||||
} else {
|
||||
document.getElementById('error').style.display = 'block';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
280
deliverable_v1.1.0/modules/01_auth.md
Normal file
280
deliverable_v1.1.0/modules/01_auth.md
Normal file
@@ -0,0 +1,280 @@
|
||||
<!-- module: auth -->
|
||||
<!-- description: Authentication — login, logout, JWT, session cookie, API key -->
|
||||
<!-- depends: -->
|
||||
|
||||
## Base URL
|
||||
|
||||
| Environment | URL | Purpose |
|
||||
|-------------|-----|---------|
|
||||
| Production | `http://localhost:3002` | Production deployment |
|
||||
| External (M5) | `https://m5api.momentry.ddns.net` | Remote access |
|
||||
|
||||
## Variables
|
||||
|
||||
All examples in this documentation use these environment variables:
|
||||
|
||||
```bash
|
||||
API="http://localhost:3002"
|
||||
KEY="your-api-key-here"
|
||||
```
|
||||
|
||||
## Authentication
|
||||
|
||||
All endpoints under `/api/v1/*` require authentication.
|
||||
The following endpoints are public (no auth needed):
|
||||
|
||||
- `GET /health`
|
||||
- `POST /api/v1/auth/login`
|
||||
- `POST /api/v1/auth/logout`
|
||||
|
||||
### Three Authentication Modes
|
||||
|
||||
The system supports three authentication methods, checked in **priority order** by the middleware:
|
||||
|
||||
```
|
||||
Middleware priority:
|
||||
1. Session Cookie (Portal/browser)
|
||||
2. JWT Bearer (API clients, CLI)
|
||||
3. API Key Header (legacy compatibility)
|
||||
4. API Key Query Param (?api_key=)
|
||||
```
|
||||
|
||||
| Mode | Transport | Expiry | Scope | Best for |
|
||||
|------|-----------|--------|-------|----------|
|
||||
| **Session Cookie** | `Cookie: session_id=<session_id>` | 24h | per-browser session | Portal (browser) |
|
||||
| **JWT** | `Authorization: Bearer <token>` | 1h | per-login token | API clients, CLI, scripts |
|
||||
| **API Key** | `X-API-Key: <key>` | 90d | fixed key for automation | Legacy scripts, WordPress |
|
||||
|
||||
---
|
||||
|
||||
### Login
|
||||
|
||||
**Default accounts & API keys:**
|
||||
|
||||
| Username | Password | API Key | Role |
|
||||
|----------|----------|---------|------|
|
||||
| `admin` | `admin` | — | admin |
|
||||
| `demo` | `demo` | `muser_demo_key_32chars_abcdef1234567890` | user |
|
||||
|
||||
The demo API key is set via `MOMENTRY_DEMO_API_KEY` env var and can be used in place of JWT for marcom integrations:
|
||||
|
||||
```bash
|
||||
# Using API key instead of JWT
|
||||
curl -s "$API/api/v1/files/scan" -H "X-API-Key: muser_demo_key_32chars_abcdef1234567890"
|
||||
```
|
||||
|
||||
```bash
|
||||
# Login as admin
|
||||
curl -s -X POST "$API/api/v1/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "admin", "password": "admin"}'
|
||||
|
||||
# Login as demo user
|
||||
curl -s -X POST "$API/api/v1/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username": "demo", "password": "demo"}'
|
||||
```
|
||||
|
||||
#### Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"jwt": "eyJhbGciOiJIUzI1NiIs...",
|
||||
"api_key": "muser_...",
|
||||
"user": {
|
||||
"username": "admin",
|
||||
"role": "admin"
|
||||
},
|
||||
"expires_at": "2026-05-18T13:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `jwt` | string | JWT access token. Use as `Authorization: Bearer <jwt>`. Expires in 1 hour. |
|
||||
| `api_key` | string | Legacy API key. Use as `X-API-Key: <key>`. Good for 90 days. |
|
||||
| `user.username` | string | Username |
|
||||
| `user.role` | string | Role: `admin`, `user`, or `readonly` |
|
||||
| `expires_at` | string | ISO8601 timestamp of JWT expiration |
|
||||
|
||||
The login endpoint also sets a `Set-Cookie` header for browser-based clients:
|
||||
|
||||
```
|
||||
Set-Cookie: session_id=<session_id>; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400
|
||||
```
|
||||
|
||||
#### Error Response (401)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "Invalid username or password"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Using JWT
|
||||
|
||||
JWT is preferred for API clients (CLI scripts, WordPress). It is validated by the middleware without a database lookup (stateless).
|
||||
|
||||
```bash
|
||||
# Login and capture JWT
|
||||
JWT=$(curl -s -X POST "$API/api/v1/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin"}' | python3 -c "import json,sys;print(json.load(sys.stdin)['jwt'])")
|
||||
|
||||
# Use JWT for all subsequent requests
|
||||
curl -H "Authorization: Bearer $JWT" "$API/api/v1/files/scan"
|
||||
curl -H "Authorization: Bearer $JWT" "$API/api/v1/resource/tmdb"
|
||||
```
|
||||
|
||||
JWT is short-lived (1 hour). When it expires, request a new one via login.
|
||||
|
||||
---
|
||||
|
||||
### Using Session Cookie (Browser)
|
||||
|
||||
Browser-based clients (Portal) get a session cookie automatically after login. The browser sends the cookie with every request—no manual header needed.
|
||||
|
||||
```bash
|
||||
# Login captures the session cookie from Set-Cookie header
|
||||
curl -v -X POST "$API/api/v1/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin"}' 2>&1 | grep "Set-Cookie"
|
||||
|
||||
# Browser automatically sends: Cookie: session_id=<session_id>
|
||||
# No manual header needed for subsequent requests
|
||||
```
|
||||
|
||||
The session cookie is HttpOnly (not accessible from JavaScript) and SameSite=Strict (protected against CSRF).
|
||||
|
||||
---
|
||||
|
||||
### Using Legacy API Key
|
||||
|
||||
```bash
|
||||
curl -H "X-API-Key: $KEY" "$API/api/v1/files/scan"
|
||||
|
||||
# Also accepted via Bearer header (non-JWT format) or query parameter:
|
||||
curl -H "Authorization: Bearer $KEY" "$API/api/v1/files/scan"
|
||||
curl "$API/api/v1/files/scan?api_key=$KEY"
|
||||
```
|
||||
|
||||
API keys are validated via SHA256 hash lookup in the database. They are long-lived (90 days) and intended for automation.
|
||||
|
||||
### Obtaining an API Key (CLI)
|
||||
|
||||
```bash
|
||||
momentry api-key create "My API Key" --key-type user
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Logout
|
||||
|
||||
```bash
|
||||
# Logout using the session cookie (browser)
|
||||
curl -X POST "$API/api/v1/auth/logout" \
|
||||
-H "Cookie: session_id=<uuid>"
|
||||
```
|
||||
|
||||
#### What logout does
|
||||
|
||||
| Auth mode | Effect |
|
||||
|-----------|--------|
|
||||
| **Session Cookie** | Session deleted from database. Same cookie returns 401 on subsequent requests. |
|
||||
| **JWT** | JWT remains valid until expiry. (JWT is stateless — logout adds JWT to a blacklist only if API key mode is used.) |
|
||||
| **API Key** | API key remains valid. (Legacy keys are shared across sessions — revoking would break other clients.) |
|
||||
|
||||
#### Example: full session lifecycle
|
||||
|
||||
```bash
|
||||
# 1. Login
|
||||
SESSION_ID=$(curl -s -D - -X POST "$API/api/v1/auth/login" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"username":"admin","password":"admin"}' | grep "Set-Cookie" | sed 's/.*session_id=\([^;]*\).*/\1/')
|
||||
|
||||
# 2. Use session (works)
|
||||
curl -s -o /dev/null -w "HTTP %{http_code}\n" "$API/api/v1/resource/tmdb" \
|
||||
-H "Cookie: session_id=$SESSION_ID"
|
||||
# → HTTP 200
|
||||
|
||||
# 3. Logout
|
||||
curl -s -X POST "$API/api/v1/auth/logout" \
|
||||
-H "Cookie: session_id=$SESSION_ID"
|
||||
# → {"success": true}
|
||||
|
||||
# 4. Use session again (rejected)
|
||||
curl -s -o /dev/null -w "HTTP %{http_code}\n" "$API/api/v1/resource/tmdb" \
|
||||
-H "Cookie: session_id=$SESSION_ID"
|
||||
# → HTTP 401
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Authentication Flow Summary
|
||||
|
||||
```
|
||||
Login Request
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 1. Check users │ ← users table (argon2 password verify)
|
||||
│ table │
|
||||
└──────┬───────────┘
|
||||
│
|
||||
┌───┴───┐
|
||||
│ match │
|
||||
└───┬───┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 2. Create JWT │ ← 1h expiry, signed with JWT_SECRET
|
||||
├──────────────────┤
|
||||
│ 3. Create │ ← 24h expiry, stored in sessions table
|
||||
│ session │
|
||||
├──────────────────┤
|
||||
│ 4. Set-Cookie │ ← HttpOnly, SameSite=Strict, Path=/
|
||||
├──────────────────┤
|
||||
│ 5. Return │ ← JWT + api_key + user info to client
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
```
|
||||
Protected Request
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Middleware checks: │
|
||||
│ │
|
||||
│ 1. Cookie session? │ → DB lookup session → get api_key → verify
|
||||
│ │
|
||||
│ 2. JWT Bearer? │ → verify JWT signature → decode claims
|
||||
│ │
|
||||
│ 3. X-API-Key? │ → SHA256 hash → DB lookup → verify
|
||||
│ │
|
||||
│ 4. ?api_key=? │ → same as #3
|
||||
│ │
|
||||
│ 5. None → 401 │
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `401` | Missing or invalid authentication |
|
||||
| `401` | Session expired or logged out |
|
||||
| `401` | JWT expired |
|
||||
| `401` | API key revoked or inactive |
|
||||
|
||||
---
|
||||
|
||||
### Related
|
||||
|
||||
- `POST /api/v1/resource/tmdb/check` — test authentication + TMDb API connectivity
|
||||
- `GET /health/detailed` — view auth status (integrations section)
|
||||
147
deliverable_v1.1.0/modules/02_health.md
Normal file
147
deliverable_v1.1.0/modules/02_health.md
Normal file
@@ -0,0 +1,147 @@
|
||||
<!-- module: health -->
|
||||
<!-- description: Health check endpoints -->
|
||||
<!-- depends: 01_auth -->
|
||||
|
||||
## Health Check
|
||||
|
||||
### `GET /health`
|
||||
|
||||
**Auth**: Public
|
||||
**Scope**: system-level
|
||||
|
||||
Returns basic server health status — used by load balancers and monitoring.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl "$API/health" | jq '{status, version}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"build_git_hash": "3a6c1865",
|
||||
"build_timestamp": "2026-05-16T13:38:15Z",
|
||||
"uptime_ms": 3015
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `status` | string | `ok` or `degraded` |
|
||||
| `version` | string | Semver version |
|
||||
| `build_git_hash` | string | Git commit hash |
|
||||
| `build_timestamp` | string | Binary build time |
|
||||
| `uptime_ms` | integer | Milliseconds since server start |
|
||||
|
||||
---
|
||||
|
||||
### `GET /health/detailed`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: system-level
|
||||
|
||||
Returns full system health including each service status, resource utilization, pipeline readiness, schema migration status, identity file sync status, and external integrations.
|
||||
|
||||
> Requires authentication (JWT, session cookie, or API key). The basic `/health` endpoint remains public for load balancer checks.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl "$API/health/detailed" | jq '{status, services, resources: {cpu: .resources.cpu_used_percent, memory: .resources.memory_used_percent}}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"services": {
|
||||
"postgres": {"status": "ok", "latency_ms": 3},
|
||||
"redis": {"status": "ok", "latency_ms": 1},
|
||||
"qdrant": {"status": "ok", "latency_ms": 5}
|
||||
},
|
||||
"resources": {
|
||||
"cpu_used_percent": 12.5,
|
||||
"memory_available_mb": 32768,
|
||||
"memory_used_percent": 31.7
|
||||
},
|
||||
"pipeline": {
|
||||
"scripts_ready": true,
|
||||
"scripts_count": 345,
|
||||
"processors": {
|
||||
"asr": true,
|
||||
"yolo": true,
|
||||
"face": true,
|
||||
"pose": true,
|
||||
"ocr": true,
|
||||
"cut": true,
|
||||
"scene": true,
|
||||
"asrx": true,
|
||||
"visual_chunk": true
|
||||
},
|
||||
"models_ready": true,
|
||||
"models_count": 42,
|
||||
"scripts_integrity": {"matched": 332, "total": 345, "ok": false},
|
||||
"ffmpeg": true
|
||||
},
|
||||
"schema": {
|
||||
"table_exists": true,
|
||||
"applied": [{"filename": "migrate_add_users_table.sql"}],
|
||||
"required": [],
|
||||
"ok": true
|
||||
},
|
||||
"identities": {
|
||||
"directory_exists": true,
|
||||
"files_count": 3481,
|
||||
"index_ok": true,
|
||||
"db_count": 3481,
|
||||
"synced": true
|
||||
},
|
||||
"integrations": {
|
||||
"tmdb": {
|
||||
"api_key_configured": false,
|
||||
"enabled": false,
|
||||
"api_reachable": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Response Fields
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `status` | string | `ok` if all essential services healthy |
|
||||
| `services` | object | Per-service status (postgres, redis, qdrant) |
|
||||
| `services.*.status` | string | `ok`, `error`, or `degraded` |
|
||||
| `services.*.latency_ms` | int | Response time in milliseconds |
|
||||
| `resources` | object | CPU, memory usage |
|
||||
| `pipeline.scripts_ready` | boolean | Scripts directory accessible |
|
||||
| `pipeline.scripts_count` | int | Number of Python processor scripts |
|
||||
| `pipeline.processors` | object | Per-processor availability |
|
||||
| `pipeline.models_ready` | boolean | Models directory accessible |
|
||||
| `pipeline.scripts_integrity` | object | SHA256 checksum verification results |
|
||||
| `schema.ok` | boolean | All required migrations applied |
|
||||
| `identities.synced` | boolean | Identity file count matches DB count |
|
||||
| `integrations.tmdb` | object | TMDB API key config and reachability |
|
||||
|
||||
#### Health status rules
|
||||
|
||||
| Condition | status |
|
||||
|-----------|--------|
|
||||
| All services ok | `ok` |
|
||||
| Any service error | `degraded` |
|
||||
| Postgres or Redis error | `degraded` (server still responds) |
|
||||
|
||||
---
|
||||
|
||||
### Stats Endpoints
|
||||
|
||||
| Method | Endpoint | Auth | Description |
|
||||
|--------|----------|------|-------------|
|
||||
| GET | `/api/v1/stats/sftpgo` | No | SFTPGo service status |
|
||||
184
deliverable_v1.1.0/modules/03_register.md
Normal file
184
deliverable_v1.1.0/modules/03_register.md
Normal file
@@ -0,0 +1,184 @@
|
||||
<!-- module: register -->
|
||||
<!-- description: File registration — register, scan -->
|
||||
<!-- depends: 01_auth -->
|
||||
|
||||
## File Registration
|
||||
|
||||
### `POST /api/v1/files/register`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Register a video file for processing. Returns the file's metadata and UUID.
|
||||
|
||||
**New in v0.1.2**: Registration now **automatically triggers the processing pipeline** — no need to call `POST /api/v1/file/:file_uuid/process` separately. The system will:
|
||||
1. Register the file and run ffprobe
|
||||
2. Auto-run offline TMDb probe (reads local identity files, no API calls)
|
||||
3. Create a monitor job for the worker
|
||||
4. Worker starts all 10 processors (Cut → ASR → ASRX → YOLO → OCR → Face → Pose → VisualChunk → Story → 5W1H)
|
||||
|
||||
If the file already exists (same content hash), returns the existing record with `already_exists: true`.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `file_path` | string | Yes | — | Path to video file on disk |
|
||||
| `pattern` | string | No | — | Regex pattern for batch register (requires `file_path` to be a directory) |
|
||||
| `user_id` | integer | No | — | User ID to associate with registration |
|
||||
| `content_hash` | string | No | — | Pre-computed SHA-256 hash (skips computation) |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
# Register a single file
|
||||
curl -s -X POST "$API/api/v1/files/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"file_path": "/path/to/video.mp4"}'
|
||||
|
||||
# Batch register files matching a pattern in a directory
|
||||
curl -s -X POST "$API/api/v1/files/register" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"file_path": "/path/to/dir", "pattern": ".*\\.mp4$"}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"file_uuid": "3a6c1865...",
|
||||
"file_name": "video.mp4",
|
||||
"file_path": "/path/to/video.mp4",
|
||||
"file_type": "video",
|
||||
"duration": 120.5,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fps": 24.0,
|
||||
"total_frames": 2892,
|
||||
"already_exists": false,
|
||||
"message": "File registered successfully"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `success` | boolean | Always true on 200 |
|
||||
| `file_uuid` | string | 32-char hex UUID of the registered file |
|
||||
| `file_name` | string | File name (auto-renamed if name conflict) |
|
||||
| `file_path` | string | Canonical path on disk |
|
||||
| `file_type` | string | `"video"`, `"audio"`, or `"unknown"` |
|
||||
| `duration` | float | Duration in seconds |
|
||||
| `width` | integer | Video width in pixels |
|
||||
| `height` | integer | Video height in pixels |
|
||||
| `fps` | float | Frames per second |
|
||||
| `total_frames` | integer | Total frame count |
|
||||
| `already_exists` | boolean | True if same content was already registered |
|
||||
| `message` | string | Human-readable status |
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `401` | Missing or invalid API key |
|
||||
| `400` | Invalid request body |
|
||||
| `404` | File path does not exist |
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/files/scan`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Scan the filesystem directory and list all media files, showing which are registered, processing, or unregistered.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `page` | integer | No | 1 | Page number (1-based) |
|
||||
| `page_size` | integer | No | all | Items per page (alias: `limit`) |
|
||||
| `limit` | integer | No | all | Max items (alias for `page_size`) |
|
||||
| `pattern` | string | No | — | Regex filter on file name (e.g., `.*\\.mp4$`) |
|
||||
| `sort_by` | string | No | `name` | Sort field: `name`, `size`, `modified`, `status` |
|
||||
| `sort_order` | string | No | `asc` | Sort direction: `asc` or `desc` |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
# Full scan
|
||||
curl -s "$API/api/v1/files/scan" -H "X-API-Key: $KEY" | jq '{total, registered_count, unregistered_count}'
|
||||
|
||||
# Paginated (page 1, 5 per page)
|
||||
curl -s "$API/api/v1/files/scan?page=1&page_size=5" -H "X-API-Key: $KEY" | jq '{page, total_pages, files: [.files[].file_name]}'
|
||||
|
||||
# Regex filter: only mp4 files
|
||||
curl -s "$API/api/v1/files/scan?pattern=.*\\.mp4$" -H "X-API-Key: $KEY" | jq '{filtered_total, files: [.files[].file_name]}'
|
||||
|
||||
# Sort by file size (largest first)
|
||||
curl -s "$API/api/v1/files/scan?sort_by=size&sort_order=desc&page_size=5" -H "X-API-Key: $KEY" | jq '[.files[] | {file_name, file_size}]'
|
||||
|
||||
# Sort by modified time (most recent first)
|
||||
curl -s "$API/api/v1/files/scan?sort_by=modified&sort_order=desc&page_size=5" -H "X-API-Key: $KEY" | jq '[.files[] | {file_name, modified_time}]'
|
||||
|
||||
# Sort by status
|
||||
curl -s "$API/api/v1/files/scan?sort_by=status&page_size=5" -H "X-API-Key: $KEY" | jq '[.files[] | {file_name, status}]'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"files": [
|
||||
{
|
||||
"file_name": "video.mp4",
|
||||
"file_size": 12345678,
|
||||
"is_registered": true,
|
||||
"file_uuid": "3a6c1865...",
|
||||
"status": "completed",
|
||||
"registration_time": "2026-05-16T12:00:00Z",
|
||||
"job_id": 42
|
||||
}
|
||||
],
|
||||
"total": 107,
|
||||
"filtered_total": 80,
|
||||
"page": 1,
|
||||
"page_size": 20,
|
||||
"total_pages": 4,
|
||||
"registered_count": 26,
|
||||
"unregistered_count": 81
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `files` | array | Array of file info objects (paginated) |
|
||||
| `files[].file_name` | string | File name |
|
||||
| `files[].relative_path` | string | Path relative to scan root |
|
||||
| `files[].file_path` | string | Absolute path on disk |
|
||||
| `files[].file_size` | integer | File size in bytes |
|
||||
| `files[].modified_time` | string | Last modified timestamp (ISO8601) |
|
||||
| `files[].is_registered` | boolean | Whether file is registered in DB |
|
||||
| `files[].file_uuid` | string | 32-char hex UUID (only if registered) |
|
||||
| `files[].status` | string | `"completed"`, `"processing"`, `"registered"`, `"unregistered"`, or `null` |
|
||||
| `files[].registration_time` | string | DB registration timestamp (only if registered) |
|
||||
| `files[].job_id` | integer | Processing job ID (only if a job exists) |
|
||||
| `total` | integer | Total files found on disk (unfiltered) |
|
||||
| `filtered_total` | integer | Files matching regex filter |
|
||||
| `page` | integer | Current page number |
|
||||
| `page_size` | integer | Items per page |
|
||||
| `total_pages` | integer | Total pages |
|
||||
| `registered_count` | integer | Files registered in DB |
|
||||
| `unregistered_count` | integer | Files not yet registered |
|
||||
|
||||
#### Notes
|
||||
|
||||
| Feature | Behavior |
|
||||
|---------|----------|
|
||||
| **Regex** | Case-insensitive (`(?i)` prefix auto-applied). Applied to `file_name`. |
|
||||
| **Sort order** | Default (`sort_by=name`): registered files first, then alphabetically. `sort_by=status`: alphabetical by status string. |
|
||||
| **Pagination** | `page_size` and `limit` are aliases. Default: show all results. |
|
||||
| **Processing order** | `pattern` regex filter → `sort_by`/`sort_order` → `page`/`page_size` slice. |
|
||||
138
deliverable_v1.1.0/modules/04_lookup.md
Normal file
138
deliverable_v1.1.0/modules/04_lookup.md
Normal file
@@ -0,0 +1,138 @@
|
||||
<!-- module: lookup -->
|
||||
<!-- description: File lookup by name and unregistration -->
|
||||
<!-- depends: 01_auth, 03_register -->
|
||||
|
||||
## File Lookup
|
||||
|
||||
### `GET /api/v1/files/lookup`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Search registered files by file name. Performs a case-insensitive LIKE search on the file name column. Returns basic info about matching files.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `file_name` | string | Yes | File name to search for (partial matches supported) |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
# Look up a specific file
|
||||
curl -s "$API/api/v1/files/lookup?file_name=video.mp4" \
|
||||
-H "X-API-Key: $KEY"
|
||||
|
||||
# Partial name search
|
||||
curl -s "$API/api/v1/files/lookup?file_name=charade" \
|
||||
-H "X-API-Key: $KEY" | jq '.matches[].file_name'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"file_name": "video.mp4",
|
||||
"exists": true,
|
||||
"matches": [
|
||||
{
|
||||
"file_uuid": "a03485a40b2df2d3",
|
||||
"file_name": "video.mp4",
|
||||
"file_type": "video",
|
||||
"status": "completed"
|
||||
}
|
||||
],
|
||||
"next_name": "video (2).mp4"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_name` | string | Searched name |
|
||||
| `exists` | boolean | Exact name match exists |
|
||||
| `matches` | array | Array of matching registered files |
|
||||
| `matches[].file_uuid` | string | 32-char hex UUID |
|
||||
| `matches[].file_name` | string | Registered file name |
|
||||
| `matches[].file_type` | string | `"video"`, `"audio"`, or `null` |
|
||||
| `matches[].status` | string | Registration/processing status |
|
||||
| `next_name` | string | Suggested name for avoiding conflicts |
|
||||
|
||||
---
|
||||
|
||||
## Unregister
|
||||
|
||||
### `POST /api/v1/unregister`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Delete a registered file from the system. Supports single file by UUID, or batch by directory + regex pattern.
|
||||
|
||||
#### What gets deleted
|
||||
|
||||
| Removed (default) | Not removed |
|
||||
|---------|-------------|
|
||||
| Database records (videos, chunks, embeddings, processor_results, pre_chunks) | The original source video file on disk |
|
||||
| Processor output JSON files (`{uuid}.*.json`) — unless `delete_output_files: false` | Temp/working directories |
|
||||
| In-memory cache entries | |
|
||||
| MongoDB cached lists | |
|
||||
|
||||
> ⚠️ Database deletion is **irreversible**. To keep output files, set `"delete_output_files": false`.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
At least one mode must be specified: either `file_uuid` alone, or `file_path` + `pattern` together.
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `file_uuid` | string | * | — | Single file UUID to delete |
|
||||
| `file_path` | string | * | — | Directory path (for batch delete) |
|
||||
| `pattern` | string | * | — | Regex pattern (requires `file_path`) |
|
||||
| `delete_output_files` | boolean | No | `true` | If `true`, also delete processor output JSON files (`{uuid}.*.json`). Set to `false` to keep them. |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
# Delete a single file by UUID (default: also deletes output JSON files)
|
||||
curl -s -X POST "$API/api/v1/unregister" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'"}'
|
||||
|
||||
# Keep output JSON files, only delete DB records
|
||||
curl -s -X POST "$API/api/v1/unregister" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'", "delete_output_files": false}'
|
||||
|
||||
# Batch delete all mp4 files in a directory
|
||||
curl -s -X POST "$API/api/v1/unregister" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"file_path": "/path/to/dir", "pattern": ".*\\.mp4$"}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"file_uuid": "a03485a40b2df2d3",
|
||||
"message": "Video unregistered successfully"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `success` | boolean | True if deletion succeeded |
|
||||
| `file_uuid` | string | UUID of the deleted file (single mode) |
|
||||
| `message` | string | Human-readable status |
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | Neither `file_uuid` nor `file_path`+`pattern` provided |
|
||||
| `404` | File UUID not found |
|
||||
| `401` | Missing or invalid API key |
|
||||
236
deliverable_v1.1.0/modules/05_process.md
Normal file
236
deliverable_v1.1.0/modules/05_process.md
Normal file
@@ -0,0 +1,236 @@
|
||||
<!-- module: process -->
|
||||
<!-- description: Processing pipeline — trigger, probe, progress, jobs -->
|
||||
<!-- depends: 01_auth, 03_register -->
|
||||
|
||||
## Processing Pipeline
|
||||
|
||||
### `POST /api/v1/file/:file_uuid/process`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Trigger the processing pipeline for a registered file. Creates a monitor job that the worker picks up and processes sequentially. Returns immediately with the job info—processing runs asynchronously in the background.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `processors` | string[] | No | all | Specific processors to run: `["cut","asr","asrx","yolo","ocr","face","pose","visual_chunk","story","5w1h"]` |
|
||||
| `rules` | string[] | No | all | Rule names to apply (currently unused) |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
# Run all processors
|
||||
curl -s -X POST "$API/api/v1/file/$FILE_UUID/process" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" -d '{}'
|
||||
|
||||
# Run specific processors only
|
||||
curl -s -X POST "$API/api/v1/file/$FILE_UUID/process" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"processors": ["asr", "face", "yolo"]}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"job_id": 42,
|
||||
"file_uuid": "3a6c1865...",
|
||||
"status": "processing",
|
||||
"pids": [12345, 12346],
|
||||
"message": "Processing triggered for video.mp4"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `success` | boolean | Always true on 200 |
|
||||
| `job_id` | integer | Monitor job ID (for job tracking) |
|
||||
| `file_uuid` | string | 32-char hex UUID of the file |
|
||||
| `status` | string | `"processing"` |
|
||||
| `pids` | integer[] | Process IDs of started processors |
|
||||
| `message` | string | Human-readable status |
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `404` | File UUID not found |
|
||||
| `401` | Missing or invalid API key |
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/file/:file_uuid/probe`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Get ffprobe metadata for a registered file. Returns video/audio stream info, codec details, duration, resolution, and frame rate.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/file/$FILE_UUID/probe" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"file_uuid": "3a6c1865...",
|
||||
"file_name": "video.mp4",
|
||||
"file_size": 794863677,
|
||||
"duration": 120.5,
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"fps": 24.0,
|
||||
"total_frames": 2892,
|
||||
"cached": true,
|
||||
"format": {
|
||||
"filename": "/path/to/video.mp4",
|
||||
"format_name": "mov,mp4,m4a,3gp",
|
||||
"duration": "120.5",
|
||||
"size": "12345678",
|
||||
"bit_rate": "819200"
|
||||
},
|
||||
"streams": [
|
||||
{
|
||||
"index": 0,
|
||||
"codec_name": "h264",
|
||||
"codec_type": "video",
|
||||
"width": 1920,
|
||||
"height": 1080,
|
||||
"r_frame_rate": "24/1",
|
||||
"duration": "120.5"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_uuid` | string | 32-char hex UUID |
|
||||
| `file_name` | string | File name |
|
||||
| `file_size` | integer | File size in bytes (from filesystem) |
|
||||
| `duration` | float | Duration in seconds |
|
||||
| `width` | integer | Video width in pixels |
|
||||
| `height` | integer | Video height in pixels |
|
||||
| `fps` | float | Frames per second |
|
||||
| `total_frames` | integer | Estimated total frames |
|
||||
| `cached` | boolean | True if result was from cached probe JSON |
|
||||
| `format` | object | Container format info (ffprobe format section) |
|
||||
| `streams` | array | Array of stream info objects |
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/progress/:file_uuid`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Get real-time processing progress for a file via Redis pub/sub. Includes per-processor status, current/total frames, ETA, and system resource stats.
|
||||
|
||||
#### Pipeline Order
|
||||
|
||||
| Order | Processor | Dependencies | Description |
|
||||
|-------|-----------|-------------|-------------|
|
||||
| 1 | `cut` | — | Scene detection |
|
||||
| 2 | `asr` | cut | Speech-to-text (per scene) |
|
||||
| 3 | `asrx` | asr | Speaker diarization |
|
||||
| 4 | `yolo` | — | Object detection |
|
||||
| 5 | `ocr` | — | Text recognition |
|
||||
| 6 | `face` | — | Face detection & embedding |
|
||||
| 7 | `pose` | — | Pose estimation |
|
||||
| 8 | `visual_chunk` | yolo | Visual scene chunks |
|
||||
| 9 | `story` | asr, asrx, cut, yolo, face | Scene summaries (template) |
|
||||
| 10 | `5w1h` | story | 5W1H analysis (Gemma4 LLM) |
|
||||
|
||||
All processors except `story` and `5w1h` run concurrently when their dependencies are met. Story and 5W1H run sequentially after their prerequisites.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/progress/$FILE_UUID" -H "X-API-Key: $KEY" | jq '{overall_progress, processors: [.processors[] | {processor_type, status}]}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"file_uuid": "3a6c1865...",
|
||||
"overall_progress": 71,
|
||||
"cpu_percent": 45.2,
|
||||
"gpu_percent": 30.1,
|
||||
"memory_percent": 62.4,
|
||||
"processors": [
|
||||
{"processor_type": "asr", "status": "complete", "progress": 100},
|
||||
{"processor_type": "yolo", "status": "running", "progress": 65},
|
||||
{"processor_type": "face", "status": "pending", "progress": 0}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_uuid` | string | 32-char hex UUID |
|
||||
| `overall_progress` | integer | Overall progress percentage (0–100) |
|
||||
| `processors` | array | Per-processor status list |
|
||||
| `processors[].processor_type` | string | Processor name (`asr`, `cut`, `yolo`, etc.) |
|
||||
| `processors[].status` | string | `"pending"`, `"running"`, `"complete"`, or `"failed"` |
|
||||
| `processors[].progress` | integer | Per-processor progress (0–100) |
|
||||
| `processors[].eta_seconds` | integer | Estimated seconds remaining (running processors) |
|
||||
| `processors[].current` | integer | Current frame count |
|
||||
| `processors[].total` | integer | Total frame count |
|
||||
| `cpu_percent` | float | Current CPU usage |
|
||||
| `gpu_percent` | float | Current GPU utilization |
|
||||
| `memory_percent` | float | Current memory usage |
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/jobs`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: system-level
|
||||
|
||||
List all processing jobs (monitor jobs) in the system. Shows job status, which file each job is processing, and current processor info.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/jobs" -H "X-API-Key: $KEY" | jq '{count, jobs: [.jobs[] | {uuid, status}]}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"jobs": [
|
||||
{
|
||||
"id": 42,
|
||||
"uuid": "3a6c1865...",
|
||||
"status": "running",
|
||||
"current_processor": "yolo",
|
||||
"created_at": "2026-05-16T12:00:00Z",
|
||||
"started_at": "2026-05-16T12:01:00Z"
|
||||
}
|
||||
],
|
||||
"count": 15,
|
||||
"page": 1,
|
||||
"page_size": 20
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `jobs` | array | Array of job info objects |
|
||||
| `jobs[].id` | integer | Job ID |
|
||||
| `jobs[].uuid` | string | File UUID being processed |
|
||||
| `jobs[].status` | string | `"pending"`, `"running"`, `"completed"`, `"failed"` |
|
||||
| `jobs[].current_processor` | string | Currently active processor, or null |
|
||||
| `count` | integer | Total job count |
|
||||
| `page` | integer | Current page number |
|
||||
| `page_size` | integer | Jobs per page |
|
||||
145
deliverable_v1.1.0/modules/06_search.md
Normal file
145
deliverable_v1.1.0/modules/06_search.md
Normal file
@@ -0,0 +1,145 @@
|
||||
<!-- module: search -->
|
||||
<!-- description: Vector search, BM25, smart search, universal search, visual search -->
|
||||
<!-- depends: 01_auth -->
|
||||
|
||||
## Search APIs
|
||||
|
||||
### `POST /api/v1/search/smart`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Semantic vector search using EmbeddingGemma-300m. Generates a query embedding via EmbeddingGemma (port 11436), then searches pgvector `story_parent` and `llm_parent` chunks by cosine similarity.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `file_uuid` | string | Yes | — | File UUID to search within |
|
||||
| `query` | string | Yes | — | Search text |
|
||||
| `limit` | integer | No | 5 | Max results to return |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 5 | Items per page |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/smart" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'", "query": "Audrey Hepburn"}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"query": "Audrey Hepburn",
|
||||
"results": [
|
||||
{
|
||||
"parent_id": 1087822,
|
||||
"scene_order": 1087822,
|
||||
"start_frame": 104438,
|
||||
"end_frame": 104538,
|
||||
"fps": 24.0,
|
||||
"start_time": 4351.6,
|
||||
"end_time": 4355.76,
|
||||
"summary": "[4352s-4356s, 4s] Cast: Audrey Hepburn. Total: 2 lines, 10 words. Speakers: Audrey Hepburn (2 lines)",
|
||||
"similarity": 0.67
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"page_size": 5,
|
||||
"strategy": "semantic_vector_search"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/universal`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Multi-type BM25 full-text search across chunks, frames, and persons. Uses PostgreSQL `tsvector`.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `query` | string | Yes | — | Search text |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file |
|
||||
| `types` | string[] | No | `["chunk","frame","person"]` | Search types |
|
||||
| `limit` | integer | No | 10 | Max results per type |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 20 | Items per page |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/universal" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'", "query": "Cary Grant"}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"type": "chunk",
|
||||
"chunk_id": "bd80fec92b0b6963d177a2c55bf713e2_2",
|
||||
"chunk_type": "story_child",
|
||||
"start_frame": 5103,
|
||||
"end_frame": 5127,
|
||||
"start_time": 212.64,
|
||||
"end_time": 213.64,
|
||||
"text": "[213s-214s] Cary Grant: \"Olá!\"",
|
||||
"score": 0.9
|
||||
}
|
||||
],
|
||||
"total": 20,
|
||||
"took_ms": 18
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/frames`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Search face detection frames by identity name or trace ID.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/identity_text`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Search text chunks spoken by a specific identity.
|
||||
|
||||
---
|
||||
|
||||
### Visual Search
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/api/v1/search/visual` | Search visual chunks |
|
||||
| POST | `/api/v1/search/visual/class` | Search by object class |
|
||||
| POST | `/api/v1/search/visual/density` | Search by object density |
|
||||
| POST | `/api/v1/search/visual/combination` | Search by object combination |
|
||||
| POST | `/api/v1/search/visual/stats` | Visual chunk statistics |
|
||||
|
||||
#### Embedding Model
|
||||
|
||||
| Detail | Value |
|
||||
|--------|-------|
|
||||
| **Model** | EmbeddingGemma-300m |
|
||||
| **Endpoint** | `POST /api/v1/embeddings` on port 11436 |
|
||||
| **Dimension** | 768 |
|
||||
| **Storage** | pgvector (`chunk.embedding` column) |
|
||||
65
deliverable_v1.1.0/modules/08_identity_agent.md
Normal file
65
deliverable_v1.1.0/modules/08_identity_agent.md
Normal file
@@ -0,0 +1,65 @@
|
||||
<!-- module: identity_agent -->
|
||||
<!-- description: Identity agent — match from photo, match from trace -->
|
||||
<!-- depends: 01_auth, 07_identity -->
|
||||
|
||||
## Identity Agent
|
||||
|
||||
### `POST /api/v1/agents/identity/match-from-photo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Upload a face photo to match against known identities. Detects face via InsightFace, extracts 512D embedding via CoreML FaceNet, then searches pgvector for the closest identity.
|
||||
|
||||
#### Request
|
||||
|
||||
`multipart/form-data` with field `image` (JPEG/PNG) and optional `file_uuid`.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/agents/identity/match-from-photo" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-F "image=@/path/to/face.jpg" \
|
||||
-F "file_uuid=$FILE_UUID"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"matches": [
|
||||
{
|
||||
"identity_uuid": "a9a90105...",
|
||||
"name": "Cary Grant",
|
||||
"similarity": 0.87
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/agents/identity/match-from-trace`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Match a face trace (tracked face across frames) against known identities. Samples 3 angles from the trace, generates embeddings, and searches pgvector.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `file_uuid` | string | Yes | File containing the trace |
|
||||
| `trace_id` | integer | Yes | Face trace ID to match |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/agents/identity/match-from-trace" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'", "trace_id": 10}'
|
||||
```
|
||||
109
deliverable_v1.1.0/modules/09_tmdb.md
Normal file
109
deliverable_v1.1.0/modules/09_tmdb.md
Normal file
@@ -0,0 +1,109 @@
|
||||
<!-- module: tmdb -->
|
||||
<!-- description: TMDb enrichment endpoints — prefetch, probe, resource, check -->
|
||||
<!-- depends: 01_auth, 03_register -->
|
||||
|
||||
## TMDb Enrichment
|
||||
|
||||
> **Offline operation**: TMDb prefetch now checks local identity files first (`identities/_index.json` + `*.tmdb.json`).
|
||||
> If local files exist, no external API call is made. Internet is only needed for initial data seeding.
|
||||
|
||||
### Overview
|
||||
|
||||
TMDb enrichment is an optional identity enrichment step that can be run after Pipeline face detection completes. The workflow is:
|
||||
|
||||
1. **Prefetch** (requires internet): Download movie cast data from TMDb API → cache to `{file_uuid}.tmdb.json`
|
||||
2. **Probe**: Read local cache → create identities for **all** cast members (`source='tmdb'`) + save `identity.json` + download profile image to `{OUTPUT}/identities/{uuid}/profile.jpg`
|
||||
3. **Match**: The worker automatically matches video faces against TMDb identities when `MOMENTRY_TMDB_PROBE_ENABLED=true`
|
||||
|
||||
### `POST /api/v1/agents/tmdb/prefetch`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Fetch TMDb cast data for a registered file and cache it locally. This is the only step requiring internet access.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `file_uuid` | string | Yes | File UUID to enrich |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/agents/tmdb/prefetch" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'"}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{"success": true, "file_uuid": "...", "cache_path": "/output/...tmdb.json"}
|
||||
```
|
||||
|
||||
### `POST /api/v1/file/:file_uuid/tmdb-probe`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
|
||||
Read local TMDb cache and create/update identities. Requires prefetch to have been run first.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/file/$FILE_UUID/tmdb-probe" \
|
||||
-H "X-API-Key: $KEY" | jq '{identities_created, movie_title}'
|
||||
```
|
||||
|
||||
#### Response (200 — identities created)
|
||||
|
||||
```json
|
||||
{"success": true, "identities_created": 15, "movie_title": "Charade"}
|
||||
```
|
||||
|
||||
#### Response (200 — no cache)
|
||||
|
||||
```json
|
||||
{"success": false, "message": "No TMDb cache found. Run tmdb-prefetch first."}
|
||||
```
|
||||
|
||||
### `GET /api/v1/resource/tmdb`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: system-level
|
||||
|
||||
View TMDb resource status including configuration, identity counts, and cache file count.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/resource/tmdb" -H "X-API-Key: $KEY" \
|
||||
| jq '{identities_seeded, cache_files}'
|
||||
```
|
||||
|
||||
### `POST /api/v1/resource/tmdb/check`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: system-level
|
||||
|
||||
Ping the TMDb API to verify connectivity and measure latency.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/resource/tmdb/check" \
|
||||
-H "X-API-Key: $KEY" | jq '.status'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"api_key_configured": true,
|
||||
"enabled": false,
|
||||
"api_reachable": true,
|
||||
"api_latency_ms": 120
|
||||
}
|
||||
```
|
||||
178
deliverable_v1.1.0/modules/10_pipeline.md
Normal file
178
deliverable_v1.1.0/modules/10_pipeline.md
Normal file
@@ -0,0 +1,178 @@
|
||||
<!-- module: pipeline -->
|
||||
<!-- description: Pipeline processors, ingestion status, stats endpoints -->
|
||||
<!-- depends: 01_auth -->
|
||||
|
||||
## Pipeline
|
||||
|
||||
### Dependency Graph
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph Processors["10 Processors"]
|
||||
Cut[Cut] --> ASR[ASR]
|
||||
ASR --> ASRX[ASRX]
|
||||
ASRX --> Story[Story]
|
||||
Cut --> Story
|
||||
YOLO[YOLO] --> VisualChunk[VisualChunk]
|
||||
VisualChunk --> Story
|
||||
Face[Face] --> Story
|
||||
Story --> FiveW1H[5W1H]
|
||||
OCR[OCR]
|
||||
Pose[Pose]
|
||||
end
|
||||
|
||||
subgraph Ingestion["入庫 (Post-Processing)"]
|
||||
ASR --> Rule1[Rule 1 Sentence]
|
||||
ASRX --> Rule1
|
||||
Rule1 --> Vectorize[Auto-Vectorize]
|
||||
Rule1 --> Phase1[Phase 1 Pack]
|
||||
|
||||
Cut --> Rule3[Rule 3 Scene]
|
||||
ASR --> Rule3
|
||||
|
||||
Face --> Trace[Face Trace]
|
||||
Trace --> Qdrant[Qdrant Sync]
|
||||
Trace --> TraceChunks[Trace Chunks]
|
||||
Trace --> TKG[TKG Builder]
|
||||
|
||||
Face --> TMDbMatch[TMDb Match]
|
||||
Face --> SceneMeta[Scene Metadata]
|
||||
YOLO --> SceneMeta
|
||||
Face --> IdentityAgent[Identity Agent]
|
||||
ASRX --> IdentityAgent
|
||||
|
||||
Cut --> Agent5W1H[5W1H Agent]
|
||||
ASR --> Agent5W1H
|
||||
Agent5W1H --> Phase2[Phase 2 Pack]
|
||||
end
|
||||
|
||||
style Processors fill:#1a1a2e,stroke:#e94560
|
||||
style Ingestion fill:#16213e,stroke:#0f3460
|
||||
```
|
||||
|
||||
### Pipeline Completion Flow
|
||||
|
||||
The pipeline is **not complete** until both the 10 processors AND the 入庫 (ingestion) steps have finished. The worker polls every 3 seconds and only marks the job as `completed` when all ingestion steps verify OK.
|
||||
|
||||
```
|
||||
10 processors done
|
||||
↓ (job status stays "running")
|
||||
Algorithm 1 Trigger: Rule 1 + Vectorize + Phase 1 Pack
|
||||
↓ (job runs in parallel)
|
||||
Algorithm 2 Trigger: Face Trace → TKG, Scene Metadata, Identity Agent, 5W1H Agent
|
||||
↓ (poll checks every 3s)
|
||||
Ingestion verification: rule1 ✓ vectorize ✓ rule3 ✓ face_trace ✓ tkg ✓ scene_meta ✓ 5w1h ✓
|
||||
↓
|
||||
job status = "completed"
|
||||
```
|
||||
|
||||
### 10 Processor Stages
|
||||
|
||||
| # | Processor | Depends On | Description |
|
||||
|---|-----------|------------|-------------|
|
||||
| 1 | `Cut` | — | Scene boundary detection (PySceneDetect) |
|
||||
| 2 | `ASR` | Cut | Automatic speech recognition (faster-whisper) |
|
||||
| 3 | `ASRX` | ASR | Speaker diarization + ASR refinement |
|
||||
| 4 | `YOLO` | — | Object detection (YOLOv8) |
|
||||
| 5 | `OCR` | — | Optical character recognition |
|
||||
| 6 | `Face` | — | Face detection + recognition (InsightFace + CoreML) |
|
||||
| 7 | `Pose` | — | Pose estimation |
|
||||
| 8 | `VisualChunk` | YOLO | Visual object chunking |
|
||||
| 9 | `Story` | ASRX + Cut + YOLO + Face | Narrative scene summarization (LLM, with embedding) |
|
||||
| 10 | `5W1H` | Story | Who/What/When/Where/Why extraction (LLM, with embedding) |
|
||||
|
||||
### 入庫 (Post-Processing / Ingestion)
|
||||
|
||||
These steps run after the 10 processors and are **required for pipeline completion**. The worker checks all of them before marking the job as done.
|
||||
|
||||
| # | Step | Triggers When | Verification |
|
||||
|---|------|--------------|-------------|
|
||||
| 1 | **Rule 1 Sentence Chunking** | ASR + ASRX done | `chunk` table has rows with `chunk_type = 'sentence'` |
|
||||
| 2 | **Auto-Vectorize** | Rule 1 done | `chunk.embedding` IS NOT NULL for sentence chunks |
|
||||
| 3 | **Phase 1 Pack** | Rule 1 done | `release_pack.py --phase 1` executed |
|
||||
| 4 | **Rule 3 Scene Chunking** | All 10 processors done + Cut + ASR | `chunk` table has rows with `chunk_type = 'cut'` |
|
||||
| 5 | **Face Trace** | All 10 processors done + Face | `face_detections.trace_id` IS NOT NULL |
|
||||
| 6 | **Qdrant Face Sync** | Face Trace done | Qdrant face_embedding collection populated |
|
||||
| 7 | **Trace Chunks** | Face Trace done | `chunk` table has rows with `chunk_type = 'trace'` |
|
||||
| 8 | **TKG Builder** | Face Trace done | `tkg_nodes` + `tkg_edges` tables have rows |
|
||||
| 9 | **TMDb Face Matching** | TMDb enabled + Face done | `face_detections.identity_id` IS NOT NULL |
|
||||
| 10 | **Heuristic Scene Metadata** | Face + YOLO done | `{file_uuid}.scene_meta.json` exists on disk |
|
||||
| 11 | **Identity Agent** | Face + ASRX done | `identities` with `source = 'identity_agent'` |
|
||||
| 12 | **5W1H Agent** | Cut + ASR done | `chunk.summary_text` IS NOT NULL for cut chunks |
|
||||
| 13 | **Release Pack** | 5W1H Agent done | `release_pack.py --phase 2` executed |
|
||||
|
||||
### Ingestion Status
|
||||
|
||||
Check real-time ingestion status for a file:
|
||||
|
||||
```bash
|
||||
curl "$API/api/v1/stats/ingestion-status/{file_uuid}"
|
||||
```
|
||||
|
||||
Returns per-step `done` / `pending` status with detail counts.
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3003/api/v1/stats/ingestion-status/bd80fec9c42afb0307eb28f22c64c76a" | jq '.steps[] | {name, status, detail}'
|
||||
```
|
||||
|
||||
#### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"file_uuid": "bd80fec9c42afb0307eb28f22c64c76a",
|
||||
"steps": [
|
||||
{ "name": "rule1_sentence", "status": "pending", "detail": "0 sentence chunks" },
|
||||
{ "name": "auto_vectorize", "status": "pending", "detail": "0 embedded" },
|
||||
{ "name": "rule3_scene", "status": "pending", "detail": "0 scene chunks" },
|
||||
{ "name": "face_trace", "status": "pending", "detail": "0 traces" },
|
||||
{ "name": "trace_chunks", "status": "pending", "detail": "0 trace chunks" },
|
||||
{ "name": "tkg", "status": "pending", "detail": "0 nodes, 0 edges" },
|
||||
{ "name": "identity_match", "status": "pending", "detail": "0 identities" },
|
||||
{ "name": "scene_metadata", "status": "pending", "detail": null },
|
||||
{ "name": "5w1h", "status": "pending", "detail": "0 scenes with 5W1H" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Stats Endpoints
|
||||
|
||||
| Method | Endpoint | Auth | Description |
|
||||
|--------|----------|------|-------------|
|
||||
| GET | `/api/v1/stats/sftpgo` | No | SFTPGo service status |
|
||||
| GET | `/api/v1/stats/ingestion-status/:file_uuid` | No | Per-file ingestion checklist |
|
||||
|
||||
### Configuration
|
||||
|
||||
### `POST /api/v1/config/cache`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: system-level
|
||||
|
||||
Toggle the Redis cache on or off.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `enabled` | boolean | Yes | `true` to enable, `false` to disable |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/config/cache" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"enabled": false}'
|
||||
```
|
||||
|
||||
### Unmounted Routes
|
||||
|
||||
The following routes are defined in source code but are **NOT** currently mounted in the router:
|
||||
|
||||
| Endpoint | Source file |
|
||||
|----------|-------------|
|
||||
| `/api/v1/search/persons` | `universal_search.rs` (not mounted) |
|
||||
| `/api/v1/who` | `who.rs` |
|
||||
| `/api/v1/who/candidates` | `who.rs` |
|
||||
57
deliverable_v1.1.0/modules/11_error_codes.md
Normal file
57
deliverable_v1.1.0/modules/11_error_codes.md
Normal file
@@ -0,0 +1,57 @@
|
||||
<!-- module: error_codes -->
|
||||
<!-- description: Standard API error codes -->
|
||||
<!-- depends: -->
|
||||
|
||||
## Error Response Format
|
||||
|
||||
All API errors follow this JSON structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": {
|
||||
"code": "E001_NOT_FOUND",
|
||||
"message": "Resource not found",
|
||||
"details": {"resource": "file_uuid", "value": "abc"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Code List
|
||||
|
||||
### Generic Errors (E0xx)
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `E001_NOT_FOUND` | 404 | Resource not found (file, identity, chunk) |
|
||||
| `E002_DUPLICATE` | 409 | Resource already exists |
|
||||
| `E003_VALIDATION` | 400 | Request parameter validation failed |
|
||||
| `E004_UNAUTHORIZED` | 401 | Invalid API key or token |
|
||||
| `E005_INTERNAL` | 500 | Internal server error |
|
||||
|
||||
### Processor Errors (E1xx)
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `E101_PROCESSOR_FAIL` | 500 | Python script execution failed |
|
||||
| `E102_TIMEOUT` | 504 | Processing timeout |
|
||||
| `E103_RESUME_FAIL` | 500 | Resume failed (checkpoint not found) |
|
||||
| `E104_NO_VIDEO` | 400 | Video file path not found |
|
||||
|
||||
### Identity Errors (E2xx)
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `E201_FACE_NOT_FOUND` | 404 | Face detection not found |
|
||||
| `E202_MERGE_CONFLICT` | 409 | Identity merge conflict |
|
||||
| `E203_CANDIDATE_EMPTY` | 404 | No candidates available for confirmation |
|
||||
|
||||
### TMDb Errors (E3xx)
|
||||
|
||||
| Code | HTTP | Description |
|
||||
|------|------|-------------|
|
||||
| `E301_TMDB_NO_KEY` | 400 | `TMDB_API_KEY` environment variable not set |
|
||||
| `E302_TMDB_UNREACHABLE` | 502 | TMDb API unreachable or timed out |
|
||||
| `E303_TMDB_CACHE_NOT_FOUND` | 200 | No local TMDb cache; run prefetch first |
|
||||
| `E304_TMDB_PROBE_FAILED` | 500 | TMDb probe execution failed |
|
||||
| `E305_TMDB_MOVIE_NOT_FOUND` | 404 | No matching TMDb movie found from filename |
|
||||
118
deliverable_v1.1.0/modules/12_agent.md
Normal file
118
deliverable_v1.1.0/modules/12_agent.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# Agent Endpoints
|
||||
|
||||
Agent endpoints provide AI-powered capabilities including translation, identity analysis, and 5W1H extraction.
|
||||
|
||||
## POST /api/v1/agents/translate
|
||||
|
||||
Translate text between languages using Gemma4 (llama.cpp, port 8082).
|
||||
|
||||
### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"text": "Hello, welcome to Momentry Core.",
|
||||
"target_language": "Traditional Chinese",
|
||||
"source_language": "English"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `text` | string | ✅ | Text to translate |
|
||||
| `target_language` | string | ✅ | Target language name (e.g. "Traditional Chinese", "Japanese") |
|
||||
| `source_language` | string | ❌ | Source language (default: "auto") |
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"translated_text": "您好,歡迎使用 Momentry Core。",
|
||||
"source_language_detected": "English",
|
||||
"model_used": "google_gemma-4-26B-A4B-it-Q5_K_M.gguf"
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Language Pairs (tested)
|
||||
|
||||
| Source | Target | Quality |
|
||||
|--------|--------|---------|
|
||||
| English | Traditional Chinese | ✅ |
|
||||
| English | Japanese | ✅ |
|
||||
| Chinese | English | ✅ |
|
||||
| English | French | ✅ |
|
||||
| Chinese | Japanese | ✅ |
|
||||
|
||||
### Model
|
||||
|
||||
- **Model**: Gemma4 26B (Q5_K_M)
|
||||
- **Engine**: llama.cpp at `localhost:8082`
|
||||
- **Endpoint**: `/v1/chat/completions` (OpenAI-compatible)
|
||||
- **Temperature**: 0.1
|
||||
- **Max tokens**: 1024
|
||||
|
||||
### Errors
|
||||
|
||||
| Status | Condition |
|
||||
|--------|-----------|
|
||||
| 500 | LLM unreachable or response parse failure |
|
||||
| 401 | Missing/invalid auth |
|
||||
|
||||
---
|
||||
|
||||
## POST /api/v1/agents/5w1h/analyze
|
||||
|
||||
Extract 5W1H (Who, What, When, Where, Why, How) from a scene. Uses Gemma4 LLM on port 8082.
|
||||
|
||||
### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"file_uuid": "3abeee81d94597629ed8cb943f182e94",
|
||||
"scene_id": 42
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"5w1h": {
|
||||
"who": ["Cary Grant"],
|
||||
"what": ["discussing plans"],
|
||||
"when": ["1963"],
|
||||
"where": ["Paris"],
|
||||
"why": ["vacation"],
|
||||
"how": ["in person"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## POST /api/v1/agents/5w1h/batch
|
||||
|
||||
Batch analyze all scenes in a file for 5W1H extraction. Uses the pipeline's `parent_chunk_5w1h.py --mode llm`.
|
||||
|
||||
### Request
|
||||
|
||||
```json
|
||||
{
|
||||
"file_uuid": "3abeee81d94597629ed8cb943f182e94"
|
||||
}
|
||||
```
|
||||
|
||||
## GET /api/v1/agents/5w1h/status
|
||||
|
||||
Get status of the 5W1H agent pipeline for a file.
|
||||
|
||||
---
|
||||
|
||||
## Embedding Model
|
||||
|
||||
| Detail | Value |
|
||||
|--------|-------|
|
||||
| **Model** | EmbeddingGemma-300m |
|
||||
| **Endpoint** | `POST /v1/embeddings` on port 11436 |
|
||||
| **Dimension** | 768 |
|
||||
| **Used by** | `parent_chunk_5w1h.py --embed`, story, 5W1H, search |
|
||||
|
||||
63
deliverable_v1.1.0/modules/_template.md
Normal file
63
deliverable_v1.1.0/modules/_template.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# {Module Name} — API Workspace Module
|
||||
|
||||
> Use this template when adding or editing API endpoint documentation modules.
|
||||
|
||||
## Module Metadata
|
||||
|
||||
Every module MUST start with:
|
||||
|
||||
```markdown
|
||||
<!-- module: <short_name> -->
|
||||
<!-- description: One-line description of what this module covers -->
|
||||
<!-- depends: <comma-separated list of dependency module names> -->
|
||||
```
|
||||
|
||||
## Endpoint Template
|
||||
|
||||
Each endpoint MUST use this structure:
|
||||
|
||||
### `METHOD /path/to/endpoint`
|
||||
|
||||
**Auth**: Required / Optional / Public
|
||||
|
||||
**Scope**: file-level / identity-level / system-level
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `param1` | string | Yes | — | Description |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
# brief description of what this example demonstrates
|
||||
curl -s -X METHOD "$API/path" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"param1": "value"}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{ "success": true }
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `success` | boolean | Always true on 200 |
|
||||
|
||||
#### Error Codes
|
||||
|
||||
| Code | HTTP | When |
|
||||
|------|------|------|
|
||||
| E0xx | 4xx | Description |
|
||||
|
||||
## Rules
|
||||
|
||||
1. Each module file covers ONE topic group (e.g., `09_tmdb.md` = all TMDb endpoints)
|
||||
2. Use `$API` and `$KEY` in all curl examples
|
||||
3. Use `$FILE_UUID`, `$IDENTITY_UUID` variables for UUID examples
|
||||
4. Module filename = `NN_topic.md` (NN = execution order, 01-99)
|
||||
5. `depends` metadata = which modules must be assembled before this one
|
||||
225
deliverable_v1.1.0/scripts/build_docs.py
Normal file
225
deliverable_v1.1.0/scripts/build_docs.py
Normal file
@@ -0,0 +1,225 @@
|
||||
#!/opt/homebrew/bin/python3.11
|
||||
"""Build HTML documentation from module source files."""
|
||||
import os, markdown, re, glob, shutil
|
||||
|
||||
MODULES_DIR = os.path.join(os.path.dirname(__file__), "..", "docs_v1.0", "API_WORKSPACE", "modules")
|
||||
DOC_DIR = os.path.join(os.path.dirname(__file__), "..", "docs_v1.0", "doc")
|
||||
DOC_DEV_DIR = os.path.join(os.path.dirname(__file__), "..", "docs_v1.0", "doc_developer")
|
||||
|
||||
# User-facing modules (no developer content)
|
||||
USER_MODULES = {
|
||||
"01_auth", "02_health", "03_register", "04_lookup", "05_process",
|
||||
"06_search", "07_identity", "08_identity_agent", "08_media",
|
||||
"09_tmdb", "10_pipeline", "12_agent",
|
||||
}
|
||||
|
||||
|
||||
def md_to_html(md_text: str) -> str:
|
||||
"""Convert Markdown to HTML."""
|
||||
html = markdown.markdown(md_text, extensions=['fenced_code', 'tables', 'codehilite'])
|
||||
# Wrap tables
|
||||
html = re.sub(r'<table>', '<table class="table">', html)
|
||||
return html
|
||||
|
||||
def build_index(files, dev=False):
|
||||
"""Build index.html."""
|
||||
links = []
|
||||
for fname in sorted(files):
|
||||
name = os.path.splitext(fname)[0]
|
||||
label = MODULE_LABELS.get(name, name.replace("_", " ").title())
|
||||
if "|" in label:
|
||||
cn, en = label.split("|", 1)
|
||||
else:
|
||||
cn, en = label, ""
|
||||
html_name = fname.replace(".md", ".html")
|
||||
links.append(f'<tr onclick="window.location=\'{html_name}\'" style="cursor:pointer"><td class="cn">{cn}</td><td class="en">{en}</td></tr>')
|
||||
|
||||
title = "Momentry API 開發者文件" if dev else "Momentry API 文件"
|
||||
subtitle = "開發者專用" if dev else "API 參考手冊 — 登入後可瀏覽各模組文件"
|
||||
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="zh-TW">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{title}</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }}
|
||||
.container {{ max-width: 900px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }}
|
||||
h1 {{ font-size: 28px; margin-bottom: 8px; }}
|
||||
p.subtitle {{ color: #666; margin-bottom: 24px; }}
|
||||
table {{ width: 100%; border-collapse: collapse; }}
|
||||
tr {{ border-bottom: 1px solid #eee; }}
|
||||
tr:last-child {{ border: none; }}
|
||||
td {{ padding: 10px 0; }}
|
||||
td.cn {{ width: 140px; font-weight: 600; color: #333; }}
|
||||
td.en {{ color: #666; font-size: 14px; }}
|
||||
a {{ color: #0066cc; text-decoration: none; display: block; }}
|
||||
a:hover td {{ background: #f8f8f8; border-radius: 4px; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>{title}</h1>
|
||||
<p class="subtitle">{subtitle}</p>
|
||||
<table>{"".join(links)}</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
MODULE_LABELS = {
|
||||
"01_auth": "安全認證|Authentication",
|
||||
"02_health": "健康檢查|Health",
|
||||
"03_register": "檔案註冊|File Registration",
|
||||
"04_lookup": "檔案屬性查詢|File Lookup",
|
||||
"05_process": "處理流程|Processing",
|
||||
"06_search": "搜尋功能|Search",
|
||||
"07_identity": "身份識別|Identity",
|
||||
"08_identity_agent": "智能身份綁定|Smart Identity Binding",
|
||||
"08_media": "串流與截圖|Streaming & Thumbnails",
|
||||
"09_tmdb": "TMDb 整合|TMDb Integration",
|
||||
"10_pipeline": "生產線|Pipeline",
|
||||
"11_error_codes": "錯誤碼|Error Codes",
|
||||
"12_agent": "智慧代理|AI Agents",
|
||||
}
|
||||
|
||||
def build_html(md_text: str, title: str) -> str:
|
||||
"""Wrap MD content in HTML page."""
|
||||
content = md_to_html(md_text)
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>{title} - Momentry API Docs</title>
|
||||
<style>
|
||||
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }}
|
||||
.container {{ max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }}
|
||||
h1 {{ font-size: 24px; margin: 24px 0 12px; }}
|
||||
h2 {{ font-size: 20px; margin: 20px 0 10px; color: #222; }}
|
||||
h3 {{ font-size: 16px; margin: 16px 0 8px; color: #444; }}
|
||||
p {{ line-height: 1.6; margin: 8px 0; }}
|
||||
table {{ border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }}
|
||||
th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
|
||||
th {{ background: #f0f0f0; font-weight: 600; }}
|
||||
code {{ background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }}
|
||||
pre {{ background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }}
|
||||
pre code {{ background: none; padding: 0; }}
|
||||
a {{ color: #0066cc; }}
|
||||
.back {{ display: inline-block; margin-bottom: 20px; color: #666; }}
|
||||
.back:hover {{ color: #333; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
{content}
|
||||
</div>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
def login_page() -> str:
|
||||
return """<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login - Momentry Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
.card { background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; width: 360px; }
|
||||
h1 { font-size: 24px; margin-bottom: 24px; text-align: center; }
|
||||
input { width: 100%; padding: 10px 12px; margin-bottom: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
||||
button { width: 100%; padding: 10px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
|
||||
button:hover { background: #0052a3; }
|
||||
.error { color: #cc0000; font-size: 13px; margin-bottom: 12px; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Momentry Docs</h1>
|
||||
<form id="loginForm">
|
||||
<input type="text" id="username" placeholder="Username" value="demo" required>
|
||||
<input type="password" id="password" placeholder="Password" value="demo" required>
|
||||
<div class="error" id="error">Invalid credentials</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('loginForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
const resp = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.href = '/doc/index.html';
|
||||
} else {
|
||||
document.getElementById('error').style.display = 'block';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
def main():
|
||||
# Clean and recreate doc dirs
|
||||
for d in [DOC_DIR, DOC_DEV_DIR]:
|
||||
if os.path.exists(d):
|
||||
shutil.rmtree(d)
|
||||
os.makedirs(d)
|
||||
|
||||
md_files = sorted(glob.glob(os.path.join(MODULES_DIR, "*.md")))
|
||||
if not md_files:
|
||||
print(f"No MD files found in {MODULES_DIR}")
|
||||
return
|
||||
|
||||
user_html = []
|
||||
dev_html = []
|
||||
for md_path in md_files:
|
||||
with open(md_path) as f:
|
||||
md_text = f.read()
|
||||
fname = os.path.basename(md_path)
|
||||
stem = os.path.splitext(fname)[0]
|
||||
|
||||
# Skip template
|
||||
if stem == "_template":
|
||||
continue
|
||||
|
||||
# Skip error codes (developer-only)
|
||||
if stem == "11_error_codes":
|
||||
dev_only = True
|
||||
else:
|
||||
dev_only = stem not in USER_MODULES
|
||||
|
||||
title = stem.replace("_", " ").title()
|
||||
html = build_html(md_text, title)
|
||||
|
||||
if dev_only:
|
||||
out_path = os.path.join(DOC_DEV_DIR, fname.replace(".md", ".html"))
|
||||
with open(out_path, "w") as f:
|
||||
f.write(html)
|
||||
dev_html.append(fname)
|
||||
print(f" [dev] {fname}")
|
||||
else:
|
||||
out_path = os.path.join(DOC_DIR, fname.replace(".md", ".html"))
|
||||
with open(out_path, "w") as f:
|
||||
f.write(html)
|
||||
user_html.append(fname)
|
||||
print(f" [doc] {fname}")
|
||||
|
||||
# Build indexes + login page
|
||||
for d, files, label in [(DOC_DIR, user_html, "User"), (DOC_DEV_DIR, dev_html, "Dev")]:
|
||||
index = build_index(files)
|
||||
with open(os.path.join(d, "index.html"), "w") as f:
|
||||
f.write(index)
|
||||
with open(os.path.join(d, "login.html"), "w") as f:
|
||||
f.write(login_page())
|
||||
print(f" {label}: {len(files)} pages -> {d}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
148
deliverable_v1.1.0/scripts/sync_dev_to_public.sh
Executable file
148
deliverable_v1.1.0/scripts/sync_dev_to_public.sh
Executable file
@@ -0,0 +1,148 @@
|
||||
#!/bin/bash
|
||||
# sync_dev_to_public.sh — 比對 dev/public schema,同步 pipeline 資料
|
||||
# Usage: ./sync_dev_to_public.sh [check|sync] [file_uuid]
|
||||
|
||||
PSQL="/opt/homebrew/opt/libpq/bin/psql"
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCHEMA="${MOMENTRY_DB_SCHEMA:-dev}"
|
||||
DB_URL="${DATABASE_URL:-postgres://accusys@localhost:5432/momentry}"
|
||||
MODE="${1:-check}"
|
||||
FILE_UUID="${2:-}"
|
||||
|
||||
TABLES=("videos" "chunk" "face_detections" "processor_results" "monitor_jobs"
|
||||
"identities" "identity_bindings" "tkg_nodes" "tkg_edges")
|
||||
|
||||
TARGET="public"
|
||||
|
||||
if [ -z "$FILE_UUID" ]; then
|
||||
echo "Usage: $0 [check|sync] <file_uuid>"
|
||||
echo ""
|
||||
echo "Examples:"
|
||||
echo " $0 check bd80fec92b0b6963d177a2c55bf713e2"
|
||||
echo " $0 sync bd80fec92b0b6963d177a2c55bf713e2"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Schema Sync: $SCHEMA → $TARGET ==="
|
||||
echo "File UUID: $FILE_UUID"
|
||||
echo "Mode: $MODE"
|
||||
echo ""
|
||||
|
||||
check_table() {
|
||||
local table=$1
|
||||
local col=$2
|
||||
local src_count dev_count pub_count
|
||||
|
||||
dev_count=$($PSQL -At "$DB_URL" -c "SELECT COUNT(*) FROM ${SCHEMA}.${table} WHERE ${col} = '${FILE_UUID}';" 2>/dev/null || echo "ERROR")
|
||||
pub_count=$($PSQL -At "$DB_URL" -c "SELECT COUNT(*) FROM ${TARGET}.${table} WHERE ${col} = '${FILE_UUID}';" 2>/dev/null || echo "ERROR")
|
||||
|
||||
if [ "$dev_count" = "ERROR" ] || [ "$pub_count" = "ERROR" ]; then
|
||||
echo " ⚠️ $table — query error (table may not exist in $TARGET)"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ "$dev_count" -eq "$pub_count" ]; then
|
||||
echo " ✅ $table — $dev_count rows (match)"
|
||||
return 0
|
||||
else
|
||||
echo " ❌ $table — dev=$dev_count pub=$pub_count (MISMATCH)"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
sync_table() {
|
||||
local table=$1
|
||||
local col=$2
|
||||
local src_count dev_count pub_count
|
||||
|
||||
dev_count=$($PSQL -At "$DB_URL" -c "SELECT COUNT(*) FROM ${SCHEMA}.${table} WHERE ${col} = '${FILE_UUID}';" 2>/dev/null || echo "0")
|
||||
pub_count=$($PSQL -At "$DB_URL" -c "SELECT COUNT(*) FROM ${TARGET}.${table} WHERE ${col} = '${FILE_UUID}';" 2>/dev/null || echo "0")
|
||||
|
||||
if [ "$dev_count" = "0" ]; then
|
||||
echo " ⏭️ $table — dev has 0 rows, skipping"
|
||||
return
|
||||
fi
|
||||
|
||||
if [ "$dev_count" -eq "$pub_count" ]; then
|
||||
echo " ✅ $table — already synced ($dev_count rows)"
|
||||
return
|
||||
fi
|
||||
|
||||
echo " 🔄 Syncing $table: dev=$dev_count → pub=$pub_count ..."
|
||||
|
||||
# Delete existing public rows, insert from dev
|
||||
$PSQL "$DB_URL" -q -c "DELETE FROM ${TARGET}.${table} WHERE ${col} = '${FILE_UUID}';" 2>/dev/null || true
|
||||
|
||||
# Get columns list (excluding id for SERIAL)
|
||||
COLS=$($PSQL -At "$DB_URL" -c "
|
||||
SELECT string_agg(column_name, ', ' ORDER BY ordinal_position)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema='${SCHEMA}' AND table_name='${table}'
|
||||
AND column_name != 'id'
|
||||
AND is_updatable='YES';
|
||||
")
|
||||
|
||||
$PSQL "$DB_URL" -q -c "
|
||||
INSERT INTO ${TARGET}.${table} (${COLS})
|
||||
SELECT ${COLS}
|
||||
FROM ${SCHEMA}.${table}
|
||||
WHERE ${col} = '${FILE_UUID}';
|
||||
" 2>/dev/null && echo " ✅ $table synced" || echo " ❌ $table sync FAILED"
|
||||
}
|
||||
|
||||
echo "=== Checking Tables ==="
|
||||
echo ""
|
||||
MISMATCH=0
|
||||
for table in "${TABLES[@]}"; do
|
||||
# Determine the UUID column name for each table
|
||||
case "$table" in
|
||||
videos) col="file_uuid" ;;
|
||||
chunk) col="file_uuid" ;;
|
||||
face_detections) col="file_uuid" ;;
|
||||
processor_results) col="file_uuid" ;;
|
||||
monitor_jobs) col="uuid" ;;
|
||||
identities) col="uuid" ;; # identities.uuid is UUID type
|
||||
identity_bindings) col="uuid" ;;
|
||||
tkg_nodes) col="file_uuid" ;;
|
||||
tkg_edges) col="file_uuid" ;;
|
||||
*) col="file_uuid" ;;
|
||||
esac
|
||||
|
||||
if ! check_table "$table" "$col"; then
|
||||
MISMATCH=$((MISMATCH + 1))
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
if [ "$MISMATCH" -eq 0 ]; then
|
||||
echo "✅ All tables in sync"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$MODE" != "sync" ]; then
|
||||
echo "⚠️ $MISMATCH table(s) have mismatches. Run '$0 sync $FILE_UUID' to fix."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Syncing Tables ==="
|
||||
echo ""
|
||||
for table in "${TABLES[@]}"; do
|
||||
case "$table" in
|
||||
videos) col="file_uuid" ;;
|
||||
chunk) col="file_uuid" ;;
|
||||
face_detections) col="file_uuid" ;;
|
||||
processor_results) col="file_uuid" ;;
|
||||
monitor_jobs) col="uuid" ;;
|
||||
identities) col="uuid" ;;
|
||||
identity_bindings) col="uuid" ;;
|
||||
tkg_nodes) col="file_uuid" ;;
|
||||
tkg_edges) col="file_uuid" ;;
|
||||
*) col="file_uuid" ;;
|
||||
esac
|
||||
sync_table "$table" "$col"
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "✅ Sync complete"
|
||||
174
deliverable_v1.1.0/scripts/update_qdrant_uuid.py
Normal file
174
deliverable_v1.1.0/scripts/update_qdrant_uuid.py
Normal file
@@ -0,0 +1,174 @@
|
||||
#!/usr/bin/env python3
|
||||
"""批量更新 Qdrant collection 中的 file_uuid (舊→新)"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
QDRANT_URL = "http://localhost:6333"
|
||||
|
||||
# UUID mapping: 舊 → 新
|
||||
UUID_MAP = {
|
||||
"aeed71342a899fe4b4c57b7d41bcb692": [
|
||||
"bd80fec92b0b6963d177a2c55bf713e2",
|
||||
],
|
||||
}
|
||||
|
||||
# Collections to process
|
||||
COLLECTIONS = [
|
||||
"momentry_dev_v1",
|
||||
"momentry_dev_stories",
|
||||
"momentry_dev_voice",
|
||||
"momentry_dev_rule1_v2",
|
||||
"momentry_dev_faces",
|
||||
"sentence_story",
|
||||
"sentence_summary",
|
||||
]
|
||||
|
||||
|
||||
def qdrant_get(path: str) -> dict:
|
||||
res = subprocess.run(
|
||||
["curl", "-s", "-X", "GET", f"{QDRANT_URL}{path}"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return json.loads(res.stdout) if res.stdout.strip() else {}
|
||||
|
||||
|
||||
def qdrant_post(path: str, body: dict) -> dict:
|
||||
tmp = "/tmp/qdrant_post.json"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(body, f)
|
||||
res = subprocess.run(
|
||||
["curl", "-s", "-X", "POST", f"{QDRANT_URL}{path}",
|
||||
"-H", "Content-Type: application/json", "-d", f"@{tmp}"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return json.loads(res.stdout) if res.stdout.strip() else {}
|
||||
|
||||
|
||||
def qdrant_put(path: str, body: dict) -> dict:
|
||||
tmp = "/tmp/qdrant_update.json"
|
||||
with open(tmp, "w") as f:
|
||||
json.dump(body, f)
|
||||
res = subprocess.run(
|
||||
["curl", "-s", "-X", "PUT", f"{QDRANT_URL}{path}",
|
||||
"-H", "Content-Type: application/json", "-d", f"@{tmp}"],
|
||||
capture_output=True, text=True
|
||||
)
|
||||
return json.loads(res.stdout) if res.stdout.strip() else {}
|
||||
|
||||
|
||||
def scroll_all(collection: str, filter_old: dict) -> list:
|
||||
"""Scroll all matching points from a collection"""
|
||||
points = []
|
||||
offset = None
|
||||
while True:
|
||||
body = {
|
||||
"limit": 1000,
|
||||
"with_payload": True,
|
||||
"with_vector": True,
|
||||
"filter": filter_old,
|
||||
}
|
||||
if offset:
|
||||
body["offset"] = offset
|
||||
result = qdrant_post(f"/collections/{collection}/points/scroll", body)
|
||||
batch = result.get("result", {}).get("points", [])
|
||||
points.extend(batch)
|
||||
next_offset = result.get("result", {}).get("next_page_offset")
|
||||
if next_offset is None:
|
||||
break
|
||||
offset = next_offset
|
||||
return points
|
||||
|
||||
|
||||
def update_points(collection: str, points: list, old_uuid: str, new_uuid: str):
|
||||
"""Update file_uuid in payload for the given points"""
|
||||
if not points:
|
||||
return 0
|
||||
|
||||
updated = []
|
||||
for p in points:
|
||||
pl = p.get("payload", {})
|
||||
# Check both 'uuid' and 'file_uuid' fields
|
||||
changed = False
|
||||
if pl.get("uuid") == old_uuid:
|
||||
pl["uuid"] = new_uuid
|
||||
changed = True
|
||||
if pl.get("file_uuid") == old_uuid:
|
||||
pl["file_uuid"] = new_uuid
|
||||
changed = True
|
||||
if changed:
|
||||
updated.append({
|
||||
"id": p["id"],
|
||||
"vector": p["vector"],
|
||||
"payload": pl,
|
||||
})
|
||||
|
||||
if not updated:
|
||||
return 0
|
||||
|
||||
# Update in batches of 500
|
||||
total = len(updated)
|
||||
for i in range(0, total, 500):
|
||||
batch = updated[i:i+500]
|
||||
result = qdrant_put(
|
||||
f"/collections/{collection}/points?wait=true",
|
||||
{"points": batch}
|
||||
)
|
||||
if result.get("status") != "ok":
|
||||
print(f" Error at {i}: {result}")
|
||||
return i
|
||||
return total
|
||||
|
||||
|
||||
def main():
|
||||
for collection in COLLECTIONS:
|
||||
# Check if collection exists
|
||||
info = qdrant_get(f"/collections/{collection}")
|
||||
if "result" not in info:
|
||||
continue
|
||||
|
||||
for old_uuid, new_uuids in UUID_MAP.items():
|
||||
for new_uuid in new_uuids:
|
||||
# Scroll all points with this old UUID
|
||||
filter_body = {
|
||||
"must": [
|
||||
{"should": [
|
||||
{"key": "uuid", "match": {"value": old_uuid}},
|
||||
{"key": "file_uuid", "match": {"value": old_uuid}},
|
||||
]}
|
||||
]
|
||||
}
|
||||
points = scroll_all(collection, filter_body)
|
||||
if not points:
|
||||
continue
|
||||
|
||||
print(f"{collection}: {len(points)} points with UUID {old_uuid[:8]}...")
|
||||
updated = update_points(collection, points, old_uuid, new_uuid)
|
||||
print(f" → {updated} points updated to {new_uuid[:8]}...")
|
||||
|
||||
# Verify
|
||||
print("\n=== Verification ===")
|
||||
for collection in COLLECTIONS:
|
||||
for old_uuid, new_uuids in UUID_MAP.items():
|
||||
for what, uuid in [("old", old_uuid), ("new", new_uuids[0])]:
|
||||
filter_body = {
|
||||
"must": [
|
||||
{"should": [
|
||||
{"key": "uuid", "match": {"value": uuid}},
|
||||
{"key": "file_uuid", "match": {"value": uuid}},
|
||||
]}
|
||||
]
|
||||
}
|
||||
result = qdrant_post(
|
||||
f"/collections/{collection}/points/count",
|
||||
{"filter": filter_body}
|
||||
)
|
||||
cnt = result.get("result", {}).get("count", 0)
|
||||
if cnt > 0:
|
||||
print(f" {collection}: {cnt} points with {what} UUID")
|
||||
print("✅ Done")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
70
docs/3002_3003_SEPARATION_STATUS.md
Normal file
70
docs/3002_3003_SEPARATION_STATUS.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# 3002/3003 Schema Separation Status
|
||||
|
||||
Date: 2026-05-17
|
||||
Status: ✅ Pipeline tables created in `public`; schema incompatibilities remain
|
||||
|
||||
## Summary
|
||||
|
||||
| Schema | Has pipeline tables | Has auth tables | Used by |
|
||||
|--------|-------------------|-----------------|---------|
|
||||
| `public` | ✅ (newly created) | ✅ (original) | 3002 (production) — currently using `dev` as workaround |
|
||||
| `dev` | ✅ (full, working) | ✅ (synced) | 3003 (playground) |
|
||||
|
||||
## What Was Done
|
||||
|
||||
### Pipeline tables created in `public` schema (11 tables)
|
||||
- `videos`, `chunk`, `chunk_vectors`, `cuts`, `frames`
|
||||
- `monitor_jobs`, `processor_results`, `processor_versions`
|
||||
- `parent_chunks`, `tkg_edges`, `tkg_nodes`
|
||||
|
||||
All include proper sequences, indexes, and constraints matching the `dev` schema.
|
||||
|
||||
## Remaining Blockers
|
||||
|
||||
### Schema incompatibilities between `dev` and `public`
|
||||
|
||||
| Table | dev cols | public cols | Status |
|
||||
|-------|---------|------------|--------|
|
||||
| identities | 17 | 16 | ⚠️ Different columns (e.g. `name` vs `real_name`/`actor_name`) |
|
||||
| face_detections | 16 | 17 | ⚠️ Column count mismatch |
|
||||
| identity_bindings | 7 | 8 | ⚠️ Column count mismatch |
|
||||
| person_identities | 16 | 15 | ⚠️ Column count mismatch |
|
||||
| pre_chunks | 19 | 10 | ⚠️ Significantly different |
|
||||
| api_keys | 19 | 19 | ✅ Match |
|
||||
| resources | 9 | 9 | ✅ Match |
|
||||
| users | 8 | 8 | ✅ Match |
|
||||
|
||||
### Identities table key differences
|
||||
- `public.identities` uses `real_name` + `actor_name` (old schema)
|
||||
- `dev.identities` uses `name` (new unified schema)
|
||||
- `dev.identities` has `tmdb_poster`, `file_uuid`, `face_embedding`, `voice_embedding`, `identity_embedding`
|
||||
- `public.identities` only has `face_embedding`, `voice_embedding` (no `identity_embedding`)
|
||||
|
||||
## Options
|
||||
|
||||
### Option A: Full data migration (recommended for later)
|
||||
1. Dump data from old public tables
|
||||
2. Drop old public tables
|
||||
3. Recreate from dev schema DDL
|
||||
4. Migrate data with column mapping
|
||||
5. Switch 3002 to `DATABASE_SCHEMA=public`
|
||||
|
||||
### Option B: Keep current workaround (simplest for now)
|
||||
- 3002 continues with `DATABASE_SCHEMA=dev`
|
||||
- 3003 uses `DATABASE_SCHEMA=dev`
|
||||
- Both share the same schema, but have separate Redis key prefixes + ports
|
||||
|
||||
### Option C: Rename dev → public (requires downtime)
|
||||
1. Stop all services
|
||||
2. Rename `dev` schema to something else
|
||||
3. Rename `public` to `public_old`
|
||||
4. Rename `dev` to `public`
|
||||
5. Update references
|
||||
|
||||
## Current Status
|
||||
|
||||
✅ Pipeline tables exist in both schemas
|
||||
✅ auth tables (users, sessions, jwt_blacklist) exist in both
|
||||
✅ Redis key prefixes separate (`momentry:` vs `momentry_dev:`)
|
||||
⚠️ 3002 still uses `DATABASE_SCHEMA=dev` workaround
|
||||
⛔ Shared tables need migration before 3002 can use `public` schema
|
||||
255
docs/CHARADE_FACE_MATCHING_EXPERIENCE.md
Normal file
255
docs/CHARADE_FACE_MATCHING_EXPERIENCE.md
Normal file
@@ -0,0 +1,255 @@
|
||||
# Charade 臉部匹配經驗總結
|
||||
|
||||
## 背景
|
||||
|
||||
Charade (1963) 影片 `a6fb22eebefaef17e62af874997c5944` 有 62,298 個人臉偵測結果,分布在 4,378 個 trace 中(TKG face tracker 輸出)。目標是將每張臉匹配到正確的 TMDb 演員 identity。
|
||||
|
||||
## 問題
|
||||
|
||||
### 1. Rust Pipeline (`face_agent.rs`) 的 Snowball 效應
|
||||
|
||||
原始 pipeline 透過多輪 propagation 來匹配:
|
||||
- Seed embedding 匹配 → propagation rounds (2-10 輪)
|
||||
- 每輪把已匹配的 face 當作新 seed 繼續擴散
|
||||
- 結果:**Antonio Passalia 被匹配 18,821 張臉**(實際應 < 50)
|
||||
- 原因:propagation 會放大初始匹配中的假陽性
|
||||
|
||||
### 2. Dev 資料庫污染
|
||||
|
||||
`dev` schema 的 `identity_bindings` 表:
|
||||
- 所有 trace-type binding 的 `file_uuid` 都是 NULL(12,828 行)
|
||||
- 這些 binding 只對應已刪除的 CCBN 檔案 (`63acd3bb`)
|
||||
- **完全無法用於 sync 到 public schema**
|
||||
|
||||
### 3. TMDb Seed Embedding 品質不均
|
||||
|
||||
22/23 個 TMDb identity 有 face_embedding(Thomas Chelimsky 因無 TMDb 照片而缺少)。但這些 seed 來自單一 TMDb 照片,品質差異大:
|
||||
|
||||
| Identity | Seed 品質 | 問題 |
|
||||
|----------|:---------:|:----:|
|
||||
| Audrey Hepburn | ✅ 高 | 特徵明顯,易區分 |
|
||||
| Cary Grant | ✅ 中 | 但 Charade 造型與 seed 照片有差異 |
|
||||
| Walter Matthau | ❌ 低 | Seed 照片與 Charade 形象差異大 |
|
||||
| Bernard Musson | ❌ 泛用 | 「典型白人男性」— seed 太泛用 |
|
||||
| Antonio Passalia | ❌ 泛用 | 同上 |
|
||||
|
||||
## 解決方案演進
|
||||
|
||||
### V1:直接 pgvector 比對 (threshold 0.50)
|
||||
|
||||
```sql
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT i.id FROM identities i
|
||||
WHERE 1 - (embedding <=> i.face_embedding) >= 0.50
|
||||
ORDER BY 1 - (embedding <=> i.face_embedding) DESC LIMIT 1
|
||||
)
|
||||
```
|
||||
|
||||
**結果**:17,066 匹配 (27.4%)
|
||||
- ✅ Audrey 9,550 (正確)
|
||||
- ✅ Antonio 降為 151 (不再 snowball)
|
||||
- ❌ Bernard Musson 847/Paul Bonifas 273 — generic seed 假陽性
|
||||
- ❌ trace-level 衝突(同一 trace 多個 identity)
|
||||
- ❌ Walter Matthau 僅 535(seed 不準導致 recall 低)
|
||||
|
||||
### V2:Trace Conflict Cleanup
|
||||
|
||||
在 V1 之後,對每個 conflict trace 做多數決 → 清除 minority identity。
|
||||
|
||||
**結果**:移除 836 個污染臉
|
||||
- ✅ trace-level 衝突降為 0
|
||||
- ❌ Bernard Musson 仍保留 847(trace 內獨佔)
|
||||
- ❌ 無法解決 generic seed 的根本問題
|
||||
|
||||
### V3:雙階段 Centroid Matching
|
||||
|
||||
設計:
|
||||
|
||||
```
|
||||
Phase 1: Seed matching @ 0.55 (stricter) → 乾淨 base set
|
||||
Phase 2: Centroid matching @ 0.45 → 用電影內平均臉擴張 recall
|
||||
```
|
||||
|
||||
**結果**:27,375 匹配 (43.9%) → trace cleanup → 24,286 (39.0%)
|
||||
- ✅ Audrey 11,347 (+19%)
|
||||
- ✅ Cary Grant 3,107 (+56%)
|
||||
- ✅ Walter Matthau 1,200 (+124%) — centroid 修正 seed!
|
||||
- ❌ **Bernard Musson 2,903 (+243%)** — centroid 放大 generic seed
|
||||
- ❌ **Antonio Passalia 898 (+642%)** — 同上
|
||||
|
||||
**教訓**:Generic seed 的 centroid 更泛用。Phase 2 的低 threshold 讓問題惡化。
|
||||
|
||||
### V4:雙重驗證 (Dual Gate)
|
||||
|
||||
在 V3 的 Phase 2 加上 seed_sim >= 0.40 條件:
|
||||
|
||||
```
|
||||
centroid_sim >= 0.45 AND seed_sim >= 0.40
|
||||
```
|
||||
|
||||
**結果**:23,023 匹配 → gap cleanup → trace cleanup → **22,548 (36.2%)**
|
||||
- ✅ Bernard / Paul / Antonio / Michel / Clément / Raoul / Roger 仍偏高但 avg_seed_sim 改善
|
||||
|
||||
### V5(最終版):排除 7 個 Generic Identity
|
||||
|
||||
核心洞察:**與其過濾假陽性,不如不讓 generic seed 參賽**。
|
||||
|
||||
只保留 11 個可靠的 TMDb identity,排除 7 個:
|
||||
- 排除:Bernard Musson · Paul Bonifas · Michel Thomass · Antonio Passalia · Clément Harari · Raoul Delfosse · Roger Trapp
|
||||
- 保留:Audrey · Cary · James Coburn · Jacques Marin · Walter Matthau · George Kennedy · Dominique Minot · Monte Landis · Stanley Donen · Ned Glass · Louis Viret
|
||||
|
||||
流程:
|
||||
|
||||
```
|
||||
1. Clear all assignments
|
||||
2. Phase 1 @ 0.55 — only against 11 identities
|
||||
3. Compute centroids
|
||||
4. Phase 2 — centroid>=0.45 AND seed>=0.40 (11 centroids)
|
||||
5. Ambiguity gate (top2 gap < 0.04 → NULL)
|
||||
6. Trace conflict cleanup
|
||||
```
|
||||
|
||||
**最終結果**:
|
||||
|
||||
| Identity | 最終 faces | traces | fpt | avg_sim |
|
||||
|----------|:----------:|:------:|:---:|:-------:|
|
||||
| Audrey Hepburn | 11,325 | 438 | 25.9 | 0.608 |
|
||||
| Cary Grant | **5,101** ≪ 大幅增加 | 269 | 19.0 | 0.497 |
|
||||
| James Coburn | 1,508 | 92 | 16.4 | 0.588 |
|
||||
| Jacques Marin | 1,438 | 84 | 17.1 | 0.631 |
|
||||
| Walter Matthau | 1,250 | 55 | 22.7 | 0.494 |
|
||||
| George Kennedy | 869 | 60 | 14.5 | 0.590 |
|
||||
| 排除的 7 個 | **0** ✅ | — | — | — |
|
||||
| Unassigned | 39,750 | — | — | — |
|
||||
|
||||
**Cary Grant 從 3,107→5,101 (+64%)**:之前被 Bernard/Antonio 攔截的臉全部釋放。
|
||||
|
||||
## 關鍵教訓
|
||||
|
||||
### 1. Generic Seed 辨識
|
||||
|
||||
可以透過以下指標辨識 generic seed:
|
||||
- **Phase 1 faces / traces 比例低**(< 5 fpt)
|
||||
- **被分配到大量短 trace**(表示非連續場景)
|
||||
- **avg_seed_sim 偏低但 face count 異常高**
|
||||
|
||||
### 2. Propagation 是雙面刃
|
||||
|
||||
Rust pipeline 的 propagation 可以增加 recall,但前提是 seed 要夠純。Generic seed + propagation = snowball。
|
||||
|
||||
### 3. Seed 數量 vs 品質
|
||||
|
||||
> 不是 identity 越多越好。11 個好 seed 勝過 22 個(含 7 個壞的)。
|
||||
|
||||
壞 seed 會攔截好 seed 的配對。排除壞 seed 後,那些臉自然會配到正確的人。
|
||||
|
||||
### 4. Centroid Matching 的適用條件
|
||||
|
||||
Centroid matching 只有在以下情況才有效:
|
||||
- Centroid 來自高信賴的 Phase 1 配對(threshold >= 0.55)
|
||||
- Centroid 的 Phase 1 base set > 200 faces
|
||||
- 搭配 seed_sim dual gate 防止 centroid 飄移
|
||||
|
||||
### 5. Trace Context 的重要性
|
||||
|
||||
- 一個 trace = 同一人(face tracker 保證)
|
||||
- Trace-level conflict cleanup 是必要的後處理
|
||||
- 但無法解決 trace 層級以下(同一 trace 內)的 contamination
|
||||
|
||||
## 可改進的方向
|
||||
|
||||
### 短期
|
||||
|
||||
1. **手動檢查 Cary Grant 的 5,101 faces**:avg_sim 0.497 偏低,部分可能是假陽性
|
||||
2. **補回已被排除的 identity**:對 Bernard Musson 等用更高 threshold(如 0.60 seed)只看能否 match 到少數高信賴臉
|
||||
3. **降低 Ambiguity Gate threshold**:從 0.04 降到 0.03 可再清除一批邊緣配對
|
||||
|
||||
### 中期
|
||||
|
||||
4. **多 seed 策略**:對每個 identity 用 3-5 張 TMDb 照片,取 centroid 作為 seed
|
||||
5. **場景約束**:利用 shot boundary 資訊限制跨場景的 identity 分配
|
||||
6. **雙向驗證**:同時用 face→identity 和 identity→trace 兩種方向互相驗證
|
||||
|
||||
### 長期
|
||||
|
||||
7. **取代 pgvector face-level matching**:改用 trace-level embedding(同一 trace 的所有 face 取平均),再對 trace 做 identity 匹配,減少 single-frame noise
|
||||
|
||||
## SQL 核心語法
|
||||
|
||||
### pgvector Nearest Neighbor
|
||||
|
||||
```sql
|
||||
SELECT fd.id, m.identity_id
|
||||
FROM eligible fd
|
||||
CROSS JOIN LATERAL (
|
||||
SELECT i.id FROM identities i
|
||||
WHERE 1 - (fd.embedding::vector <=> i.face_embedding) >= {threshold}
|
||||
ORDER BY 1 - (fd.embedding::vector <=> i.face_embedding) DESC
|
||||
LIMIT 1
|
||||
) m
|
||||
```
|
||||
|
||||
### Centroid 計算
|
||||
|
||||
```sql
|
||||
CREATE TABLE centroids AS
|
||||
SELECT identity_id, AVG(embedding::vector) as centroid
|
||||
FROM face_detections
|
||||
WHERE file_uuid = '{uuid}' AND identity_id IS NOT NULL
|
||||
GROUP BY identity_id
|
||||
HAVING COUNT(*) >= 5;
|
||||
```
|
||||
|
||||
### Trace Conflict Cleanup
|
||||
|
||||
```sql
|
||||
WITH conflict_traces AS (
|
||||
SELECT trace_id FROM face_detections
|
||||
WHERE file_uuid = '{uuid}' AND identity_id IS NOT NULL
|
||||
GROUP BY trace_id HAVING COUNT(DISTINCT identity_id) > 1
|
||||
),
|
||||
trace_majority AS (
|
||||
SELECT DISTINCT ON (ct.trace_id) ct.trace_id, fd.identity_id
|
||||
FROM conflict_traces ct
|
||||
JOIN face_detections fd ON fd.trace_id = ct.trace_id
|
||||
WHERE fd.file_uuid = '{uuid}' AND fd.identity_id IS NOT NULL
|
||||
GROUP BY ct.trace_id, fd.identity_id
|
||||
ORDER BY ct.trace_id, COUNT(*) DESC
|
||||
)
|
||||
UPDATE face_detections fd SET identity_id = NULL
|
||||
FROM trace_majority tm
|
||||
WHERE fd.file_uuid = '{uuid}' AND fd.trace_id = tm.trace_id
|
||||
AND fd.identity_id != tm.identity_id;
|
||||
```
|
||||
|
||||
### Ambiguity Gate
|
||||
|
||||
```sql
|
||||
WITH all_sims AS (
|
||||
SELECT fd.id, c.identity_id,
|
||||
1 - (fd.embedding::vector <=> c.centroid) as sim
|
||||
FROM face_detections fd
|
||||
CROSS JOIN centroids c
|
||||
WHERE fd.file_uuid = '{uuid}' AND fd.identity_id IS NOT NULL
|
||||
),
|
||||
ranked AS (
|
||||
SELECT id, sim, LEAD(sim) OVER (PARTITION BY id ORDER BY sim DESC) as sim2
|
||||
FROM all_sims
|
||||
),
|
||||
ambiguous AS (
|
||||
SELECT id FROM ranked
|
||||
WHERE rn = 1 AND sim - COALESCE(sim2, 0) < 0.04
|
||||
)
|
||||
UPDATE face_detections fd SET identity_id = NULL
|
||||
FROM ambiguous a WHERE fd.id = a.id;
|
||||
```
|
||||
|
||||
## 資料庫備份
|
||||
|
||||
每次關鍵操作都有備份:
|
||||
|
||||
| Backup | Rows | 內容 |
|
||||
|--------|:----:|:------|
|
||||
| `fd_charade_bak` | 62,298 | 原始無 identity 的 Charade face_detections |
|
||||
| `fd_state_bak2` | 24,286 | V5 執行前的 assignment snapshot |
|
||||
| `wp_snippets_backup_20260601_11940.sql` | — | WordPress snippets 備份 |
|
||||
@@ -2,15 +2,15 @@
|
||||
document_type: "reference_doc"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "Momentry Core Release API Reference v1.0.0"
|
||||
date: "2026-05-14"
|
||||
version: "V4.1"
|
||||
date: "2026-05-25"
|
||||
version: "V4.2"
|
||||
status: "active"
|
||||
owner: "Warren"
|
||||
---
|
||||
|
||||
# Momentry Core API Reference v1.0.0
|
||||
|
||||
58 endpoints across 10 categories, with real curl examples and responses.
|
||||
55 endpoints across 10 categories, with real curl examples and responses.
|
||||
|
||||
## Base
|
||||
|
||||
@@ -30,12 +30,13 @@ owner: "Warren"
|
||||
|---|--------|------|-------------|
|
||||
| 1 | GET | `/health` | Server status (ok/degraded) |
|
||||
| 2 | GET | `/health/detailed` | Per-service health + latency |
|
||||
| 3 | POST | `/api/v1/auth/login` | Username/password → API key |
|
||||
| 4 | POST | `/api/v1/auth/logout` | Invalidate session |
|
||||
| 5 | GET | `/api/v1/stats/ingest` | Ingest statistics |
|
||||
| 3 | GET | `/health/consistency` | Data consistency check |
|
||||
| 4 | POST | `/api/v1/auth/login` | Username/password → API key |
|
||||
| 5 | POST | `/api/v1/auth/logout` | Invalidate session |
|
||||
| 6 | GET | `/api/v1/stats/sftpgo` | SFTPGo status |
|
||||
| 7 | GET | `/api/v1/stats/inference` | LLM/Embedding health |
|
||||
| 8 | POST | `/api/v1/config/cache` | Toggle Redis cache |
|
||||
| 7 | POST | `/api/v1/config/cache` | Toggle Redis cache |
|
||||
| 8 | POST | `/api/v1/config/auto-pipeline` | Toggle auto-pipeline on register |
|
||||
| 9 | POST | `/api/v1/config/watcher-auto-register` | Toggle watcher auto-register |
|
||||
|
||||
```bash
|
||||
curl http://localhost:3002/health
|
||||
@@ -44,8 +45,8 @@ curl http://localhost:3002/health
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"build_git_hash": "26f2434",
|
||||
"build_timestamp": "2026-05-14T09:09:17Z",
|
||||
"build_git_hash": "de88fd4e",
|
||||
"build_timestamp": "2026-05-25",
|
||||
"uptime_ms": 7052517
|
||||
}
|
||||
```
|
||||
@@ -68,8 +69,8 @@ Supports all file types (video, image, document, audio). SHA256 content_hash com
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"build_git_hash": "26f2434",
|
||||
"build_timestamp": "2026-05-14T09:09:17Z",
|
||||
"build_git_hash": "de88fd4e",
|
||||
"build_timestamp": "2026-05-25",
|
||||
"services": {
|
||||
"postgres": {"status": "ok", "latency_ms": 6},
|
||||
"redis": {"status": "ok", "latency_ms": 0},
|
||||
@@ -103,17 +104,17 @@ Supports all file types (video, image, document, audio). SHA256 content_hash com
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 9 | POST | `/api/v1/files/register` | Register file → file_uuid. Body: `{"file_path":"...", "content_hash":"optional"}` |
|
||||
| 10 | GET | `/api/v1/files/lookup?file_name=` | Pre-upload name conflict check. Returns matches + `next_name` for auto-rename |
|
||||
| 11 | POST | `/api/v1/unregister` | Unregister file(s): by `file_uuid` or pattern match (`file_path`+`pattern`) |
|
||||
| 12 | GET | `/api/v1/files/scan` | Scan directory for new files |
|
||||
| 13 | GET | `/api/v1/files` | List files (paginated) |
|
||||
| 14 | GET | `/api/v1/file/:file_uuid` | Single file detail |
|
||||
| 15 | GET | `/api/v1/file/:file_uuid/probe` | ffprobe metadata |
|
||||
| 16 | POST | `/api/v1/file/:file_uuid/process` | Start pipeline |
|
||||
| 17 | GET | `/api/v1/file/:file_uuid/chunk/:chunk_id` | Single chunk detail (V1.0.2+) |
|
||||
| 18 | GET | `/api/v1/progress/:file_uuid` | Processing progress |
|
||||
| 19 | GET | `/api/v1/jobs` | Monitor jobs (filterable) |
|
||||
| 10 | POST | `/api/v1/files/register` | Register file → file_uuid. Body: `{"file_path":"...", "content_hash":"optional"}` |
|
||||
| 11 | GET | `/api/v1/files/lookup?file_name=` | Pre-upload name conflict check. Returns matches + `next_name` for auto-rename |
|
||||
| 12 | POST | `/api/v1/unregister` | Unregister file(s): by `file_uuid` or pattern match (`file_path`+`pattern`) |
|
||||
| 13 | GET | `/api/v1/files/scan` | Scan directory for new files |
|
||||
| 14 | GET | `/api/v1/files` | List files (paginated) |
|
||||
| 15 | GET | `/api/v1/file/:file_uuid` | Single file detail |
|
||||
| 16 | GET | `/api/v1/file/:file_uuid/probe` | ffprobe metadata |
|
||||
| 17 | POST | `/api/v1/file/:file_uuid/process` | Start pipeline |
|
||||
| 18 | POST | `/api/v1/file/:file_uuid/chunk/:chunk_id` | Single chunk detail (V1.0.2+) |
|
||||
| 19 | POST | `/api/v1/progress/:file_uuid` | Processing progress |
|
||||
| 20 | POST | `/api/v1/jobs` | Monitor jobs (filterable) |
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/files/register -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"file_path":"/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"}'
|
||||
@@ -154,14 +155,14 @@ curl "http://localhost:3002/api/v1/files?page=1&page_size=2" -H "X-API-Key: muse
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 20 | POST | `/api/v1/search/visual` | Visual chunk search |
|
||||
| 21 | POST | `/api/v1/search/visual/class` | By object class |
|
||||
| 22 | POST | `/api/v1/search/visual/density` | By spatial density |
|
||||
| 23 | POST | `/api/v1/search/visual/combination` | Combined visual search |
|
||||
| 24 | POST | `/api/v1/search/visual/stats` | Visual stats |
|
||||
| 25 | POST | `/api/v1/search/smart` | Semantic (EmbeddingGemma + pgvector) |
|
||||
| 26 | POST | `/api/v1/search/universal` | BM25 keyword (requires file_uuid) |
|
||||
| 27 | POST | `/api/v1/search/frames` | Frame-level search |
|
||||
| 21 | POST | `/api/v1/search/visual` | Visual chunk search |
|
||||
| 22 | POST | `/api/v1/search/visual/class` | By object class |
|
||||
| 23 | POST | `/api/v1/search/visual/density` | By spatial density |
|
||||
| 24 | POST | `/api/v1/search/visual/combination` | Combined visual search |
|
||||
| 25 | POST | `/api/v1/search/visual/stats` | Visual stats |
|
||||
| 26 | POST | `/api/v1/search/smart` | Semantic (EmbeddingGemma + pgvector) |
|
||||
| 27 | POST | `/api/v1/search/universal` | BM25 keyword (requires file_uuid) |
|
||||
| 28 | POST | `/api/v1/search/frames` | Frame-level search |
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/search/universal -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"query":"name","limit":2,"mode":"bm25","file_uuid":"3abeee81d94597629ed8cb943f182e94"}'
|
||||
@@ -183,10 +184,10 @@ curl -X POST http://localhost:3002/api/v1/search/universal -H "X-API-Key: muser
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 28 | POST | `/api/v1/file/:file_uuid/face_trace/sortby` | List traces (sorted/filtered) |
|
||||
| 29 | GET | `/api/v1/file/:file_uuid/trace/:trace_id/faces` | Trace detections (+ interpolation) |
|
||||
| 29 | POST | `/api/v1/file/:file_uuid/traces` | List traces (sorted/filtered) |
|
||||
| 30 | GET | `/api/v1/file/:file_uuid/trace/:trace_id/faces` | Trace detections (+ interpolation) |
|
||||
|
||||
### sortby — list traces
|
||||
### traces — list traces
|
||||
|
||||
Parameters:
|
||||
- `sort_by`: `face_count` | `duration` | `first_appearance`
|
||||
@@ -194,7 +195,7 @@ Parameters:
|
||||
- `limit`: max results
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:3002/api/v1/file/3abeee81d94597629ed8cb943f182e94/face_trace/sortby" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"sort_by":"face_count","limit":2}'
|
||||
curl -X POST "http://localhost:3002/api/v1/file/3abeee81d94597629ed8cb943f182e94/traces" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"sort_by":"face_count","limit":2}'
|
||||
```
|
||||
```json
|
||||
{"success":true,"total_traces":6892,"total_faces":108204,"traces":[
|
||||
@@ -224,10 +225,10 @@ curl "http://localhost:3002/api/v1/file/3abeee81d94597629ed8cb943f182e94/trace/2
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 30 | GET | `/api/v1/file/:file_uuid/thumbnail` | Frame JPEG (?frame=&x=&y=&w=&h=) |
|
||||
| 31 | GET | `/api/v1/file/:file_uuid/video` | Raw video stream. Dual input: `?start_time=&end_time=` (seconds) or `?start_frame=&end_frame=` (frames). |
|
||||
| 32 | GET | `/api/v1/file/:file_uuid/video/bbox` | Bbox overlay. `?start_frame=&end_frame=&face_uuid=&duration=` (all frame numbers). Dual input via `start_time`/`end_time`. |
|
||||
| 33 | GET | `/api/v1/file/:file_uuid/trace/:trace_id/video` | Trace clip (?mode=&padding=&audio=) |
|
||||
| 31 | GET | `/api/v1/file/:file_uuid/thumbnail` | Frame JPEG (?frame=&x=&y=&w=&h=) |
|
||||
| 32 | GET | `/api/v1/file/:file_uuid/video` | Raw video stream. Dual input: `?start_time=&end_time=` (seconds) or `?start_frame=&end_frame=` (frames). |
|
||||
| 33 | GET | `/api/v1/file/:file_uuid/video/bbox` | Bbox overlay. `?start_frame=&end_frame=&face_uuid=&duration=` (all frame numbers). Dual input via `start_time`/`end_time`. |
|
||||
| 34 | GET | `/api/v1/file/:file_uuid/trace/:trace_id/video` | Trace clip (?mode=&padding=&audio=) |
|
||||
|
||||
All video endpoints support:
|
||||
- `mode=normal|debug` (default: `normal`)
|
||||
@@ -260,16 +261,16 @@ Green bbox per face detection: actual frames `thickness=4`, interpolated `thickn
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 33 | GET | `/api/v1/identities` | List all identities |
|
||||
| 34 | GET | `/api/v1/file/:file_uuid/identities` | Identities in a file |
|
||||
| 35 | POST | `/api/v1/identity` | Register new identity |
|
||||
| 36 | GET | `/api/v1/identity/:identity_uuid` | Identity detail |
|
||||
| 37 | DELETE | `/api/v1/identity/:identity_uuid` | Delete identity |
|
||||
| 38 | GET | `/api/v1/identity/:identity_uuid/files` | Files for identity |
|
||||
| 39 | GET | `/api/v1/identity/:identity_uuid/chunks` | Chunks for identity |
|
||||
| 40 | GET | `/api/v1/faces/candidates` | Unbound face gallery |
|
||||
| 41 | GET | `/api/v1/identities/search?q=` | Search identities by name → chunks |
|
||||
| 42 | GET | `/api/v1/search/identity_text?q=&file_uuid=` | Full-text search → identity-bound chunks |
|
||||
| 35 | GET | `/api/v1/identities` | List all identities |
|
||||
| 36 | GET | `/api/v1/file/:file_uuid/identities` | Identities in a file |
|
||||
| 37 | POST | `/api/v1/identity` | Register new identity |
|
||||
| 38 | GET | `/api/v1/identity/:identity_uuid` | Identity detail |
|
||||
| 39 | DELETE | `/api/v1/identity/:identity_uuid` | Delete identity |
|
||||
| 40 | GET | `/api/v1/identity/:identity_uuid/files` | Files for identity |
|
||||
| 41 | GET | `/api/v1/identity/:identity_uuid/chunks` | Chunks for identity |
|
||||
| 42 | GET | `/api/v1/faces/candidates` | Unbound face gallery |
|
||||
| 43 | GET | `/api/v1/identities/search?q=` | Search identities by name → chunks |
|
||||
| 44 | GET | `/api/v1/search/identity_text?q=&file_uuid=` | Full-text search → identity-bound chunks |
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3002/api/v1/identities?page=1&page_size=3" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
|
||||
@@ -307,9 +308,9 @@ curl "http://localhost:3002/api/v1/faces/candidates?page=1&page_size=2" -H "X-A
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 43 | POST | `/api/v1/identity/:identity_uuid/bind` | Bind face → identity |
|
||||
| 44 | POST | `/api/v1/identity/:identity_uuid/unbind` | Unbind face from identity |
|
||||
| 45 | POST | `/api/v1/identity/:identity_uuid/mergeinto` | Merge into another identity |
|
||||
| 45 | POST | `/api/v1/identity/:identity_uuid/bind` | Bind face → identity |
|
||||
| 46 | POST | `/api/v1/identity/:identity_uuid/unbind` | Unbind face from identity |
|
||||
| 47 | POST | `/api/v1/identity/:identity_uuid/mergeinto` | Merge into another identity |
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:3002/api/v1/identity/a9a90105-6d6b-46ff-92da-0c3c1a57dff4/bind" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"file_uuid":"3abeee81d94597629ed8cb943f182e94","face_id":"face_42"}'
|
||||
@@ -324,9 +325,9 @@ curl -X POST "http://localhost:3002/api/v1/identity/a9a90105-6d6b-46ff-92da-0c3c
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 46 | POST | `/api/v1/resource/register` | Register processing resource |
|
||||
| 47 | POST | `/api/v1/resource/heartbeat` | Resource heartbeat |
|
||||
| 48 | GET | `/api/v1/resources` | List all resources |
|
||||
| 48 | POST | `/api/v1/resource/register` | Register processing resource |
|
||||
| 49 | POST | `/api/v1/resource/heartbeat` | Resource heartbeat |
|
||||
| 50 | GET | `/api/v1/resources` | List all resources |
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3002/api/v1/resources" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
|
||||
@@ -341,10 +342,10 @@ curl "http://localhost:3002/api/v1/resources" -H "X-API-Key: muser_686008560363
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 49 | POST | `/api/v1/agents/translate` | AI text translation |
|
||||
| 50 | POST | `/api/v1/agents/5w1h/analyze` | Single chunk analysis |
|
||||
| 51 | POST | `/api/v1/agents/5w1h/batch` | Batch analysis |
|
||||
| 52 | GET | `/api/v1/agents/5w1h/status` | Job status |
|
||||
| 51 | POST | `/api/v1/agents/translate` | AI text translation |
|
||||
| 52 | POST | `/api/v1/agents/5w1h/analyze` | Single chunk analysis |
|
||||
| 53 | POST | `/api/v1/agents/5w1h/batch` | Batch analysis |
|
||||
| 54 | GET | `/api/v1/agents/5w1h/status` | Job status |
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:3002/api/v1/agents/translate" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"text":"Hello world","target_language":"zh-TW"}'
|
||||
@@ -359,11 +360,10 @@ curl -X POST "http://localhost:3002/api/v1/agents/translate" -H "X-API-Key: mus
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 53 | POST | `/api/v1/agents/identity/analyze` | Identify faces in file |
|
||||
| 54 | GET | `/api/v1/agents/identity/status` | Analysis status |
|
||||
| 55 | POST | `/api/v1/agents/identity/suggest` | Name suggestions |
|
||||
| 56 | POST | `/api/v1/agents/suggest/merge` | Suggest merge |
|
||||
| 57 | POST | `/api/v1/agents/suggest/clustering` | Suggest re-clustering |
|
||||
| 55 | POST | `/api/v1/agents/identity/match-from-photo` | Match face from photo |
|
||||
| 56 | POST | `/api/v1/agents/identity/match-from-trace` | Match face from trace |
|
||||
| 57 | POST | `/api/v1/agents/suggest/merge` | Suggest merge |
|
||||
| 58 | POST | `/api/v1/agents/suggest/clustering` | Suggest re-clustering |
|
||||
|
||||
---
|
||||
|
||||
@@ -371,10 +371,11 @@ curl -X POST "http://localhost:3002/api/v1/agents/translate" -H "X-API-Key: mus
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| V4.2 | 2026-05-25 | Removed phantom routes (stats/ingest, stats/inference, agents/identity/status); fixed HTTP methods (chunk, progress, jobs → POST); renamed endpoints (face_trace/sortby → traces, analyze → match-from-photo, suggest → match-from-trace); added config endpoints (consistency, auto-pipeline, watcher-auto-register); updated git hash to de88fd4e |
|
||||
| V4.1 | 2026-05-14 | Added `build_timestamp` + `resources` + `pipeline` to health APIs; identity search endpoints; trace debug rework (green bbox, text overlay, all traces listed) |
|
||||
|
||||
## Related
|
||||
|
||||
- `API_DICTIONARY_V1.0.0.md` — Quick reference (58 endpoints)
|
||||
- `API_DICTIONARY_V1.0.0.md` — Quick reference (55 endpoints)
|
||||
- `API_DOCUMENTATION_v1.0.0.md` — Detailed spec with examples
|
||||
- `TRACE/TRACE_API_REFERENCE_V1.0.0.md` — Trace-specific reference
|
||||
|
||||
@@ -2,21 +2,21 @@
|
||||
document_type: "reference_doc"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "Momentry Core Release API Reference v1.0.0"
|
||||
date: "2026-05-14"
|
||||
version: "V4.1"
|
||||
date: "2026-05-25"
|
||||
version: "V4.2"
|
||||
status: "active"
|
||||
owner: "Warren"
|
||||
---
|
||||
|
||||
# Momentry Core API Reference v1.0.0
|
||||
|
||||
58 endpoints across 10 categories, with real curl examples and responses.
|
||||
55 endpoints across 10 categories, with real curl examples and responses.
|
||||
|
||||
## Base
|
||||
|
||||
| Environment | URL |
|
||||
|-------------|-----|
|
||||
| Production | `http://localhost:3002` or `https://m5api.momentry.ddns.net` |
|
||||
| Production | `http://localhost:3002` or `https://api.momentry.ddns.net` |
|
||||
| Development | `http://localhost:3003` |
|
||||
| Auth | Header `X-API-Key: <key>` (login endpoint unprotected) |
|
||||
|
||||
@@ -30,14 +30,13 @@ owner: "Warren"
|
||||
|---|--------|------|-------------|
|
||||
| 1 | GET | `/health` | Server status (ok/degraded) |
|
||||
| 2 | GET | `/health/detailed` | Per-service health + latency |
|
||||
| 3 | POST | `/api/v1/auth/login` | Username/password → API key |
|
||||
| 4 | POST | `/api/v1/auth/logout` | Invalidate session |
|
||||
| 5 | GET | `/api/v1/stats/ingest` | Ingest statistics |
|
||||
| 3 | GET | `/health/consistency` | Data consistency check |
|
||||
| 4 | POST | `/api/v1/auth/login` | Username/password → API key |
|
||||
| 5 | POST | `/api/v1/auth/logout` | Invalidate session |
|
||||
| 6 | GET | `/api/v1/stats/sftpgo` | SFTPGo status |
|
||||
| 7 | GET | `/api/v1/stats/inference` | LLM/Embedding health |
|
||||
| 8 | POST | `/api/v1/config/cache` | Toggle Redis cache |
|
||||
| 9 | POST | `/api/v1/config/auto-pipeline` | Toggle auto-pipeline on register |
|
||||
| 10 | POST | `/api/v1/config/watcher-auto-register` | Toggle watcher auto-register |
|
||||
| 7 | POST | `/api/v1/config/cache` | Toggle Redis cache |
|
||||
| 8 | POST | `/api/v1/config/auto-pipeline` | Toggle auto-pipeline on register |
|
||||
| 9 | POST | `/api/v1/config/watcher-auto-register` | Toggle watcher auto-register |
|
||||
|
||||
```bash
|
||||
curl http://localhost:3002/health
|
||||
@@ -46,8 +45,8 @@ curl http://localhost:3002/health
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "1.0.0",
|
||||
"build_git_hash": "26f2434",
|
||||
"build_timestamp": "2026-05-14T09:09:17Z",
|
||||
"build_git_hash": "de88fd4e",
|
||||
"build_timestamp": "2026-05-25",
|
||||
"uptime_ms": 7052517
|
||||
}
|
||||
```
|
||||
@@ -70,8 +69,8 @@ Supports all file types (video, image, document, audio). SHA256 content_hash com
|
||||
```json
|
||||
{
|
||||
"status": "ok",
|
||||
"build_git_hash": "26f2434",
|
||||
"build_timestamp": "2026-05-14T09:09:17Z",
|
||||
"build_git_hash": "de88fd4e",
|
||||
"build_timestamp": "2026-05-25",
|
||||
"services": {
|
||||
"postgres": {"status": "ok", "latency_ms": 6},
|
||||
"redis": {"status": "ok", "latency_ms": 0},
|
||||
@@ -105,17 +104,17 @@ Supports all file types (video, image, document, audio). SHA256 content_hash com
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 9 | POST | `/api/v1/files/register` | Register file → file_uuid. Body: `{"file_path":"...", "content_hash":"optional"}` |
|
||||
| 10 | GET | `/api/v1/files/lookup?file_name=` | Pre-upload name conflict check. Returns matches + `next_name` for auto-rename |
|
||||
| 11 | POST | `/api/v1/unregister` | Unregister file(s): by `file_uuid` or pattern match (`file_path`+`pattern`) |
|
||||
| 12 | GET | `/api/v1/files/scan` | Scan directory for new files |
|
||||
| 13 | GET | `/api/v1/files` | List files (paginated) |
|
||||
| 14 | GET | `/api/v1/file/:file_uuid` | Single file detail |
|
||||
| 15 | GET | `/api/v1/file/:file_uuid/probe` | ffprobe metadata |
|
||||
| 16 | POST | `/api/v1/file/:file_uuid/process` | Start pipeline |
|
||||
| 17 | GET | `/api/v1/file/:file_uuid/chunk/:chunk_id` | Single chunk detail (V1.0.2+) |
|
||||
| 18 | GET | `/api/v1/progress/:file_uuid` | Processing progress |
|
||||
| 19 | GET | `/api/v1/jobs` | Monitor jobs (filterable) |
|
||||
| 10 | POST | `/api/v1/files/register` | Register file → file_uuid. Body: `{"file_path":"...", "content_hash":"optional"}` |
|
||||
| 11 | GET | `/api/v1/files/lookup?file_name=` | Pre-upload name conflict check. Returns matches + `next_name` for auto-rename |
|
||||
| 12 | POST | `/api/v1/unregister` | Unregister file(s): by `file_uuid` or pattern match (`file_path`+`pattern`) |
|
||||
| 13 | GET | `/api/v1/files/scan` | Scan directory for new files |
|
||||
| 14 | GET | `/api/v1/files` | List files (paginated) |
|
||||
| 15 | GET | `/api/v1/file/:file_uuid` | Single file detail |
|
||||
| 16 | GET | `/api/v1/file/:file_uuid/probe` | ffprobe metadata |
|
||||
| 17 | POST | `/api/v1/file/:file_uuid/process` | Start pipeline |
|
||||
| 18 | POST | `/api/v1/file/:file_uuid/chunk/:chunk_id` | Single chunk detail (V1.0.2+) |
|
||||
| 19 | POST | `/api/v1/progress/:file_uuid` | Processing progress |
|
||||
| 20 | POST | `/api/v1/jobs` | Monitor jobs (filterable) |
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/files/register -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"file_path":"/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"}'
|
||||
@@ -156,14 +155,14 @@ curl "http://localhost:3002/api/v1/files?page=1&page_size=2" -H "X-API-Key: muse
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 20 | POST | `/api/v1/search/visual` | Visual chunk search |
|
||||
| 21 | POST | `/api/v1/search/visual/class` | By object class |
|
||||
| 22 | POST | `/api/v1/search/visual/density` | By spatial density |
|
||||
| 23 | POST | `/api/v1/search/visual/combination` | Combined visual search |
|
||||
| 24 | POST | `/api/v1/search/visual/stats` | Visual stats |
|
||||
| 25 | POST | `/api/v1/search/smart` | Semantic (EmbeddingGemma + pgvector) |
|
||||
| 26 | POST | `/api/v1/search/universal` | BM25 keyword (requires file_uuid) |
|
||||
| 27 | POST | `/api/v1/search/frames` | Frame-level search |
|
||||
| 21 | POST | `/api/v1/search/visual` | Visual chunk search |
|
||||
| 22 | POST | `/api/v1/search/visual/class` | By object class |
|
||||
| 23 | POST | `/api/v1/search/visual/density` | By spatial density |
|
||||
| 24 | POST | `/api/v1/search/visual/combination` | Combined visual search |
|
||||
| 25 | POST | `/api/v1/search/visual/stats` | Visual stats |
|
||||
| 26 | POST | `/api/v1/search/smart` | Semantic (EmbeddingGemma + pgvector) |
|
||||
| 27 | POST | `/api/v1/search/universal` | BM25 keyword (requires file_uuid) |
|
||||
| 28 | POST | `/api/v1/search/frames` | Frame-level search |
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3002/api/v1/search/universal -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"query":"name","limit":2,"mode":"bm25","file_uuid":"3abeee81d94597629ed8cb943f182e94"}'
|
||||
@@ -185,10 +184,10 @@ curl -X POST http://localhost:3002/api/v1/search/universal -H "X-API-Key: muser
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 28 | POST | `/api/v1/file/:file_uuid/face_trace/sortby` | List traces (sorted/filtered) |
|
||||
| 29 | GET | `/api/v1/file/:file_uuid/trace/:trace_id/faces` | Trace detections (+ interpolation) |
|
||||
| 29 | POST | `/api/v1/file/:file_uuid/traces` | List traces (sorted/filtered) |
|
||||
| 30 | GET | `/api/v1/file/:file_uuid/trace/:trace_id/faces` | Trace detections (+ interpolation) |
|
||||
|
||||
### sortby — list traces
|
||||
### traces — list traces
|
||||
|
||||
Parameters:
|
||||
- `sort_by`: `face_count` | `duration` | `first_appearance`
|
||||
@@ -196,7 +195,7 @@ Parameters:
|
||||
- `limit`: max results
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:3002/api/v1/file/3abeee81d94597629ed8cb943f182e94/face_trace/sortby" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"sort_by":"face_count","limit":2}'
|
||||
curl -X POST "http://localhost:3002/api/v1/file/3abeee81d94597629ed8cb943f182e94/traces" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"sort_by":"face_count","limit":2}'
|
||||
```
|
||||
```json
|
||||
{"success":true,"total_traces":6892,"total_faces":108204,"traces":[
|
||||
@@ -226,10 +225,10 @@ curl "http://localhost:3002/api/v1/file/3abeee81d94597629ed8cb943f182e94/trace/2
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 30 | GET | `/api/v1/file/:file_uuid/thumbnail` | Frame JPEG (?frame=&x=&y=&w=&h=) |
|
||||
| 31 | GET | `/api/v1/file/:file_uuid/video` | Raw video stream. Dual input: `?start_time=&end_time=` (seconds) or `?start_frame=&end_frame=` (frames). |
|
||||
| 32 | GET | `/api/v1/file/:file_uuid/video/bbox` | Bbox overlay. `?start_frame=&end_frame=&face_uuid=&duration=` (all frame numbers). Dual input via `start_time`/`end_time`. |
|
||||
| 33 | GET | `/api/v1/file/:file_uuid/trace/:trace_id/video` | Trace clip (?mode=&padding=&audio=) |
|
||||
| 31 | GET | `/api/v1/file/:file_uuid/thumbnail` | Frame JPEG (?frame=&x=&y=&w=&h=) |
|
||||
| 32 | GET | `/api/v1/file/:file_uuid/video` | Raw video stream. Dual input: `?start_time=&end_time=` (seconds) or `?start_frame=&end_frame=` (frames). |
|
||||
| 33 | GET | `/api/v1/file/:file_uuid/video/bbox` | Bbox overlay. `?start_frame=&end_frame=&face_uuid=&duration=` (all frame numbers). Dual input via `start_time`/`end_time`. |
|
||||
| 34 | GET | `/api/v1/file/:file_uuid/trace/:trace_id/video` | Trace clip (?mode=&padding=&audio=) |
|
||||
|
||||
All video endpoints support:
|
||||
- `mode=normal|debug` (default: `normal`)
|
||||
@@ -262,16 +261,16 @@ Green bbox per face detection: actual frames `thickness=4`, interpolated `thickn
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 33 | GET | `/api/v1/identities` | List all identities |
|
||||
| 34 | GET | `/api/v1/file/:file_uuid/identities` | Identities in a file |
|
||||
| 35 | POST | `/api/v1/identity` | Register new identity |
|
||||
| 36 | GET | `/api/v1/identity/:identity_uuid` | Identity detail |
|
||||
| 37 | DELETE | `/api/v1/identity/:identity_uuid` | Delete identity |
|
||||
| 38 | GET | `/api/v1/identity/:identity_uuid/files` | Files for identity |
|
||||
| 39 | GET | `/api/v1/identity/:identity_uuid/chunks` | Chunks for identity |
|
||||
| 40 | GET | `/api/v1/faces/candidates` | Unbound face gallery |
|
||||
| 41 | GET | `/api/v1/identities/search?q=` | Search identities by name → chunks |
|
||||
| 42 | GET | `/api/v1/search/identity_text?q=&file_uuid=` | Full-text search → identity-bound chunks |
|
||||
| 35 | GET | `/api/v1/identities` | List all identities |
|
||||
| 36 | GET | `/api/v1/file/:file_uuid/identities` | Identities in a file |
|
||||
| 37 | POST | `/api/v1/identity` | Register new identity |
|
||||
| 38 | GET | `/api/v1/identity/:identity_uuid` | Identity detail |
|
||||
| 39 | DELETE | `/api/v1/identity/:identity_uuid` | Delete identity |
|
||||
| 40 | GET | `/api/v1/identity/:identity_uuid/files` | Files for identity |
|
||||
| 41 | GET | `/api/v1/identity/:identity_uuid/chunks` | Chunks for identity |
|
||||
| 42 | GET | `/api/v1/faces/candidates` | Unbound face gallery |
|
||||
| 43 | GET | `/api/v1/identities/search?q=` | Search identities by name → chunks |
|
||||
| 44 | GET | `/api/v1/search/identity_text?q=&file_uuid=` | Full-text search → identity-bound chunks |
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3002/api/v1/identities?page=1&page_size=3" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
|
||||
@@ -309,9 +308,9 @@ curl "http://localhost:3002/api/v1/faces/candidates?page=1&page_size=2" -H "X-A
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 43 | POST | `/api/v1/identity/:identity_uuid/bind` | Bind face → identity |
|
||||
| 44 | POST | `/api/v1/identity/:identity_uuid/unbind` | Unbind face from identity |
|
||||
| 45 | POST | `/api/v1/identity/:identity_uuid/mergeinto` | Merge into another identity |
|
||||
| 45 | POST | `/api/v1/identity/:identity_uuid/bind` | Bind face → identity |
|
||||
| 46 | POST | `/api/v1/identity/:identity_uuid/unbind` | Unbind face from identity |
|
||||
| 47 | POST | `/api/v1/identity/:identity_uuid/mergeinto` | Merge into another identity |
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:3002/api/v1/identity/a9a90105-6d6b-46ff-92da-0c3c1a57dff4/bind" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"file_uuid":"3abeee81d94597629ed8cb943f182e94","face_id":"face_42"}'
|
||||
@@ -326,9 +325,9 @@ curl -X POST "http://localhost:3002/api/v1/identity/a9a90105-6d6b-46ff-92da-0c3c
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 46 | POST | `/api/v1/resource/register` | Register processing resource |
|
||||
| 47 | POST | `/api/v1/resource/heartbeat` | Resource heartbeat |
|
||||
| 48 | GET | `/api/v1/resources` | List all resources |
|
||||
| 48 | POST | `/api/v1/resource/register` | Register processing resource |
|
||||
| 49 | POST | `/api/v1/resource/heartbeat` | Resource heartbeat |
|
||||
| 50 | GET | `/api/v1/resources` | List all resources |
|
||||
|
||||
```bash
|
||||
curl "http://localhost:3002/api/v1/resources" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
|
||||
@@ -343,10 +342,10 @@ curl "http://localhost:3002/api/v1/resources" -H "X-API-Key: muser_686008560363
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 49 | POST | `/api/v1/agents/translate` | AI text translation |
|
||||
| 50 | POST | `/api/v1/agents/5w1h/analyze` | Single chunk analysis |
|
||||
| 51 | POST | `/api/v1/agents/5w1h/batch` | Batch analysis |
|
||||
| 52 | GET | `/api/v1/agents/5w1h/status` | Job status |
|
||||
| 51 | POST | `/api/v1/agents/translate` | AI text translation |
|
||||
| 52 | POST | `/api/v1/agents/5w1h/analyze` | Single chunk analysis |
|
||||
| 53 | POST | `/api/v1/agents/5w1h/batch` | Batch analysis |
|
||||
| 54 | GET | `/api/v1/agents/5w1h/status` | Job status |
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:3002/api/v1/agents/translate" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" -H "Content-Type: application/json" -d '{"text":"Hello world","target_language":"zh-TW"}'
|
||||
@@ -361,11 +360,10 @@ curl -X POST "http://localhost:3002/api/v1/agents/translate" -H "X-API-Key: mus
|
||||
|
||||
| # | Method | Path | Description |
|
||||
|---|--------|------|-------------|
|
||||
| 53 | POST | `/api/v1/agents/identity/analyze` | Identify faces in file |
|
||||
| 54 | GET | `/api/v1/agents/identity/status` | Analysis status |
|
||||
| 55 | POST | `/api/v1/agents/identity/suggest` | Name suggestions |
|
||||
| 56 | POST | `/api/v1/agents/suggest/merge` | Suggest merge |
|
||||
| 57 | POST | `/api/v1/agents/suggest/clustering` | Suggest re-clustering |
|
||||
| 55 | POST | `/api/v1/agents/identity/match-from-photo` | Match face from photo |
|
||||
| 56 | POST | `/api/v1/agents/identity/match-from-trace` | Match face from trace |
|
||||
| 57 | POST | `/api/v1/agents/suggest/merge` | Suggest merge |
|
||||
| 58 | POST | `/api/v1/agents/suggest/clustering` | Suggest re-clustering |
|
||||
|
||||
---
|
||||
|
||||
@@ -373,10 +371,11 @@ curl -X POST "http://localhost:3002/api/v1/agents/translate" -H "X-API-Key: mus
|
||||
|
||||
| Version | Date | Changes |
|
||||
|---------|------|---------|
|
||||
| V4.2 | 2026-05-25 | Removed phantom routes (stats/ingest, stats/inference, agents/identity/status); fixed HTTP methods (chunk, progress, jobs → POST); renamed endpoints (face_trace/sortby → traces, analyze → match-from-photo, suggest → match-from-trace); added config endpoints (consistency, auto-pipeline, watcher-auto-register); updated git hash to de88fd4e |
|
||||
| V4.1 | 2026-05-14 | Added `build_timestamp` + `resources` + `pipeline` to health APIs; identity search endpoints; trace debug rework (green bbox, text overlay, all traces listed) |
|
||||
|
||||
## Related
|
||||
|
||||
- `API_DICTIONARY_V1.0.0.md` — Quick reference (58 endpoints)
|
||||
- `API_DICTIONARY_V1.0.0.md` — Quick reference (55 endpoints)
|
||||
- `API_DOCUMENTATION_v1.0.0.md` — Detailed spec with examples
|
||||
- `TRACE/TRACE_API_REFERENCE_V1.0.0.md` — Trace-specific reference
|
||||
|
||||
@@ -158,6 +158,8 @@ related_documents:
|
||||
| 51 | GET | `/api/v1/stats/sftpgo` | SFTPGo 使用者狀態 | ✅ |
|
||||
| 52 | GET | `/api/v1/stats/inference` | 推理叢集健康狀態 | ✅ |
|
||||
| 53 | POST | `/api/v1/config/cache` | 切換快取開關 | ✅ |
|
||||
| 54 | POST | `/api/v1/config/auto-pipeline` | 註冊後自動處理 | ✅ |
|
||||
| 55 | POST | `/api/v1/config/watcher-auto-register` | Watcher 自動註冊 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
|
||||
2
docs_v1.0/API_WORKSPACE/.gitignore
vendored
Normal file
2
docs_v1.0/API_WORKSPACE/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
_build/
|
||||
.DS_Store
|
||||
60
docs_v1.0/API_WORKSPACE/README.md
Normal file
60
docs_v1.0/API_WORKSPACE/README.md
Normal file
@@ -0,0 +1,60 @@
|
||||
# API Workspace
|
||||
|
||||
## Purpose
|
||||
|
||||
This directory is the **single source of truth** for all API documentation modules.
|
||||
Generated outputs go to `../GUIDES/` as assembled deliverable documents.
|
||||
|
||||
## Workflow
|
||||
|
||||
```bash
|
||||
# 1. Edit a module
|
||||
vim modules/09_tmdb.md
|
||||
|
||||
# 2. Preview the generated output
|
||||
make _build/API_ENDPOINTS.md
|
||||
|
||||
# 3. Check diff against current GUIDES/ content
|
||||
make check
|
||||
|
||||
# 4. Deploy to GUIDES/
|
||||
make deploy
|
||||
|
||||
# 5. Regenerate all
|
||||
make all
|
||||
```
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
API_WORKSPACE/
|
||||
├── modules/ ← 11 module files (01_auth ... 11_error_codes)
|
||||
├── configs/ ← 7 assembly recipies (.toml)
|
||||
├── narratives/ ← narrative intros for specific output files
|
||||
├── _build/ ← generated output (gitignored)
|
||||
├── Makefile ← build targets
|
||||
├── assemble_docs.sh ← assembly engine
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Available `make` Targets
|
||||
|
||||
| Target | Output |
|
||||
|--------|--------|
|
||||
| `make reference` | `_build/API_REFERENCE.md` |
|
||||
| `make endpoints` | `_build/API_ENDPOINTS.md` |
|
||||
| `make quickref` | `_build/API_QUICK_REFERENCE.md` |
|
||||
| `make errors` | `_build/API_ERROR_CODES.md` |
|
||||
| `make index` | `_build/API_INDEX.md` |
|
||||
| `make marcom` | `_build/API_TRAINING_MARCOM.md` |
|
||||
| `make tmdb` | `_build/TMDb_User_Guide.md` |
|
||||
| `make all` | All of the above |
|
||||
| `make deploy` | Copy `_build/*` → `../GUIDES/` |
|
||||
| `make check` | `diff` against existing `../GUIDES/` files |
|
||||
|
||||
## Adding a New Endpoint
|
||||
|
||||
1. Add the endpoint to the appropriate module (e.g., `modules/XX_files.md`)
|
||||
2. Follow the template in `modules/_template.md`
|
||||
3. `make all && make check`
|
||||
4. `make deploy`
|
||||
@@ -7,7 +7,7 @@
|
||||
### `POST /api/v1/search/smart`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Semantic vector search using EmbeddingGemma-300m. Generates a query embedding via EmbeddingGemma (port 11436), then searches pgvector `story_parent` and `llm_parent` chunks by cosine similarity.
|
||||
|
||||
@@ -15,13 +15,22 @@ Semantic vector search using EmbeddingGemma-300m. Generates a query embedding vi
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `file_uuid` | string | Yes | — | File UUID to search within |
|
||||
| `query` | string | Yes | — | Search text |
|
||||
| `file_uuid` | string | No | — | File UUID to search within. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 5 | Max results to return |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 5 | Items per page |
|
||||
|
||||
#### Example
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/smart" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-d '{"query": "Audrey Hepburn"}'
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/smart" \
|
||||
@@ -37,6 +46,7 @@ curl -s -X POST "$API/api/v1/search/smart" \
|
||||
"query": "Audrey Hepburn",
|
||||
"results": [
|
||||
{
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"parent_id": 1087822,
|
||||
"scene_order": 1087822,
|
||||
"start_frame": 104438,
|
||||
@@ -54,12 +64,16 @@ curl -s -X POST "$API/api/v1/search/smart" \
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].file_uuid` | string | File UUID where result was found |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/universal`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Multi-type BM25 full-text search across chunks, frames, and persons. Uses PostgreSQL `tsvector`.
|
||||
|
||||
@@ -68,13 +82,22 @@ Multi-type BM25 full-text search across chunks, frames, and persons. Uses Postgr
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `query` | string | Yes | — | Search text |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `types` | string[] | No | `["chunk","frame","person"]` | Search types |
|
||||
| `limit` | integer | No | 10 | Max results per type |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 20 | Items per page |
|
||||
|
||||
#### Example
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/universal" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-d '{"query": "Cary Grant"}'
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/universal" \
|
||||
@@ -90,6 +113,7 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
"results": [
|
||||
{
|
||||
"type": "chunk",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"chunk_id": "bd80fec92b0b6963d177a2c55bf713e2_2",
|
||||
"chunk_type": "story_child",
|
||||
"start_frame": 5103,
|
||||
@@ -98,6 +122,25 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
"end_time": 213.64,
|
||||
"text": "[213s-214s] Cary Grant: \"Olá!\"",
|
||||
"score": 0.9
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"frame_number": 5105,
|
||||
"timestamp": 212.72,
|
||||
"score": 0.7,
|
||||
"objects": null,
|
||||
"ocr_texts": null,
|
||||
"faces": null
|
||||
},
|
||||
{
|
||||
"type": "person",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"identity_id": 12,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"appearance_count": 542,
|
||||
"score": 0.95
|
||||
}
|
||||
],
|
||||
"total": 20,
|
||||
@@ -105,23 +148,78 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].type` | string | Result type: `chunk`, `frame`, or `person` |
|
||||
| `results[].file_uuid` | string | File UUID where result was found (all types) |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/frames`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search face detection frames by identity name or trace ID.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/identity_text`
|
||||
### `GET /api/v1/search/identity_text`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search text chunks spoken by a specific identity.
|
||||
Search text chunks → find associated identities. Returns chunks where face detections overlap with text content.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `q` | string | Yes | — | Search text (ILIKE match) |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 50 | Max results |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 50 | Items per page |
|
||||
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/search/identity_text?q=love" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/search/identity_text?file_uuid=$FILE_UUID&q=love" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"chunk_id": "llm_parent_..._256_270",
|
||||
"start_time": 256.256,
|
||||
"end_time": 270.228,
|
||||
"text_content": "...lack of affection...",
|
||||
"identity_id": 9,
|
||||
"identity_name": "Audrey Hepburn",
|
||||
"identity_source": "tmdb",
|
||||
"trace_id": 94
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].file_uuid` | string | File UUID where chunk was found |
|
||||
| `results[].identity_id` | integer | Identity ID if face was detected |
|
||||
| `results[].trace_id` | integer | Face trace ID |
|
||||
|
||||
---
|
||||
|
||||
@@ -145,4 +243,4 @@ Search text chunks spoken by a specific identity.
|
||||
| **Storage** | pgvector (`chunk.embedding` column) |
|
||||
|
||||
---
|
||||
*Updated: 2026-05-19 12:49:24*
|
||||
*Updated: 2026-05-27 — Added global search support for smart, universal, identity_text APIs*
|
||||
|
||||
@@ -70,7 +70,16 @@ curl -s "$API/api/v1/identity/$IDENTITY_UUID" -H "X-API-Key: $KEY"
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Delete an identity permanently.
|
||||
Delete an identity permanently. All face detections bound to this identity are unbound (`identity_id` set to `NULL`). The identity JSON file is deleted from disk.
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Every DELETE records a full snapshot of the identity and its unbound faces. See [`14_identity_history.md`](14_identity_history.md#4-delete-history--undoredo) for:
|
||||
|
||||
- Undo via `POST /api/v1/identity/:identity_uuid/undo` — recreates identity and re-binds faces
|
||||
- Redo via `POST /api/v1/identity/:identity_uuid/redo` — re-deletes the identity
|
||||
|
||||
**Note**: Delete undo/redo reuses the same endpoints as PATCH undo/redo. The endpoint automatically detects whether the identity was deleted (undo) or needs to be re-deleted (redo) based on the history record.
|
||||
|
||||
---
|
||||
|
||||
@@ -129,124 +138,75 @@ curl -s -X PATCH "$API/api/v1/identity/$IDENTITY_UUID" \
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No fields to update or invalid UUID format |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Every bind records a before/after snapshot. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for:
|
||||
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/undo` — Revert a bind
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/redo` — Reapply an undone bind
|
||||
- `GET /api/v1/identity/:identity_uuid/bind/history` — Query bind operations
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/files`
|
||||
## Metadata (Embedded JSON)
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
The `identities.metadata` column is a **JSONB** field that stores arbitrary structured data alongside the identity's core fields (name, status, identity_type). No schema is enforced — any valid JSON object is accepted.
|
||||
|
||||
Get all files where this identity appears. Returns per-file summary including face count, confidence, and appearance time range.
|
||||
### Merge Behavior
|
||||
|
||||
#### Example
|
||||
| Operation | Strategy | Example |
|
||||
|-----------|----------|---------|
|
||||
| **PATCH** | Shallow top-level merge: `COALESCE(metadata,'{}'::jsonb) \|\| $1::jsonb` | Sending `{"tmdb_rating": 8.5}` only adds/overwrites `tmdb_rating`; all other existing keys are preserved. |
|
||||
| **mergeinto** | Recursive deep merge — nested sub-keys are merged individually, not replaced wholesale | Target has `{"tmdb": {"biography": "..."}}`, source has `{"tmdb": {"birthday": "1904-01-18"}}` → result is `{"tmdb": {"biography": "...", "birthday": "1904-01-18"}}`. |
|
||||
| **Upload (`POST`)** | Direct overwrite — the entire `metadata` field is replaced with the request value. | |
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/files" -H "X-API-Key: $KEY"
|
||||
```
|
||||
### Validation
|
||||
|
||||
---
|
||||
| Scenario | Result |
|
||||
|----------|--------|
|
||||
| PATCH with non-object metadata (`string`, `array`, `number`, `null`) | `400 Bad Request: "metadata must be a JSON object"` |
|
||||
| mergeinto with non-object metadata | Accepted (mergeinto validates at application level) |
|
||||
| Upload with non-object metadata | Accepted (upload replaces directly) |
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/faces`
|
||||
### Conventional Keys
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
| Key | Type | Writer | Purpose |
|
||||
|-----|------|--------|---------|
|
||||
| `aliases` | `[{locale, name}]` | PATCH, mergeinto | Multilingual display names (see [Alias System](#alias-system-bcp-47-locale-tags)) |
|
||||
| `merged_into` | `{uuid, at}` | mergeinto | Marks an identity as merged (undo mechanism reads this) |
|
||||
| `tmdb_*` | various | TMDb probe | Movie metadata (biography, birthday, known_for, etc.). Written only when `MOMENTRY_TMDB_PROBE_ENABLED=true`. |
|
||||
| `source` | string | mergeinto | Tagged on aliases/metadata when added by merge (`"merge"` value) |
|
||||
|
||||
Get all face detection records associated with this identity.
|
||||
Custom keys are fully supported — no registration required.
|
||||
|
||||
#### Example
|
||||
### Search Coverage
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/faces" -H "X-API-Key: $KEY"
|
||||
```
|
||||
The identity search endpoint (`GET /api/v1/identity/search`) matches across three scopes:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_uuid` | string | File where face was detected |
|
||||
| `frame_number` | integer | Frame number of detection |
|
||||
| `face_id` | string | Face ID (format: `face_{frame_number}`) |
|
||||
| `confidence` | float | Detection confidence |
|
||||
1. `i.name` — exact and ILIKE against display name
|
||||
2. `jsonb_array_elements(i.metadata->'aliases')->>'name'` — locale-tagged alias names
|
||||
3. `i.metadata::text ILIKE $1` — raw string search across the entire JSON blob (all keys, all values)
|
||||
|
||||
---
|
||||
This means searching for `"1904-01-18"` or `"biography"` will match identities whose metadata contains those strings anywhere.
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/chunks`
|
||||
### History Snapshots
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
Every `identity_history` record captures the **full metadata** in both `before_snapshot` and `after_snapshot` (as part of the complete identity JSONB dump). Undo restores the identity row — including metadata — to the `before_snapshot` state.
|
||||
|
||||
Get all text chunks (sentences) spoken while this identity's face was on screen. Useful for finding what a person said.
|
||||
For merge operations, the MongoDB merge history records `metadata_fields_added` and `metadata_fields_added_paths` (dot-separated paths like `"tmdb.biography"`). Merge undo removes only those specific paths, preserving subsequent manual edits to other metadata keys.
|
||||
|
||||
#### Example
|
||||
### Best Practices
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/chunks" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"data": [
|
||||
{
|
||||
"id": 0,
|
||||
"file_uuid": "bd80fec92b0b6963d177a2c55bf713e2",
|
||||
"chunk_id": "bd80fec92b0b6963d177a2c55bf713e2_2",
|
||||
"chunk_type": "sentence",
|
||||
"start_frame": 5103,
|
||||
"end_frame": 5127,
|
||||
"fps": 24.0,
|
||||
"start_time": 212.64,
|
||||
"end_time": 213.64,
|
||||
"text_content": "[213s-214s] Cary Grant: \"Olá!\""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_uuid` | string | File identifier |
|
||||
| `chunk_id` | string | Sentence chunk identifier |
|
||||
| `start_frame` | integer | Frame-accurate start position |
|
||||
| `end_frame` | integer | Frame-accurate end position |
|
||||
| `fps` | float | Frames per second |
|
||||
| `start_time` | float | Start time in seconds |
|
||||
| `end_time` | float | End time in seconds |
|
||||
| `text_content` | string | Spoken text content |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/:identity_uuid/bind`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Bind a face detection to an identity. Associates the face trace with the identity for future search and recognition.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `file_uuid` | string | Yes | File where face is detected |
|
||||
| `face_id` | string | Yes | Face ID (format: `{frame}_{idx}`) |
|
||||
|
||||
#### Side Effects
|
||||
|
||||
- 清除該 face detection row 的 `stranger_id`(設為 NULL)
|
||||
- 不影響 `identities` 表中原有的 stranger auto-identity 記錄
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'", "face_id": "1_5"}'
|
||||
```
|
||||
| Guideline | Reason |
|
||||
|-----------|--------|
|
||||
| Deep nesting is allowed in metadata | All metadata merge operations use `jsonb_deep_merge()` — nested sub-keys are merged recursively, not replaced wholesale |
|
||||
| Use `aliases` for display names | Frontend has built-in locale fallback logic (see [Alias System](#alias-system-bcp-47-locale-tags)) |
|
||||
| Avoid >1MB per identity | Metadata is included in search indexing (`metadata::text ILIKE`); large blobs degrade query performance |
|
||||
| Don't rely on metadata ordering | JSONB preserves insertion order but PostgreSQL does not guarantee it across operations |
|
||||
| No LLM/Gemma4 agent writes to metadata | Only API endpoints (PATCH, mergeinto, upload) and TMDb probe modify `identities.metadata` |
|
||||
|
||||
---
|
||||
|
||||
@@ -295,6 +255,10 @@ curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/trace" \
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Trace bind operations share the same history/undo/redo system as single-face binds. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for endpoints.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/traces`
|
||||
@@ -382,6 +346,13 @@ Unbind a face detection from an identity. Removes the identity association from
|
||||
- 被 unbind 的 face 不會自動成為 stranger
|
||||
- 要重新標記為 stranger 需重新跑 Agent API(`identity/analyze`)
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Unbind records a before/after snapshot. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for:
|
||||
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/undo` — Revert an unbind
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/redo` — Reapply an undone unbind
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/:identity_uuid/mergeinto`
|
||||
@@ -391,6 +362,13 @@ Unbind a face detection from an identity. Removes the identity association from
|
||||
|
||||
Transfer all face bindings from this identity to another identity, then optionally delete or mark the source as merged.
|
||||
|
||||
#### Two Merge Cases
|
||||
|
||||
| Case | Description | Undo/Redo Support |
|
||||
|------|-------------|-------------------|
|
||||
| **stranger → identity** | Merge an auto-generated stranger identity into a known identity (TMDb or user-defined) | ✅ 24hr undo/redo |
|
||||
| **identity A → identity B** | Merge two known identities (e.g., duplicate entries) | ✅ 24hr undo/redo |
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
@@ -402,8 +380,12 @@ Transfer all face bindings from this identity to another identity, then optional
|
||||
|
||||
- 轉移所有 `face_detections.identity_id` 到目標 identity
|
||||
- 同時清除所有被轉移 rows 的 `stranger_id`
|
||||
- 將 source name 加入 target aliases (with `source: "merge"` tag)
|
||||
- 將 source aliases 加入 target aliases (if not already present)
|
||||
- 將 source metadata fields 加入 target metadata (if not already present)
|
||||
- `keep_history: true`(預設):source identity 設為 `status='merged'`,保留記錄
|
||||
- `keep_history: false`:**刪除** source identity 及其 identity JSON 檔案
|
||||
- **記錄 merge history 到 MongoDB**(支援 undo/redo)
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -411,7 +393,7 @@ Transfer all face bindings from this identity to another identity, then optional
|
||||
curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"into_uuid": "'"$TARGET_UUID"'", "keep_history": false}'
|
||||
-d '{"into_uuid": "'"$TARGET_UUID"'", "keep_history": true}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
@@ -419,11 +401,23 @@ curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Merged 'stranger_13894' into 'Louis Viret' (52 faces transferred, source deleted)",
|
||||
"data": { "faces_transferred": 52 }
|
||||
"message": "Merged 'stranger_13894' into 'Louis Viret' (52 faces transferred, history kept)",
|
||||
"data": {
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"faces_transferred": 52,
|
||||
"aliases_added": 1,
|
||||
"metadata_fields_added": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merge_id` | string | Unique merge operation ID (for undo) |
|
||||
| `faces_transferred` | integer | Number of face detections transferred |
|
||||
| `aliases_added` | integer | Number of aliases added to target |
|
||||
| `metadata_fields_added` | integer | Number of metadata fields added to target |
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
@@ -433,25 +427,189 @@ curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identities/search`
|
||||
### `POST /api/v1/identity/merge/:merge_id/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Search identities by name (ILIKE search). Returns matching identity records.
|
||||
Undo a merge operation within 24 hours. Restores the source identity and reverts face bindings.
|
||||
|
||||
#### Undo Behavior
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| Restore source identity | If `keep_history=true`: restore status to `confirmed`<br>If `keep_history=false`: recreate identity from MongoDB snapshot |
|
||||
| Restore faces | Transfer faces back to source identity |
|
||||
| Remove aliases from target | Remove aliases with `source: "merge"` tag |
|
||||
| Remove metadata fields from target | Remove fields that were added from source |
|
||||
| **Preserve manual changes** | Keep aliases/metadata manually added after merge |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Cary" -H "X-API-Key: $KEY"
|
||||
curl -s -X POST "$API/api/v1/identity/merge/550e8400-e29b-41d4-a716-446655440000/undo" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Undo merge completed: 'stranger_13894' restored, 52 faces reverted",
|
||||
"data": {
|
||||
"source_identity_restored": {
|
||||
"uuid": "a9a90105...",
|
||||
"name": "stranger_13894",
|
||||
"status": "confirmed"
|
||||
},
|
||||
"faces_reverted": 52,
|
||||
"aliases_removed_from_target": 1,
|
||||
"metadata_fields_removed_from_target": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | Undo deadline expired (>24hr) or already undone |
|
||||
| `404` | Merge record not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/merge/:merge_id/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo a previously undone merge operation. See [`14_identity_history.md`](14_identity_history.md#post-apiv1identitymergemerge_idredo) for full details.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/merge/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query merge history records from MongoDB.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `source_uuid` | string | No | — | Filter by source identity UUID |
|
||||
| `target_uuid` | string | No | — | Filter by target identity UUID |
|
||||
| `merge_id` | string | No | — | Filter by specific merge ID |
|
||||
| `undone` | bool | No | — | Filter by undone status |
|
||||
| `page` | int | No | 1 | Page number |
|
||||
| `page_size` | int | No | 20 | Items per page |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/merge/history?page=1&page_size=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"results": [
|
||||
{
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"source_name": "stranger_13894",
|
||||
"target_name": "Louis Viret",
|
||||
"faces_transferred": 52,
|
||||
"merged_at": "2026-05-27T10:00:00Z",
|
||||
"undo_deadline": "2026-05-28T10:00:00Z",
|
||||
"undone": false,
|
||||
"undo_expired": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Identity name |
|
||||
| `source` | string | Identity source |
|
||||
| `tmdb_id` | integer | TMDb ID (if source = tmdb) |
|
||||
| `file_uuid` | string | Associated file |
|
||||
| `merge_id` | string | Unique merge operation ID |
|
||||
| `source_name` | string | Source identity name |
|
||||
| `target_name` | string | Target identity name |
|
||||
| `faces_transferred` | integer | Number of faces transferred |
|
||||
| `merged_at` | datetime | When merge occurred |
|
||||
| `undo_deadline` | datetime | 24hr deadline for undo |
|
||||
| `undone` | bool | Whether merge was undone |
|
||||
| `undo_expired` | bool | Whether undo deadline passed |
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identities/search`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search identity name → find associated chunks. Searches identity name and aliases, returns identities with their associated text chunks.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `q` | string | Yes | — | Search text (ILIKE match on name and aliases) |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 50 | Max results |
|
||||
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Audrey" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Audrey&file_uuid=$FILE_UUID" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"identity_id": 9,
|
||||
"name": "Audrey Hepburn",
|
||||
"source": "tmdb",
|
||||
"tmdb_id": 1932,
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"trace_id": 41,
|
||||
"chunk_id": "llm_parent_..._204_207",
|
||||
"start_time": 204.162,
|
||||
"text_content": "...confrontation..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].identity_id` | integer | Identity ID |
|
||||
| `results[].name` | string | Identity name |
|
||||
| `results[].source` | string | Identity source (`tmdb`, `user_defined`, etc.) |
|
||||
| `results[].tmdb_id` | integer | TMDb person ID (if source = tmdb) |
|
||||
| `results[].file_uuid` | string | File where identity appears |
|
||||
| `results[].trace_id` | integer | Face trace ID |
|
||||
| `results[].chunk_id` | string | Associated chunk ID |
|
||||
| `results[].start_time` | float | Chunk start time |
|
||||
| `results[].text_content` | string | Chunk text content |
|
||||
|
||||
---
|
||||
|
||||
@@ -628,4 +786,4 @@ PATCH /api/v1/identity/:identity_uuid
|
||||
This **replaces** the entire `aliases` array. To add to existing aliases, include all existing entries in the request.
|
||||
|
||||
---
|
||||
*Updated: 2026-05-25
|
||||
*Updated: 2026-05-25 — Added `GET /api/v1/file/:file_uuid/faces` with 4 binding states, filters, strangers table split
|
||||
|
||||
696
docs_v1.0/API_WORKSPACE/modules/14_identity_history.md
Normal file
696
docs_v1.0/API_WORKSPACE/modules/14_identity_history.md
Normal file
@@ -0,0 +1,696 @@
|
||||
<!-- module: identity_history -->
|
||||
<!-- description: Identity operation history, undo, and redo (PATCH, bind, unbind, bind_trace, mergeinto) -->
|
||||
<!-- depends: 01_auth, 07_identity -->
|
||||
|
||||
## Identity Operation History
|
||||
|
||||
Every mutation on an identity automatically records a before/after snapshot. Use undo/redo to revert or reapply changes, and history to inspect the operation log.
|
||||
|
||||
Three independent undo/redo systems exist:
|
||||
|
||||
| System | Storage | Operations Covered |
|
||||
|--------|---------|-------------------|
|
||||
| **PATCH** | PostgreSQL `identity_history` | `update` |
|
||||
| **Bind** | PostgreSQL `identity_history` | `bind`, `unbind`, `bind_trace` |
|
||||
| **Merge** | MongoDB `identity_merge_history` | mergeinto |
|
||||
| **Delete** | PostgreSQL `identity_history` | `delete` |
|
||||
|
||||
---
|
||||
|
||||
### 1. PATCH History & Undo/Redo
|
||||
|
||||
#### Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table |
|
||||
| Snapshot | Full identity record (all fields) before and after each PATCH |
|
||||
| Max records | 256 per identity (oldest auto-deleted when limit exceeded) |
|
||||
| Undo steps | Unlimited (no expiry, no step limit) |
|
||||
| Redo stack | Cleared on new PATCH (`is_undone=true` + `operation='update'` records are deleted) |
|
||||
|
||||
##### Stack Model
|
||||
|
||||
```
|
||||
PATCH 1 → PATCH 2 → PATCH 3 (undo stack, is_undone=false)
|
||||
↓ undo
|
||||
PATCH 1 → PATCH 2 (undo stack)
|
||||
PATCH 3 (redo stack, is_undone=true)
|
||||
↓ redo
|
||||
PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
|
||||
```
|
||||
|
||||
A new PATCH after undo clears only the operation='update' redo stack (PATCH 3 is lost). Bind/merge redo stacks are not affected.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Undo the most recent PATCH operations. Restores the identity's `before_snapshot` and marks the history records as undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of undo steps to apply (max records undone in one call) |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=false` records with `operation='update'`, ordered by `created_at DESC`
|
||||
- Restores `name`, `identity_type`, `source`, `status`, `metadata`, `tmdb_id`, `tmdb_profile` from the last record's `before_snapshot`
|
||||
- Marks the undone records as `is_undone=true` with `undone_at=NOW()`
|
||||
- Syncs `identity.json` to disk
|
||||
- Updates `_index.json` if name changed
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/undo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"undone_count": 1,
|
||||
"current_state": {
|
||||
"id": 9,
|
||||
"uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": {},
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `undone_count` | integer | Number of history records undone |
|
||||
| `current_state` | object | Full identity state after undo |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No undo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo previously undone PATCH operations. Restores the identity's `after_snapshot` and marks the history records as no longer undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of redo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=true` records with `operation='update'`, ordered by `created_at DESC`
|
||||
- Restores all identity fields from the last record's `after_snapshot`
|
||||
- Marks records as `is_undone=false` with `undone_at=NULL`
|
||||
- Syncs `identity.json` to disk
|
||||
- Updates `_index.json` if name changed
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/redo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"redone_count": 1,
|
||||
"current_state": {
|
||||
"id": 9,
|
||||
"uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "John Smith",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": { "aliases": [...] },
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `redone_count` | integer | Number of history records redone |
|
||||
| `current_state` | object | Full identity state after redo |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No redo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `GET /api/v1/identity/:identity_uuid/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query the PATCH operation history for an identity. Returns paginated records with undo/redo stack counts (filtered to `operation='update'`).
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `page` | integer | No | `1` | Page number (1-indexed) |
|
||||
| `limit` | integer | No | `20` | Items per page (max 100) |
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"total": 5,
|
||||
"undo_stack_count": 3,
|
||||
"redo_stack_count": 2,
|
||||
"results": [
|
||||
{
|
||||
"history_id": 42,
|
||||
"operation": "update",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T12:00:00Z",
|
||||
"undone_at": null
|
||||
},
|
||||
{
|
||||
"history_id": 41,
|
||||
"operation": "update",
|
||||
"is_undone": true,
|
||||
"created_at": "2026-05-27T11:30:00Z",
|
||||
"undone_at": "2026-05-27T13:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total PATCH history records for this identity |
|
||||
| `undo_stack_count` | integer | Records available for undo (`is_undone=false`) |
|
||||
| `redo_stack_count` | integer | Records available for redo (`is_undone=true`) |
|
||||
| `results[].history_id` | integer | History record ID |
|
||||
| `results[].operation` | string | Operation type (`"update"` for PATCH) |
|
||||
| `results[].is_undone` | boolean | Whether the operation has been undone |
|
||||
| `results[].created_at` | string | When the PATCH was applied |
|
||||
| `results[].undone_at` | string | When the undo occurred (null if not undone) |
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/history?page=1&limit=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 2. Bind/Unbind/Trace History & Undo/Redo
|
||||
|
||||
All three operations (`bind`, `unbind`, `bind_trace`) share a single history table and undo/redo stack.
|
||||
|
||||
#### Bind Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table (same table as PATCH) |
|
||||
| Snapshot | `{"file_uuid", "face_id" (or "trace_id"), "identity_id_before/after"}` |
|
||||
| Max records | 256 per identity (shared limit across all operation types) |
|
||||
| Undo steps | Unlimited (`steps` param) |
|
||||
| Redo stack | Cleared on new bind/unbind/bind_trace (`operation IN ('bind','unbind','bind_trace')` + `is_undone=true` records deleted) |
|
||||
| Stack isolation | Bind redo stack is **independent** from PATCH redo stack — clearing one does not affect the other |
|
||||
|
||||
##### Stack Model
|
||||
|
||||
```
|
||||
bind face_1 (to id=9) → unbind face_1 → bind trace 906 (to id=9)
|
||||
(undo stack, is_undone=false) (undo stack) (undo stack)
|
||||
↓ undo (first undone: bind_trace)
|
||||
bind trace 906 (is_undone=true)
|
||||
(redo stack)
|
||||
↓ redo
|
||||
bind face_1 → unbind face_1 → bind trace 906
|
||||
(undo stack)
|
||||
```
|
||||
|
||||
A new bind/unbind/trace after undo clears only the bind redo stack (operations with `IN ('bind','unbind','bind_trace')`).
|
||||
|
||||
##### Snapshot Format
|
||||
|
||||
**Before (bind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_before": null
|
||||
}
|
||||
```
|
||||
|
||||
**After (bind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_after": 9
|
||||
}
|
||||
```
|
||||
|
||||
**Before (unbind) — binding existed before:**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_before": 9
|
||||
}
|
||||
```
|
||||
|
||||
**After (unbind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_after": null
|
||||
}
|
||||
```
|
||||
|
||||
For `bind_trace`, the snapshot uses `trace_id` instead of `face_id`, with `identity_id_before` capturing the first face's identity in that trace.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/bind/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Undo the most recent bind/unbind/bind_trace operations. Restores `identity_id_before` from the snapshot and marks records as undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of undo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=false` records with `operation IN ('bind','unbind','bind_trace')`, ordered by `created_at DESC`
|
||||
- Restores `identity_id_before` — for bind this is `null` (face was unbound), for unbind this is the original identity (face goes back), for bind_trace this is the trace's previous identity
|
||||
- Marks the undone records as `is_undone=true` with `undone_at=NOW()`
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/undo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"operation": "bind",
|
||||
"undone_count": 1,
|
||||
"affected_rows": 53
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `operation` | string | The actual operation undone (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `undone_count` | integer | Number of history records undone |
|
||||
| `affected_rows` | integer | Number of `face_detections` rows updated |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No bind undo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/bind/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo previously undone bind/unbind/bind_trace operations. Restores `identity_id_after` from the snapshot.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of redo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=true` records with `operation IN ('bind','unbind','bind_trace')`, ordered by `created_at DESC`
|
||||
- Restores `identity_id_after` — for bind this is the identity the face was bound to, for unbind this is `null`
|
||||
- Marks records as `is_undone=false` with `undone_at=NULL`
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/redo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"operation": "unbind",
|
||||
"redone_count": 1,
|
||||
"affected_rows": 1
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `operation` | string | The actual operation redone (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `redone_count` | integer | Number of history records redone |
|
||||
| `affected_rows` | integer | Number of `face_detections` rows updated |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No bind redo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `GET /api/v1/identity/:identity_uuid/bind/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query the bind/unbind/bind_trace operation history for an identity. Returns paginated records with undo/redo stack counts.
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `page` | integer | No | `1` | Page number (1-indexed) |
|
||||
| `limit` | integer | No | `20` | Items per page (max 100) |
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"total": 3,
|
||||
"undo_stack_count": 2,
|
||||
"redo_stack_count": 1,
|
||||
"results": [
|
||||
{
|
||||
"history_id": 52,
|
||||
"operation": "bind_trace",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T14:00:00Z",
|
||||
"undone_at": null
|
||||
},
|
||||
{
|
||||
"history_id": 51,
|
||||
"operation": "unbind",
|
||||
"is_undone": true,
|
||||
"created_at": "2026-05-27T13:00:00Z",
|
||||
"undone_at": "2026-05-27T14:30:00Z"
|
||||
},
|
||||
{
|
||||
"history_id": 50,
|
||||
"operation": "bind",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T12:00:00Z",
|
||||
"undone_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total bind history records for this identity |
|
||||
| `undo_stack_count` | integer | Records available for undo (`is_undone=false`) |
|
||||
| `redo_stack_count` | integer | Records available for redo (`is_undone=true`) |
|
||||
| `results[].history_id` | integer | History record ID |
|
||||
| `results[].operation` | string | Operation type (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `results[].is_undone` | boolean | Whether the operation has been undone |
|
||||
| `results[].created_at` | string | When the operation was applied |
|
||||
| `results[].undone_at` | string | When the undo occurred (null if not undone) |
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/bind/history?page=1&limit=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 3. Merge History & Undo/Redo
|
||||
|
||||
Merge operations use MongoDB for richer record-keeping, with a 24-hour undo deadline.
|
||||
|
||||
#### Merge Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | MongoDB `identity_merge_history` collection |
|
||||
| Snapshot | Full source identity state + target identity state + aliases/metadata diffs |
|
||||
| Trigger | Every mergeinto with `keep_history=true` |
|
||||
| Undo deadline | 24 hours (renewed on redo) |
|
||||
| Redo support | Yes — restores undone merges with new 24hr deadline |
|
||||
| Max records | Unlimited |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/merge/:merge_id/undo`
|
||||
|
||||
Already documented in [`07_identity.md`](07_identity.md#post-apiv1identitymergemerge_idundo). See that document for full details.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/merge/:merge_id/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo a previously undone merge operation within the renewed 24-hour deadline.
|
||||
|
||||
##### Request
|
||||
|
||||
No body required. The merge ID is taken from the URL path.
|
||||
|
||||
##### Behavior
|
||||
|
||||
1. Validates the merge record exists and `undone=true` (not already active)
|
||||
2. Checks the 24-hour undo deadline (if expired, the redo is rejected)
|
||||
3. Restores face bindings: moves all faces from `target_identity` back to `source_identity`
|
||||
4. Re-adds aliases that were removed by the undo (aliases with `source: "merge"` tag)
|
||||
5. Re-adds metadata fields that were removed by the undo
|
||||
6. If `keep_history=true`: sets `source_identity.status = 'merged'` again
|
||||
7. If `keep_history=false`: recreates source identity from the `undone_snapshot` stored at undo time
|
||||
8. Syncs both identity JSON files to disk
|
||||
9. Sets `undone=false`, clears `undone_snapshot`, renews `undo_deadline = NOW() + 24h`
|
||||
10. Records `redone_by` user for audit
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/merge/550e8400-e29b-41d4-a716-446655440000/redo" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Redo merge completed: merged 'stranger_13894' into 'Louis Viret' (52 faces transferred)",
|
||||
"data": {
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"faces_transferred": 52,
|
||||
"aliases_re_added": 1,
|
||||
"metadata_fields_re_added": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merge_id` | string | The merge operation ID |
|
||||
| `faces_transferred` | integer | Number of faces transferred from source to target |
|
||||
| `aliases_re_added` | integer | Number of aliases restored to target |
|
||||
| `metadata_fields_re_added` | integer | Number of metadata fields restored to target |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | Merge not undone, deadline expired, or cannot redo |
|
||||
| `404` | Merge record not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 4. Delete History & Undo/Redo
|
||||
|
||||
#### Delete Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table |
|
||||
| Snapshot | `{"identity": {...full row...}, "unbound_faces": [{file_uuid, face_id, trace_id}, ...]}` |
|
||||
| Max records | 1 active delete record per identity (redo stack cleared on new delete) |
|
||||
| Undo support | Yes — recreates identity row, re-binds faces |
|
||||
| Redo support | Yes — re-deletes the identity |
|
||||
| Identity file | Deleted on delete, recreated on undo |
|
||||
|
||||
#### Snapshot Format
|
||||
|
||||
```json
|
||||
{
|
||||
"identity": {
|
||||
"id": 9,
|
||||
"uuid": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": {},
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
},
|
||||
"unbound_faces": [
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"trace_id": null
|
||||
},
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_6",
|
||||
"trace_id": 906
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Stack Model
|
||||
|
||||
```
|
||||
DELETE identity (undo stack, is_undone=false)
|
||||
↓ undo
|
||||
Identity recreated, faces re-bound
|
||||
→ delete history marked is_undone=true
|
||||
↓ redo (re-delete)
|
||||
Identity deleted again, faces unbound
|
||||
→ delete history marked is_undone=false
|
||||
```
|
||||
|
||||
A new delete after an undo clears the delete redo stack (no redo possible for the old delete).
|
||||
|
||||
#### Undo Behavior (via existing `POST /api/v1/identity/:identity_uuid/undo`)
|
||||
|
||||
1. Normal identity lookup fails (row was deleted)
|
||||
2. Checks `identity_history` for `operation='delete' AND is_undone=false` matching the UUID in the snapshot
|
||||
3. Recreates the identity row (new internal `id`, same UUID)
|
||||
4. Re-binds all faces listed in `unbound_faces` to the new identity
|
||||
5. Deletes the `identity_history` delete record as `is_undone=true` with `undone_at=NOW()`
|
||||
6. Syncs `identity.json` to disk
|
||||
7. Updates `_index.json`
|
||||
|
||||
#### Redo Behavior (via existing `POST /api/v1/identity/:identity_uuid/redo`)
|
||||
|
||||
1. Identity lookup succeeds (identity was restored by prior undo)
|
||||
2. Checks `identity_history` for `operation='delete' AND is_undone=true` matching the identity_id
|
||||
3. Deletes `identity.json` from disk
|
||||
4. Unbinds all faces (`identity_id = NULL`)
|
||||
5. Deletes the identity row
|
||||
6. Marks the delete history record as `is_undone=false`
|
||||
7. Returns success
|
||||
|
||||
#### Error Responses (delete undo/redo)
|
||||
|
||||
| HTTP | Scenario |
|
||||
|------|----------|
|
||||
| `400` | No delete history available (either no delete or already undone/redone) |
|
||||
| `404` | Identity not found (for redo — identity wasn't restored) |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### Comparison: PATCH vs Bind vs Merge vs Delete Undo/Redo
|
||||
|
||||
| Aspect | PATCH Undo/Redo | Bind Undo/Redo | Merge Undo/Redo | Delete Undo/Redo |
|
||||
|--------|----------------|----------------|-----------------|------------------|
|
||||
| Storage | PostgreSQL `identity_history` | PostgreSQL `identity_history` | MongoDB `identity_merge_history` | PostgreSQL `identity_history` |
|
||||
| Operation filter | `operation='update'` | `operation IN ('bind','unbind','bind_trace')` | — | `operation='delete'` |
|
||||
| Trigger | Every PATCH | Every bind/unbind/bind_trace | Every mergeinto with `keep_history=true` | Every DELETE |
|
||||
| Undo deadline | None (unlimited) | None (unlimited) | 24 hours (renewed on redo) | None (unlimited) |
|
||||
| Redo support | Yes | Yes | Yes | Yes |
|
||||
| Step undo | Yes (`steps` param) | Yes (`steps` param) | No (full undo/redo only) | No (single record) |
|
||||
| Max records | 256 per identity | 256 per identity (shared) | Unlimited | 256 per identity (shared) |
|
||||
| User tracking | `user_id` + `user_source` | `user_id` + `user_source` | `performed_by_user` + `undone_by` / `redone_by` | `user_id` + `user_source` |
|
||||
|
||||
---
|
||||
|
||||
*Updated: 2026-05-28*
|
||||
36
docs_v1.0/API_WORKSPACE/narratives/marcom_intro.md
Normal file
36
docs_v1.0/API_WORKSPACE/narratives/marcom_intro.md
Normal file
@@ -0,0 +1,36 @@
|
||||
<!-- narrative: marcom_intro -->
|
||||
<!-- description: Intro section for Marcom training manual -->
|
||||
<!-- depends: -->
|
||||
|
||||
## About This Manual
|
||||
|
||||
This training manual is designed for the Marcom team to understand and use the Momentry Core API.
|
||||
|
||||
### Demo Credentials
|
||||
|
||||
**API Key**: `muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69`
|
||||
|
||||
**SFTPGo** (for video upload):
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| SFTP Host | `sftpgo.momentry.ddns.net` |
|
||||
| SFTP Port | `2022` |
|
||||
| Username | `demo` |
|
||||
| Password | `demopassword123` |
|
||||
| Web UI | `https://sftpgo.momentry.ddns.net` |
|
||||
|
||||
### Quick Examples
|
||||
|
||||
**List all videos:**
|
||||
```bash
|
||||
curl -s -H "X-API-Key: $KEY" "$API/api/v1/files/scan"
|
||||
```
|
||||
|
||||
**Search:**
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"query": "example", "limit": 5}'
|
||||
```
|
||||
588
docs_v1.0/DESIGN/ASRX_HYBRID_PIPELINE_V1.0.md
Normal file
588
docs_v1.0/DESIGN/ASRX_HYBRID_PIPELINE_V1.0.md
Normal file
@@ -0,0 +1,588 @@
|
||||
# ASRX Hybrid Pipeline v1.0 — 聲紋分離混合架構
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| **範圍** | ASRX 處理器重構:whisperx → VAD-first hybrid pipeline |
|
||||
| **狀態** | Draft |
|
||||
| **適用版本** | Momentry Core V4.0+ |
|
||||
| **作者** | OpenCode / Warren |
|
||||
| **建立日期** | 2026-06-01 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 問題
|
||||
|
||||
### 1.1 現有問題
|
||||
|
||||
| 問題 | 說明 | 影響 |
|
||||
|------|------|------|
|
||||
| **Whisper 合併短句** | `whisper small` 會將兩個人的對話錯認成一個連續段 (A+B → 一句) | ASR segment 內混兩人話語,speaker 無法分離 |
|
||||
| **ASRX v2 speaker_id = null** | `asrx_processor_v2.py` 使用 `whisperx.DiarizationPipeline()` 但該 API 未在 whisperx `__init__.py` 暴露 | 所有 segment speaker 均為 null |
|
||||
| **文字丟失** | `asrx_processor_custom.py` 的 `SelfASRXFixed.process_with_segments()` 只輸出 `text: ""` | Rule 1 合併時無文字可用 |
|
||||
| **錯誤的聲紋後端** | `asrx_processor_v2.py` 依賴 whisperx 內建 diarization,但該功能不穩定 | 準確度 ~85%,需 HF token |
|
||||
| **多版本混亂** | 7 個 root-level 變體、14 個 asrx_self 檔案,生產環境使用錯誤版本 | 維護困難,不知哪個是對的 |
|
||||
|
||||
### 1.2 痛點場景
|
||||
|
||||
**兩個說話人短句來回切換**(訪談、對話):
|
||||
|
||||
```
|
||||
Audio: A(2s) → B(1.5s) → A(3s)
|
||||
Whisper: ───────[0-7s, "A+B+A 全部混在一起"]───────
|
||||
```
|
||||
|
||||
Whisper 在句間停頓處不切段,導致 ASR 時間邊界無法反映 speaker 切換。
|
||||
|
||||
---
|
||||
|
||||
## 2. 架構
|
||||
|
||||
### 2.1 核心原則
|
||||
|
||||
1. **VAD 先定邊界** — 用 VAD 在句間停頓處切段,取代 whisper 的邊界
|
||||
2. **ASR 後做** — 每段各自轉錄,保有獨立文字
|
||||
3. **聲紋聚類定 speaker** — ECAPA-TDNN + AgglomerativeClustering
|
||||
|
||||
### 2.2 5 步 Pipeline
|
||||
|
||||
```
|
||||
Audio
|
||||
│
|
||||
① whisper (一次, 粗略定位)
|
||||
│ 找到說話段 + 初步文字 + 語種
|
||||
│ [0-7s, "今天天氣很好我覺得也不錯對啊", zh]
|
||||
│
|
||||
② VAD scan (在每段內細切)
|
||||
│ 利用句間停頓切開
|
||||
│ 段1 [0-2s] 段2 [2-3.5s] 段3 [3.5-7s]
|
||||
│
|
||||
③ whisper per refined segment (各段轉錄)
|
||||
│ 段1 → "今天天氣很好" (zh, 0.98)
|
||||
│ 段2 → "我覺得也不錯" (zh, 0.97)
|
||||
│ 段3 → "對啊" (zh, 0.96)
|
||||
│
|
||||
④ ECAPA-TDNN per refined segment (聲紋提取)
|
||||
│ 段1 → emb[0] (192-dim)
|
||||
│ 段2 → emb[1] (192-dim)
|
||||
│ 段3 → emb[2] (192-dim)
|
||||
│
|
||||
⑤ AgglomerativeClustering (聚類定 speaker)
|
||||
│ emb[0]=SPEAKER_0, emb[1]=SPEAKER_1, emb[2]=SPEAKER_0
|
||||
│
|
||||
輸出:
|
||||
start end text language speaker_id
|
||||
0.0 2.0 今天天氣很好 zh SPEAKER_0
|
||||
2.0 3.5 我覺得也不錯 zh SPEAKER_1
|
||||
3.5 7.0 對啊 zh SPEAKER_0
|
||||
```
|
||||
|
||||
### 2.3 流程圖
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ asrx_processor.py │
|
||||
│ (wrapper) │
|
||||
│ │
|
||||
│ ① ffprobe → select best track → ffmpeg → 16kHz WAV │
|
||||
│ │
|
||||
│ ② SelfASRXFixed.process(audio_wav, file_uuid) │
|
||||
│ │ │
|
||||
│ ├─ Step 1: whisper.transcribe() → rough segments │
|
||||
│ ├─ Step 2: VAD scan each rough segment │
|
||||
│ ├─ Step 3: whisper per refined segment → text+language │
|
||||
│ ├─ Step 4: ECAPA-TDNN per segment → 192-dim embedding │
|
||||
│ ├─ Step 5: AgglomerativeClustering → speaker_labels │
|
||||
│ │ │
|
||||
│ ├─ Step 6: Store embeddings in Qdrant │
|
||||
│ │ └─ {file_uuid, speaker_id, text, language, start, end} │
|
||||
│ │ │
|
||||
│ └─ Step 7: Classify high-quality embeddings │
|
||||
│ ├─ quality > threshold → reference profile │
|
||||
│ ├─ 送入聲音分類模型推論性別/屬性 │
|
||||
│ └─ 寫入 Qdrant (type: speaker_reference) │
|
||||
│ │
|
||||
│ ③ 輸出 JSON 格式 (不含 embedding) │
|
||||
│ │
|
||||
│ Rust: rule1_ingest.rs │
|
||||
│ └─ pre_chunks(processor_type='asrx') → chunks │
|
||||
└─────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 檔案組織
|
||||
|
||||
### 3.1 最終檔案結構
|
||||
|
||||
```
|
||||
scripts/
|
||||
├── asrx_processor.py ← production (cleaned custom.py)
|
||||
│
|
||||
└── asrx_self/ ← 核心庫
|
||||
├── __init__.py ← package marker
|
||||
├── vad.py ← Silero VAD (新增 scan_within_segment)
|
||||
├── whisper_local.py ← 🆕 封裝 whisper 載入+轉錄
|
||||
├── speaker_encoder.py ← ECAPA-TDNN 192-dim
|
||||
├── speaker_cluster_fixed.py ← AgglomerativeClustering
|
||||
└── main_fixed.py ← 🔧 重寫為 5 步 pipeline
|
||||
```
|
||||
|
||||
### 3.2 刪除清單
|
||||
|
||||
**Root-level 變體**(全部刪除):
|
||||
|
||||
| 檔案 | 原因 |
|
||||
|------|------|
|
||||
| `asrx_processor.py` | 原始 whisperx 版,diarization 壞的 |
|
||||
| `asrx_processor_v2.py` | 同上,Rust 目前錯誤呼叫此檔 |
|
||||
| `asrx_processor_v2_noalign.py` | 跳過對齊但 diarization 仍壞 |
|
||||
| `asrx_processor_v2_transcribe.py` | 只轉錄不做 speaker |
|
||||
| `asrx_processor_simplified.py` | 變體 |
|
||||
| `asrx_processor_contract_v1.py` | 18KB,pyannote,需 HF token |
|
||||
|
||||
**asrx_self 內被取代的舊版**:
|
||||
|
||||
| 檔案 | 原因 | 取代者 |
|
||||
|------|------|--------|
|
||||
| `main.py` | 用 SpectralClustering,有 NaN 問題 | `main_fixed.py` |
|
||||
| `speaker_cluster.py` | 用 SpectralClustering,不穩定 | `speaker_cluster_fixed.py` |
|
||||
|
||||
### 3.3 搬離清單
|
||||
|
||||
非生產工具搬至 `tools/asrx/`:
|
||||
|
||||
```
|
||||
tools/asrx/
|
||||
├── integrate_face_asrx_speaker.py
|
||||
├── speaker_player_gui.py
|
||||
├── speaker_player_gui_face.py
|
||||
├── speaker_player_interactive.py
|
||||
├── speaker_audio_player.py
|
||||
├── test_long_movie.py
|
||||
├── test_gui_face_player.py
|
||||
└── docs/
|
||||
├── FINAL_TEST_REPORT.md
|
||||
├── GUI_FACE_PLAYER_USAGE.md
|
||||
├── LONG_MOVIE_TEST_SUMMARY.md
|
||||
└── SPEAKER_PLAYER_GUIDE.md
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## 4. Qdrant 聲紋向量儲存
|
||||
|
||||
### 4.1 儲存流程
|
||||
|
||||
```
|
||||
Step 4 輸出: 每個 refined segment 有 {embedding: [192-dim], text, language, start, end}
|
||||
Step 5 輸出: 每個 segment 被標上 speaker_id {SPEAKER_0, SPEAKER_1, ...}
|
||||
|
||||
Step 6: Qdrant 儲存
|
||||
┌─ 每個 segment → Qdrant point
|
||||
│ point_id = hash(file_uuid + segment_index) ← 可重複查詢
|
||||
│ vector = embedding (192-dim)
|
||||
│ payload = {
|
||||
│ "file_uuid": str, ← 聚類後填入
|
||||
│ "speaker_id": str, ← 聚類後填入
|
||||
│ "text": str, ← ASR 轉錄結果
|
||||
│ "language": str, ← 語種 (zh/en/...)
|
||||
│ "start_time": f64, ← 秒
|
||||
│ "end_time": f64, ← 秒
|
||||
│ "type": "speaker_embedding" ← 便於區分
|
||||
│ }
|
||||
└─
|
||||
```
|
||||
|
||||
### 4.2 Qdrant Collection
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| Collection Name | `momentry_speaker` (或共用現有 collection) |
|
||||
| Vector Dimension | 192 (ECAPA-TDNN 輸出) |
|
||||
| Distance Metric | Cosine |
|
||||
| Point ID | `hash(file_uuid + "_" + segment_index)` |
|
||||
|
||||
### 4.3 Rust `upsert_speaker_embedding`
|
||||
|
||||
```rust
|
||||
impl QdrantDb {
|
||||
pub async fn upsert_speaker_embedding(
|
||||
&self,
|
||||
point_id: u64,
|
||||
vector: &[f32],
|
||||
file_uuid: &str,
|
||||
speaker_id: &str,
|
||||
text: &str,
|
||||
language: &str,
|
||||
start_time: f64,
|
||||
end_time: f64,
|
||||
) -> Result<()> {
|
||||
// Qdrant PUT /collections/{collection}/points?wait=true
|
||||
// payload: {file_uuid, speaker_id, text, language, start_time, end_time, type: "speaker_embedding"}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4.4 與現有 Face Embedding 的關係
|
||||
|
||||
| 類別 | Qdrant Collection | Dim | Payload |
|
||||
|------|-------------------|-----|---------|
|
||||
| Face | `momentry` (self.collection_name) | 512 (FaceNet) | `file_uuid, trace_id, frame_number` |
|
||||
| **Speaker** | `momentry` 或獨立 collection | **192** (ECAPA-TDNN) | `file_uuid, speaker_id, text, language, start, end` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 模組詳細設計
|
||||
|
||||
### 5.1 `vad.py` — 語音活動檢測
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 模型 | Silero VAD (torch.hub, snakers4/silero-vad) |
|
||||
| 現有函數 | `load_vad_model()`, `extract_speech_segments()` |
|
||||
| **新增函數** | **`scan_within_segment(wav, start_sec, end_sec, model, utils, min_speech_duration_ms=500)`** |
|
||||
|
||||
`scan_within_segment` 作用:
|
||||
- 在一個時間範圍 `[start_sec, end_sec]` 內執行 VAD 掃描
|
||||
- 只回傳該範圍內的語音子片段 `[(s1, e1), (s2, e2), ...]`
|
||||
- 利用句間停頓切分,解決 whisper 合併問題
|
||||
|
||||
### 5.2 `whisper_local.py` 🆕 — Whisper 封裝
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 模型 | `whisper.load_model("base")` (可設定) |
|
||||
| 函數 | `load_model()`, `transcribe_segment(audio, start, end)` |
|
||||
|
||||
```python
|
||||
def transcribe_segment(wav, sample_rate, start_sec, end_sec, model) -> dict:
|
||||
"""轉錄單一段落,回傳 {text, language, lang_prob, segments}"""
|
||||
```
|
||||
|
||||
每段獨立轉錄,保留語言與信心度。
|
||||
|
||||
### 5.3 `speaker_encoder.py` — 聲紋編碼器
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 模型 | SpeechBrain ECAPA-TDNN (`spkrec-ecapa-voxceleb`) |
|
||||
| 輸出維度 | 192-dim |
|
||||
| EER | 0.80% (VoxCeleb1) |
|
||||
| 授權 | MIT (不需要 HuggingFace token) |
|
||||
| 函數 | `load_speaker_encoder()`, `extract_speaker_embedding()`, `extract_speaker_embeddings_batch()` |
|
||||
|
||||
### 5.4 `speaker_cluster_fixed.py` — 說話人聚類
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 演算法 | AgglomerativeClustering (cosine + average linkage) |
|
||||
| 取代 | `speaker_cluster.py` (SpectralClustering, NaN 問題) |
|
||||
| 函數 | `robust_speaker_clustering(embeddings, n_speakers=None, max_speakers=10)` |
|
||||
|
||||
### 5.5 `main_fixed.py` 🔧 — 核心調度器(7 步 Pipeline)
|
||||
|
||||
```python
|
||||
class SelfASRXFixed:
|
||||
def process(self, audio_path, output_path=None, file_uuid=None):
|
||||
"""
|
||||
7 步 speaker diarization pipeline
|
||||
|
||||
Steps:
|
||||
1. whisper.transcribe(audio) → rough segments + text + language
|
||||
2. VAD scan each rough segment → refined segments
|
||||
3. whisper per refined segment → {text, language, lang_prob}
|
||||
4. ECAPA-TDNN per refined segment → 192-dim embeddings
|
||||
5. AgglomerativeClustering → speaker_labels
|
||||
6. Store all embeddings in Qdrant (if file_uuid provided)
|
||||
payload: {file_uuid, speaker_id, text, language, start_time, end_time, type: "speaker_embedding"}
|
||||
7. High-quality embeddings (quality > threshold) → classify + store reference
|
||||
payload: {type: "speaker_reference", file_uuid, speaker_id, n_segments, avg_quality, ...}
|
||||
|
||||
Returns:
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"start": float, "end": float,
|
||||
"text": str, "language": str,
|
||||
"lang_prob": float, "speaker": str,
|
||||
"speaker_id": str, "quality": float
|
||||
},
|
||||
...
|
||||
],
|
||||
"speaker_stats": {...},
|
||||
"n_speakers": int,
|
||||
"total_duration": float,
|
||||
"references": [
|
||||
{
|
||||
"speaker_id": str,
|
||||
"n_segments": int,
|
||||
"avg_quality": float,
|
||||
"gender": str
|
||||
}
|
||||
]
|
||||
}
|
||||
"""
|
||||
|
||||
def _store_speaker_embeddings(self, segments, file_uuid):
|
||||
"""Step 6: 每個 segment 的 192-dim embedding 存入 Qdrant"""
|
||||
|
||||
def _classify_high_quality_speakers(self, segments, embeddings, labels, file_uuid):
|
||||
"""Step 7: 高品質聲紋分級 + 分類 → Qdrant reference profile"""
|
||||
|
||||
**移除**:
|
||||
|
||||
| 舊方法 | 原因 |
|
||||
|--------|------|
|
||||
| `process_with_segments(audio, asr_segments)` | 外部 ASR 邊界來源不可靠,被 VAD 取代 |
|
||||
| `process()` VAD-only fallback | 無文字輸出,被完整 pipeline 取代 |
|
||||
|
||||
### 5.6 `speaker_classifier.py` 🆕 — 高品質聲紋分級與分類
|
||||
|
||||
#### 目的
|
||||
|
||||
聚類後,對每個 cluster 的 embedding 進行品質評估,高於閾值的獨立建檔,並用外部模型做自動分類。
|
||||
|
||||
#### 流程
|
||||
|
||||
```
|
||||
Step ⑤ 聚類後,每個 segment 有 {embedding, speaker_id}
|
||||
│
|
||||
└─ Compute quality score per embedding
|
||||
│
|
||||
├─ 低於閾值 → 寫入 Qdrant (一般 speaker_embedding)
|
||||
│
|
||||
└─ 高於閾值 (quality > 0.85)
|
||||
├─ 獨立建 reference profile
|
||||
└─ 送入「支持聲音的模型」做分類
|
||||
├─ 語者性別 (male/female)
|
||||
├─ 語種口音 (zh-CN / zh-TW / en-US)
|
||||
└─ 或跨影片 speaker 匹配用
|
||||
```
|
||||
|
||||
#### Quality Score 計算
|
||||
|
||||
```python
|
||||
def compute_embedding_quality(embeddings, labels, threshold=0.85):
|
||||
"""
|
||||
每個 embedding 到所屬 cluster centroid 的餘弦相似度
|
||||
|
||||
Args:
|
||||
embeddings: [n_segments, 192]
|
||||
labels: [n_segments] 聚類標籤
|
||||
threshold: 高品質門檻
|
||||
|
||||
Returns:
|
||||
qualities: [n_segments] 每個 embedding 的品質分數
|
||||
high_quality_mask: [n_segments] bool 陣列
|
||||
"""
|
||||
from sklearn.metrics.pairwise import cosine_similarity
|
||||
|
||||
unique_labels = set(labels)
|
||||
centroids = {}
|
||||
for label in unique_labels:
|
||||
mask = labels == label
|
||||
centroid = np.mean(embeddings[mask], axis=0)
|
||||
centroid = centroid / np.linalg.norm(centroid)
|
||||
centroids[label] = centroid
|
||||
|
||||
qualities = []
|
||||
for i, (emb, label) in enumerate(zip(embeddings, labels)):
|
||||
sim = cosine_similarity([emb], [centroids[label]])[0][0]
|
||||
qualities.append(sim)
|
||||
|
||||
return np.array(qualities), np.array(qualities) >= threshold
|
||||
```
|
||||
|
||||
#### Reference Profile 格式
|
||||
|
||||
```json
|
||||
{
|
||||
"point_id": "hash(speaker_reference_" + file_uuid + "_" + speaker_id + "_" + cluster_index)",
|
||||
"vector": "[192-dim centroid embedding]",
|
||||
"payload": {
|
||||
"type": "speaker_reference",
|
||||
"file_uuid": "來源影片",
|
||||
"speaker_id": "SPEAKER_0",
|
||||
"n_segments": 25,
|
||||
"avg_quality": 0.92,
|
||||
"total_duration": 45.3,
|
||||
"language": "zh",
|
||||
"gender": "male",
|
||||
"text_samples": ["今天天氣很好", "我覺得也不錯", "..."]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 支援的聲音分類模型(選項)
|
||||
|
||||
| 模型 | 用途 | 優點 | 缺點 |
|
||||
|------|------|------|------|
|
||||
| **SpeechBrain gender classifier** | 性別分類 | 已整合 ECAPA-TDNN | 只分 male/female |
|
||||
| **CLAP** (LAION) | 零樣本音頻分類 | 可自訂 label text | 需額外安裝 |
|
||||
| **YAMNet** | 聲音事件分類 | Google 出品,521 classes | 不擅長語者屬性 |
|
||||
| **Wav2Vec2-BERT** (speechbrain) | 情感/屬性 | 多維度分類 | 模型較大 |
|
||||
| **自建 identity classifier** | 跨影片 speaker 匹配 | 與現有 identity 系統對接 | 需累積 reference data |
|
||||
|
||||
> **待決定**: 選擇哪個分類模型,由後續 POC 決定。
|
||||
|
||||
#### `main_fixed.py` 新增方法
|
||||
|
||||
```python
|
||||
class SelfASRXFixed:
|
||||
# ... 既有 6 個步驟 ...
|
||||
|
||||
def _classify_high_quality_speakers(self, segments, embeddings, labels, file_uuid):
|
||||
"""
|
||||
步驟 7: 高品質聲紋分級與分類
|
||||
|
||||
1. 計算 quality score
|
||||
2. 高於閾值者建立 reference profile
|
||||
3. 用分類模型推論性別/屬性
|
||||
4. 寫入 Qdrant (type: speaker_reference)
|
||||
"""
|
||||
qualities, mask = compute_embedding_quality(embeddings, labels)
|
||||
|
||||
for i, (seg, emb, label, quality, is_high) in enumerate(
|
||||
zip(segments, embeddings, labels, qualities, mask)
|
||||
):
|
||||
seg["quality"] = float(quality)
|
||||
if is_high:
|
||||
profile = self._build_reference_profile(
|
||||
emb, seg, file_uuid
|
||||
)
|
||||
# 分類 (placeholder)
|
||||
# gender = classify_gender(embedding)
|
||||
self._store_speaker_reference(profile)
|
||||
```
|
||||
|
||||
### 5.7 `asrx_processor.py` — 清理後的 wrapper
|
||||
|
||||
清理項目:
|
||||
|
||||
| 問題 | 位置 | 修法 |
|
||||
|------|------|------|
|
||||
| 硬編碼 UUID `dd61fda8...` | line 155 | 移除該 fallback path |
|
||||
| `os.chdir(script_dir)` | line 112 | 改區域性 Path 操作 |
|
||||
| ASR 文字丟棄 | line 258 | `text` 來自新 pipeline |
|
||||
| `_debug` dict | line 222 | 移除 |
|
||||
| `max_speakers=10` 寫死 | line 201 | 改 CLI 參數 `--max-speakers` |
|
||||
| 載入外部 ASR segments | line 148-174 | 移除(不再需要) |
|
||||
|
||||
---
|
||||
|
||||
## 6. 輸出格式
|
||||
|
||||
### 6.1 ASRX JSON Output (由 `asrx_processor.py` 寫入)
|
||||
|
||||
> **注意**: 192-dim embedding 不在此 JSON 中。embedding 在 Python 端直接送入 Qdrant,JSON 只保留中繼資料。
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "zh",
|
||||
"segments": [
|
||||
{
|
||||
"start_time": 0.0,
|
||||
"end_time": 2.0,
|
||||
"start_frame": 0,
|
||||
"end_frame": 60,
|
||||
"text": "今天天氣很好",
|
||||
"speaker_id": "SPEAKER_0",
|
||||
"language": "zh",
|
||||
"lang_prob": 0.98
|
||||
},
|
||||
{
|
||||
"start_time": 2.0,
|
||||
"end_time": 3.5,
|
||||
"start_frame": 60,
|
||||
"end_frame": 105,
|
||||
"text": "我覺得也不錯",
|
||||
"speaker_id": "SPEAKER_1",
|
||||
"language": "zh",
|
||||
"lang_prob": 0.97
|
||||
}
|
||||
],
|
||||
"n_speakers": 2,
|
||||
"speaker_stats": {
|
||||
"SPEAKER_0": {"count": 1, "duration": 2.0},
|
||||
"SPEAKER_1": {"count": 1, "duration": 1.5}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Qdrant Point 格式 (由 Python `_store_speaker_embeddings` 寫入)
|
||||
|
||||
> Embedding 不經過 Rust,直接在 Python 端完成 Qdrant HTTP PUT。
|
||||
|
||||
| Qdrant 欄位 | 值 | 說明 |
|
||||
|-------------|-----|------|
|
||||
| `id` | `hash(file_uuid + "_" + segment_index)` | 可重複查詢的 point ID |
|
||||
| `vector` | `[f32; 192]` | ECAPA-TDNN 聲紋向量 |
|
||||
| `payload.file_uuid` | `str` | 影片識別碼 |
|
||||
| `payload.speaker_id` | `str` | 聚類後的 speaker 標籤 |
|
||||
| `payload.text` | `str` | 該段的轉錄文字 |
|
||||
| `payload.language` | `str` | 語種 (`zh`/`en`) |
|
||||
| `payload.start_time` | `f64` | 開始時間(秒) |
|
||||
| `payload.end_time` | `f64` | 結束時間(秒) |
|
||||
| `payload.type` | `"speaker_embedding"` | 便於與 face_embedding 區分 |
|
||||
|
||||
### 6.3 Rust `AsrxResult` 對應
|
||||
|
||||
```rust
|
||||
pub struct AsrxSegment {
|
||||
pub start_time: f64, // serde(alias = "start")
|
||||
pub end_time: f64, // serde(alias = "end")
|
||||
pub start_frame: u64, // default 0
|
||||
pub end_frame: u64, // default 0
|
||||
pub text: String,
|
||||
pub speaker_id: Option<String>,
|
||||
pub language: Option<String>, // 🆕 新增
|
||||
pub lang_prob: Option<f64>, // 🆕 新增
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Rust 端變動
|
||||
|
||||
| 檔案 | 變動 |
|
||||
|------|------|
|
||||
| `src/core/processor/asrx.rs` | `asrx_processor_v2.py` → `asrx_processor.py` |
|
||||
| `src/core/processor/asrx.rs` | `AsrxSegment` 新增 `language`, `lang_prob` 欄位 |
|
||||
| `src/core/processor/asrx.rs` | 傳遞 `--file-uuid` 給 Python 腳本,讓 Python 端可直接寫入 Qdrant |
|
||||
| `src/core/chunk/rule1_ingest.rs` | 若 `pre_chunks` data 含 `language` 則帶入 chunk metadata |
|
||||
| `src/core/db/qdrant_db.rs` | 🆕 新增 `upsert_speaker_embedding()` 方法 (可選,若 Python 端直接寫 Qdrant 則不需) |
|
||||
|
||||
---
|
||||
|
||||
## 8. 遷移計畫
|
||||
|
||||
### 實作順序 (依賴關係排序)
|
||||
|
||||
| 步驟 | 內容 | 檔案 | 風險 |
|
||||
|------|------|------|------|
|
||||
| **S1** | `vad.py`: 新增 `scan_within_segment()` | `asrx_self/vad.py` | 低 |
|
||||
| **S2** | 🆕 `whisper_local.py`: 封裝 whisper 載入 + 轉錄 | `asrx_self/whisper_local.py` | 低 |
|
||||
| **S3** | 🔧 `main_fixed.py`: 重寫為 7 步 pipeline | `asrx_self/main_fixed.py` | 中 |
|
||||
| **S4** | 🆕 `speaker_classifier.py`: 性別分類器 | `asrx_self/speaker_classifier.py` | 低 |
|
||||
| **S5** | 🔧 `custom.py` cleanup + rename → `asrx_processor.py` | `asrx_processor_custom.py` | 低 |
|
||||
| **S6** | 🔧 Rust `asrx.rs`: 改指向 + 傳 `--file-uuid` | `src/core/processor/asrx.rs` | 低 |
|
||||
| **S7** | ✅ 驗證:build + playground 測試 | — | 中 |
|
||||
| **S8** | 🧹 刪除變體 + 搬離工具 | — | 低 |
|
||||
|
||||
### 驗證標準
|
||||
|
||||
1. `cargo build` 通過
|
||||
2. Playground 3003: 註冊影片 → ASRX processor 完成
|
||||
3. 輸出 JSON 中 `speaker_id` 非 `null`
|
||||
4. Qdrant collection 有 `speaker_embedding` 點
|
||||
5. 性別正確標記 (male/female)
|
||||
|
||||
---
|
||||
|
||||
## 9. 版本歷史
|
||||
|
||||
| 版本 | 日期 | 修改者 | 說明 |
|
||||
|------|------|--------|------|
|
||||
| V1.0 | 2026-06-01 | OpenCode | 初始版本:7 步 hybrid pipeline + Qdrant 聲紋儲存 + 高品質分類 |
|
||||
421
docs_v1.0/DESIGN/LaunchDaemon_Config_M5Max128.md
Normal file
421
docs_v1.0/DESIGN/LaunchDaemon_Config_M5Max128.md
Normal file
@@ -0,0 +1,421 @@
|
||||
---
|
||||
title: LaunchDaemon Architecture (M5Max128 Reference)
|
||||
version: 1.0
|
||||
date: 2026-05-27
|
||||
author: M5Max128
|
||||
status: reference
|
||||
---
|
||||
|
||||
# LaunchDaemon Architecture Reference
|
||||
|
||||
> **Scope**: M5Max128 local configuration (resource-managed binaries)
|
||||
> **Note**: M5Max48 uses build-from-source approach via start_momentry.sh. Both approaches are valid and independent.
|
||||
|
||||
## Overview
|
||||
|
||||
| Machine | Approach | Status |
|
||||
|---------|----------|--------|
|
||||
| M5Max128 | LaunchDaemon + resource binaries | Reference document |
|
||||
| M5Max48 | start_momentry.sh + build from source | Main branch |
|
||||
|
||||
## Architecture Principles
|
||||
|
||||
```
|
||||
/Library/LaunchDaemons/ (system-level, boot before login)
|
||||
├── com.momentry.postgresql.plist (P1, no dependency)
|
||||
├── com.momentry.redis.plist (P1, no dependency)
|
||||
├── com.momentry.qdrant.plist (P2, no dependency)
|
||||
├── com.momentry.mongodb.plist (P2, no dependency)
|
||||
└── com.momentry.gitea.plist (P3, depends on PostgreSQL)
|
||||
|
||||
Experimental services:
|
||||
└── com.momentry.startup.plist (LLM, Embedding, Playground, etc.)
|
||||
```
|
||||
|
||||
## Key Design Points
|
||||
|
||||
### 1. Binary Location
|
||||
|
||||
All binaries are resource-managed under `/Users/accusys/momentry_resources/bin/`:
|
||||
|
||||
| Service | Binary Path |
|
||||
|---------|-------------|
|
||||
| PostgreSQL | `/Users/accusys/pgsql/18.3/bin/postgres` |
|
||||
| Redis | `/Users/accusys/momentry_resources/bin/redis-server` |
|
||||
| Qdrant | `/Users/accusys/momentry_resources/bin/qdrant` |
|
||||
| MongoDB | `/Users/accusys/momentry_resources/bin/mongod` |
|
||||
| Gitea | `/Users/accusys/momentry_resources/bin/gitea` |
|
||||
|
||||
### 2. Root Boot → User Execution
|
||||
|
||||
LaunchDaemons run at boot (root), but use `UserName` key to switch to user:
|
||||
|
||||
```xml
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
```
|
||||
|
||||
### 3. Unified Log Path
|
||||
|
||||
All logs go to `/Users/accusys/momentry/logs/`:
|
||||
|
||||
```xml
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/logs/<service>.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/logs/<service>.error.log</string>
|
||||
```
|
||||
|
||||
## Plist Templates
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.postgresql</string>
|
||||
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/accusys/momentry/var/postgresql</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Users/accusys/pgsql/18.3/bin/postgres</string>
|
||||
<string>-D</string>
|
||||
<string>/Users/accusys/momentry/var/postgresql</string>
|
||||
</array>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/logs/postgresql.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/logs/postgresql.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### Redis (ACL Authentication)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.redis</string>
|
||||
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/accusys/momentry/var/redis</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Users/accusys/momentry_resources/bin/redis-server</string>
|
||||
<string>--port</string>
|
||||
<string>6379</string>
|
||||
<string>--bind</string>
|
||||
<string>0.0.0.0</string>
|
||||
<string>--aclfile</string>
|
||||
<string>/Users/accusys/momentry/etc/redis/users.acl</string>
|
||||
<string>--dir</string>
|
||||
<string>/Users/accusys/momentry/var/redis</string>
|
||||
<string>--logfile</string>
|
||||
<string>/Users/accusys/momentry/logs/redis.log</string>
|
||||
</array>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/logs/redis.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/logs/redis.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### Redis ACL File
|
||||
|
||||
Location: `/Users/accusys/momentry/etc/redis/users.acl`
|
||||
|
||||
```
|
||||
user default on sanitize-payload ~* &* +@all >accusys
|
||||
user accusys on sanitize-payload ~* &* +@all >accusys
|
||||
```
|
||||
|
||||
**Redis 8.x Authentication**:
|
||||
```bash
|
||||
# Old (deprecated): redis-cli -a accusys ping
|
||||
# New (recommended): redis-cli --user default --pass accusys ping
|
||||
```
|
||||
|
||||
### Qdrant
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.qdrant</string>
|
||||
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/accusys/momentry/var/qdrant/</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Users/accusys/momentry_resources/bin/qdrant</string>
|
||||
</array>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>QDRANT__STORAGE__STORAGE_PATH</key>
|
||||
<string>/Users/accusys/momentry/var/qdrant/</string>
|
||||
<key>QDRANT__SERVICE__HOST</key>
|
||||
<string>0.0.0.0</string>
|
||||
<key>QDRANT__SERVICE__HTTP_PORT</key>
|
||||
<string>6333</string>
|
||||
<key>HOME</key>
|
||||
<string>/Users/accusys</string>
|
||||
</dict>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/logs/qdrant.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/logs/qdrant.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### MongoDB
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.mongodb</string>
|
||||
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Users/accusys/momentry_resources/bin/mongod</string>
|
||||
<string>--dbpath</string>
|
||||
<string>/Users/accusys/momentry/var/mongodb</string>
|
||||
<string>--logpath</string>
|
||||
<string>/Users/accusys/momentry/logs/mongodb.log</string>
|
||||
<string>--port</string>
|
||||
<string>27017</string>
|
||||
<string>--bind_ip</string>
|
||||
<string>0.0.0.0</string>
|
||||
</array>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/logs/mongodb.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/logs/mongodb.error.log</string>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/accusys/momentry/var/mongodb</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### Gitea (with Wrapper Script)
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>com.momentry.gitea</string>
|
||||
|
||||
<key>UserName</key>
|
||||
<string>accusys</string>
|
||||
|
||||
<key>WorkingDirectory</key>
|
||||
<string>/Users/accusys/momentry/var/gitea</string>
|
||||
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>/Users/accusys/momentry_core/scripts/start_gitea.sh</string>
|
||||
</array>
|
||||
|
||||
<key>EnvironmentVariables</key>
|
||||
<dict>
|
||||
<key>HOME</key>
|
||||
<string>/Users/accusys</string>
|
||||
<key>GITEA_WORK_DIR</key>
|
||||
<string>/Users/accusys/momentry/var/gitea</string>
|
||||
</dict>
|
||||
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
|
||||
<key>StandardOutPath</key>
|
||||
<string>/Users/accusys/momentry/logs/gitea.log</string>
|
||||
|
||||
<key>StandardErrorPath</key>
|
||||
<string>/Users/accusys/momentry/logs/gitea.error.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
## Wrapper Script: start_gitea.sh
|
||||
|
||||
Gitea depends on PostgreSQL. Wrapper script ensures PostgreSQL is ready:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
PG_BIN="/Users/accusys/pgsql/18.3/bin"
|
||||
GITEA_BIN="/Users/accusys/momentry_resources/bin/gitea"
|
||||
GITEA_CONFIG="/Users/accusys/momentry/etc/gitea/app.ini"
|
||||
|
||||
MAX_WAIT=60
|
||||
WAITED=0
|
||||
|
||||
# Wait for PostgreSQL
|
||||
while ! "$PG_BIN/pg_isready" -q 2>/dev/null; do
|
||||
if [ $WAITED -ge $MAX_WAIT ]; then
|
||||
echo "ERROR: PostgreSQL not ready after $MAX_WAIT seconds"
|
||||
exit 1
|
||||
fi
|
||||
sleep 2
|
||||
WAITED=$((WAITED + 2))
|
||||
done
|
||||
|
||||
# Start Gitea
|
||||
"$GITEA_BIN" web --config "$GITEA_CONFIG"
|
||||
```
|
||||
|
||||
## Install Script: install_launchdaemons.sh
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
|
||||
PLIST_DIR="/Users/accusys/momentry_core/momentry_runtime/plist"
|
||||
DAEMON_DIR="/Library/LaunchDaemons"
|
||||
LOG_DIR="/Users/accusys/momentry/logs"
|
||||
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
DAEMONS=(
|
||||
"com.momentry.postgresql"
|
||||
"com.momentry.redis"
|
||||
"com.momentry.qdrant"
|
||||
"com.momentry.mongodb"
|
||||
"com.momentry.gitea"
|
||||
)
|
||||
|
||||
for daemon in "${DAEMONS[@]}"; do
|
||||
plist_name="${daemon}.plist"
|
||||
src="${PLIST_DIR}/${plist_name}"
|
||||
dest="${DAEMON_DIR}/${plist_name}"
|
||||
|
||||
if launchctl list "$daemon" >/dev/null 2>&1; then
|
||||
sudo launchctl unload -w "$dest" 2>/dev/null
|
||||
fi
|
||||
|
||||
sudo cp "$src" "$dest"
|
||||
sudo chown root:wheel "$dest"
|
||||
sudo chmod 644 "$dest"
|
||||
sudo launchctl load -w "$dest"
|
||||
done
|
||||
```
|
||||
|
||||
## Comparison: M5Max128 vs M5Max48
|
||||
|
||||
| Aspect | M5Max128 | M5Max48 |
|
||||
|--------|----------|---------|
|
||||
| **Approach** | LaunchDaemon (system-level) | start_momentry.sh (user script) |
|
||||
| **Binaries** | Resource-managed (`momentry_resources/bin/`) | Build from source (`services/*/target/`) |
|
||||
| **PostgreSQL data** | `/Users/accusys/momentry/var/postgresql` | `/Users/accusys/pgsql/data` |
|
||||
| **Redis auth** | ACL file (`users.acl`) | `--requirepass` (deprecated) |
|
||||
| **LLM path** | Resource binary | `/Users/accusys/llama/bin/` |
|
||||
| **Gitea** | Independent LaunchDaemon | Not in startup script |
|
||||
| **MongoDB** | Independent LaunchDaemon | Not in startup script |
|
||||
|
||||
## Installation Steps (M5Max128)
|
||||
|
||||
```bash
|
||||
# 1. Ensure directories exist
|
||||
mkdir -p /Users/accusys/momentry/logs
|
||||
mkdir -p /Users/accusys/momentry/var/{postgresql,redis,qdrant,mongodb,gitea}
|
||||
|
||||
# 2. Install LaunchDaemons (requires sudo)
|
||||
sudo /Users/accusys/momentry_core/scripts/install_launchdaemons.sh
|
||||
|
||||
# 3. Verify services
|
||||
/Users/accusys/pgsql/18.3/bin/pg_isready
|
||||
/Users/accusys/momentry_resources/bin/redis-cli --user default --pass accusys ping
|
||||
curl http://localhost:6333/healthz
|
||||
curl http://localhost:3000/
|
||||
|
||||
# 4. Reboot test
|
||||
sudo reboot
|
||||
|
||||
# 5. Post-reboot verification
|
||||
launchctl list | grep com.momentry
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
1. **Independence**: M5Max128's LaunchDaemons do not conflict with M5Max48's startup script. Each machine has its own approach.
|
||||
|
||||
2. **Resource Management**: M5Max128 uses pre-built binaries from `momentry_resources/bin/`, avoiding build dependencies.
|
||||
|
||||
3. **Redis ACL**: Redis 8.x uses ACL authentication, not `--requirepass`. This is the modern approach.
|
||||
|
||||
4. **Gitea Wrapper**: Essential because Gitea depends on PostgreSQL. The wrapper ensures PostgreSQL is ready before starting Gitea.
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-05-27 | M5Max128 | Initial reference document |
|
||||
385
docs_v1.0/DESIGN/Modular_Doc_System_V1.0.md
Normal file
385
docs_v1.0/DESIGN/Modular_Doc_System_V1.0.md
Normal file
@@ -0,0 +1,385 @@
|
||||
---
|
||||
document_type: "design"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "模組生成式文件產出系統"
|
||||
date: "2026-05-17"
|
||||
version: "V1.0"
|
||||
status: "active"
|
||||
owner: "M5"
|
||||
created_by: "OpenCode"
|
||||
tags:
|
||||
- "documentation"
|
||||
- "modular"
|
||||
- "generated-docs"
|
||||
- "workspace"
|
||||
ai_query_hints:
|
||||
- "查詢模組生成式文件產出系統的設計理念"
|
||||
- "如何使用 API_WORKSPACE"
|
||||
- "如何新增 API endpoint 文檔"
|
||||
- "make deploy 流程"
|
||||
- "自定義交付文件"
|
||||
related_documents:
|
||||
- "STANDARDS/USER_DOCS_STANDARD.md"
|
||||
- "STANDARDS/DOCS_STANDARD.md"
|
||||
- "API_WORKSPACE/README.md"
|
||||
- "API_WORKSPACE/modules/_template.md"
|
||||
---
|
||||
|
||||
# 模組生成式文件產出系統
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| 建立者 | OpenCode |
|
||||
| 建立時間 | 2026-05-17 |
|
||||
| 文件版本 | V1.0 |
|
||||
| 目標讀者 | developer, documentation maintainer |
|
||||
|
||||
---
|
||||
|
||||
## 版本歷史
|
||||
|
||||
| 版本 | 日期 | 目的 | 操作人 |
|
||||
|------|------|------|--------|
|
||||
| V1.0 | 2026-05-17 | 建立設計文件 | OpenCode |
|
||||
|
||||
---
|
||||
|
||||
## 1. 設計理念
|
||||
|
||||
### 1.1 痛點
|
||||
|
||||
傳統 API 文件維護有常見問題:
|
||||
|
||||
| 問題 | 具體表現 |
|
||||
|------|----------|
|
||||
| **內容重複** | 同一個 endpoint 在快速參考、完整手冊、教育訓練文件中寫三次 |
|
||||
| **更新遺漏** | 修改 curl 範例後,忘記同步到另一份文件 |
|
||||
| **交付僵化** | 無法按對象產出不同版本的 API 文件 |
|
||||
| **版本失靈** | YAML frontmatter 版本號與實際內容脫節 |
|
||||
|
||||
### 1.2 核心原則
|
||||
|
||||
```
|
||||
單一真理源(modules/)→ 組裝引擎(assemble_docs.sh)→ 多種交付產品(GUIDES/)
|
||||
|
||||
編輯 ──→ 生成 ──→ 部署
|
||||
1 處修改模組 make all make deploy
|
||||
```
|
||||
|
||||
| 原則 | 說明 |
|
||||
|------|------|
|
||||
| **單一真理源** | 每個 endpoint 只在 `modules/` 中定義一次 |
|
||||
| **組裝而非撰寫** | 交付文件是 modules 的組合,不是手寫 |
|
||||
| **開發與交付分離** | `API_WORKSPACE/` 開發,`GUIDES/` 交付 |
|
||||
| **模組為最小可測試單位** | 每個 module 可獨立驗證正確性 |
|
||||
| **配置驅動** | `.toml` 配置定義哪些 module 以何種模式組裝成何種輸出 |
|
||||
|
||||
### 1.3 檔案類型對照
|
||||
|
||||
| 類型 | 角色 | 可編輯 | 位置 |
|
||||
|------|------|--------|------|
|
||||
| Module (模組) | 不可再拆的內容最小單位 | ✅ 是 | `API_WORKSPACE/modules/` |
|
||||
| Config (配方) | 定義組裝規則 | ✅ 是 | `API_WORKSPACE/configs/` |
|
||||
| Narrative (敘事) | 非結構化的前言/背景 | ✅ 是 | `API_WORKSPACE/narratives/` |
|
||||
| Assembled (產出) | 從模組組裝的交付文件 | ❌ 否(generated) | `API_WORKSPACE/_build/` → `GUIDES/` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 目錄結構
|
||||
|
||||
```
|
||||
docs_v1.0/
|
||||
├── API_WORKSPACE/ ← 開發區
|
||||
│ ├── modules/ ← 端點模組(單一真理源)
|
||||
│ │ ├── _template.md ← 模組撰寫規範
|
||||
│ │ ├── 01_auth.md ← 認證、Base URL
|
||||
│ │ ├── 02_health.md ← 健康檢查
|
||||
│ │ ├── 03_register.md ← 註冊、掃描
|
||||
│ │ ├── 04_lookup.md ← 查詢、刪除
|
||||
│ │ ├── 05_process.md ← 處理、進度、任務
|
||||
│ │ ├── 06_search.md ← 搜尋(向量、n8n、視覺)
|
||||
│ │ ├── 07_identity.md ← 身份 CRUD、bind/unbind
|
||||
│ │ ├── 08_identity_agent.md ← Identity Agent
|
||||
│ │ ├── 09_tmdb.md ← TMDb Enrichment
|
||||
│ │ ├── 10_pipeline.md ← Stats、配置、未掛載端點
|
||||
│ │ └── 11_error_codes.md ← 錯誤碼對照表
|
||||
│ │
|
||||
│ ├── configs/ ← 組裝配方(每個輸出一份)
|
||||
│ │ ├── reference.toml → API_REFERENCE.md
|
||||
│ │ ├── endpoints.toml → API_ENDPOINTS.md
|
||||
│ │ ├── quickref.toml → API_QUICK_REFERENCE.md
|
||||
│ │ ├── errors.toml → API_ERROR_CODES.md
|
||||
│ │ ├── index.toml → API_INDEX.md
|
||||
│ │ ├── marcom.toml → API_TRAINING_MARCOM.md
|
||||
│ │ └── tmdb.toml → TMDb_User_Guide.md
|
||||
│ │
|
||||
│ ├── narratives/ ← 非端點敘事前言
|
||||
│ │ └── marcom_intro.md
|
||||
│ │
|
||||
│ ├── _build/ ← 生成暫存區(gitignored)
|
||||
│ ├── Makefile ← 組裝自動化入口
|
||||
│ ├── assemble_docs.sh ← 組裝引擎
|
||||
│ └── README.md ← 開發者速查
|
||||
│
|
||||
├── GUIDES/ ← 交付區
|
||||
│ ├── API_REFERENCE.md (generated)
|
||||
│ ├── API_ENDPOINTS.md (generated)
|
||||
│ ├── API_QUICK_REFERENCE.md (generated)
|
||||
│ ├── API_ERROR_CODES.md (generated)
|
||||
│ ├── API_INDEX.md (generated)
|
||||
│ ├── API_TRAINING_MARCOM.md (generated)
|
||||
│ ├── TMDb_User_Guide.md (generated)
|
||||
│ ├── Demo_EndToEnd.md (手寫保留)
|
||||
│ ├── Pipeline_API_Demo.md (手寫保留)
|
||||
│ └── ... (其他手寫文件)
|
||||
│
|
||||
├── DESIGN/
|
||||
├── REFERENCE/
|
||||
├── OPERATIONS/
|
||||
├── INTEGRATIONS/
|
||||
└── STANDARDS/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 模組規範
|
||||
|
||||
### 3.1 檔名規則
|
||||
|
||||
- 格式:`NN_<name>.md`(NN = 兩位數排序 01-99)
|
||||
- 範例:`03_register.md`, `09_tmdb.md`
|
||||
- 依賴序號決定組裝時的 endpoint 順序
|
||||
|
||||
### 3.2 Module Metadata 註解
|
||||
|
||||
每個 module 開頭必須有 metadata 註解:
|
||||
|
||||
```markdown
|
||||
<!-- module: auth -->
|
||||
<!-- description: Authentication, API Key, Base URL configuration -->
|
||||
<!-- depends: -->
|
||||
```
|
||||
|
||||
| 欄位 | 必填 | 說明 |
|
||||
|------|------|------|
|
||||
| `module` | Yes | 唯一名稱,無空格無數字開頭 |
|
||||
| `description` | Yes | 一句話說明 |
|
||||
| `depends` | No | 依賴的其他 module 名稱(逗號分隔) |
|
||||
|
||||
### 3.3 Endpoint 結構
|
||||
|
||||
每個 endpoint 必須使用一致結構:
|
||||
|
||||
```markdown
|
||||
### `METHOD /path/to/endpoint`
|
||||
|
||||
**Auth**: Required / Optional / Public
|
||||
**Scope**: file-level / identity-level / system-level
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X METHOD "$API/path" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-d '{"field": "value"}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{ ... }
|
||||
```
|
||||
|
||||
#### Error Codes
|
||||
|
||||
| Code | HTTP | When |
|
||||
|------|------|------|
|
||||
```
|
||||
```
|
||||
|
||||
### 3.4 變數規則
|
||||
|
||||
| 變數 | 用途 | 範例值 |
|
||||
|------|------|--------|
|
||||
| `$API` | Base URL | `http://localhost:3003` |
|
||||
| `$KEY` | API Key | `your-api-key-here` |
|
||||
| `$FILE_UUID` | File UUID | `3a6c1865...` |
|
||||
| `$IDENTITY_UUID` | Identity UUID | `a9a90105...` |
|
||||
|
||||
---
|
||||
|
||||
## 4. 組裝引擎
|
||||
|
||||
### 4.1 `assemble_docs.sh`
|
||||
|
||||
Shell 腳本,接收三個參數:
|
||||
|
||||
| 參數 | 說明 | 範例 |
|
||||
|------|------|------|
|
||||
| `--config` | TOML 配方路徑 | `configs/reference.toml` |
|
||||
| `--modules` | Module 目錄 | `modules/` |
|
||||
| `--build` | 輸出目錄 | `_build/` |
|
||||
|
||||
### 4.2 三種組裝模式
|
||||
|
||||
| mode | 行為 | 適用 |
|
||||
|------|------|------|
|
||||
| `full` | 完整包含 module 全部內容(除 metadata) | API_REFERENCE, API_ENDPOINTS |
|
||||
| `summary` | 僅擷取 endpoint 表格 + curl 範例 | API_QUICK_REFERENCE |
|
||||
| `index` | 生成文件總覽(掃描 modules 目錄自動產生索引) | API_INDEX |
|
||||
|
||||
### 4.3 組裝流程
|
||||
|
||||
```
|
||||
1. 讀取 config.toml → 解析 title, modules, mode, narrative
|
||||
2. 生成 YAML frontmatter(含 document_type, date, version)
|
||||
3. 生成 title heading + info block
|
||||
4. (可選)摘自 TOC:從 modules ## headings 生成目錄
|
||||
5. (可選)插入 narrative intro
|
||||
6. 遍歷 modules:
|
||||
- full mode: 複製整份內容(跳過 <!-- --> 註解)
|
||||
- summary mode: 只提取 | table | + ```bash code block
|
||||
- index mode: 自動掃描 modules 目錄生成清單
|
||||
7. 寫入 _build/ 輸出檔案
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 配方格式(config.toml)
|
||||
|
||||
```toml
|
||||
title = "輸出文件標題"
|
||||
output = "_build/FILENAME.md" # 輸出路徑(相對於 API_WORKSPACE)
|
||||
mode = "full" # full | summary | index
|
||||
modules = ["01_auth", "03_register"] # 要包含的 module 名稱
|
||||
narrative = "narratives/xxx.md" # (可選)包含的敘事前言
|
||||
toc = true # (可選)是否生成目錄
|
||||
|
||||
[frontmatter]
|
||||
document_type = "api_reference" # 用於 YAML frontmatter
|
||||
service = "MOMENTRY_CORE"
|
||||
version = "V1.0"
|
||||
owner = "M5"
|
||||
created_by = "OpenCode"
|
||||
```
|
||||
|
||||
### 內建配方一覽
|
||||
|
||||
| 檔案 | 輸出 | Modules | Mode |
|
||||
|------|------|---------|------|
|
||||
| `reference.toml` | API_REFERENCE.md | 01-11 | full |
|
||||
| `endpoints.toml` | API_ENDPOINTS.md | 01-10 | full |
|
||||
| `quickref.toml` | API_QUICK_REFERENCE.md | 01-06,09 | summary |
|
||||
| `errors.toml` | API_ERROR_CODES.md | 11 | full |
|
||||
| `index.toml` | API_INDEX.md | (auto) | index |
|
||||
| `marcom.toml` | API_TRAINING_MARCOM.md | 01,03,06 + narrative | full |
|
||||
| `tmdb.toml` | TMDb_User_Guide.md | 01,03,09 | full |
|
||||
|
||||
---
|
||||
|
||||
## 6. 工作流程
|
||||
|
||||
### 6.1 日常修改
|
||||
|
||||
```bash
|
||||
# 1. 編輯模組
|
||||
cd API_WORKSPACE
|
||||
vim modules/09_tmdb.md
|
||||
|
||||
# 2. 重新生成單一文件
|
||||
make tmdb
|
||||
|
||||
# 3. 預覽結果
|
||||
less _build/TMDb_User_Guide.md
|
||||
|
||||
# 4. 部署
|
||||
make deploy
|
||||
```
|
||||
|
||||
### 6.2 新增端點
|
||||
|
||||
```bash
|
||||
# 1. 找到所屬模組
|
||||
ls modules/
|
||||
# 決定該 endpoint 屬於哪個模組(如 tmdb, identity, search)
|
||||
|
||||
# 2. 在對應模組加入 endpoint 文檔
|
||||
vim modules/09_tmdb.md
|
||||
|
||||
# 3. 重新生成所有文件
|
||||
make all
|
||||
|
||||
# 4. 確認所有引用此端點的文件都有正確更新
|
||||
make check
|
||||
|
||||
# 5. 部署
|
||||
make deploy
|
||||
```
|
||||
|
||||
### 6.3 客製化交付
|
||||
|
||||
```bash
|
||||
# 新增一個客製化配方
|
||||
cat > configs/integration_partner.toml << TOML
|
||||
title = "Integration Partner API Guide"
|
||||
output = "_build/PARTNER_GUIDE.md"
|
||||
mode = "full"
|
||||
modules = ["01_auth", "06_search", "09_tmdb", "11_error_codes"]
|
||||
toc = true
|
||||
[frontmatter]
|
||||
document_type = "user_manual"
|
||||
service = "MOMENTRY_CORE"
|
||||
version = "V1.0"
|
||||
owner = "M5"
|
||||
created_by = "OpenCode"
|
||||
TOML
|
||||
|
||||
# 在 Makefile 中加入對應 target
|
||||
echo "partner:" >> Makefile
|
||||
echo ' @$$(SCRIPT) --config configs/integration_partner.toml --modules $$(MODULES) --build $$(BUILD)' >> Makefile
|
||||
|
||||
# 生成
|
||||
make partner
|
||||
|
||||
# 部署
|
||||
make deploy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 交付客製化對照表
|
||||
|
||||
| 對象 | 需要 modules | make target | 輸出 |
|
||||
|------|-------------|-------------|------|
|
||||
| API Developer | 01-11 (all) | `make reference` | API_REFERENCE.md |
|
||||
| Quick Start User | 01-06,09 | `make quickref` | API_QUICK_REFERENCE.md |
|
||||
| Marcom Team | 01,03,06 + narrative | `make marcom` | API_TRAINING_MARCOM.md |
|
||||
| TMDb User | 01,03,09 | `make tmdb` | TMDb_User_Guide.md |
|
||||
| Integration Partner | 01,06,09,11 | Custom config | PARTNER_GUIDE.md |
|
||||
|
||||
---
|
||||
|
||||
## 8. GUIDES/ 文件類型說明
|
||||
|
||||
| 類型 | 來源 | 說明 |
|
||||
|------|------|------|
|
||||
| `API_*.md` (7 files) | Generated from API_WORKSPACE | API 功能文件,endpoint 列表 + curl 範例 |
|
||||
| `Demo_*.md`, `M5API_*.md` | 手寫 | 敘事性指引,含完整 step-by-step 流程 |
|
||||
| `PORTAL_*.md` | 手寫 | Portal 開發計畫與 Demo 指引 |
|
||||
| `USER_MANUAL.md` | 手寫 | 系統操作使用手冊 |
|
||||
|
||||
> **提醒**:不要直接修改 GUIDES/ 中的 generated files。修改應在 API_WORKSPACE/modules/ 中進行,然後執行 `make deploy`。
|
||||
|
||||
---
|
||||
|
||||
## 相關文件
|
||||
|
||||
- `API_WORKSPACE/README.md` — 開發者快速上手指南
|
||||
- `API_WORKSPACE/modules/_template.md` — 模組撰寫範本
|
||||
- `STANDARDS/DOCS_STANDARD.md` — 文件創建規範
|
||||
- `STANDARDS/USER_DOCS_STANDARD.md` — 使用者文件規範
|
||||
352
docs_v1.0/DESIGN/Processor_Refactoring_Assessment.md
Normal file
352
docs_v1.0/DESIGN/Processor_Refactoring_Assessment.md
Normal file
@@ -0,0 +1,352 @@
|
||||
---
|
||||
title: Processor Refactoring Assessment (M5Max128 Research)
|
||||
version: 1.0
|
||||
date: 2026-05-27
|
||||
author: M5Max128
|
||||
status: reference
|
||||
---
|
||||
|
||||
# Processor Refactoring Assessment
|
||||
|
||||
> **Scope**: M5Max128 research documentation for M5Max48 implementation reference
|
||||
> **Workspace**: ~/workspace/ (22 modules)
|
||||
|
||||
## Executive Summary
|
||||
|
||||
22 processor modules evaluated for Rust/Swift/Python refactoring feasibility.
|
||||
|
||||
### Priority Matrix
|
||||
|
||||
| Phase | Language | Modules | Effort | Benefit |
|
||||
|-------|----------|---------|--------|---------|
|
||||
| 1 | Swift | OCR, Pose, Face | Low | Remove Python wrappers |
|
||||
| 2 | Rust | TKG, Resume, Redis | Low | Remove infrastructure deps |
|
||||
| 3 | Rust | Cut | Medium | Pure CPU logic |
|
||||
| 4 | Swift | YOLO | Medium | ANE acceleration |
|
||||
| 5 | Python | Others (keep) | - | ML/LLM dependencies |
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Swift Modules (Immediate Gain)
|
||||
|
||||
### workspace_ocr
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Swift Suitability | 10/10 |
|
||||
| Current State | Thin Python wrapper around swift_ocr |
|
||||
| Refactoring | Delete Python wrapper, Rust calls swift_ocr directly |
|
||||
| LOC Change | Python: -122, Rust: ~50 |
|
||||
| Risk | Low |
|
||||
| Effort | 1 day |
|
||||
|
||||
**Current Architecture**:
|
||||
```
|
||||
Rust (ocr.rs) → PythonExecutor → ocr_processor.py → subprocess → swift_ocr
|
||||
```
|
||||
|
||||
**Target Architecture**:
|
||||
```
|
||||
Rust (ocr.rs) → subprocess → swift_ocr
|
||||
```
|
||||
|
||||
### workspace_pose
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Swift Suitability | 10/10 |
|
||||
| Current State | Thin Python wrapper around swift_pose |
|
||||
| Refactoring | Delete Python wrapper, Rust calls swift_pose directly |
|
||||
| LOC Change | Python: -150, Rust: ~50 |
|
||||
| Risk | Low |
|
||||
| Effort | 1 day |
|
||||
|
||||
**Current Architecture**:
|
||||
```
|
||||
Rust (pose.rs) → PythonExecutor → pose_processor.py → subprocess → swift_pose
|
||||
```
|
||||
|
||||
**Target Architecture**:
|
||||
```
|
||||
Rust (pose.rs) → subprocess → swift_pose
|
||||
```
|
||||
|
||||
### workspace_face
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Swift Suitability | 9/10 |
|
||||
| Current State | Swift detect + Python embedding (FaceNet CoreML) |
|
||||
| Refactoring | Merge detection + embedding into single Swift binary |
|
||||
| LOC Change | Python: -337, Swift: +100 (embedding) |
|
||||
| Risk | Medium |
|
||||
| Effort | 2-3 days |
|
||||
|
||||
**Current Architecture**:
|
||||
```
|
||||
Stage 1: Python → swift_face (Vision detect) → bbox + landmarks
|
||||
Stage 2: Python → OpenCV crop → CoreML FaceNet → 512D embedding
|
||||
```
|
||||
|
||||
**Target Architecture**:
|
||||
```
|
||||
Swift: Vision detect → crop → VNCoreMLModel (FaceNet) → embedding → face.json
|
||||
```
|
||||
|
||||
### workspace_face_recognition
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Status | **Superseded** |
|
||||
| Recommendation | Do not refactor. Archive/remove. |
|
||||
| Note | Replaced by face_processor.py (Apple Vision + CoreML) |
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Rust Modules (Infrastructure)
|
||||
|
||||
### workspace_tkg
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Rust Suitability | **10/10** |
|
||||
| Current State | Python psycopg2 + SQL queries |
|
||||
| Dependencies | PostgreSQL, JSON I/O (no ML) |
|
||||
| Refactoring | Pure Rust with sqlx/tokio-postgres |
|
||||
| LOC Change | Python: -469, Rust: ~350 |
|
||||
| Risk | Low |
|
||||
| Effort | 1-2 days |
|
||||
|
||||
**Graph Structure**:
|
||||
```
|
||||
NODES:
|
||||
(face_trace) - one per trace_id
|
||||
(object) - one per YOLO class
|
||||
(speaker) - one per speaker_id
|
||||
|
||||
EDGES:
|
||||
(face) -[:CO_OCCURS_WITH]-> (object) same frame
|
||||
(face) -[:SPEAKS_AS]-> (speaker) temporal overlap
|
||||
(face) -[:CO_OCCURS_WITH]-> (face) same frame
|
||||
```
|
||||
|
||||
### workspace_resume_framework
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Rust Suitability | **10/10** |
|
||||
| Current State | Python file I/O + signal handling |
|
||||
| Dependencies | File I/O, timers (no ML) |
|
||||
| Refactoring | Pure Rust struct with auto-save |
|
||||
| LOC Change | Python: -484, Rust: ~150 |
|
||||
| Risk | Low |
|
||||
| Effort | 1 day |
|
||||
|
||||
**Rust Design**:
|
||||
```rust
|
||||
struct ResumeFramework {
|
||||
path: PathBuf,
|
||||
save_interval: Duration,
|
||||
last_save: Instant,
|
||||
position: Option<u64>,
|
||||
}
|
||||
|
||||
impl ResumeFramework {
|
||||
fn load_checkpoint(&mut self) -> Result<Option<u64>>
|
||||
fn save_checkpoint(&self, position: u64) -> Result<()>
|
||||
fn auto_save_tick(&mut self, position: u64) -> Result<bool>
|
||||
fn finalize(&mut self, total: u64) -> Result<()>
|
||||
}
|
||||
```
|
||||
|
||||
### workspace_redis_publisher
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Rust Suitability | **10/10** |
|
||||
| Current State | Python redis-py pub/sub |
|
||||
| Dependencies | Redis TCP (no ML) |
|
||||
| Refactoring | Pure Rust with redis-rs |
|
||||
| LOC Change | Python: -195, Rust: ~100 |
|
||||
| Risk | Low |
|
||||
| Effort | 1 day |
|
||||
|
||||
**Rust Design**:
|
||||
```rust
|
||||
use redis::AsyncCommands;
|
||||
|
||||
struct ProgressPublisher {
|
||||
client: redis::Client,
|
||||
channel: String,
|
||||
}
|
||||
|
||||
impl ProgressPublisher {
|
||||
async fn info(&self, processor: &str, msg: &str) -> Result<()>
|
||||
async fn progress(&self, processor: &str, current: u32, total: u32, msg: &str) -> Result<()>
|
||||
async fn complete(&self, processor: &str, msg: &str) -> Result<()>
|
||||
async fn error(&self, processor: &str, msg: &str) -> Result<()>
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Rust CPU Logic
|
||||
|
||||
### workspace_cut
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Rust Suitability | 8/10 |
|
||||
| Current State | Python PySceneDetect |
|
||||
| Dependencies | Pure CPU (histogram diff) |
|
||||
| Refactoring | Port ContentDetector algorithm to Rust |
|
||||
| LOC Change | Python: -106, Rust: ~300 |
|
||||
| Risk | Medium |
|
||||
| Effort | 2-3 days |
|
||||
| Challenge | HSV histogram + adaptive threshold |
|
||||
|
||||
**Algorithm to Port**:
|
||||
- Frame-to-frame HSV/Luma histogram difference
|
||||
- Rolling average threshold
|
||||
- min_scene_len enforcement
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Swift ANE Acceleration
|
||||
|
||||
### workspace_yolo
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Swift Suitability | 8/10 |
|
||||
| Current State | Python ultralytics (YOLOv8) |
|
||||
| Dependencies | CoreML model conversion needed |
|
||||
| Refactoring | Create swift_yolo with VNCoreMLModel |
|
||||
| LOC Change | Python: -496, Swift: ~300 |
|
||||
| Risk | Medium |
|
||||
| Effort | 2-3 days |
|
||||
| Challenge | CoreML model conversion, async handling |
|
||||
|
||||
**Swift Approach**:
|
||||
1. Convert YOLOv8 → CoreML: `yolo export model=yolov8s.pt format=coreml`
|
||||
2. Create swift_yolo.swift with VNCoreMLModel
|
||||
3. AVAssetReader for frame extraction
|
||||
4. ANE-accelerated inference
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: Python Keep (ML/LLM Dependencies)
|
||||
|
||||
### Modules to Keep in Python
|
||||
|
||||
| Module | Reason |
|
||||
|--------|--------|
|
||||
| asr | whisper/faster-whisper (no Rust/Swift equivalent) |
|
||||
| asrx | speaker diarization (pyannote) |
|
||||
| audio_taxonomy | librosa/tensorflow |
|
||||
| lip | MediaPipe lip tracking |
|
||||
| caption | LLM generation |
|
||||
| scene | ML scene classification |
|
||||
| story | LLM generation |
|
||||
| story_pipeline | LLM pipeline |
|
||||
| tmdb_agent | API agent |
|
||||
| identity_agent | LLM agent |
|
||||
| voice_embedding | ML embedding |
|
||||
| mediapipe_holistic | MediaPipe (no Rust/Swift binding) |
|
||||
| visual_chunk | Visual processing |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Roadmap
|
||||
|
||||
### Week 1: Swift Wrapper Removal
|
||||
|
||||
1. OCR: Modify `ocr.rs` to call swift_ocr directly
|
||||
2. Pose: Modify `pose.rs` to call swift_pose directly
|
||||
3. Test both with sample videos
|
||||
|
||||
### Week 2: Rust Infrastructure
|
||||
|
||||
4. redis_publisher: Create `src/core/redis_publisher.rs`
|
||||
5. resume_framework: Create `src/core/resume.rs`
|
||||
6. TKG: Create `src/core/processor/tkg.rs`
|
||||
|
||||
### Week 3: Swift Enhancement
|
||||
|
||||
7. Face: Extend swift_face.swift with CoreML embedding
|
||||
8. Test face embedding pipeline
|
||||
|
||||
### Week 4: Rust Algorithm Port
|
||||
|
||||
9. Cut: Port ContentDetector to Rust
|
||||
10. Test scene detection
|
||||
|
||||
### Week 5: Swift ANE
|
||||
|
||||
11. YOLO: Convert yolov8s → CoreML
|
||||
12. Create swift_yolo.swift
|
||||
13. Test object detection
|
||||
|
||||
---
|
||||
|
||||
## Total Effort Estimate
|
||||
|
||||
| Phase | LOC (Rust/Swift) | Effort |
|
||||
|-------|------------------|--------|
|
||||
| 1 | ~100 | 1-2 days |
|
||||
| 2 | ~600 | 3-4 days |
|
||||
| 3 | ~100 | 2-3 days |
|
||||
| 4 | ~300 | 2-3 days |
|
||||
| 5 | ~300 | 2-3 days |
|
||||
| **Total** | ~1400 | **10-15 days** |
|
||||
|
||||
---
|
||||
|
||||
## Dependency Removal Summary
|
||||
|
||||
| Dependency | Removed By |
|
||||
|------------|------------|
|
||||
| Python runtime | All Swift/Rust refactors |
|
||||
| redis-py | redis_publisher (Rust) |
|
||||
| psycopg2 | TKG (Rust) |
|
||||
| PySceneDetect | Cut (Rust) |
|
||||
| ultralytics (YOLO) | swift_yolo |
|
||||
| OpenCV (face crop) | Face Swift embedding |
|
||||
| InsightFace | Already superseded |
|
||||
|
||||
---
|
||||
|
||||
## Appendix: Module Summary Table
|
||||
|
||||
| Module | Language | Suitability | Status | Action |
|
||||
|--------|----------|-------------|--------|--------|
|
||||
| ocr | Swift | 10/10 | Active | Delete wrapper |
|
||||
| pose | Swift | 10/10 | Active | Delete wrapper |
|
||||
| face | Swift | 9/10 | Active | Extend Swift |
|
||||
| face_recognition | - | - | Superseded | Archive |
|
||||
| yolo | Swift | 8/10 | Active | Create Swift |
|
||||
| cut | Rust | 8/10 | Active | Port algorithm |
|
||||
| tkg | Rust | 10/10 | Active | Pure Rust |
|
||||
| resume_framework | Rust | 10/10 | Active | Pure Rust |
|
||||
| redis_publisher | Rust | 10/10 | Active | Pure Rust |
|
||||
| asr | Python | 2/10 | Keep | ML dependency |
|
||||
| asrx | Python | 2/10 | Keep | ML dependency |
|
||||
| audio_taxonomy | Python | 2/10 | Keep | ML dependency |
|
||||
| lip | Python | 2/10 | Keep | ML dependency |
|
||||
| caption | Python | 2/10 | Keep | LLM |
|
||||
| scene | Python | 2/10 | Keep | ML |
|
||||
| story | Python | 2/10 | Keep | LLM |
|
||||
| story_pipeline | Python | 2/10 | Keep | LLM |
|
||||
| tmdb_agent | Python | 4/10 | Keep | API |
|
||||
| identity_agent | Python | 4/10 | Keep | LLM |
|
||||
| voice_embedding | Python | 2/10 | Keep | ML |
|
||||
| mediapipe_holistic | Python | 2/10 | Keep | ML |
|
||||
| visual_chunk | Python | 3/10 | Keep | Visual |
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-05-27 | M5Max128 | Initial assessment from workspace research |
|
||||
484
docs_v1.0/DESIGN/Processor_State_Machine_V1.0.md
Normal file
484
docs_v1.0/DESIGN/Processor_State_Machine_V1.0.md
Normal file
@@ -0,0 +1,484 @@
|
||||
---
|
||||
title: Processor State Machine V1.0
|
||||
version: 1.0
|
||||
date: 2026-05-30
|
||||
author: M5Max128
|
||||
status: draft
|
||||
---
|
||||
|
||||
# Processor State Machine V1.0
|
||||
|
||||
## Overview
|
||||
|
||||
| Attribute | Value |
|
||||
|-----------|-------|
|
||||
| Scope | Backend, Worker, Pipeline |
|
||||
| Status | Draft |
|
||||
| Applicable To | M5Max128, M5Max48 |
|
||||
| Dependencies | migrations/034, job_worker.rs, redis_client.rs |
|
||||
| Related Docs | [Pipeline Module](../API_WORKSPACE/modules/10_pipeline.md), [TKG Query API](TKG_QUERY_API_V1.0.md) |
|
||||
|
||||
---
|
||||
|
||||
## 1. Design Goals
|
||||
|
||||
### 1.1 Problem Statement
|
||||
|
||||
The Momentry Core pipeline lacks unified state management for processors:
|
||||
|
||||
- **Opaque dependency chains**: Processors depend on each other (ASR → Cut, ASRX → ASR, Story → ASRX + Cut + YOLO + Face), but failures or delays are not explicitly tracked
|
||||
- **No alert mechanism**: When dependencies are not met or resources are exhausted, there is no systematic way to notify operators or trigger retries
|
||||
- **Coarse-grained status**: Existing `pending/running/completed/failed` states do not capture intermediate conditions like "waiting for dependencies" or "ready but not scheduled"
|
||||
|
||||
### 1.2 Solution
|
||||
|
||||
Introduce a **State Machine** with **Alert Mechanism**:
|
||||
|
||||
- **8 explicit states** for each processor job: `Idle → Waiting → Ready → Pending → Running → Completed/Failed/Skipped`
|
||||
- **Dependency checking**: `check_dependencies()` validates prerequisites before execution
|
||||
- **Alert emission**: Emit alerts to Redis pub/sub and PostgreSQL for monitoring and debugging
|
||||
|
||||
### 1.3 Scope
|
||||
|
||||
This design **complements** the existing polling mechanism:
|
||||
|
||||
| Component | Responsibility |
|
||||
|-----------|---------------|
|
||||
| **State Machine** | Fine-grained processor status management (Idle → Running → Completed) |
|
||||
| **Polling** | Coarse-grained ingestion verification (Rule 1 chunks exist? Vectorize done? TKG nodes exist?) |
|
||||
|
||||
**Non-Goals**:
|
||||
|
||||
- Does NOT replace polling for post-processing steps (入庫)
|
||||
- Does NOT auto-retry failed processors (future evolution)
|
||||
- Does NOT manage distributed state across workers
|
||||
|
||||
---
|
||||
|
||||
## 2. State Definitions
|
||||
|
||||
### 2.1 Eight States
|
||||
|
||||
| State | Semantics | Trigger | Next States |
|
||||
|-------|-----------|---------|--------------|
|
||||
| `Idle` | Initial state, no work assigned | Job created | `Waiting` |
|
||||
| `Waiting` | Dependencies not met, awaiting prerequisites | Dependency check fails | `Ready`, `Failed` |
|
||||
| `Ready` | Dependencies met, awaiting execution | Dependency check passes | `Pending` |
|
||||
| `Pending` | Queued for execution, waiting for worker | Scheduler accepts | `Running` |
|
||||
| `Running` | Currently processing | Worker starts | `Completed`, `Failed`, `Skipped` |
|
||||
| `Completed` | Success, output valid | Output validated | - (terminal) |
|
||||
| `Failed` | Error occurred, unrecoverable | Exception or timeout | - (terminal) |
|
||||
| `Skipped` | Conditional skip (optional processor) | Unmet optional conditions | - (terminal) |
|
||||
|
||||
### 2.2 State Transition Examples
|
||||
|
||||
**Example 1: ASR depends on Cut**
|
||||
|
||||
```
|
||||
ASR: Idle → Waiting (Cut not completed)
|
||||
Cut: Running → Completed
|
||||
ASR: Waiting → Ready (Cut completed) → Pending → Running → Completed
|
||||
```
|
||||
|
||||
**Example 2: Story depends on multiple processors**
|
||||
|
||||
```
|
||||
Story: Idle → Waiting (ASRX not completed)
|
||||
ASRX: Running → Completed
|
||||
Story: Waiting → Waiting (Cut not completed)
|
||||
Cut: Running → Completed
|
||||
Story: Waiting → Waiting (YOLO not completed)
|
||||
YOLO: Running → Completed
|
||||
Story: Waiting → Waiting (Face not completed)
|
||||
Face: Running → Completed
|
||||
Story: Waiting → Ready (all dependencies met) → Pending → Running → Completed
|
||||
```
|
||||
|
||||
**Example 3: Optional processor skipped**
|
||||
|
||||
```
|
||||
Pose: Idle → Ready → Pending → Running
|
||||
Pose: Running → Skipped (no pose detected, optional processing)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. State Transitions
|
||||
|
||||
### 3.1 Transition Diagram
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Idle: Job created
|
||||
|
||||
Idle --> Waiting: Initialize
|
||||
|
||||
Waiting --> Ready: Dependencies met
|
||||
Waiting --> Failed: Timeout
|
||||
|
||||
Ready --> Pending: Scheduled
|
||||
|
||||
Pending --> Running: Worker pickup
|
||||
|
||||
Running --> Completed: Success
|
||||
Running --> Failed: Error
|
||||
Running --> Skipped: Conditional skip
|
||||
|
||||
Completed --> [*]
|
||||
Failed --> [*]
|
||||
Skipped --> [*]
|
||||
```
|
||||
|
||||
### 3.2 Transition Rules
|
||||
|
||||
| From State | To State | Condition | Action |
|
||||
|------------|-----------|-----------|--------|
|
||||
| `Idle` | `Waiting` | Always (initial transition) | - |
|
||||
| `Waiting` | `Ready` | `check_dependencies() == Ok` | - |
|
||||
| `Waiting` | `Failed` | Timeout (default 7200s) | Emit `timeout` alert |
|
||||
| `Ready` | `Pending` | Resource available | - |
|
||||
| `Pending` | `Running` | Worker starts | - |
|
||||
| `Running` | `Completed` | Output valid | - |
|
||||
| `Running` | `Failed` | Exception or output invalid | Emit `output_invalid` alert |
|
||||
| `Running` | `Skipped` | Optional processor, conditions not met | - |
|
||||
|
||||
### 3.3 Edge Cases
|
||||
|
||||
| Scenario | Detection | Resolution |
|
||||
|----------|-----------|------------|
|
||||
| **Circular dependencies** | `check_dependencies()` detects cycle | Mark as `Failed`, emit `dependency_not_met` alert |
|
||||
| **Resource exhaustion** | GPU/CPU unavailable | Stay in `Waiting`, emit `resource_exhausted` alert |
|
||||
| **Partial output** | Output validation fails | Mark as `Failed`, emit `output_invalid` alert |
|
||||
| **Transient failure** | Network/API timeout | Stay in `Waiting`, retry after delay |
|
||||
|
||||
---
|
||||
|
||||
## 4. Alert Mechanism
|
||||
|
||||
### 4.1 Alert Types
|
||||
|
||||
| Type | Trigger | Severity | Action |
|
||||
|------|---------|----------|--------|
|
||||
| `dependency_not_met` | `check_dependencies()` fails | Warning | Retry after delay |
|
||||
| `resource_exhausted` | GPU/CPU unavailable | Warning | Wait + retry |
|
||||
| `output_invalid` | Validation fails | Error | Mark `Failed` |
|
||||
| `timeout` | Exceeds `MOMENTRY_*_TIMEOUT` | Error | Mark `Failed` |
|
||||
|
||||
### 4.2 Alert Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Worker as job_worker.rs
|
||||
participant Checker as check_dependencies()
|
||||
participant Redis as Redis Pub/Sub
|
||||
participant PostgreSQL as processor_alerts table
|
||||
|
||||
Worker->>Checker: check_dependencies(processor, file_uuid)
|
||||
alt Dependencies not met
|
||||
Checker-->>Worker: ConditionResult::NotMet(reason)
|
||||
Worker->>Redis: emit_processor_alert(file_uuid, processor, "dependency_not_met", reason)
|
||||
Redis-->>PostgreSQL: INSERT INTO processor_alerts
|
||||
Worker->>Worker: update_status(file_uuid, processor, Waiting)
|
||||
else Resource exhausted
|
||||
Checker-->>Worker: ConditionResult::ResourceExhausted
|
||||
Worker->>Redis: emit_processor_alert(file_uuid, processor, "resource_exhausted", "GPU unavailable")
|
||||
Redis-->>PostgreSQL: INSERT INTO processor_alerts
|
||||
Worker->>Worker: update_status(file_uuid, processor, Waiting)
|
||||
else Output invalid
|
||||
Checker-->>Worker: ConditionResult::OutputInvalid(reason)
|
||||
Worker->>Redis: emit_processor_alert(file_uuid, processor, "output_invalid", reason)
|
||||
Redis-->>PostgreSQL: INSERT INTO processor_alerts
|
||||
Worker->>Worker: update_status(file_uuid, processor, Failed)
|
||||
else OK
|
||||
Checker-->>Worker: ConditionResult::Ok
|
||||
Worker->>Worker: update_status(file_uuid, processor, Running)
|
||||
end
|
||||
```
|
||||
|
||||
### 4.3 Redis Channel
|
||||
|
||||
- **Channel**: `momentry:processor:alerts`
|
||||
- **Message Format**:
|
||||
```json
|
||||
{
|
||||
"file_uuid": "bd80fec9c42afb0307eb28f22c64c76a",
|
||||
"processor": "ASR",
|
||||
"alert_type": "dependency_not_met",
|
||||
"message": "Cut not completed",
|
||||
"timestamp": "2026-05-30T10:15:30Z"
|
||||
}
|
||||
```
|
||||
- **Consumers**: None (current implementation logs only, future: monitoring service)
|
||||
|
||||
### 4.4 PostgreSQL Table
|
||||
|
||||
**Table**: `processor_alerts` (defined in `migrations/034_processor_state_machine.sql`)
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS processor_alerts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
file_uuid VARCHAR(32),
|
||||
processor_type VARCHAR(32) NOT NULL,
|
||||
alert_type VARCHAR(32) NOT NULL,
|
||||
message TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_alerts_file_uuid ON processor_alerts(file_uuid);
|
||||
CREATE INDEX idx_alerts_processor_type ON processor_alerts(processor_type);
|
||||
CREATE INDEX idx_alerts_alert_type ON processor_alerts(alert_type);
|
||||
CREATE INDEX idx_alerts_created_at ON processor_alerts(created_at);
|
||||
```
|
||||
|
||||
**Retention Policy**: 30 days (TBD, future: implement cleanup job)
|
||||
|
||||
---
|
||||
|
||||
## 5. Dependency Checking
|
||||
|
||||
### 5.1 ConditionResult Enum
|
||||
|
||||
Defined in `src/worker/job_worker.rs`:
|
||||
|
||||
```rust
|
||||
pub enum ConditionResult {
|
||||
Ok, // All dependencies met
|
||||
NotMet(String), // Missing dependency (reason)
|
||||
ResourceExhausted, // GPU/CPU unavailable
|
||||
OutputInvalid(String), // Validation failed (reason)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 check_dependencies() Logic
|
||||
|
||||
Defined in `src/worker/job_worker.rs`:
|
||||
|
||||
```rust
|
||||
pub async fn check_dependencies(
|
||||
processor: ProcessorType,
|
||||
file_uuid: &str,
|
||||
db: &PostgresDb,
|
||||
) -> Result<ConditionResult> {
|
||||
match processor {
|
||||
ProcessorType::ASR => {
|
||||
// Check if Cut is completed
|
||||
if !db.is_processor_completed(file_uuid, ProcessorType::Cut).await? {
|
||||
return Ok(ConditionResult::NotMet("Cut not completed".into()));
|
||||
}
|
||||
}
|
||||
ProcessorType::ASRX => {
|
||||
// Check if ASR is completed
|
||||
if !db.is_processor_completed(file_uuid, ProcessorType::ASR).await? {
|
||||
return Ok(ConditionResult::NotMet("ASR not completed".into()));
|
||||
}
|
||||
}
|
||||
ProcessorType::Story => {
|
||||
// Check if ASRX + Cut + YOLO + Face are completed
|
||||
let deps = [
|
||||
ProcessorType::ASRX,
|
||||
ProcessorType::Cut,
|
||||
ProcessorType::YOLO,
|
||||
ProcessorType::Face,
|
||||
];
|
||||
for dep in deps {
|
||||
if !db.is_processor_completed(file_uuid, dep).await? {
|
||||
return Ok(ConditionResult::NotMet(format!("{:?} not completed", dep)));
|
||||
}
|
||||
}
|
||||
}
|
||||
ProcessorType::_5W1H => {
|
||||
// Check if Story is completed
|
||||
if !db.is_processor_completed(file_uuid, ProcessorType::Story).await? {
|
||||
return Ok(ConditionResult::NotMet("Story not completed".into()));
|
||||
}
|
||||
}
|
||||
// Other processors have no dependencies
|
||||
_ => {}
|
||||
}
|
||||
Ok(ConditionResult::Ok)
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Integration with job_worker.rs
|
||||
|
||||
```rust
|
||||
// In execute_processor()
|
||||
let condition = check_dependencies(processor, file_uuid, &db).await?;
|
||||
match condition {
|
||||
ConditionResult::Ok => {
|
||||
// Proceed to Running state
|
||||
self.update_status(file_uuid, processor, ProcessorJobStatus::Running).await?;
|
||||
// Execute processor...
|
||||
}
|
||||
ConditionResult::NotMet(reason) => {
|
||||
// Emit alert and mark as Waiting
|
||||
emit_processor_alert(file_uuid, processor, "dependency_not_met", &reason).await?;
|
||||
self.update_status(file_uuid, processor, ProcessorJobStatus::Waiting).await?;
|
||||
}
|
||||
ConditionResult::ResourceExhausted => {
|
||||
// Emit alert and mark as Waiting
|
||||
emit_processor_alert(file_uuid, processor, "resource_exhausted", "GPU unavailable").await?;
|
||||
self.update_status(file_uuid, processor, ProcessorJobStatus::Waiting).await?;
|
||||
}
|
||||
ConditionResult::OutputInvalid(reason) => {
|
||||
// Emit alert and mark as Failed
|
||||
emit_processor_alert(file_uuid, processor, "output_invalid", &reason).await?;
|
||||
self.update_status(file_uuid, processor, ProcessorJobStatus::Failed).await?;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Integration Points
|
||||
|
||||
### 6.1 With TKG Builder
|
||||
|
||||
- **TKG Builder** is NOT a processor, it's a **post-processing step** (入庫 step 8)
|
||||
- Triggers after Face Trace is completed
|
||||
- **State Machine does NOT manage TKG Builder state**
|
||||
- TKG Builder has its own verification mechanism in polling
|
||||
|
||||
### 6.2 With Face Trace
|
||||
|
||||
- **Face Trace** is NOT a processor, it's a **post-processing step** (入庫 step 5)
|
||||
- Triggers after all 10 processors are completed
|
||||
- **State Machine does NOT manage Face Trace state**
|
||||
- Face Trace has its own verification mechanism in polling
|
||||
|
||||
### 6.3 With 入庫 Flow
|
||||
|
||||
| Component | Manages | Scope |
|
||||
|-----------|---------|-------|
|
||||
| **State Machine** | Processor states | `Idle → Waiting → Ready → Pending → Running → Completed/Failed/Skipped` |
|
||||
| **Polling** | Post-processing verification | Rule 1 chunks, Vectorize, TKG nodes, Face Trace, etc. |
|
||||
|
||||
**Key Insight**: Two mechanisms are **independent but complementary**:
|
||||
|
||||
1. **State Machine**: Granular processor status, handles dependencies
|
||||
2. **Polling**: Coarse-grained ingestion verification, handles post-processing
|
||||
|
||||
### 6.4 Example Flow
|
||||
|
||||
```
|
||||
=== Processor State Machine (per processor) ===
|
||||
Cut: Idle → Waiting → Ready → Pending → Running → Completed ✓
|
||||
ASR: Idle → Waiting (Cut not done) → Waiting → Ready → Pending → Running → Completed ✓
|
||||
YOLO: Idle → Ready → Pending → Running → Completed ✓
|
||||
Face: Idle → Ready → Pending → Running → Completed ✓
|
||||
Story: Idle → Waiting (ASRX not done) → Waiting → Ready → Pending → Running → Completed ✓
|
||||
|
||||
=== 入庫 Polling (every 3s) ===
|
||||
[00:00] Check: Rule 1 chunks exist? → No (ASR not done)
|
||||
[00:03] Check: Rule 1 chunks exist? → Yes ✓
|
||||
Check: Vectorize done? → Yes ✓
|
||||
Check: TKG nodes exist? → No (Face Trace not done)
|
||||
[00:06] Check: TKG nodes exist? → Yes ✓
|
||||
Check: All 17 steps verified ✓
|
||||
Mark job as completed
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Implementation Checklist
|
||||
|
||||
### 7.1 Completed ✅
|
||||
|
||||
- [x] Migration 034: `processor_alerts` table
|
||||
- [x] Enum: `ProcessorJobStatus` (8 states) - `postgres_db.rs:585-594`
|
||||
- [x] Function: `emit_processor_alert()` - `redis_client.rs`
|
||||
- [x] Function: `check_dependencies()` - `job_worker.rs`
|
||||
- [x] Enum: `ConditionResult` - `job_worker.rs`
|
||||
|
||||
### 7.2 Pending 🔄
|
||||
|
||||
- [ ] Tests: State transitions (unit tests)
|
||||
- [ ] Tests: Alert emission (integration tests)
|
||||
- [ ] Tests: Dependency checking (unit tests)
|
||||
- [ ] Monitoring: Alert dashboard (TBD)
|
||||
- [ ] Retention: `processor_alerts` cleanup job (TBD)
|
||||
|
||||
---
|
||||
|
||||
## 8. Performance Considerations
|
||||
|
||||
### 8.1 Alert Emission
|
||||
|
||||
- **Non-blocking**: Redis pub/sub is fire-and-forget
|
||||
- **Low latency**: < 1ms per alert
|
||||
- **No retry**: If Redis is down, alert is lost (acceptable for debugging)
|
||||
|
||||
### 8.2 Dependency Checking
|
||||
|
||||
- **Synchronous DB queries**: `is_processor_completed()` queries PostgreSQL
|
||||
- **Cacheable**: Results can be cached for 1-3 seconds (TTL based on processor duration)
|
||||
- **Index usage**: Queries use `idx_processor_jobs_file_uuid_processor_type` index
|
||||
|
||||
### 8.3 State Updates
|
||||
|
||||
- **Single-row UPDATE**: `UPDATE processor_jobs SET status = $1 WHERE file_uuid = $2 AND processor_type = $3`
|
||||
- **Index usage**: Uses `idx_processor_jobs_file_uuid_processor_type` index
|
||||
- **Low contention**: Each processor has its own row
|
||||
|
||||
---
|
||||
|
||||
## 9. Future Evolution
|
||||
|
||||
### 9.1 Phase 1 (Current)
|
||||
|
||||
- Alert emission + PostgreSQL logging
|
||||
- Manual monitoring via `processor_alerts` table
|
||||
- No auto-retry
|
||||
|
||||
### 9.2 Phase 2 (Near-term)
|
||||
|
||||
- Alert consumer service (subscribes to Redis channel)
|
||||
- Auto-retry for `dependency_not_met` and `resource_exhausted` alerts
|
||||
- Exponential backoff for retries
|
||||
|
||||
### 9.3 Phase 3 (Medium-term)
|
||||
|
||||
- Event-driven pipeline (replace polling with Redis Streams)
|
||||
- Real-time status updates via WebSocket
|
||||
- Distributed state management (Redis-based)
|
||||
|
||||
### 9.4 Phase 4 (Long-term)
|
||||
|
||||
- DAG-based scheduling (Airflow/Temporal)
|
||||
- Cross-worker coordination
|
||||
- Priority-based resource allocation
|
||||
|
||||
---
|
||||
|
||||
## 10. Glossary
|
||||
|
||||
| Term | Definition |
|
||||
|------|-----------|
|
||||
| **State Machine** | Finite state automaton managing processor lifecycle (8 states) |
|
||||
| **Alert** | Asynchronous notification of state machine events (4 types) |
|
||||
| **Dependency** | Prerequisite processor that must complete before execution |
|
||||
| **Polling** | Periodic verification of post-processing steps (every 3s) |
|
||||
| **入庫** | Post-processing steps after 10 processors complete (17 steps) |
|
||||
| **file_uuid** | Unique identifier for a video file (32-char hex string) |
|
||||
| **Processor** | One of 10 processing stages (Cut, ASR, ASRX, YOLO, OCR, Face, Pose, VisualChunk, Story, 5W1H) |
|
||||
| **Post-processing** | Steps that run after processors (Rule 1, Vectorize, TKG, Face Trace, etc.) |
|
||||
|
||||
---
|
||||
|
||||
## 11. References
|
||||
|
||||
- [Pipeline Module](../API_WORKSPACE/modules/10_pipeline.md) - Pipeline overview and 入庫 steps
|
||||
- [TKG Query API V1.0](TKG_QUERY_API_V1.0.md) - TKG integration details
|
||||
- [Processor Refactoring Assessment](Processor_Refactoring_Assessment.md) - Processor refactoring plans
|
||||
- `migrations/034_processor_state_machine.sql` - Database schema
|
||||
- `src/core/db/postgres_db.rs` - ProcessorJobStatus enum
|
||||
- `src/core/db/redis_client.rs` - emit_processor_alert() function
|
||||
- `src/worker/job_worker.rs` - ConditionResult enum and check_dependencies()
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-05-30 | M5Max128 | Initial design document |
|
||||
128
docs_v1.0/DESIGN/REPRESENTATIVE_FRAME_API_V1.md
Normal file
128
docs_v1.0/DESIGN/REPRESENTATIVE_FRAME_API_V1.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# Representative Frame API V1.0
|
||||
|
||||
Portal 影片代表畫面 API — 沒有指定 frame_number 時自動偵測男女主角找到最佳互動 frame。
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
### Purpose
|
||||
|
||||
Portal 需要為每個影片顯示一張代表畫面(thumbnail),內容應為該影片最具代表性的 scene — 通常包含男女主角同框且互看的時刻。
|
||||
|
||||
### Principle
|
||||
|
||||
**沒有指定 frame_number → auto-detect representative frame**
|
||||
|
||||
既有端點不需改動,只需在 `frame` 參數為空時自動偵測。
|
||||
|
||||
---
|
||||
|
||||
## 2. Endpoint
|
||||
|
||||
### `GET /api/v1/file/:file_uuid/thumbnail`
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
| Param | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `frame` | i64 | ❌ | 指定 frame;不傳則 auto-detect |
|
||||
| `x` | i32 | ❌ | bbox crop x |
|
||||
| `y` | i32 | ❌ | bbox crop y |
|
||||
| `w` | i32 | ❌ | bbox crop width |
|
||||
| `h` | i32 | ❌ | bbox crop height |
|
||||
|
||||
**Response**: Pure JPEG bytes (Content-Type: image/jpeg)
|
||||
|
||||
**Examples**:
|
||||
```
|
||||
GET /api/v1/file/:uuid/thumbnail → auto-detect
|
||||
GET /api/v1/file/:uuid/thumbnail?frame=38165 → 指定 frame
|
||||
GET /api/v1/file/:uuid/thumbnail?frame=38165&x=723&y=205&w=221&h=221 → 指定 crop
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Internal Algorithm
|
||||
|
||||
### Auto-detect Fallback Chain
|
||||
|
||||
```
|
||||
Step 1: Auto-detect 主角 (top 2 by face count)
|
||||
└─ face_detections JOIN identities
|
||||
|
||||
Step 2: TKG Bridge — mutual_gaze?
|
||||
├── 有 mutual_gaze edge → first_frame ✅
|
||||
└── 無 → face_detections 第一次同框 frame ✅
|
||||
|
||||
Step 3: 只有一個主角?
|
||||
└─ 該主角 face_quality (w×h×confidence) 最高 frame
|
||||
|
||||
Step 4: 完全無 identity?
|
||||
└─ 任 identity 的 face_quality 最高 frame
|
||||
|
||||
Step 5: 完全無 face?
|
||||
└─ 404 "No faces in this file"
|
||||
```
|
||||
|
||||
### TKG Bridge Query
|
||||
|
||||
```sql
|
||||
-- 找兩主角各自的 main trace
|
||||
SELECT trace_id FROM face_detections
|
||||
WHERE file_uuid = $1 AND identity_id = $2 AND trace_id IS NOT NULL
|
||||
GROUP BY trace_id ORDER BY COUNT(*) DESC LIMIT 1;
|
||||
|
||||
-- TKG mutual_gaze 查詢
|
||||
SELECT (e.properties->>'first_frame')::bigint
|
||||
FROM tkg_edges e
|
||||
JOIN tkg_nodes a ON a.id = e.source_node_id
|
||||
JOIN tkg_nodes b ON b.id = e.target_node_id
|
||||
WHERE e.file_uuid = $1
|
||||
AND a.external_id = concat('trace_', $4)
|
||||
AND b.external_id = concat('trace_', $5)
|
||||
AND e.properties->>'mutual_gaze' = 'true'
|
||||
LIMIT 1;
|
||||
|
||||
-- Fallback: 第一次同框
|
||||
SELECT MIN(fd_a.frame_number)::bigint
|
||||
FROM face_detections fd_a
|
||||
JOIN face_detections fd_b ON fd_a.frame_number = fd_b.frame_number
|
||||
WHERE fd_a.file_uuid = $1 AND fd_a.identity_id = $2 AND fd_b.identity_id = $3;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementation
|
||||
|
||||
### Files Changed
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/api/media_api.rs` | `ThumbQuery.frame` → `Option<i64>`; add auto-detect fallback |
|
||||
| `src/core/processor/tkg.rs` | Add `query_auto_representative_frame()` + structs (已實作) |
|
||||
| `src/core/processor/mod.rs` | Export new function + structs (已實作) |
|
||||
|
||||
### Existing Trace-level Endpoints (不變)
|
||||
|
||||
```
|
||||
GET /api/v1/file/:uuid/trace/:tid/representative-face → JSON (legacy)
|
||||
GET /api/v1/file/:uuid/trace/:tid/thumbnail → JPEG (auto via select_rep_face)
|
||||
```
|
||||
|
||||
### No Changes
|
||||
|
||||
- ❌ No new DB tables / migrations
|
||||
- ❌ No changes to `select_rep_face` / blurdetect
|
||||
- ❌ No chunk / cut / pre_chunks dependency
|
||||
|
||||
---
|
||||
|
||||
## 5. Version History
|
||||
|
||||
| Date | Version | Author | Change |
|
||||
|------|---------|--------|--------|
|
||||
| 2026-05-22 | 1.0 | OpenCode | Initial design |
|
||||
| 2026-05-22 | 1.1 | OpenCode | 簡化為單一 endpoint: frame 為 None 時 auto-detect |
|
||||
|
||||
*Updated: 2026-05-22*
|
||||
270
docs_v1.0/DESIGN/Redis_Progress_Reporting_V1.0.md
Normal file
270
docs_v1.0/DESIGN/Redis_Progress_Reporting_V1.0.md
Normal file
@@ -0,0 +1,270 @@
|
||||
---
|
||||
document_type: "design_doc"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "Redis Progress Reporting V1.0"
|
||||
version: "V1.0"
|
||||
date: "2026-05-17"
|
||||
author: "M5"
|
||||
status: "draft"
|
||||
---
|
||||
|
||||
# Redis Progress Reporting V1.0
|
||||
|
||||
| 項目 | 內容 |
|
||||
|------|------|
|
||||
| Service | `MOMENTRY_CORE` |
|
||||
| Version | V1.0 |
|
||||
| Date | 2026-05-17 |
|
||||
| Author | M5 (OpenCode) |
|
||||
| Status | Draft |
|
||||
|
||||
## 1. Overview
|
||||
|
||||
This document defines the standardized progress reporting architecture for Momentry Core processors. It replaces the inconsistent ad-hoc progress patterns found across `scripts/`, `src/worker/`, and `src/api/`.
|
||||
|
||||
### 1.1 Problems Addressed
|
||||
|
||||
| # | Problem | Detail |
|
||||
|---|---------|--------|
|
||||
| 1 | Worker Redis key does not match `OPERATIONS/MOMENTRY_CORE_REDIS_KEYS.md` V1.0 spec | Worker writes `worker:job:{uuid}:processor:{name}` instead of spec `job:{uuid}:processor:{name}` |
|
||||
| 2 | Progress API reads wrong key | `get_progress()` reads `worker:job:{uuid}:processor:{name}` — unresolved with Playground subscriber which writes `job:{uuid}:processor:{name}` |
|
||||
| 3 | Swift processors (Face/OCR/Pose) lack RedisPublisher | Progress lost — only stdout text |
|
||||
| 4 | ASRX/Story/Visual chunk have no incremental progress | Start + complete only, no `current/total` updates |
|
||||
| 5 | `frames_processed` / `chunks_produced` never updated in real-time | Worker only writes processor hash at start and exit |
|
||||
| 6 | No `output_count` / `output_type` fields | Impossible to know how many faces/objects/segments were produced |
|
||||
|
||||
### 1.2 Key Design Decisions
|
||||
|
||||
| Decision | Rationale |
|
||||
|----------|-----------|
|
||||
| Progress unit = frames for video processors | All media-level processors work frame by frame |
|
||||
| Output count separate from progress | Processors may produce N outputs per frame (multiple faces, objects) |
|
||||
| Pub/sub for real-time, Hash for final state | Pub/sub is transient; Hash persists for API queries |
|
||||
|
||||
---
|
||||
|
||||
## 2. Redis Key Architecture
|
||||
|
||||
### 2.1 Key Patterns
|
||||
|
||||
All keys use the configured `REDIS_KEY_PREFIX` (default: `momentry:` for production, `momentry_dev:` for playground).
|
||||
|
||||
| Pattern | Type | TTL | Purpose | Owner |
|
||||
|---------|------|-----|---------|-------|
|
||||
| `{prefix}progress:{uuid}` | Pub/Sub | — | Real-time progress messages | Python scripts |
|
||||
| `{prefix}job:{uuid}` | Hash | 24h | Per-video job state | Worker |
|
||||
| `{prefix}job:{uuid}:processor:{name}` | Hash | 24h | Per-processor final state | Worker |
|
||||
| `{prefix}job:{uuid}:processor:{name}:output_count` | String | 24h | Output count by type | Worker |
|
||||
|
||||
### 2.2 Processor Hash Fields
|
||||
|
||||
```
|
||||
{prefix}job:{uuid}:processor:{name}
|
||||
├── status String running / completed / failed / pending
|
||||
├── current u32 Units processed (frames for video processors)
|
||||
├── total u32 Total units
|
||||
├── output_count u32 Output items produced (faces, objects, segments)
|
||||
├── output_type String Type name of output: faces / objects / segments / cuts / etc.
|
||||
├── pid i32 OS process ID (0 if not running)
|
||||
├── error String Error message if failed
|
||||
└── updated_at String ISO 8601 timestamp
|
||||
```
|
||||
|
||||
### 2.3 Migrated Keys
|
||||
|
||||
The following key patterns from the original implementation are REMOVED:
|
||||
|
||||
| Old Key | Reason |
|
||||
|---------|--------|
|
||||
| `{prefix}worker:job:{uuid}:processor:{name}` | Non-standard prefix — not in `MOMENTRY_CORE_REDIS_KEYS.md` spec |
|
||||
| `{prefix}job:{uuid}:processor:{name}:status` (flat) | Redundant — status stored in Hash |
|
||||
| `{prefix}job:{uuid}:processor:{name}:progress` (flat) | Replaced by `current` + `total` for percent calculation |
|
||||
| `{prefix}job:{uuid}:processor:{name}:current` (flat) | Replaced by Hash fields |
|
||||
| `{prefix}job:{uuid}:processor:{name}:total` (flat) | Replaced by Hash fields |
|
||||
| `{prefix}job:{uuid}:processor:{name}:started_at` (flat) | Replaced by Hash `updated_at` |
|
||||
|
||||
---
|
||||
|
||||
## 3. Pub/Sub Message Format
|
||||
|
||||
### 3.1 Channel
|
||||
|
||||
```
|
||||
{prefix}progress:{uuid}
|
||||
```
|
||||
|
||||
### 3.2 Message JSON
|
||||
|
||||
```json
|
||||
{
|
||||
"processor": "face",
|
||||
"current": 150,
|
||||
"total": 162696,
|
||||
"output_count": 423,
|
||||
"output_type": "faces",
|
||||
"message": "Processing frame 150",
|
||||
"timestamp": 1700000000
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Field Definitions
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `processor` | String | ✅ | Processor name: asr / asrx / yolo / ocr / face / pose / cut / story / visual_chunk |
|
||||
| `current` | u32 | ✅ | Units processed (frames for video processors) |
|
||||
| `total` | u32 | ✅ | Total units |
|
||||
| `output_count` | u32 | ❌ | Output items produced so far |
|
||||
| `output_type` | String | ❌ | Type name: faces / objects / segments / cuts / text_regions / persons / speakers / stories / visual_chunks |
|
||||
| `message` | String | ❌ | Human-readable progress description |
|
||||
| `timestamp` | u64 | ✅ | Unix timestamp |
|
||||
|
||||
---
|
||||
|
||||
## 4. Per-Processor Metrics
|
||||
|
||||
| Processor | current/total Unit | output_type | When to Publish |
|
||||
|-----------|-------------------|-------------|-----------------|
|
||||
| ASR | frames | `segments` | Every 100 segments processed |
|
||||
| ASRX | frames | `speakers` | Every processing stage |
|
||||
| YOLO | frames | `objects` | Every 500 frames |
|
||||
| OCR | frames | `text_regions` | Every 5% |
|
||||
| Face | frames | `faces` | Every batch (5% of frames) |
|
||||
| Pose | frames | `persons` | Every 10% |
|
||||
| CUT | frames | `cuts` | Every scene detected |
|
||||
| Story | chunks | `stories` | Every chunk processed |
|
||||
| Visual chunk | frames | `visual_chunks` | Every chunk processed |
|
||||
|
||||
### 4.1 Output Type Enum
|
||||
|
||||
```rust
|
||||
pub enum OutputType {
|
||||
Segments, // ASR
|
||||
Speakers, // ASRX
|
||||
Objects, // YOLO
|
||||
TextRegions, // OCR
|
||||
Faces, // Face
|
||||
Persons, // Pose
|
||||
Cuts, // CUT
|
||||
Stories, // Story
|
||||
VisualChunks, // Visual chunk
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flow
|
||||
|
||||
```
|
||||
┌──────────────────┐ Pub/Sub ┌──────────────────────┐
|
||||
│ Python Processor │ ───────── progress:{uuid} ──────────→│ Worker (subscriber) │
|
||||
│ (ASR/YOLO/Face) │ {current, total, │ │
|
||||
│ │ output_count, output_type} │ ──→ HSET │
|
||||
└──────────────────┘ │ job:{uuid}: │
|
||||
│ processor:{name} │
|
||||
┌──────────────────┐ │ │
|
||||
│ Swift Processor │ ──→ Python wrapper ──→ pub/sub │ (status, current, │
|
||||
│ (Face/OCR/Pose) │ (add RedisPublisher) │ total, output_count,│
|
||||
└──────────────────┘ │ output_type) │
|
||||
└──────────┬───────────┘
|
||||
│ HGETALL
|
||||
┌──────────▼───────────┐
|
||||
│ Progress API │
|
||||
│ GET /progress/:uuid │
|
||||
│ │
|
||||
│ ─→ compute % │
|
||||
│ ─→ return JSON │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Implementation Plan
|
||||
|
||||
### Phase 1: Python Processor RedisPublisher
|
||||
|
||||
| Task | Files | Effort |
|
||||
|------|-------|--------|
|
||||
| Add `RedisPublisher` to `face_processor.py` | `scripts/face_processor.py` | Medium |
|
||||
| Add `RedisPublisher` to `ocr_processor.py` | `scripts/ocr_processor.py` | Medium |
|
||||
| Add `RedisPublisher` to `pose_processor.py` | `scripts/pose_processor.py` | Medium |
|
||||
| Add incremental `.progress()` to `asrx_processor_custom.py` | `scripts/asrx_processor_custom.py` | Low |
|
||||
| Standardize pub/sub message to include `output_count`, `output_type` | All processor scripts | Low |
|
||||
|
||||
### Phase 2: Worker
|
||||
|
||||
| Task | Files | Effort |
|
||||
|------|-------|--------|
|
||||
| Fix Redis key from `worker:job:` to `job:` | `src/worker/processor.rs`, `src/core/db/redis_client.rs` | Low |
|
||||
| Subscribe to `progress:{uuid}` channel in `run_processor()` | `src/worker/processor.rs` | Medium |
|
||||
| HSET Processor Hash on each progress message | `src/worker/processor.rs` | Medium |
|
||||
| Set `output_count` and `output_type` from pub/sub message | `src/worker/processor.rs` | Low |
|
||||
|
||||
### Phase 3: Progress API
|
||||
|
||||
| Task | Files | Effort |
|
||||
|------|-------|--------|
|
||||
| Read `output_count`, `output_type` from Redis Hash | `src/api/server.rs` | Low |
|
||||
| Compute percentage from `current` / `total` | `src/api/server.rs` | Low |
|
||||
| Return `output_count`, `output_type` in response JSON | `src/api/server.rs` | Low |
|
||||
| Remove `worker:` fallback path | `src/api/server.rs` | Low |
|
||||
|
||||
### Phase 4: Cleanup
|
||||
|
||||
| Task | Files | Effort |
|
||||
|------|-------|--------|
|
||||
| Remove old `worker:job:` keys from Redis | Deployment script | Low |
|
||||
| Remove `update_processor_progress()` DB path (stale `processing_status` JSONB) | `src/core/db/postgres_db.rs` | Medium |
|
||||
|
||||
---
|
||||
|
||||
## 7. API Response Changes
|
||||
|
||||
### ProgressResponse (new fields)
|
||||
|
||||
```json
|
||||
{
|
||||
"processors": [
|
||||
{
|
||||
"name": "face",
|
||||
"status": "running",
|
||||
"current": 150,
|
||||
"total": 162696,
|
||||
"progress": 0,
|
||||
"frames_processed": 150,
|
||||
"output_count": 423,
|
||||
"output_type": "faces"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Dependencies
|
||||
|
||||
| Component | Version | Role |
|
||||
|-----------|---------|------|
|
||||
| Redis | ≥ 6.0 | Pub/Sub + Hash storage |
|
||||
| `redis_publisher.py` | Existing | Python → Redis pub/sub client |
|
||||
| `redis_client.rs` | Existing | Rust Redis client for worker + API |
|
||||
|
||||
---
|
||||
|
||||
## 9. References
|
||||
|
||||
| Doc | Relation |
|
||||
|-----|----------|
|
||||
| `OPERATIONS/MOMENTRY_CORE_REDIS_KEYS.md` | Parent spec — this doc supersedes sections 4, 7, 8 |
|
||||
| `DESIGN/VIDEO_PROCESSING_SPEC.md` §2.3 | Original progress design (ProcessProgress struct) |
|
||||
| `src/worker/processor.rs` | Worker progress write implementation |
|
||||
| `scripts/redis_publisher.py` | Python pub/sub client |
|
||||
| `src/api/server.rs` (get_progress) | Progress API handler |
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Change |
|
||||
|---------|------|--------|--------|
|
||||
| V1.0 | 2026-05-17 | M5 (OpenCode) | Initial draft — replaces ad-hoc progress patterns |
|
||||
279
docs_v1.0/DESIGN/Thumbnail_JPEG_Validation_Impl.md
Normal file
279
docs_v1.0/DESIGN/Thumbnail_JPEG_Validation_Impl.md
Normal file
@@ -0,0 +1,279 @@
|
||||
---
|
||||
title: Thumbnail JPEG Validation Implementation
|
||||
version: 1.0.0
|
||||
date: 2026-05-27
|
||||
author: M5Max128
|
||||
status: ready_for_implementation
|
||||
---
|
||||
|
||||
# Thumbnail JPEG Validation Implementation
|
||||
|
||||
## Overview
|
||||
|
||||
Add JPEG quality validation to all ffmpeg image extraction endpoints to prevent:
|
||||
- Empty images (0 bytes)
|
||||
- Corrupted JPEG (missing header/footer)
|
||||
- Incomplete JPEG (truncated output)
|
||||
|
||||
## Files to Create/Modify
|
||||
|
||||
### 1. Create: `src/core/thumbnail/validator.rs`
|
||||
|
||||
```rust
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
pub const JPEG_MIN_SIZE: usize = 100;
|
||||
pub const JPEG_SOI_MARKER: [u8; 3] = [0xFF, 0xD8, 0xFF];
|
||||
pub const JPEG_EOI_MARKER: [u8; 2] = [0xFF, 0xD9];
|
||||
|
||||
pub fn validate_jpeg(data: &[u8]) -> Result<()> {
|
||||
if data.len() < JPEG_MIN_SIZE {
|
||||
bail!("JPEG too small: {} bytes (minimum {})", data.len(), JPEG_MIN_SIZE);
|
||||
}
|
||||
|
||||
if data[0..3] != JPEG_SOI_MARKER {
|
||||
bail!("Invalid JPEG header: expected {:02X?}, got {:02X?}", JPEG_SOI_MARKER, &data[0..3]);
|
||||
}
|
||||
|
||||
if data[data.len() - 2..] != JPEG_EOI_MARKER {
|
||||
bail!("Incomplete JPEG: missing EOI marker, got {:02X?}", &data[data.len() - 2..]);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn is_valid_jpeg(data: &[u8]) -> bool {
|
||||
validate_jpeg(data).is_ok()
|
||||
}
|
||||
|
||||
pub fn jpeg_size_ok(data: &[u8]) -> bool {
|
||||
data.len() >= JPEG_MIN_SIZE
|
||||
}
|
||||
|
||||
pub fn jpeg_header_ok(data: &[u8]) -> bool {
|
||||
data.len() >= 3 && data[0..3] == JPEG_SOI_MARKER
|
||||
}
|
||||
|
||||
pub fn jpeg_footer_ok(data: &[u8]) -> bool {
|
||||
data.len() >= 2 && data[data.len() - 2..] == JPEG_EOI_MARKER
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Modify: `src/core/thumbnail/mod.rs`
|
||||
|
||||
Add module declaration at line 1:
|
||||
|
||||
```rust
|
||||
pub mod validator;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
// ... rest of file
|
||||
```
|
||||
|
||||
### 3. Modify: `src/api/media_api.rs`
|
||||
|
||||
Location: `face_thumbnail()` function, after ffmpeg output check (around line 754)
|
||||
|
||||
Add validation:
|
||||
|
||||
```rust
|
||||
if !output.status.success() {
|
||||
return Err(StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
// ADD THIS LINE:
|
||||
crate::core::thumbnail::validator::validate_jpeg(&output.stdout)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
|
||||
Ok(Response::builder()
|
||||
// ... rest of response
|
||||
```
|
||||
|
||||
### 4. Modify: `src/api/trace_agent_api.rs`
|
||||
|
||||
Location: `get_trace_thumbnail()` function, after reading bytes (around line 544)
|
||||
|
||||
Add validation:
|
||||
|
||||
```rust
|
||||
let bytes = tokio::fs::read(&tmp).await.map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})))
|
||||
})?;
|
||||
|
||||
let _ = tokio::fs::remove_file(&tmp).await;
|
||||
|
||||
// ADD THIS LINE:
|
||||
crate::core::thumbnail::validator::validate_jpeg(&bytes)
|
||||
.map_err(|e| {
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"error": e.to_string()})))
|
||||
})?;
|
||||
|
||||
Ok(Response::builder()
|
||||
// ... rest of response
|
||||
```
|
||||
|
||||
### 5. Modify: `src/core/frame_cache.rs`
|
||||
|
||||
Location: `FrameManager::extract()`, when iterating extracted frames (around line 73)
|
||||
|
||||
Replace the frame collection logic:
|
||||
|
||||
```rust
|
||||
for entry in &entries {
|
||||
let fname = entry.file_name();
|
||||
let fname_str = fname.to_string_lossy();
|
||||
if let Some(num_str) = fname_str
|
||||
.strip_prefix("frame_")
|
||||
.and_then(|s| s.strip_suffix(".jpg"))
|
||||
{
|
||||
if let Ok(frame_num) = num_str.parse::<u64>() {
|
||||
let frame_path = entry.path();
|
||||
// ADD VALIDATION:
|
||||
if let Ok(data) = std::fs::read(&frame_path) {
|
||||
if crate::core::thumbnail::validator::is_valid_jpeg(&data) {
|
||||
let timestamp = frame_num as f64 / fps;
|
||||
frames.push(CachedFrame {
|
||||
path: frame_path,
|
||||
frame_number: frame_num,
|
||||
timestamp_secs: timestamp,
|
||||
});
|
||||
} else {
|
||||
info!("[FrameCache] Skipping invalid JPEG: {:?}", frame_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Python Scripts (Optional Enhancement)
|
||||
|
||||
### 6. Create: `scripts/utils/jpeg_validator.py`
|
||||
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
"""JPEG validation utilities for ffmpeg-extracted frames."""
|
||||
|
||||
JPEG_MIN_SIZE = 100
|
||||
JPEG_SOI_MARKER = bytes([0xFF, 0xD8, 0xFF])
|
||||
JPEG_EOI_MARKER = bytes([0xFF, 0xD9])
|
||||
|
||||
|
||||
def validate_jpeg(data: bytes) -> bool:
|
||||
"""Validate JPEG by checking header, footer, and minimum size."""
|
||||
if len(data) < JPEG_MIN_SIZE:
|
||||
return False
|
||||
if data[:3] != JPEG_SOI_MARKER:
|
||||
return False
|
||||
if data[-2:] != JPEG_EOI_MARKER:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def validate_jpeg_file(path: str) -> bool:
|
||||
"""Validate JPEG file on disk."""
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
data = f.read()
|
||||
return validate_jpeg(data)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def filter_valid_jpegs(paths: list[str]) -> list[str]:
|
||||
"""Filter list of paths to only valid JPEGs."""
|
||||
return [p for p in paths if validate_jpeg_file(p)]
|
||||
```
|
||||
|
||||
### 7. Modify: `scripts/thumbnail_extractor.py`
|
||||
|
||||
Location: After extracting each thumbnail (around line 65)
|
||||
|
||||
Add validation:
|
||||
|
||||
```python
|
||||
if result.returncode == 0 and os.path.exists(output_file):
|
||||
# ADD VALIDATION:
|
||||
if validate_jpeg_file(output_file):
|
||||
extracted.append(output_file)
|
||||
print(f" Extracted: {output_file} at {ts:.1f}s", file=sys.stderr)
|
||||
else:
|
||||
print(f" Invalid JPEG at {ts:.1f}s", file=sys.stderr)
|
||||
os.remove(output_file) # Clean up invalid file
|
||||
else:
|
||||
print(f" Failed to extract frame at {ts:.1f}s", file=sys.stderr)
|
||||
```
|
||||
|
||||
### 8. Modify: `scripts/caption_processor.py`
|
||||
|
||||
Location: `extract_frames()` function, after ffmpeg extraction (around line 70)
|
||||
|
||||
Add validation:
|
||||
|
||||
```python
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, check=False)
|
||||
if os.path.exists(output_file):
|
||||
# ADD VALIDATION:
|
||||
if validate_jpeg_file(output_file):
|
||||
frames.append({"index": i, "timestamp": timestamp, "path": output_file})
|
||||
else:
|
||||
os.remove(output_file) # Clean up invalid file
|
||||
except Exception:
|
||||
pass
|
||||
```
|
||||
|
||||
### Python Scripts Affected
|
||||
|
||||
| Script | Function | Line | Priority |
|
||||
|--------|----------|------|----------|
|
||||
| `thumbnail_extractor.py` | `extract_thumbnails()` | 65 | High (user-facing) |
|
||||
| `caption_processor.py` | `extract_frames()` | 70 | Medium |
|
||||
| `caption_processor_contract_v1.py` | `extract_frames()` | 310 | Medium |
|
||||
| `ocr_processor_contract_v1.py` | `extract_frames()` | 367 | Medium |
|
||||
| `qa/executor.py` | `extract_frames()` | 93 | Low (QA only) |
|
||||
| `face_cross_validate.py` | `extract_frames()` | 16 | Low (testing) |
|
||||
| `face_mediapipe_test.py` | `extract_frames()` | 25 | Low (testing) |
|
||||
| `analyze_video_faces.py` | `extract_video_frames()` | 61 | Low (analysis) |
|
||||
|
||||
## Validation Logic
|
||||
|
||||
| Check | Condition | Error if failed |
|
||||
|-------|-----------|-----------------|
|
||||
| Minimum size | `len() >= 100` | "JPEG too small" |
|
||||
| SOI marker | `[0..3] == [0xFF,0xD8,0xFF]` | "Invalid JPEG header" |
|
||||
| EOI marker | `[-2..] == [0xFF,0xD9]` | "Incomplete JPEG" |
|
||||
|
||||
## Testing
|
||||
|
||||
After implementation, run:
|
||||
|
||||
```bash
|
||||
source ~/.cargo/env
|
||||
export MOMENTRY_PYTHON_PATH="/Users/accusys/momentry_core/venv/bin/python"
|
||||
cargo clippy --lib
|
||||
cargo test --lib
|
||||
```
|
||||
|
||||
Expected: 220 passed, 0 failed
|
||||
|
||||
## Commit Message
|
||||
|
||||
```
|
||||
feat: add JPEG validation to thumbnail endpoints
|
||||
|
||||
- Create validator module with JPEG header/footer/size checks
|
||||
- Add validation to face_thumbnail endpoint
|
||||
- Add validation to get_trace_thumbnail endpoint
|
||||
- Filter invalid JPEGs in FrameManager::extract
|
||||
- (Optional) Add Python jpeg_validator utility for script validation
|
||||
|
||||
Prevents serving corrupted/incomplete JPEG images to frontend.
|
||||
```
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0.0 | 2026-05-27 | M5Max128 | Implementation plan ready |
|
||||
| 1.1.0 | 2026-05-27 | M5Max128 | Added Python scripts section |
|
||||
340
docs_v1.0/DESIGN/Thumbnail_QA_Analysis.md
Normal file
340
docs_v1.0/DESIGN/Thumbnail_QA_Analysis.md
Normal file
@@ -0,0 +1,340 @@
|
||||
---
|
||||
title: Thumbnail Endpoint Quality Assurance Analysis
|
||||
version: 1.0.0
|
||||
date: 2026-05-27
|
||||
author: M5Max128
|
||||
status: research_complete
|
||||
---
|
||||
|
||||
# Thumbnail Endpoint Quality Assurance Analysis
|
||||
|
||||
## Scope
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| Research | Complete |
|
||||
| Implementation | Pending (M5Max48) |
|
||||
| Affected Endpoints | 2 |
|
||||
|
||||
## Overview
|
||||
|
||||
Thumbnail endpoints currently lack quality validation, resulting in potential anomalies:
|
||||
- **Empty images** - ffmpeg produces 0 bytes output
|
||||
- **Black frames** - extracted frame is all black
|
||||
- **Corrupted JPEG** - incomplete ffmpeg output
|
||||
|
||||
## Affected Endpoints
|
||||
|
||||
| Endpoint | File | Line |
|
||||
|----------|------|------|
|
||||
| `/api/v1/file/:file_uuid/thumbnail` | `src/api/media_api.rs` | 700-764 |
|
||||
| `/api/v1/file/:file_uuid/trace/:trace_id/thumbnail` | `src/api/trace_agent_api.rs` | 514-556 |
|
||||
|
||||
---
|
||||
|
||||
## Anomaly Classification
|
||||
|
||||
### Type 1: Empty Image (No Frame)
|
||||
|
||||
**Symptom**: Returns 0 bytes or very small JPEG
|
||||
|
||||
**Root Causes**:
|
||||
1. `frame_number > total_frames` - requested frame exceeds video length
|
||||
2. Video file missing or corrupted
|
||||
3. Codec does not support frame-level seek
|
||||
4. ffmpeg `-vf select` filter finds no matching frame
|
||||
|
||||
**Code Locations**:
|
||||
- `media_api.rs:710-716` - `query_auto_representative_frame()` may return invalid frame
|
||||
- `media_api.rs:720-728` - `file_path` query may return non-existent file
|
||||
- `media_api.rs:754-756` - only checks `output.status.success()`, not output content
|
||||
|
||||
### Type 2: Black Frame
|
||||
|
||||
**Symptom**: Returns valid JPEG but all black or very dark
|
||||
|
||||
**Root Causes**:
|
||||
1. `crop` parameters exceed video dimensions (`x+w > width` or `y+h > height`)
|
||||
2. Extracted frame is from fade-in/fade-out transition
|
||||
3. Video has black opening/closing credits
|
||||
4. Low-light scene
|
||||
|
||||
**Code Locations**:
|
||||
- `media_api.rs:731-735` - crop validation missing
|
||||
- `trace_agent_api.rs:530` - crop may exceed dimensions
|
||||
|
||||
### Type 3: Corrupted JPEG
|
||||
|
||||
**Symptom**: Returns incomplete JPEG (browser shows broken image)
|
||||
|
||||
**Root Causes**:
|
||||
1. ffmpeg stdout pipe interrupted before completion
|
||||
2. ffmpeg process killed mid-output
|
||||
3. JPEG encoder failure
|
||||
4. Incomplete write to stdout buffer
|
||||
|
||||
**Code Locations**:
|
||||
- `media_api.rs:751` - pipe output may be truncated
|
||||
- `media_api.rs:758-763` - no JPEG validation before serving
|
||||
|
||||
---
|
||||
|
||||
## Current Quality Mechanisms
|
||||
|
||||
### Endpoint 1: `face_thumbnail`
|
||||
|
||||
| Mechanism | Status | Location |
|
||||
|-----------|--------|----------|
|
||||
| Representative frame selection | Present | `tkg::query_auto_representative_frame()` |
|
||||
| ffmpeg success check | Present | `output.status.success()` |
|
||||
| JPEG validation | Missing | - |
|
||||
| Size validation | Missing | - |
|
||||
| Black frame detection | Missing | - |
|
||||
| Retry mechanism | Missing | - |
|
||||
|
||||
### Endpoint 2: `get_trace_thumbnail`
|
||||
|
||||
| Mechanism | Status | Location |
|
||||
|-----------|--------|----------|
|
||||
| Blur detection (candidate selection) | Present | `select_rep_face()` lines 463-480 |
|
||||
| Confidence filter (>0.7) | Present | `select_rep_face()` line 429 |
|
||||
| QC metadata filter | Present | `select_rep_face()` line 430 |
|
||||
| ffmpeg success check | Present | `status.status.success()` |
|
||||
| JPEG validation | Missing | - |
|
||||
| Black frame detection (extraction) | Missing | - |
|
||||
| Retry mechanism | Missing | - |
|
||||
|
||||
**Note**: `select_rep_face()` has sophisticated quality control for SELECTING the representative face, but the actual EXTRACTION step lacks validation.
|
||||
|
||||
---
|
||||
|
||||
## Root Cause Analysis
|
||||
|
||||
### A. Input Data Problems
|
||||
|
||||
| Problem | Impact | Condition |
|
||||
|---------|--------|-----------|
|
||||
| `frame_number > total_frames` | Empty image | TKG returns wrong frame, user passes invalid value |
|
||||
| `crop exceeds dimensions` | Black frame / error | face bbox incorrect, video resolution changed |
|
||||
| Video file missing | 500 error | File deleted/moved |
|
||||
| Codec不支持seek | Empty/corrupted | Some codecs only support sequential read |
|
||||
|
||||
### B. ffmpeg Execution Problems
|
||||
|
||||
| Problem | Impact | Cause |
|
||||
|---------|--------|-------|
|
||||
| `select` no output | Empty JPEG | frame超出範圍 → ffmpeg skips all frames |
|
||||
| Pipe interrupted | Corrupted JPEG | stdout buffer full, ffmpeg terminated early |
|
||||
| `-ss` imprecise | Wrong frame | input seeking approximate, error ±5 frames |
|
||||
| crop failure | Black frame / 500 | `x+w > width` or `y+h > height` |
|
||||
|
||||
### C. Quality Control Gaps
|
||||
|
||||
| Gap | Impact | Current |
|
||||
|-----|--------|---------|
|
||||
| No JPEG validation | Corrupted image served | Only checks exit code |
|
||||
| No size check | 0 bytes returned | No output length check |
|
||||
| No black detection | Black frame served | blurdetect only in candidate selection |
|
||||
| No retry | Single failure = error | No retry mechanism |
|
||||
|
||||
---
|
||||
|
||||
## Concrete Failure Cases
|
||||
|
||||
### Case 1: Frame Exceeds Range
|
||||
|
||||
```
|
||||
Video: total_frames=1000 (DB record)
|
||||
Actual: video has only 950 frames (file truncated)
|
||||
Request: frame=980
|
||||
ffmpeg: select=eq(n\,980) → no match
|
||||
Output: 0 bytes JPEG
|
||||
Frontend: blank image
|
||||
```
|
||||
|
||||
### Case 2: Crop Exceeds Dimensions
|
||||
|
||||
```
|
||||
Video: 1920x1080
|
||||
face_bbox: x=1850, y=1050, w=100, h=100
|
||||
ffmpeg: crop=100:100:1850:1050
|
||||
Result: x+100=1950 > 1920 → ffmpeg error or black border
|
||||
```
|
||||
|
||||
### Case 3: Seek Imprecise
|
||||
|
||||
```
|
||||
Video: 25fps
|
||||
Request: frame=1000 (40 seconds)
|
||||
ffmpeg -ss 40.0 -i video
|
||||
Actual: seeks to frame 995~1005 range
|
||||
Result: extracts different face than select_rep_face chose
|
||||
```
|
||||
|
||||
### Case 4: Pipe Interrupted
|
||||
|
||||
```
|
||||
ffmpeg -i large_video -vf select=eq(n\,50000) -f image2pipe -
|
||||
Video large, select needs scan to frame 50000
|
||||
Pipe buffer full → ffmpeg may be killed or terminate early
|
||||
Output: incomplete JPEG (missing FFD9 footer)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Recommended Fixes
|
||||
|
||||
### Phase P0: Critical (Must Implement)
|
||||
|
||||
| Fix | Description | LOC | Location |
|
||||
|-----|-------------|-----|----------|
|
||||
| **Frame validation** | `frame <= total_frames` | ~20 | `media_api.rs:707-718` |
|
||||
| **Crop validation** | `x+w <= width, y+h <= height` | ~15 | `media_api.rs:731-735` |
|
||||
| **JPEG header check** | `data[0..3] == [0xFF,0xD8,0xFF]` | ~10 | Helper function |
|
||||
| **JPEG footer check** | `data[-2..] == [0xFF,0xD9]` | ~10 | Helper function |
|
||||
| **Minimum size check** | `data.len() > 100` | ~5 | Helper function |
|
||||
|
||||
### Phase P1: Important (Should Implement)
|
||||
|
||||
| Fix | Description | LOC | Location |
|
||||
|-----|-------------|-----|----------|
|
||||
| **Black frame detection** | ffmpeg `-vf blackdetect` filter | ~30 | After extraction |
|
||||
| **Output seeking** | Move `-ss` after `-i` for precision | ~5 | `trace_agent_api.rs:527` |
|
||||
|
||||
### Phase P2: Enhancement (Nice to Have)
|
||||
|
||||
| Fix | Description | LOC | Location |
|
||||
|-----|-------------|-----|----------|
|
||||
| **Retry mechanism** | Max 3 attempts, offset +30 frames each | ~50 | Both endpoints |
|
||||
| **Fallback frame** | Extract middle frame if all fail | ~30 | Both endpoints |
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Create Validation Module
|
||||
|
||||
Create `src/core/thumbnail/validator.rs`:
|
||||
|
||||
```rust
|
||||
pub fn validate_jpeg(data: &[u8]) -> Result<()> {
|
||||
// P0-1: Minimum size
|
||||
if data.len() < 100 {
|
||||
bail!("JPEG too small: {} bytes", data.len());
|
||||
}
|
||||
|
||||
// P0-2: JPEG header (SOI marker)
|
||||
if data[0..3] != [0xFF, 0xD8, 0xFF] {
|
||||
bail!("Invalid JPEG header");
|
||||
}
|
||||
|
||||
// P0-3: JPEG footer (EOI marker)
|
||||
if data[data.len()-2..] != [0xFF, 0xD9] {
|
||||
bail!("Incomplete JPEG");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2: Add Frame/Crop Validation
|
||||
|
||||
In `media_api.rs`:
|
||||
|
||||
```rust
|
||||
// P0-4: Validate frame number
|
||||
let total_frames: i64 = sqlx::query_scalar(...)
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
if frame > total_frames {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
// P0-5: Validate crop dimensions
|
||||
if let (Some(x), Some(y), Some(w), Some(h)) = (q.x, q.y, q.w, q.h) {
|
||||
let (width, height): (i32, i32) = sqlx::query_as(...)
|
||||
.bind(&file_uuid)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
if x + w > width || y + h > height {
|
||||
return Err(StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Integrate Validation
|
||||
|
||||
In both endpoints, after ffmpeg extraction:
|
||||
|
||||
```rust
|
||||
// Apply validation
|
||||
validate_jpeg(&output.stdout)
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Test Cases
|
||||
|
||||
| Test | Input | Expected |
|
||||
|------|-------|----------|
|
||||
| Valid frame | `frame=500` (valid) | JPEG returned |
|
||||
| Frame exceeds | `frame=999999` | 400 BAD_REQUEST |
|
||||
| Valid crop | `x=100,y=100,w=200,h=200` | JPEG returned |
|
||||
| Crop exceeds | `x=1800,y=1000,w=200,h=200` | 400 BAD_REQUEST |
|
||||
| Empty video | corrupted video file | 500 INTERNAL_ERROR |
|
||||
| Black frame | fade-out frame | Retry or fallback |
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `src/core/thumbnail/mod.rs` | Add validator module |
|
||||
| `src/core/thumbnail/validator.rs` | New file (validation helpers) |
|
||||
| `src/api/media_api.rs` | Add validation in `face_thumbnail()` |
|
||||
| `src/api/trace_agent_api.rs` | Add validation in `get_trace_thumbnail()` |
|
||||
|
||||
---
|
||||
|
||||
## Estimated Effort
|
||||
|
||||
| Phase | LOC | Time |
|
||||
|-------|-----|------|
|
||||
| P0 (Critical) | ~60 | 1-2 days |
|
||||
| P1 (Important) | ~35 | 1 day |
|
||||
| P2 (Enhancement) | ~80 | 2-3 days |
|
||||
| **Total** | ~175 | 4-6 days |
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0.0 | 2026-05-27 | M5Max128 | Initial analysis complete |
|
||||
|
||||
---
|
||||
|
||||
## Next Steps for M5Max48
|
||||
|
||||
1. Read this document
|
||||
2. Implement P0 fixes first
|
||||
3. Test with edge cases
|
||||
4. Add P1/P2 as needed
|
||||
5. Update `AGENTS.md` if adding new validation commands
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- `docs_v1.0/DESIGN/Processor_Refactoring_Assessment.md` - Processor refactoring priorities
|
||||
- `src/api/media_api.rs:700-764` - face_thumbnail implementation
|
||||
- `src/api/trace_agent_api.rs:394-556` - select_rep_face and get_trace_thumbnail
|
||||
- `ffmpeg -vf blackdetect` documentation
|
||||
242
docs_v1.0/M4_workspace/2026-05-27_charade_pipeline_checklist.md
Normal file
242
docs_v1.0/M4_workspace/2026-05-27_charade_pipeline_checklist.md
Normal file
@@ -0,0 +1,242 @@
|
||||
---
|
||||
title: Charade Full Movie Pipeline Checklist
|
||||
version: 1.0
|
||||
date: 2026-05-27
|
||||
author: M5Max48
|
||||
status: in_progress
|
||||
---
|
||||
|
||||
# Charade Full Movie Pipeline Checklist
|
||||
|
||||
**File UUID**: `c3c635e3641da80dde10cc555ffcdda5`
|
||||
**File Name**: Charade (1963) Cary Grant & Audrey Hepburn | Comedy Mystery Romance Thriller | Full Movie.mp4
|
||||
**Duration**: 6785 seconds (113 minutes)
|
||||
**Total Frames**: 169,625
|
||||
|
||||
---
|
||||
|
||||
## P0: Processor Outputs
|
||||
|
||||
### Purpose
|
||||
原始處理器輸出檔案,存放在 `/Users/accusys/momentry/output_dev/`。這些是後續 ingestion 的資料來源。
|
||||
|
||||
### Processor Details
|
||||
|
||||
| Processor | Expected Output | Size Estimate | Purpose | Status |
|
||||
|-----------|-----------------|---------------|---------|--------|
|
||||
| CUT | `c3c635e3641da80dde10cc555ffcdda5.cut.json` | ~170KB | Scene boundary detection,切割點用於 Rule 3 chunking | ✅ Done |
|
||||
| YOLO | `c3c635e3641da80dde10cc555ffcdda5.yolo.json` | ~50-80MB | Object detection,每幀的物件類別與位置 | 🔄 Running |
|
||||
| Face | `c3c635e3641da80dde10cc555ffcdda5.face.json` | ~1.5GB | Face detection + 512-dim embedding (FaceNet CoreML) | 🔄 44% |
|
||||
| Face Traced | `c3c635e3641da80dde10cc555ffcdda5.face_traced.json` | ~1.2GB | Face tracking,同一人物的連續出現 → trace_id | ⏳ Pending (after Face) |
|
||||
| OCR | `c3c635e3641da80dde10cc555ffcdda5.ocr.json` | ~50KB | Text recognition from frames | ❌ Skipped |
|
||||
| Pose | `c3c635e3641da80dde10cc555ffcdda5.pose.json` | ~20MB | Body pose estimation | 🔄 Running |
|
||||
| ASRX | `c3c635e3641da80dde10cc555ffcdda5.asrx.json` | ~8MB | Speaker diarization,語者分段 | ✅ Done (reuse from public) |
|
||||
| Visual Chunk | `c3c635e3641da80dde10cc555ffcdda5.visual_chunk.json` | ~60KB | Visual scene chunk metadata | ✅ Done |
|
||||
| Scene | `c3c635e3641da80dde10cc555ffcdda5.scene.json` | ~300B | Scene list from CUT | ✅ Done |
|
||||
| Scene Meta | `c3c635e3641da80dde10cc555ffcdda5.scene_meta.json` | ~50KB | Heuristic scene metadata (人物 + 物件統計) | ⏳ Pending |
|
||||
| Story LLM | `c3c635e3641da80dde10cc555ffcdda5.story_llm.json` | ~800KB | LLM-generated story summaries per chunk | ✅ Done |
|
||||
| Story Story | `c3c635e3641da80dde10cc555ffcdda5.story_story.json` | ~800KB | Story parent-child relationships | ✅ Done |
|
||||
| TMDb | `c3c635e3641da80dde10cc555ffcdda5.tmdb.json` | ~5KB | TMDb cast list with face embeddings | ⏳ Pending |
|
||||
| 5W1H | `c3c635e3641da80dde10cc555ffcdda5.5w1h.json` | ~500KB | 5W1H agent output (who/when/where/what/why/how) | ✅ Done |
|
||||
|
||||
### Key Dependencies
|
||||
- Face Traced 需要 Face 完成後才能執行 (face_traced.json = face.json + tracking)
|
||||
- Scene Meta 需要 Face + YOLO 完成
|
||||
- TMDb 需要 Face Traced 完成後執行 matching
|
||||
|
||||
---
|
||||
|
||||
## P1: Database Records
|
||||
|
||||
### Purpose
|
||||
將 processor outputs 存入 PostgreSQL,供 API query 使用。
|
||||
|
||||
### Table Details
|
||||
|
||||
| Table | Expected Records | Purpose | Verification Query | Status |
|
||||
|-------|------------------|---------|-------------------|--------|
|
||||
| `dev.videos` | 1 row | Video metadata (duration, fps, status) | `SELECT file_uuid, status FROM dev.videos WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ✅ Registered |
|
||||
| `dev.monitor_jobs` | 1 row | Processing job state machine | `SELECT uuid, status, completed_processors FROM dev.monitor_jobs WHERE uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | 🔄 Running |
|
||||
| `dev.pre_chunks` | ~7,000 rows | Raw processor outputs (ASR sentences, YOLO objects, etc.) | `SELECT COUNT(*) FROM dev.pre_chunks WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
| `dev.face_detections` | ~70,000 rows | Face detection records (每幀每張臉) | `SELECT COUNT(*) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
| `dev.face_detections.embedding` | ~70,000 non-NULL | 512-dim FaceNet embedding (用於 identity matching) | `SELECT COUNT(embedding) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
| `dev.face_detections.trace_id` | ~70,000 non-NULL | Face tracking ID (同一人物跨幀連續出現) | `SELECT COUNT(trace_id) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
| `dev.face_detections.identity_id` | ~50,000 non-NULL | TMDb identity binding (Audrey, Cary, etc.) | `SELECT COUNT(identity_id) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
|
||||
### Key Points
|
||||
- `embedding` 必須非 NULL 才能進行 TMDb matching (之前 store_traced_faces.py bug 修復)
|
||||
- `trace_id` 由 `store_traced_faces.py` 從 face_traced.json 計算
|
||||
- `identity_id` 由 `match_faces_to_tmdb.py` 計算 (cosine similarity > 0.5)
|
||||
|
||||
---
|
||||
|
||||
## P2: Chunk Ingestion
|
||||
|
||||
### Purpose
|
||||
將 raw processor outputs 轉換為 searchable chunks,用於 RAG query。
|
||||
|
||||
### Chunk Types
|
||||
|
||||
| Chunk Type | Expected Count | Purpose | Source | Verification Query | Status |
|
||||
|------------|----------------|---------|--------|-------------------|--------|
|
||||
| sentence (Rule 1) | ~1,700 | Sentence-level chunks for text search | ASR output → sentence split | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'sentence'` | ⏳ Pending |
|
||||
| llm_parent | ~800 | LLM-generated summary parent chunks | Story LLM output | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'llm_parent'` | ⏳ Pending |
|
||||
| story_parent | ~800 | Story parent chunks (narrative segments) | Story processor | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'story_parent'` | ⏳ Pending |
|
||||
| story_child | ~1,700 | Story child chunks (linked to sentence) | Story processor | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'story_child'` | ⏳ Pending |
|
||||
| cut (Rule 3) | ~500 | Scene-level chunks for scene search | CUT output → scene boundaries | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'cut'` | ⏳ Pending |
|
||||
| trace | ~3,600 | Face trace chunks (identity-centric) | Face Traced output | `SELECT COUNT(*) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND chunk_type = 'trace'` | ⏳ Pending |
|
||||
|
||||
### Ingestion Pipeline
|
||||
1. **Rule 1**: ASR → sentence split → chunk + embedding → Qdrant
|
||||
2. **Rule 3**: CUT + ASR → scene chunks → chunk + embedding → Qdrant
|
||||
3. **Trace**: Face Traced → trace chunks → TKG nodes → Qdrant
|
||||
|
||||
### Key Points
|
||||
- `start_frame` / `end_frame` 必須正確計算 (之前 bug: frame=0)
|
||||
- Chunks 必須有 `embedding` 才能 search
|
||||
|
||||
---
|
||||
|
||||
## P3: Vector Embeddings
|
||||
|
||||
### Purpose
|
||||
將 chunks 的 text 轉換為 768-dim embeddings,存入 PostgreSQL + Qdrant,用於 semantic search。
|
||||
|
||||
### Embedding Targets
|
||||
|
||||
| Target | Expected Count | Model | Purpose | Verification | Status |
|
||||
|--------|----------------|-------|---------|--------------|--------|
|
||||
| PostgreSQL `dev.chunk.embedding` | ~5,000 | Gemma-2-9B (768-dim) | Text semantic search | `SELECT COUNT(embedding) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | ⏳ Pending |
|
||||
| Qdrant `momentry_dev_rule1_v2` | ~5,000 points | Gemma-2-9B | Fast vector similarity search | `curl -H "api-key: Test3200Test3200Test3200" "http://localhost:6333/collections/momentry_dev_rule1_v2"` | ⏳ Pending |
|
||||
| Qdrant `_face` collection | ~70,000 points | FaceNet-512 (512-dim) | Face identity search | Face embeddings sync via `sync_face_embeddings()` | ⏳ Pending |
|
||||
|
||||
### Embedding Pipeline
|
||||
1. **Text chunks**: `embeddinggemma_server.py` (port 11436) → 768-dim embedding
|
||||
2. **Face embeddings**: FaceNet CoreML (from face.json) → 512-dim embedding (已在 P0 產生)
|
||||
3. **Sync to Qdrant**: `sync_face_embeddings()` function in Rust
|
||||
|
||||
### Key Points
|
||||
- Text embeddings 使用 Gemma-2-9B (local LLM server)
|
||||
- Face embeddings 使用 FaceNet-512 (CoreML ANE accelerated)
|
||||
- Qdrant 提供 fast similarity search (cosine similarity)
|
||||
|
||||
---
|
||||
|
||||
## P4: Identity Binding
|
||||
|
||||
### Purpose
|
||||
將 detected faces 綁定到 TMDb identities (Audrey Hepburn, Cary Grant, etc.),用於 identity_text search。
|
||||
|
||||
### Identity Matching Pipeline
|
||||
|
||||
| Step | Expected Result | Method | Verification | Status |
|
||||
|------|-----------------|--------|--------------|--------|
|
||||
| TMDb seeds loaded | 23 identities | `tmdb_embed_extractor.py` → TMDb profile face embeddings | `SELECT COUNT(*) FROM dev.identities WHERE source = 'tmdb' AND face_embedding IS NOT NULL` | ✅ Done |
|
||||
| Face matching | ~50,000 bindings | `match_faces_to_tmdb.py` → cosine similarity > 0.5 | `SELECT COUNT(identity_id) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND identity_id IS NOT NULL` | ⏳ Pending |
|
||||
| Audrey Hepburn faces | ~16,000 | Highest similarity match | `SELECT COUNT(*) FROM dev.face_detections fd JOIN dev.identities i ON fd.identity_id = i.id WHERE fd.file_uuid = 'c3c635e3641da80dde10cc555ffcdda5' AND i.name = 'Audrey Hepburn'` | ⏳ Pending |
|
||||
| Cary Grant faces | ~5,000 | Second highest match | Same query for Cary Grant | ⏳ Pending |
|
||||
|
||||
### Matching Algorithm
|
||||
```python
|
||||
# match_faces_to_tmdb.py
|
||||
for trace_id in traces:
|
||||
for face_embedding in trace_faces:
|
||||
for tmdb_identity in tmdb_identities:
|
||||
similarity = cosine_similarity(face_embedding, tmdb_identity.face_embedding)
|
||||
if similarity >= 0.5:
|
||||
match trace_id → tmdb_identity
|
||||
```
|
||||
|
||||
### Key Points
|
||||
- TMDb seeds 需要 `face_embedding` (之前已驗證: 23 identities with embeddings)
|
||||
- Face `embedding` 必須非 NULL (之前 store_traced_faces.py bug 修復)
|
||||
- Threshold: 0.5 (可調整)
|
||||
|
||||
---
|
||||
|
||||
## P5: API Endpoints
|
||||
|
||||
### Purpose
|
||||
驗證 API endpoints 可以正確返回 identity_text search results。
|
||||
|
||||
### API Tests
|
||||
|
||||
| Endpoint | Purpose | Expected Response | Test Command | Status |
|
||||
|----------|---------|-------------------|--------------|--------|
|
||||
| `/api/v1/search/identity_text` | Search chunk text → identities | Results with `identity_name`, `trace_id`, `identity_source` | `curl "http://localhost:3003/api/v1/search/identity_text?file_uuid=c3c635e3641da80dde10cc555ffcdda5&q=Regina&limit=5"` | ⏳ Pending |
|
||||
| `/api/v1/identities` | List identities with TMDb | Identity list with `tmdb_id`, `face_embedding` | `curl "http://localhost:3003/api/v1/identities?name=Audrey"` | ⏳ Pending |
|
||||
| `/api/v1/progress/:file_uuid` | Check processing progress | JSON with `status`, `completed_processors` | `curl "http://localhost:3003/api/v1/progress/c3c635e3641da80dde10cc555ffcdda5"` | ⏳ Pending |
|
||||
|
||||
### Expected API Response Example
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"chunk_id": "sentence_123",
|
||||
"start_time": 355.0,
|
||||
"text_content": "Oh, mine's Regina Lampert.",
|
||||
"identity_id": 9,
|
||||
"identity_name": "Audrey Hepburn",
|
||||
"identity_source": "tmdb",
|
||||
"trace_id": 169
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Key Points
|
||||
- `identity_text` API 需要 `chunk.start_frame` / `chunk.end_frame` 正確 (之前 bug: frame=0)
|
||||
- `identity_id` 必須非 NULL 才能返回 identity_name
|
||||
|
||||
---
|
||||
|
||||
## P6: Completion Criteria
|
||||
|
||||
### Purpose
|
||||
驗證 pipeline 完整完成,所有 ingestion steps 成功。
|
||||
|
||||
### Final Verification Checklist
|
||||
|
||||
| Criteria | Purpose | Check Command | Expected Result | Status |
|
||||
|----------|---------|---------------|-----------------|--------|
|
||||
| All processor outputs exist | 確認所有 processor JSON 檔案產生 | `ls -la output_dev/c3c635e3641da80dde10cc555ffcdda5.*` | 14+ files with size > 0 | ⏳ Pending |
|
||||
| Job status = completed | 確認 worker 完成 job | `SELECT status FROM dev.monitor_jobs WHERE uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | `completed` | ⏳ Pending |
|
||||
| Video status = completed | 確認 video state 更新 | `SELECT status FROM dev.videos WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | `completed` | ⏳ Pending |
|
||||
| All chunks have embeddings | 確認 text embeddings 完成 | `SELECT COUNT(*) = COUNT(embedding) FROM dev.chunk WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | `true` (all chunks have embedding) | ⏳ Pending |
|
||||
| Face traces assigned | 確認 face tracking 完成 | `SELECT COUNT(*) = COUNT(trace_id) FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | `true` (all faces have trace_id) | ⏳ Pending |
|
||||
| TMDb matching done | 確認 identity binding 完成 | `SELECT COUNT(identity_id) > 40000 FROM dev.face_detections WHERE file_uuid = 'c3c635e3641da80dde10cc555ffcdda5'` | `true` (> 40K identity bindings) | ⏳ Pending |
|
||||
| Qdrant synced | 確認 vector search ready | Check Qdrant points count | Points increased by ~5,000 | ⏳ Pending |
|
||||
|
||||
### Success Thresholds
|
||||
- **Face detections**: ~70,000 (169K frames / 3 sample interval)
|
||||
- **Identity bindings**: > 40,000 (60% match rate)
|
||||
- **Chunks with embeddings**: > 4,000 (all chunk types)
|
||||
- **Qdrant points**: > 90,000 (current) → > 95,000 (after Charade)
|
||||
|
||||
---
|
||||
|
||||
## Verification Script
|
||||
|
||||
```bash
|
||||
# Run after completion
|
||||
./scripts/verify_charade_pipeline.sh c3c635e3641da80dde10cc555ffcdda5
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- OCR processor failed, skipped
|
||||
- Face detection using SwiftFace (ANE accelerated)
|
||||
- TMDb matching using `scripts/match_faces_to_tmdb.py`
|
||||
- Expected total processing time: ~2-3 hours
|
||||
|
||||
---
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Changes |
|
||||
|---------|------|--------|---------|
|
||||
| 1.0 | 2026-05-27 | M5Max48 | Initial checklist |
|
||||
@@ -0,0 +1,49 @@
|
||||
# Session Summary: Identity Fixes + WP Proxy Fixes + Data Sync
|
||||
|
||||
**Date**: 2026-05-29
|
||||
**Author**: OpenCode
|
||||
**Status**: Completed (marcom team testing)
|
||||
|
||||
## What Was Done (Chronological)
|
||||
|
||||
### 1. Production Identity Fixes (3002)
|
||||
- **James Coburn restored** (id=18738, confirmed)
|
||||
- **Chantal Goya restored** (id=18737, confirmed)
|
||||
- **Louis Viret name/status fixed**
|
||||
- **Sequences fixed**: `identities_id_seq` (48→18734), `face_detections_id_seq` (141383→932413), `identity_history_id_seq`, `identity_bindings_id_seq`, `pre_chunks_id_seq`, `file_identities_id_seq`
|
||||
- **COALESCE fix** for `reference_data` NULL crash (`postgres_db.rs:3198`, `storage.rs:196`)
|
||||
|
||||
### 2. Bug Fixes
|
||||
- **DELETE identity**: Fixed binding order bug + removed `identity_confidence` column reference
|
||||
- **PATCH identity**: `jsonb_deep_merge` Nested JSON metadata
|
||||
- **mergeinto UNDO/REDO**: MongoDB deserialization fix (`Collection<Document>`)
|
||||
|
||||
### 3. Library Page Infinite Load Fix
|
||||
- **Root cause**: WP scan proxy (snippet 48) didn't forward query params → infinite pagination loop
|
||||
- **Fix**: Added `$request->get_query_params()` forwarding in scan proxy
|
||||
- **Safety**: Added `maxPages = 10` limit in JS pagination
|
||||
|
||||
### 4. Identity Data Sync (Dev → Production)
|
||||
- **Full replacement** of `public.identities`, `public.identity_bindings`, `public.identity_history` with dev data
|
||||
- James Coburn id: 18738 → 11
|
||||
- Bindings: 11,892 → 12,834 (+942)
|
||||
- **Verification**: 0 differences between schemas
|
||||
|
||||
### 5. Snippet 55 Filter
|
||||
- Added `.filter(f => f.is_registered)` to show only registered files on library page
|
||||
- Changed `status:'unregistered'` → `status: f.status || 'unregistered'`
|
||||
|
||||
## Key Decisions
|
||||
- Library page filter: default show registered files only
|
||||
- Identity sync: full DELETE + INSERT (not UPDATE) to ensure consistency
|
||||
- No user-defined metadata fields (starred/notes/role) preserved — matches dev exactly
|
||||
|
||||
## Handoff to Marcom
|
||||
- `/people/` page should show correct identity state
|
||||
- `/library/` page should show only registered files (4 currently)
|
||||
- Login required for `/library/` — redirects to `/login/` if not authenticated
|
||||
|
||||
## Files Modified
|
||||
- `snippet 48` (/scan WP proxy — query param forwarding)
|
||||
- `snippet 55` (library page JS — registered-only filter, maxPages safety)
|
||||
- `docs_v1.0/M4_workspace/2026-05-29_identity_sync_prod.md` (sync record)
|
||||
45
docs_v1.0/M4_workspace/2026-05-29_identity_sync_prod.md
Normal file
45
docs_v1.0/M4_workspace/2026-05-29_identity_sync_prod.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# Identity Data Sync: Dev (3003) → Production (3002)
|
||||
|
||||
**Date**: 2026-05-29
|
||||
**Author**: OpenCode
|
||||
**Status**: Completed
|
||||
|
||||
## Summary
|
||||
|
||||
Fully synced all identity-related tables from dev schema to public schema on PostgreSQL `momentry` database.
|
||||
|
||||
## What Was Done
|
||||
|
||||
1. **Identities table** (`public.identities`): Replaced with `dev.identities` (69 records, original ids preserved)
|
||||
2. **Identity_bindings** (`public.identity_bindings`): Replaced with `dev.identity_bindings` (12,834 records)
|
||||
3. **Identity_history** (`public.identity_history`): Replaced with `dev.identity_history` (10 records)
|
||||
4. **Sequences**: Updated `identities_id_seq`, `identity_bindings_id_seq`, `identity_history_id_seq` to match
|
||||
|
||||
### Key Changes
|
||||
- **James Coburn**: Changed from id=18738 → id=11 (dev's original id)
|
||||
- **Chantal Goya**: Changed from id=18737 → id=18736 (dev's id)
|
||||
- **Metadata**: Now matches dev schema — TMDB fields only, no user-defined fields (starred, notes, role, aliases, user_confirmed are removed as expected)
|
||||
- **Bindings**: Increased from 11,892 → 12,834 (+942 bindings)
|
||||
|
||||
### Not Changed
|
||||
- `face_detections` — identical in both schemas (135,521 records)
|
||||
- `pre_chunks` — large difference (public: 1.3M vs dev: 3.3M) but NOT related to identity
|
||||
- All other non-identity tables unchanged
|
||||
|
||||
## Verification
|
||||
|
||||
```sql
|
||||
-- Counts match
|
||||
identities: 69 = 69 ✅
|
||||
identity_bindings: 12,834 = 12,834 ✅
|
||||
identity_history: 10 = 10 ✅
|
||||
|
||||
-- No differences
|
||||
id/uuid mismatch: 0
|
||||
metadata/status/name diffs: 0
|
||||
```
|
||||
|
||||
## Files Referenced
|
||||
|
||||
- `AGENTS.md` — Development isolation rules
|
||||
- `/Users/accusys/momentry_core/docs_v1.0/M4_workspace/2026-05-29_wp_api_url_update.md` — Previous session handoff
|
||||
@@ -0,0 +1,66 @@
|
||||
# Library Page: Flash & Filter Fix
|
||||
|
||||
- **Date**: 2026-05-29
|
||||
- **Author**: OpenCode
|
||||
- **Status**: Completed
|
||||
|
||||
## Summary
|
||||
|
||||
Fixed three interconnected issues on the library page (`/library/`) where video cards would flash 3 times on load, and the enhanced filter panel (size slider, duration, registered/unregistered) stopped working after flash fixes.
|
||||
|
||||
## Root Causes & Fixes
|
||||
|
||||
### Issue 1: 3x Flash on Load
|
||||
|
||||
**Root Cause**: Multiple redundant render cycles triggered by:
|
||||
|
||||
1. **`delayedPeopleFilesLoader`** (snippet 55) schedules **6x** `setTimeout(startPeopleFilesLoader, ...)` — 3 from `DOMContentLoaded`, 3 from `window 'load'`. Each creates a `setInterval` that retries `initPeopleFilesMediaLoader` every 200ms.
|
||||
|
||||
2. **`loadMediaItems`** (snippet 55) resets `root.dataset.mediaLoaded = ''` after successful load, allowing the next pending `setTimeout(startPeopleFilesLoader, 500/1200)` to trigger a second/third `loadMediaItems` call → each calls `renderItems()` → re-renders all cards.
|
||||
|
||||
3. **`bootFilterOnly()`** (snippet 58) has no guard, runs 5+ times from multiple `setTimeout(start, 300/1000/2000)` and event listeners.
|
||||
|
||||
4. **`loadMediaMeta()`** (snippet 58) had no guard, ran on every `bootFilterOnly()` call → `debouncedApply()` → `applyEnhancedFilters()` reordered cards via DOM appendChild after async completion.
|
||||
|
||||
**Fix**:
|
||||
- Snippet 55: Removed `root.dataset.mediaLoaded = ''` reset in `loadMediaItems` success path. `mediaLoaded` stays `'1'` after first successful load, preventing re-triggers.
|
||||
- Snippet 58: Removed `debouncedApply()` from `loadMediaMeta()`.
|
||||
- Snippet 58: `setGridView()` already had a class-duplicate guard.
|
||||
- Snippet 58: `renderFinderRows()` already had a skip guard.
|
||||
|
||||
### Issue 2: Filter Not Working
|
||||
|
||||
**Root Cause**: `debouncedApply()` (which calls `applyEnhancedFilters()`) was only triggered automatically from `loadMediaMeta()`. After removing it (fix #1), the filter state was never applied to cards.
|
||||
|
||||
**Fix** (snippet 58):
|
||||
- Added `applyEnhancedFilters()` to the `ltPeopleFilesFiltered` event handler (after `renderFinderRows()`).
|
||||
- Removed the `setTimeout(0)` re-dispatch loop inside `applyEnhancedFilters` that would cause infinite event chaining. Replaced with simple `isApplyingFilter = false`.
|
||||
|
||||
### Issue 3: Infinite Event Loop
|
||||
|
||||
**Root Cause**: `applyEnhancedFilters()` used `setTimeout(0)` to set `isApplyingFilter = false` and re-dispatch `ltPeopleFilesFiltered`, which would call back into the handler → `applyEnhancedFilters()` → re-dispatch → loop.
|
||||
|
||||
**Fix**: Directly set `isApplyingFilter = false` at the end of `applyEnhancedFilters()`.
|
||||
|
||||
## Files Modified
|
||||
|
||||
| Snippet | ID | Changes |
|
||||
|---------|-----|---------|
|
||||
| LT-檔案管理-註冊 | 55 | Removed `mediaLoaded = ''` reset in `loadMediaItems` success |
|
||||
| LT-檔案管理-篩選功能 | 58 | Added `applyEnhancedFilters()` to `ltPeopleFilesFiltered` handler; removed `debouncedApply()` from `loadMediaMeta`; removed re-dispatch loop in `applyEnhancedFilters` |
|
||||
|
||||
## Verification
|
||||
|
||||
- ✅ No flashes on page load (single paint)
|
||||
- ✅ Filter panel works (registered/unregistered, search, sort, sliders)
|
||||
- ✅ Video streaming works (snippet 61, curl-based proxy)
|
||||
- ✅ `cargo clippy --lib` — N/A (WordPress PHP)
|
||||
- ✅ `cargo test --lib` — N/A
|
||||
|
||||
## Context Saved At
|
||||
|
||||
- User confirmed "沒有閃了" (no more flashes) and filter working
|
||||
- AGENTS.md development boundary: WordPress snippets #55, #58, #61 (Code Snippets plugin)
|
||||
- All edits done via direct MySQL UPDATE on `wp_snippets` table
|
||||
- Working directory: `/Users/accusys/momentry_core`
|
||||
- Latest context: user asked to save handoff before changing topic
|
||||
@@ -0,0 +1,27 @@
|
||||
# 2026-05-29: Mergeinto NULL face_id Fix
|
||||
|
||||
## Problem
|
||||
Production server (3002) returned `"error":"error occurred while decoding column 0: unexpected null; try decoding as an 'Option'"` when using mergeinto after clicking undo on a merge.
|
||||
|
||||
## Root Cause
|
||||
`src/api/identity_binding.rs:428` decodes `face_id` from `face_detections` as `String` (non-Option), but **135,521 records** in the production `face_detections` table have NULL `face_id`. When merging an identity whose face_detections include NULL face_ids, the SQLx decode panics.
|
||||
|
||||
## Fix
|
||||
- Changed `(String, Option<i32>)` → `(Option<String>, Option<i32>)` at line 428
|
||||
- Changed `face_id_list` to use `filter_map` instead of `map` to skip NULL face_ids
|
||||
- Changed `faces_count` to use `face_id_list.len()` instead of `face_ids.len()` (matching the actual transferred count)
|
||||
|
||||
## Files Changed
|
||||
- `momentry_core/src/api/identity_binding.rs` — 3 lines changed
|
||||
|
||||
## Verification
|
||||
- 234 library tests pass
|
||||
- `cargo fmt` passes
|
||||
- Production binary rebuilt (`target/release/momentry`)
|
||||
- Production server restarted on port 3002 (PID 92043)
|
||||
|
||||
## Identities with NULL face_id (20 identities, ~135k records)
|
||||
Audrey Hepburn (36k), Cary Grant (15k), Bernard Musson, Walter Matthau, Jacques Marin, George Kennedy, Michel Thomass, Antonio Passalia, etc. — all `type=people, status=confirmed`. These identities were likely imported from bulk face detection data without face_id generation.
|
||||
|
||||
## Data Note
|
||||
The NULL face_ids are a pre-existing data quality issue. The fix prevents crashes but doesn't clean up the NULL data. Faces with NULL face_id won't be tracked in undo history (they stay with the target after undo), but the bulk transfer (`WHERE identity_id = $1`) still works correctly.
|
||||
156
docs_v1.0/M4_workspace/2026-05-29_wp_api_url_update.md
Normal file
156
docs_v1.0/M4_workspace/2026-05-29_wp_api_url_update.md
Normal file
@@ -0,0 +1,156 @@
|
||||
---
|
||||
title: WordPress API URL Update - 2026-05-29
|
||||
version: "1.0"
|
||||
date: 2026-05-29
|
||||
author: OpenCode
|
||||
status: in_progress
|
||||
---
|
||||
|
||||
# WordPress API URL Update Session
|
||||
|
||||
## Scope
|
||||
|
||||
Update WordPress Code Snippets to point momentry_core API from `m5api.momentry.ddns.net` / `api.momentry.ddns.net` to `192.168.110.201:3002` (M5Max48 LAN IP).
|
||||
|
||||
## Summary
|
||||
|
||||
| Item | Status |
|
||||
|------|--------|
|
||||
| URL update | ✅ Done |
|
||||
| `/scan` route | ✅ Working (122 files) |
|
||||
| `/search-proxy?mode=people` | ✅ Working (3788 results) |
|
||||
| `/search-proxy?mode=semantic` | ❌ Returns 0 results (direct API works with 20 results) |
|
||||
| `/search-proxy?mode=keyword` | ❌ Returns 0 results (direct API works with 21 results) |
|
||||
| Snippet #66 PHP syntax fix | ✅ Fixed (removed `.` before array keys) |
|
||||
| Added `limit/page/page_size` | ✅ Added to search bodies |
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. URL Updates
|
||||
|
||||
Changed in multiple snippets:
|
||||
|
||||
| Old URL | New URL |
|
||||
|---------|---------|
|
||||
| `https://m5api.momentry.ddns.net` | `http://192.168.110.201:3002` |
|
||||
| `https://api.momentry.ddns.net` | `http://192.168.110.201:3002` |
|
||||
| `localhost:3002` | `192.168.110.201:3002` |
|
||||
|
||||
Affected snippets: #37, #43, #44, #48, #55, #59, #60, #61, #62, #63, #64, #66, #67
|
||||
|
||||
### 2. Snippet #66 Fixes
|
||||
|
||||
**Before (syntax error)**:
|
||||
```php
|
||||
$body = [
|
||||
. 'query' => $query, // ❌ Invalid PHP syntax
|
||||
. 'limit' => 20,
|
||||
];
|
||||
```
|
||||
|
||||
**After (fixed)**:
|
||||
```php
|
||||
// Semantic search body
|
||||
$body = [
|
||||
'query' => $query,
|
||||
'limit' => 20,
|
||||
'page' => 1,
|
||||
'page_size' => 20,
|
||||
];
|
||||
|
||||
// Universal search body
|
||||
$body = [
|
||||
'query' => $query,
|
||||
'limit' => 20,
|
||||
'page' => 1,
|
||||
'page_size' => 20,
|
||||
];
|
||||
```
|
||||
|
||||
Note: `file_uuid` was NOT added per user request.
|
||||
|
||||
## Backup Location
|
||||
|
||||
```
|
||||
/Users/accusys/momentry_core/backups/wp_snippets_20260529_181847/
|
||||
```
|
||||
|
||||
Contains:
|
||||
- `wp_snippets_full.sql` - Full backup before any changes
|
||||
- `snippets_with_old_url.sql` - Snippets containing old URLs
|
||||
- `snippets_43_44_48_54_before_api_fix.sql`
|
||||
- `snippet_66_before_syntax_fix.sql`
|
||||
|
||||
## Restore Command
|
||||
|
||||
```bash
|
||||
mysql -u wp_user -p'wp_password_123' wordpress < /Users/accusys/momentry_core/backups/wp_snippets_20260529_181847/wp_snippets_full.sql
|
||||
```
|
||||
|
||||
## Pending Issue: Semantic/Keyword Search Returns Empty
|
||||
|
||||
### Symptoms
|
||||
|
||||
- Direct API call to momentry_core: Returns results
|
||||
- WP proxy call: Returns `{"results": [], "total": 0}`
|
||||
|
||||
### Direct API Test (Works)
|
||||
|
||||
```bash
|
||||
curl -s http://192.168.110.201:3002/api/v1/search/smart \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69' \
|
||||
-d '{"query":"love","limit":20,"page":1,"page_size":20}'
|
||||
# Returns 20 results
|
||||
```
|
||||
|
||||
### WP Proxy Test (Empty)
|
||||
|
||||
```bash
|
||||
curl -sk 'https://m5wp.momentry.ddns.net/wp-json/momentry/v1/search-proxy?mode=semantic&query=love'
|
||||
# Returns {"query":"love","results":[],"page":1,"page_size":20,"strategy":"semantic_vector_search"}
|
||||
```
|
||||
|
||||
### Hypothesis
|
||||
|
||||
1. WordPress `wp_remote_request` may encode JSON differently
|
||||
2. Header mismatch between WordPress and curl
|
||||
3. PHP `$body` array construction issue
|
||||
|
||||
### Debug Steps Needed
|
||||
|
||||
1. Add debug output to snippet to return the exact `$body` JSON being sent
|
||||
2. Check WordPress HTTP request logs
|
||||
3. Compare raw request payload from WordPress vs curl
|
||||
|
||||
### Temporary Workaround
|
||||
|
||||
Use people search (works) or call momentry_core directly from frontend bypassing WP proxy.
|
||||
|
||||
## Environment Context
|
||||
|
||||
| Server | IP | Port | Role |
|
||||
|--------|-----|------|------|
|
||||
| M5Max48 | 192.168.110.201 | 3002 | momentry_core production |
|
||||
| M5Max48 | 192.168.110.201 | 3003 | momentry_core playground (dev) |
|
||||
| M4mini | 192.168.110.210 | 443 | Caddy reverse proxy for WordPress |
|
||||
| WordPress | - | - | MariaDB, PHP-FPM 8.5, Code Snippets plugin |
|
||||
|
||||
## API Key
|
||||
|
||||
```
|
||||
muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69
|
||||
```
|
||||
|
||||
## Database State
|
||||
|
||||
- PostgreSQL: `momentry` database
|
||||
- `public.chunk`: 294,531 rows (has embeddings)
|
||||
- `public.videos`: 4 registered files including Charade_YouTube_24fps.mp4
|
||||
- Qdrant: `momentry_rule1` collection with embeddings
|
||||
|
||||
## Version History
|
||||
|
||||
| Version | Date | Author | Change |
|
||||
|---------|------|--------|--------|
|
||||
| 1.0 | 2026-05-29 | OpenCode | Initial session record |
|
||||
166
docs_v1.0/M4_workspace/2026-06-01_hybrid_search_test_report.md
Normal file
166
docs_v1.0/M4_workspace/2026-06-01_hybrid_search_test_report.md
Normal file
@@ -0,0 +1,166 @@
|
||||
---
|
||||
title: Hybrid Search Deployment & Testing Report
|
||||
version: 1.0
|
||||
date: 2026-06-01
|
||||
author: OpenCode
|
||||
status: completed
|
||||
---
|
||||
|
||||
# Hybrid Search Deployment & Testing Report
|
||||
|
||||
## Summary
|
||||
|
||||
Successfully deployed hybrid search (semantic + keyword + identity with RRF) to production and tested with new video registration.
|
||||
|
||||
## Deployment
|
||||
|
||||
### Production (Port 3002)
|
||||
- **Strategy**: `hybrid_semantic+keyword+identity`
|
||||
- **RRF K**: 60
|
||||
- **Status**: ✅ Deployed and functional
|
||||
- **Commit**: Replaced entire smart_search implementation
|
||||
|
||||
### Identity Fixes
|
||||
- Deleted 36 Stranger identities (no file_uuid)
|
||||
- Deleted 6 test identities
|
||||
- Fixed 25 TMDb identities → file_uuid=Charade
|
||||
- Removed 6462 duplicate identity_bindings
|
||||
- Set file_uuid for 6347 bindings
|
||||
- Synced 49,881 face_detections (80% of Charade)
|
||||
|
||||
## New Video Registration
|
||||
|
||||
### Video Details
|
||||
- **Filename**: "ExaSAN PCIe series - Director Ou Yu-Zhi Shares His Experience.mp4"
|
||||
- **file_uuid**: `c4e33d129aa8f5512d1d28a92941b047`
|
||||
- **Duration**: 159.6 seconds
|
||||
- **Size**: 6.8MB
|
||||
- **Resolution**: 640x360
|
||||
- **FPS**: 22
|
||||
|
||||
### Processing
|
||||
- **Processors**: CUT (1 scene), ASRX (6 segments)
|
||||
- **Output**: `/Users/accusys/momentry/output/c4e33d129aa8f5512d1d28a92941b047.asrx.json`
|
||||
- **ASRX Content**: 6 Traditional Chinese speech segments (25-30 seconds each)
|
||||
|
||||
## Critical Bugs Fixed
|
||||
|
||||
### Bug 1: Case Mismatch
|
||||
- **Problem**: Job had `processors={ASRX}` (uppercase)
|
||||
- **Cause**: `ProcessorType::from_db_str()` only matches lowercase `"asrx"`
|
||||
- **Fix**: Changed to `processors={cut,asrx}` (lowercase)
|
||||
- **Impact**: Worker couldn't start processors
|
||||
|
||||
### Bug 2: Missing Dependency
|
||||
- **Problem**: ASRX depends on CUT being completed
|
||||
- **Cause**: User specified only ASRX processor
|
||||
- **Fix**: Added CUT to processors list
|
||||
- **Impact**: Worker deferred ASRX indefinitely
|
||||
|
||||
## Test Results
|
||||
|
||||
### Hybrid Search
|
||||
```bash
|
||||
curl -X POST "http://localhost:3003/api/v1/search/smart" \
|
||||
-d '{"query":"剪輯室 調光師"}'
|
||||
|
||||
# Results: Found Chinese text matches from existing videos
|
||||
# Strategy: hybrid_semantic+keyword+identity
|
||||
# RRF fusion working correctly
|
||||
```
|
||||
|
||||
### Search Coverage
|
||||
- ✅ Semantic search (Qdrant vectors)
|
||||
- ✅ Keyword search (BM25 PostgreSQL)
|
||||
- ✅ Identity search (face bindings)
|
||||
- ✅ RRF fusion (K=60)
|
||||
|
||||
## Design Discovery
|
||||
|
||||
### ASRX vs ASR Segments
|
||||
- **Issue**: Rule 1 expects ASR segments (processor_type='asr')
|
||||
- **Current**: We ran ASRX (processor_type='asrx')
|
||||
- **Result**: 0 sentence chunks created
|
||||
- **Impact**: New video ASRX data not searchable yet
|
||||
|
||||
### Root Cause
|
||||
Rule 1 `fetch_asr_segments()` queries `WHERE processor_type = 'asr'`, but ASRX segments are stored as `'asrx'`.
|
||||
|
||||
### Options
|
||||
1. Run ASR processor separately (ASRX includes ASR internally)
|
||||
2. Modify Rule 1 to use ASRX segments
|
||||
3. Keep current design (ASR + ASRX separate)
|
||||
|
||||
## Current Status
|
||||
|
||||
### Job Status
|
||||
- **monitor_jobs.job_id=46**: status=`running`
|
||||
- **completed_processors**: {cut, asrx}
|
||||
- **Why not completed**: Waiting for ingestion (no sentence chunks, no face traces)
|
||||
|
||||
### Ingestion Prerequisites
|
||||
Per `ingestion_complete()`:
|
||||
- ❌ Sentence chunks (Rule 1 returned 0)
|
||||
- ❌ Vector embeddings (no chunks to vectorize)
|
||||
- ✅ Cut chunks (1 scene)
|
||||
- ❌ Face traces (Face processor not run)
|
||||
|
||||
## Files Modified
|
||||
|
||||
### Production Code
|
||||
- `src/api/search.rs` - Hybrid search implementation
|
||||
- `src/core/db/postgres_db.rs` - Identity fixes (SQL)
|
||||
- `docs_v1.0/OPERATIONS/IDENTITY_SYSTEM_V4.0.md` - Updated
|
||||
|
||||
### Debug Code Added
|
||||
- `src/worker/job_worker.rs` - Added debug logs (removed after testing)
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate
|
||||
1. Document ASR vs ASRX distinction for Rule 1
|
||||
2. Consider running ASR + ASRX separately or modifying Rule 1
|
||||
3. Update worker docs about case sensitivity
|
||||
|
||||
### Future
|
||||
1. Test full processing pipeline (Face, YOLO, Pose)
|
||||
2. Verify ingestion_complete logic with all processors
|
||||
3. Add API endpoint for manual vectorization
|
||||
|
||||
## Metrics
|
||||
|
||||
### Identity Cleanup
|
||||
- Deleted: 42 identities
|
||||
- Fixed: 25 identities
|
||||
- Removed: 6462 duplicates
|
||||
- Synced: 49,881 faces
|
||||
|
||||
### Processing Time
|
||||
- CUT: ~2 seconds (1 scene)
|
||||
- ASRX: ~7 minutes (6 segments, 159s video)
|
||||
- Worker loop detection: ~2 minutes (case mismatch)
|
||||
|
||||
### Search Performance
|
||||
- Query time: <100ms
|
||||
- Results: 3-5 matches
|
||||
- Strategy: hybrid_semantic+keyword+identity
|
||||
- RRF K: 60
|
||||
|
||||
---
|
||||
|
||||
## Appendix: ASRX Output Sample
|
||||
|
||||
```json
|
||||
{
|
||||
"segments": [
|
||||
{
|
||||
"start": 0.323,
|
||||
"end": 25.496,
|
||||
"text": "正常來講我們是剪輯室用完之後再套片給我們的調光師...",
|
||||
"speaker_id": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Note**: speaker_id=null indicates diarization phase incomplete or single speaker detected.
|
||||
68
docs_v1.0/OPERATIONS/TMDb_Pipeline_Test_2026-05-17.md
Normal file
68
docs_v1.0/OPERATIONS/TMDb_Pipeline_Test_2026-05-17.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# TMDb Pipeline Test 2026-05-17
|
||||
|
||||
## Purpose
|
||||
Verify full TMDb enrichment pipeline: register → process → TMDb prefetch → probe → identity files → downloads.
|
||||
|
||||
## Environment
|
||||
- **Server**: playground (port 3003)
|
||||
- **Schema**: `dev`
|
||||
- **TMDB_API_KEY**: `e9cde52197f6f8df4d9db99da93db1fb`
|
||||
- **Build**: `momentry_playground` (debug, 0 errors)
|
||||
|
||||
## Pre-cleanup
|
||||
Unregistered old files + deleted output files:
|
||||
```bash
|
||||
POST /api/v1/unregister {"file_uuid": "3abeee81..."}
|
||||
POST /api/v1.unregister {"file_uuid": "23b1c872..."}
|
||||
```
|
||||
|
||||
## Step 1: Register
|
||||
|
||||
| File | UUID | Result |
|
||||
|------|------|--------|
|
||||
| Charade main | `bd80fec92b0b6963d177a2c55bf713e2` | ✅ Registered (already_exists due to content_hash match) |
|
||||
| Charade YouTube | `a6fb22eebefaef17e62af874997c5944` | ✅ Fresh registration |
|
||||
|
||||
Register phase completed: probe → CUT → scene classification.
|
||||
|
||||
## Step 2: Trigger Processing
|
||||
|
||||
```bash
|
||||
POST /api/v1/file/:uuid/process {}
|
||||
```
|
||||
|
||||
Jobs created:
|
||||
- Main: job_id=167, status=PENDING
|
||||
- YouTube: job_id=168, status=PENDING
|
||||
|
||||
Worker blocked by schema issue: `processor_results` missing `retry_count` column + `jsonb_set(text, text, jsonb)` signature mismatch. Fixed `retry_count` via ALTER TABLE.
|
||||
|
||||
## Step 3: TMDb Prefetch (requires pipeline completion first)
|
||||
|
||||
```bash
|
||||
POST /api/v1/agents/tmdb/prefetch
|
||||
```
|
||||
|
||||
## Step 4: TMDb Probe
|
||||
|
||||
```bash
|
||||
POST /api/v1/file/:uuid/tmdb-probe
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
1. `jsonb_set(jsonb, text, jsonb)` → should be `jsonb_set(jsonb, text[], jsonb)` — pre-existing worker bug
|
||||
2. `processor_results.retry_count` column missing — fixed via ALTER TABLE
|
||||
3. Worker requires running as separate process: `./target/debug/momentry_playground worker`
|
||||
|
||||
## Endpoint Changes in This Test
|
||||
| Endpoint | Status |
|
||||
|----------|--------|
|
||||
| `GET /api/v1/stats/ingest` | ❌ Removed (stats moved to files/scan + identities) |
|
||||
| `GET /api/v1/files/scan` | ➕ Added `total_chunks`, `searchable_chunks`, `pending_videos` |
|
||||
| `GET /api/v1/identities` | ➕ Added `total_identities`, `tmdb_identities`, `auto_identities` |
|
||||
| `POST /api/v1/agents/tmdb/prefetch` | ✅ Writes identity files directly |
|
||||
| `POST /api/v1/file/:uuid/tmdb-probe` | ✅ Upserts from disk identity files |
|
||||
| `GET /api/v1/identity/:uuid/json` | ✅ Download identity JSON |
|
||||
| `GET /api/v1/file/:uuid/json/:processor` | ✅ Download processor JSON |
|
||||
| `POST /api/v1/agents/identity/match-from-photo` | 🆕 New |
|
||||
| `POST /api/v1/agents/identity/match-from-trace` | 🆕 New |
|
||||
26
docs_v1.0/OPERATIONS/VERIFICATION_RUSTDESK_2026-05-22.md
Normal file
26
docs_v1.0/OPERATIONS/VERIFICATION_RUSTDESK_2026-05-22.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# RustDesk Verification Report
|
||||
|
||||
**Date**: 2026-05-22
|
||||
**Status**: Verified
|
||||
|
||||
## Source
|
||||
|
||||
| Item | Value |
|
||||
|------|-------|
|
||||
| Version | 1.4.6 |
|
||||
| Source | GitHub release |
|
||||
| Source repo | `admin/rustdesk` (Gitea) |
|
||||
| Binary | `~/Applications/RustDesk.app` |
|
||||
| Install | DMG → copy to ~/Applications |
|
||||
|
||||
## Verification
|
||||
|
||||
| Check | Result |
|
||||
|-------|--------|
|
||||
| DMG downloaded | ✅ 24MB, aarch64 |
|
||||
| .app bundle | ✅ 59MB |
|
||||
| Architecture | Apple Silicon (arm64) |
|
||||
|
||||
## Linked Documents
|
||||
|
||||
- `docs_v1.0/OPERATIONS/Services_Inventory.md`
|
||||
375
docs_v1.0/REFERENCE/FACE_BINDING_STATES.md
Normal file
375
docs_v1.0/REFERENCE/FACE_BINDING_STATES.md
Normal file
@@ -0,0 +1,375 @@
|
||||
# Face Binding States — Data Model Reference
|
||||
|
||||
**Version**: 1.0.0
|
||||
**Date**: 2026-05-25
|
||||
**Related**: `GET /api/v1/file/:file_uuid/faces`, `identities`, `strangers`, `face_detections`
|
||||
|
||||
---
|
||||
|
||||
## Glossary
|
||||
|
||||
| Term | Definition |
|
||||
|------|------------|
|
||||
| **face detection** | A single face bounding box detected in one video frame. Stored in `face_detections` table. |
|
||||
| **trace** | A sequence of face detections belonging to the same person across consecutive frames. Assigned by the face tracker. `trace_id` groups multiple face detections. |
|
||||
| **identity** | A known person with a name. Sources: TMDb (movie stars), user-defined (manual entry). Stored in `identities` table with `source='tmdb'` or `source='user_defined'`. |
|
||||
| **stranger** | An unknown person detected but not matched to any known identity. Created automatically for unmatched traces. Stored in `strangers` table. |
|
||||
| **binding** | The association between a face detection and either an identity or a stranger. Represented by `identity_id` or `stranger_id` FK in `face_detections`. |
|
||||
| **TMDb** | The Movie Database. Source of celebrity identity seeds with `face_embedding` for matching. |
|
||||
| **auto identity** | Legacy term for identities created from `face_clustered.json` analysis. Now migrated to `strangers` table as reference records. |
|
||||
| **dangling** | A face detection whose `identity_id` points to a deleted identity (e.g., auto identities removed during migration). |
|
||||
| **unbound** | A face detection with no binding at all — `identity_id IS NULL AND stranger_id IS NULL`. |
|
||||
| **PK** | Primary Key. A unique identifier for each row in a table. Example: `identities.id`, `strangers.id`, `face_detections.id`. |
|
||||
| **FK** | Foreign Key. A column that references the PK of another table, creating a relationship. Example: `face_detections.identity_id` → `identities.id`, `face_detections.stranger_id` → `strangers.id`. FK ensures referential integrity — a face cannot point to a non-existent identity. |
|
||||
|
||||
---
|
||||
|
||||
## Three Core Tables
|
||||
|
||||
### ER Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌─────────────────────┐
|
||||
│ identities │ │ strangers │
|
||||
│─────────────────────│ │─────────────────────│
|
||||
│ id (PK) │ │ id (PK) │
|
||||
│ uuid │ │ file_uuid │
|
||||
│ name │ │ trace_id │
|
||||
│ source │ │ metadata │
|
||||
│ tmdb_id │ │ created_at │
|
||||
│ face_embedding │ │ │
|
||||
│ metadata │ │ UNIQUE(file_uuid, │
|
||||
│ status │ │ trace_id) │
|
||||
│ ... │ │ │
|
||||
└─────────┬───────────┘ └─────────┬───────────┘
|
||||
│ │
|
||||
│ FK │ FK
|
||||
│ (ON DELETE SET NULL) │ (ON DELETE SET NULL)
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ face_detections │
|
||||
│─────────────────────────────────────────────────────│
|
||||
│ id (PK) │
|
||||
│ file_uuid — Video file identifier │
|
||||
│ frame_number — Frame where face was detected│
|
||||
│ timestamp_secs — Frame number / fps │
|
||||
│ trace_id — Face tracking ID │
|
||||
│ face_id — Format: `{frame}_{idx}` │
|
||||
│ identity_id (FK) — → identities.id │
|
||||
│ stranger_id (FK) — → strangers.id │
|
||||
│ x, y, width, height — Bounding box │
|
||||
│ confidence — Detection confidence (0–1) │
|
||||
│ embedding — Face embedding vector │
|
||||
│ metadata — JSON metadata │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Table Summary
|
||||
|
||||
| Table | Role | Record Count (public) | Primary Key |
|
||||
|-------|------|----------------------|-------------|
|
||||
| `identities` | Known persons (TMDb, user-defined) | 70 | `id`, `uuid` |
|
||||
| `strangers` | Unknown persons (unmatched traces) | 0–N per file | `id`, `(file_uuid, trace_id)` |
|
||||
| `face_detections` | Individual face detections | 70691 per file | `id` |
|
||||
|
||||
### Key Columns in `face_detections`
|
||||
|
||||
| Column | Type | Purpose |
|
||||
|--------|------|---------|
|
||||
| `identity_id` | INTEGER FK | Points to `identities.id` if matched to known person |
|
||||
| `stranger_id` | INTEGER FK | Points to `strangers.id` if unmatched trace |
|
||||
| `trace_id` | INTEGER | Groups faces belonging to same person across frames |
|
||||
|
||||
**Design Rule**: `identity_id` and `stranger_id` are mutually exclusive in normal operation. A face should have only one binding.
|
||||
|
||||
---
|
||||
|
||||
## Four Binding States
|
||||
|
||||
### State Definitions
|
||||
|
||||
| # | State | `binding` JSON | SQL Condition | Meaning |
|
||||
|---|-------|----------------|---------------|---------|
|
||||
| 1 | **identity** | `{"identity_id": 9, "identity_uuid": "...", "identity_name": "Audrey Hepburn"}` | `identity_id IN (SELECT id FROM identities)` | Face matched to a known TMDb or user-defined identity |
|
||||
| 2 | **stranger** | `{"stranger_id": 845, "metadata": {}}` | `stranger_id IS NOT NULL` | Face belongs to an unmatched trace (unknown person) |
|
||||
| 3 | **dangling** | `{"old_identity_id": 18052}` | `identity_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM identities WHERE id = face_detections.identity_id)` | Face was bound to an identity that has been deleted (orphaned reference) |
|
||||
| 4 | **unbound** | `null` | `identity_id IS NULL AND stranger_id IS NULL` | Face has no binding at all |
|
||||
|
||||
### State Detection Logic (Rust)
|
||||
|
||||
```rust
|
||||
let binding = if let (Some(iid), Some(iuuid), Some(iname)) =
|
||||
(identity_id, identity_uuid, identity_name)
|
||||
{
|
||||
FaceBinding::Identity { identity_id: iid, identity_uuid: iuuid, identity_name: iname }
|
||||
} else if let Some(sid) = stranger_id {
|
||||
FaceBinding::Stranger { stranger_id: sid, metadata: stranger_metadata }
|
||||
} else if let Some(iid) = identity_id {
|
||||
FaceBinding::Dangling { old_identity_id: iid }
|
||||
} else {
|
||||
FaceBinding::Unbound
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Lifecycle Flow
|
||||
|
||||
### Processing Pipeline
|
||||
|
||||
```
|
||||
Video Registration
|
||||
│
|
||||
▼
|
||||
Face Detection
|
||||
(face_detections created)
|
||||
│
|
||||
▼
|
||||
Face Tracking
|
||||
(trace_id assigned)
|
||||
│
|
||||
▼
|
||||
┌────────────────┐
|
||||
│ Identity Agent │
|
||||
│ Face Matching │
|
||||
└────────────────┘
|
||||
│
|
||||
┌─────────┴─────────┐
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────┐ ┌──────────┐
|
||||
│ MATCHED │ │ UNMATCHED│
|
||||
│ to TMDb │ │ trace │
|
||||
└─────┬────┘ └────┬─────┘
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
identity_id=X stranger_id=S
|
||||
│ │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ IDENTITY│ │ STRANGER│
|
||||
│ state │ │ state │
|
||||
└─────────┘ └─────────┘
|
||||
```
|
||||
|
||||
### User Operations
|
||||
|
||||
```
|
||||
┌─────────┐ bind ┌─────────┐
|
||||
│ STRANGER│──────────────▶│ IDENTITY│
|
||||
└────┬────┘ └────┬────┘
|
||||
│ │
|
||||
│ unbind │
|
||||
│ (if stranger_id │
|
||||
│ preserved) │
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────┐ ┌─────────┐
|
||||
│ STRANGER│◀─────────────│ UNBOUND │
|
||||
│ (rollback) │ (if no │
|
||||
└─────────┘ │ stranger)│
|
||||
└─────────┘
|
||||
```
|
||||
|
||||
### Migration Effect
|
||||
|
||||
```
|
||||
┌─────────────────────┐
|
||||
│ auto identities │
|
||||
│ (source='auto') │
|
||||
│ 943 records │
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
│ DELETE
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ face_detections │
|
||||
│ identity_id=18052 │
|
||||
│ (points to deleted) │
|
||||
└─────────┬───────────┘
|
||||
│
|
||||
│ Cleanup SQL
|
||||
│ SET identity_id=NULL
|
||||
│
|
||||
▼
|
||||
┌─────────────────────┐
|
||||
│ DANGLING → UNBOUND │
|
||||
│ 18641 faces cleaned │
|
||||
└─────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SQL Query Examples
|
||||
|
||||
### Count by State
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
COUNT(*) FILTER (WHERE identity_id IN (SELECT id FROM identities)) AS identity,
|
||||
COUNT(*) FILTER (WHERE stranger_id IS NOT NULL) AS stranger,
|
||||
COUNT(*) FILTER (WHERE identity_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM identities WHERE id = face_detections.identity_id)) AS dangling,
|
||||
COUNT(*) FILTER (WHERE identity_id IS NULL AND stranger_id IS NULL) AS unbound
|
||||
FROM face_detections
|
||||
WHERE file_uuid = 'aeed71342a899fe4b4c57b7d41bcb692';
|
||||
```
|
||||
|
||||
### Filter by State
|
||||
|
||||
```sql
|
||||
-- Identity
|
||||
SELECT * FROM face_detections fd
|
||||
WHERE fd.identity_id IN (SELECT id FROM identities);
|
||||
|
||||
-- Stranger
|
||||
SELECT * FROM face_detections WHERE stranger_id IS NOT NULL;
|
||||
|
||||
-- Dangling
|
||||
SELECT * FROM face_detections fd
|
||||
WHERE fd.identity_id IS NOT NULL
|
||||
AND NOT EXISTS (SELECT 1 FROM identities WHERE id = fd.identity_id);
|
||||
|
||||
-- Unbound
|
||||
SELECT * FROM face_detections
|
||||
WHERE identity_id IS NULL AND stranger_id IS NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## bind/unbind Behavior
|
||||
|
||||
### Current Implementation (stranger_id cleared on bind)
|
||||
|
||||
| Operation | SQL Effect | Result |
|
||||
|-----------|------------|--------|
|
||||
| `bind_face_to_identity` | `SET identity_id=X, stranger_id=NULL` | Stranger info lost |
|
||||
| `bind_trace_to_identity` | `SET identity_id=X, stranger_id=NULL` | Stranger info lost |
|
||||
| `merge_identity` | `SET identity_id=X, stranger_id=NULL` | Stranger info lost |
|
||||
| `unbind_face` | `SET identity_id=NULL` | Becomes unbound (cannot rollback) |
|
||||
|
||||
**Problem**: After bind → unbind, face becomes unbound instead of returning to stranger.
|
||||
|
||||
### Proposed Fix (preserve stranger_id on bind)
|
||||
|
||||
| Operation | SQL Effect | Result |
|
||||
|-----------|------------|--------|
|
||||
| `bind_face_to_identity` | `SET identity_id=X` (keep stranger_id) | Stranger info preserved |
|
||||
| `bind_trace_to_identity` | `SET identity_id=X` (keep stranger_id) | Stranger info preserved |
|
||||
| `merge_identity` | `SET identity_id=X` (keep stranger_id) | Stranger info preserved |
|
||||
| `unbind_face` | `SET identity_id=NULL` | Returns to stranger (if stranger_id exists) |
|
||||
|
||||
**Change Required**: Remove `, stranger_id = NULL` from three UPDATE queries in `identity_binding.rs`.
|
||||
|
||||
---
|
||||
|
||||
## Why Dangling Happens
|
||||
|
||||
Dangling occurs when `face_detections.identity_id` points to a deleted row in `identities` table.
|
||||
|
||||
### Root Cause
|
||||
|
||||
At the time of migration, `face_detections.identity_id` **had no FK constraint** to `identities.id`. This allowed:
|
||||
|
||||
1. `DELETE FROM identities WHERE source='auto'` succeeded without error
|
||||
2. `face_detections.identity_id` values remained unchanged (pointing to deleted IDs)
|
||||
3. No `ON DELETE SET NULL` triggered because no FK existed
|
||||
|
||||
### Prevention
|
||||
|
||||
With FK constraint in place:
|
||||
```sql
|
||||
ALTER TABLE face_detections
|
||||
ADD CONSTRAINT fk_face_detections_identity
|
||||
FOREIGN KEY (identity_id) REFERENCES identities(id) ON DELETE SET NULL;
|
||||
```
|
||||
|
||||
Deleting an identity would automatically set `face_detections.identity_id = NULL` (no dangling).
|
||||
|
||||
### Current Status
|
||||
|
||||
After migration cleanup:
|
||||
- Public schema: FK `fk_face_detections_stranger` exists (on `stranger_id`)
|
||||
- Public schema: FK `fk_face_detections_identity` **does not exist** (historical reason)
|
||||
- Dev schema: Same state as public
|
||||
|
||||
---
|
||||
|
||||
## API Endpoint
|
||||
|
||||
### `GET /api/v1/file/:file_uuid/faces`
|
||||
|
||||
**Purpose**: List all face detections in a file with binding state.
|
||||
|
||||
**Query Parameters**:
|
||||
|
||||
| Param | Type | Default | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `page` | int | 1 | Page number |
|
||||
| `page_size` | int | 50 | Items per page |
|
||||
| `binding` | string | — | Filter: `identity`, `stranger`, `dangling`, `unbound` |
|
||||
| `trace_id` | int | — | Filter by trace ID |
|
||||
| `min_confidence` | float | — | Minimum confidence (0.0–1.0) |
|
||||
| `start_frame` | int | — | Start frame (inclusive) |
|
||||
| `end_frame` | int | — | End frame (inclusive) |
|
||||
|
||||
**Response Example**:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"total": 52244,
|
||||
"page": 1,
|
||||
"page_size": 2,
|
||||
"data": [
|
||||
{
|
||||
"id": 661508,
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"frame_number": 21297,
|
||||
"timestamp_secs": 851.88,
|
||||
"face_id": "21297_0",
|
||||
"trace_id": 485,
|
||||
"bbox": { "x": 1072, "y": 390, "width": 56, "height": 56 },
|
||||
"confidence": 0.6114,
|
||||
"binding": {
|
||||
"identity_id": 9,
|
||||
"identity_uuid": "c3545906-c82d-4b66-aa1d-150bc02decce",
|
||||
"identity_name": "Audrey Hepburn"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration Reference
|
||||
|
||||
### `migrate_strangers_table.sql` (Summary)
|
||||
|
||||
1. `CREATE TABLE strangers`
|
||||
2. Insert unmatched traces → strangers
|
||||
3. Preserve auto identity metadata → strangers (NULL file_uuid/trace_id)
|
||||
4. Update `face_detections.stranger_id` → FK
|
||||
5. Add FK constraint
|
||||
6. Delete legacy `identity_bindings` for auto identities
|
||||
7. Delete `identities` where `source='auto'`
|
||||
8. Cleanup dangling `identity_id` (set to NULL)
|
||||
|
||||
### Cleanup SQL (Dangling)
|
||||
|
||||
```sql
|
||||
UPDATE face_detections fd
|
||||
SET identity_id = NULL
|
||||
WHERE NOT EXISTS (SELECT 1 FROM identities i WHERE i.id = fd.identity_id)
|
||||
AND fd.identity_id IS NOT NULL;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
*Updated: 2026-05-25*
|
||||
@@ -1,7 +1,7 @@
|
||||
# Release Notes — v1.0.0 (Production 3002)
|
||||
# Release Notes — v1.0.1 (Production 3002)
|
||||
|
||||
**Date**: 2026-05-13
|
||||
**Build**: `301da08`
|
||||
**Date**: 2026-05-25
|
||||
**Build**: `de88fd4e`
|
||||
**Deployed by**: M4
|
||||
|
||||
---
|
||||
@@ -101,6 +101,36 @@
|
||||
|
||||
---
|
||||
|
||||
## Changes Since v1.0.0 (301da08 → de88fd4e)
|
||||
|
||||
### Added
|
||||
- POST `/api/v1/agents/search` — Gemma4 function calling agent
|
||||
- POST `/api/v1/identity/:uuid/bind/trace` — trace-level identity binding
|
||||
- GET `/api/v1/file/:uuid/identities/:a/co-occur-with/:b` — co-occurrence
|
||||
- GET `/api/v1/file/:uuid/trace/:tid/thumbnail` — trace thumbnail
|
||||
- GET `/api/v1/file/:uuid/trace/:tid/representative-face` — representative face
|
||||
- PATCH `/api/v1/identity/:identity_uuid` — identity update + alias system
|
||||
- TKG extension: pose data + mutual gaze detection
|
||||
- `/health/consistency` — data consistency check
|
||||
- Config endpoints: cache toggle, auto-pipeline, watcher-auto-register
|
||||
- Representative frame auto-detection
|
||||
|
||||
### Fixed
|
||||
- System consistency: store_vector, search, worker trigger reliability
|
||||
- trigger_processing: remove fake QUEUED state, create monitor_job if missing
|
||||
- stranger_id set to NULL on bind/merge operations
|
||||
- frame_number type: i32→i64 to match BIGINT schema
|
||||
- Compilation errors: restored PipelineType enum, pipeline() method, constants
|
||||
|
||||
### Changed
|
||||
- Unified LLM config: CHAT_URL/VISION_URL/SUMMARY_URL with env var overrides
|
||||
- Port config centralized (8082 conflict resolved)
|
||||
- Resources API returns data (config+metadata)
|
||||
- server.rs split into modular route files
|
||||
- API Reference: 55 endpoints, removed phantom routes, fixed methods, renamed endpoints
|
||||
|
||||
---
|
||||
|
||||
## Known Notes
|
||||
|
||||
| Item | Note |
|
||||
|
||||
@@ -6,13 +6,135 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Compilation errors: restore PipelineType enum, pipeline() method, OLLAMA_URL/EMBED_URL/LLM_HEALTH_URL constants
|
||||
- frame_number type: changed i32→i64 to match BIGINT schema
|
||||
|
||||
## [1.0.1] - 2026-05-25
|
||||
|
||||
### Fixed
|
||||
- System consistency: store_vector, search, worker trigger reliability
|
||||
- trigger_processing: remove fake QUEUED state, create monitor_job if missing
|
||||
- stranger_id set to NULL on bind/merge operations
|
||||
- resource path cleanup
|
||||
|
||||
### Added
|
||||
- Gitea API token integration
|
||||
- n8n API key integration
|
||||
- API key caching with Moka
|
||||
- Rate limiting for API key validation
|
||||
- Constant-time hash comparison
|
||||
- OpenAPI documentation with utoipa
|
||||
- Frame/time pipeline split with output validation
|
||||
- `/api/v1/agents/search` — Gemma4 function calling agent
|
||||
- `/api/v1/identity/:uuid/bind/trace` — trace-level identity binding
|
||||
- `/api/v1/file/:uuid/identities/:a/co-occur-with/:b` — co-occurrence endpoint
|
||||
- `/api/v1/file/:uuid/trace/:tid/thumbnail` — trace thumbnail
|
||||
- `/api/v1/file/:uuid/trace/:tid/representative-face` — representative face
|
||||
- identity PATCH update, alias system, name UNIQUE removal
|
||||
- TKG extension: pose data + mutual gaze detection
|
||||
|
||||
### Changed
|
||||
- Unified LLM config: CHAT_URL/VISION_URL/SUMMARY_URL with env var overrides
|
||||
- Refactored server.rs into modular route files
|
||||
- Port config centralized (8082 conflict resolved)
|
||||
- Resources API returns data (config+metadata); register source code resource
|
||||
|
||||
## [1.0.0] - 2026-05-14
|
||||
|
||||
### Added
|
||||
- Release version v1.0.0 tag
|
||||
- Production binary at port 3002
|
||||
- Playground binary at port 3003
|
||||
- Dedicated Gitea sync pipeline
|
||||
|
||||
### Changed
|
||||
- Full V4.0 API reference published (55 endpoints)
|
||||
- Health APIs include build_timestamp + resources + pipeline
|
||||
- Qdrant refactored to use i64 for frame_number
|
||||
- Rust edition 2021, max_width=100, tab_spaces=4
|
||||
|
||||
## [0.6.0] - 2026-05-01
|
||||
|
||||
### Added
|
||||
- `/api/v1/agents/translate` — AI text translation
|
||||
- `/api/v1/agents/5w1h/batch` — batch analysis
|
||||
- `/api/v1/agents/identity/match-from-photo` — face matching from photo
|
||||
- `/api/v1/agents/identity/match-from-trace` — face matching from trace
|
||||
- `/api/v1/agents/suggest/merge` — merge suggestions
|
||||
- `/api/v1/agents/suggest/clustering` — re-clustering suggestions
|
||||
- `/api/v1/search/visual` endpoints (class, density, combination, stats)
|
||||
- `/api/v1/search/frames` — frame-level search
|
||||
- `/api/v1/faces/candidates` — unbound face gallery
|
||||
- `/api/v1/identities/search` — identity name search
|
||||
- `/api/v1/search/identity_text` — full-text identity-bound chunk search
|
||||
- `/health/consistency` — data consistency check
|
||||
- Config endpoints: auto-pipeline toggle, watcher-auto-register toggle
|
||||
- Cache toggle via `/api/v1/config/cache`
|
||||
|
||||
### Fixed
|
||||
- Various type mismatches in DB layer (i64/i32 alignment)
|
||||
- Trace debug mode bbox rendering
|
||||
- Interpolation for sparse face detections
|
||||
|
||||
## [0.5.0] - 2026-04-15
|
||||
|
||||
### Added
|
||||
- `/api/v1/file/:file_uuid/traces` — trace listing (replaces face_trace/sortby)
|
||||
- `/api/v1/file/:file_uuid/trace/:trace_id/faces` — trace detections with interpolation
|
||||
- `/api/v1/file/:file_uuid/video/bbox` — bbox overlay video
|
||||
- `/api/v1/file/:file_uuid/trace/:trace_id/video` — trace clip (normal/debug mode)
|
||||
- File probe endpoint (`/api/v1/file/:file_uuid/probe`)
|
||||
- Jobs monitoring (`/api/v1/jobs`)
|
||||
- Processing progress (`/api/v1/progress/:file_uuid`)
|
||||
- Chunk detail (`/api/v1/file/:file_uuid/chunk/:chunk_id`)
|
||||
- `/api/v1/unregister` — file unregistration by uuid or pattern
|
||||
|
||||
### Changed
|
||||
- face_trace/sortby renamed to `/api/v1/file/:file_uuid/traces`
|
||||
- Chunk/progress/jobs endpoints changed from GET to POST
|
||||
- Qdrant MongoDB dependency replaced with Rust-native driver
|
||||
- Redis upgraded to 1.0.x for performance
|
||||
|
||||
## [0.4.0] - 2026-04-01
|
||||
|
||||
### Added
|
||||
- Identity management: register, detail, delete, files, chunks
|
||||
- Identity binding: bind/unbind face → identity
|
||||
- Identity merging: mergeinto endpoint
|
||||
- Resource management: register, heartbeat, list
|
||||
- Docker health check integration
|
||||
- SFTPGo status endpoint
|
||||
- Pagination support for list endpoints
|
||||
|
||||
### Changed
|
||||
- Database schema: `person_identities` table removed
|
||||
- N:N relationship via `file_identities` table
|
||||
- Architecture shift: Face → Identity (two-layer direct binding)
|
||||
- Terminology: `video_uuid` → `file_uuid` across all APIs
|
||||
|
||||
## [0.3.0] - 2026-03-25
|
||||
|
||||
### Added
|
||||
- `/api/v1/search/universal` — BM25 keyword search
|
||||
- `/api/v1/search/smart` — semantic search (pgvector)
|
||||
- `/api/v1/files/register` — file registration
|
||||
- `/api/v1/files/lookup` — name conflict check
|
||||
- `/api/v1/files/scan` — directory scan
|
||||
- File listing and detail endpoints
|
||||
- POST `/api/v1/file/:file_uuid/process` — processing pipeline trigger
|
||||
- `/api/v1/auth/login` and `/api/v1/auth/logout`
|
||||
- `/health/detailed` — per-service health check
|
||||
|
||||
### Changed
|
||||
- Video processing pipeline refactored (ASR, OCR, YOLO, Face, Pose)
|
||||
- Centralized Python executor with timeout support
|
||||
- Logging unified under `logs/` directory
|
||||
- Startup scripts auto-build and kill old processes
|
||||
|
||||
## [0.2.0] - 2026-03-22
|
||||
|
||||
### Added
|
||||
- `/health` endpoint with status/build info
|
||||
- Video stream endpoint (`/api/v1/file/:file_uuid/video`)
|
||||
- Thumbnail extraction endpoint
|
||||
- FaceNet CoreML model integration
|
||||
- Basic API key authentication
|
||||
- PostgreSQL, MongoDB, Redis, Qdrant service initialization
|
||||
|
||||
## [0.1.0] - 2026-03-21
|
||||
|
||||
@@ -140,4 +262,11 @@ N8N_URL=https://n8n.momentry.ddns.net
|
||||
|
||||
| Version | Date | Description |
|
||||
|---------|------|-------------|
|
||||
| 1.0.1 | 2026-05-25 | Bug fixes, agent endpoints, identity alias system, TKG extension |
|
||||
| 1.0.0 | 2026-05-14 | Release v1.0.0, V4.0 API reference, production binary |
|
||||
| 0.6.0 | 2026-05-01 | Agent endpoints (translate, 5w1h, identity match), search visual endpoints |
|
||||
| 0.5.0 | 2026-04-15 | Traces API, media endpoints, jobs/progress/chunk, rename face_trace→traces |
|
||||
| 0.4.0 | 2026-04-01 | Identity management, resource API, video_uuid→file_uuid rename |
|
||||
| 0.3.0 | 2026-03-25 | Search (BM25/semantic), file registration, health check, auth |
|
||||
| 0.2.0 | 2026-03-22 | Health endpoint, video stream, thumbnail, FaceNet model, DB init |
|
||||
| 0.1.0 | 2026-03-21 | Initial release with API Key Management |
|
||||
|
||||
1
docs_v1.0/doc-wasm
Symbolic link
1
docs_v1.0/doc-wasm
Symbolic link
@@ -0,0 +1 @@
|
||||
doc_wasm
|
||||
@@ -38,7 +38,7 @@ a { color: #0066cc; }
|
||||
<h2>Search APIs</h2>
|
||||
<h3><code>POST /api/v1/search/smart</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<strong>Scope</strong>: global / file-level</p>
|
||||
<p>Semantic vector search using EmbeddingGemma-300m. Generates a query embedding via EmbeddingGemma (port 11436), then searches pgvector <code>story_parent</code> and <code>llm_parent</code> chunks by cosine similarity.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
@@ -53,13 +53,6 @@ a { color: #0066cc; }
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>File UUID to search within</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>query</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
@@ -67,6 +60,13 @@ a { color: #0066cc; }
|
||||
<td>Search text</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>File UUID to search within. If omitted, searches all files (global search)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
@@ -89,7 +89,14 @@ a { color: #0066cc; }
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<h4>Example (Global Search)</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/smart"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"query": "Audrey Hepburn"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Example (File-specific Search)</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/smart"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
@@ -101,6 +108,7 @@ a { color: #0066cc; }
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"results"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a6fb22eebefaef17e62af874997c5944"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"parent_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1087822</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"scene_order"</span><span class="p">:</span><span class="w"> </span><span class="mi">1087822</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">104438</span><span class="p">,</span>
|
||||
@@ -118,10 +126,26 @@ a { color: #0066cc; }
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>results[].file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>File UUID where result was found</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/search/universal</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<strong>Scope</strong>: global / file-level</p>
|
||||
<p>Multi-type BM25 full-text search across chunks, frames, and persons. Uses PostgreSQL <code>tsvector</code>.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
@@ -147,7 +171,7 @@ a { color: #0066cc; }
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Restrict to specific file</td>
|
||||
<td>Restrict to specific file. If omitted, searches all files (global search)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>types</code></td>
|
||||
@@ -179,7 +203,14 @@ a { color: #0066cc; }
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<h4>Example (Global Search)</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"query": "Cary Grant"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Example (File-specific Search)</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
@@ -191,6 +222,7 @@ a { color: #0066cc; }
|
||||
<span class="w"> </span><span class="nt">"results"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"chunk"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a6fb22eebefaef17e62af874997c5944"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"bd80fec92b0b6963d177a2c55bf713e2_2"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"story_child"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">5103</span><span class="p">,</span>
|
||||
@@ -199,6 +231,25 @@ a { color: #0066cc; }
|
||||
<span class="w"> </span><span class="nt">"end_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">213.64</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[213s-214s] Cary Grant: \"Olá!\""</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"frame"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a6fb22eebefaef17e62af874997c5944"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"frame_number"</span><span class="p">:</span><span class="w"> </span><span class="mi">5105</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"timestamp"</span><span class="p">:</span><span class="w"> </span><span class="mf">212.72</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.7</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"objects"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"ocr_texts"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"faces"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"person"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a6fb22eebefaef17e62af874997c5944"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">12</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"appearance_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">542</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.95</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span>
|
||||
@@ -206,16 +257,140 @@ a { color: #0066cc; }
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>results[].type</code></td>
|
||||
<td>string</td>
|
||||
<td>Result type: <code>chunk</code>, <code>frame</code>, or <code>person</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>File UUID where result was found (all types)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/search/frames</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<strong>Scope</strong>: global / file-level</p>
|
||||
<p>Search face detection frames by identity name or trace ID.</p>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/search/identity_text</code></h3>
|
||||
<h3><code>GET /api/v1/search/identity_text</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Search text chunks spoken by a specific identity.</p>
|
||||
<strong>Scope</strong>: global / file-level</p>
|
||||
<p>Search text chunks → find associated identities. Returns chunks where face detections overlap with text content.</p>
|
||||
<h4>Query Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>q</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>Search text (ILIKE match)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Restrict to specific file. If omitted, searches all files (global search)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>50</td>
|
||||
<td>Max results</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>1</td>
|
||||
<td>Page number</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>50</td>
|
||||
<td>Items per page</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example (Global Search)</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/identity_text?q=love"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Example (File-specific Search)</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/identity_text?file_uuid=</span><span class="nv">$FILE_UUID</span><span class="s2">&q=love"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"results"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a6fb22eebefaef17e62af874997c5944"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"llm_parent_..._256_270"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">256.256</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">270.228</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text_content"</span><span class="p">:</span><span class="w"> </span><span class="s2">"...lack of affection..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tmdb"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"trace_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">94</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>results[].file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>File UUID where chunk was found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].identity_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Identity ID if face was detected</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].trace_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Face trace ID</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3>Visual Search</h3>
|
||||
<table class="table">
|
||||
@@ -282,7 +457,7 @@ a { color: #0066cc; }
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<p><em>Updated: 2026-05-19 12:49:24</em></p>
|
||||
<p><em>Updated: 2026-05-27 — Added global search support for smart, universal, identity_text APIs</em></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -294,6 +294,7 @@ curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</s
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/file/:file_uuid/thumbnail</code></h3>
|
||||
<p>Extract a single frame from a video as JPEG image. Uses FFmpeg <code>select</code> filter.</p>
|
||||
<p>When <code>frame</code> is omitted, the system automatically selects the best representative frame using the TKG bridge (see algorithm below).</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<h4>Query Parameters</h4>
|
||||
@@ -311,9 +312,9 @@ curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</s
|
||||
<tr>
|
||||
<td><code>frame</code></td>
|
||||
<td>integer</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>Zero-based frame number to extract</td>
|
||||
<td>No</td>
|
||||
<td>auto-detect</td>
|
||||
<td>Zero-based frame number to extract. Omit for auto-detect.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>x</code></td>
|
||||
@@ -346,8 +347,23 @@ curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</s
|
||||
</tbody>
|
||||
</table>
|
||||
<p>All four crop params (<code>x</code>, <code>y</code>, <code>w</code>, <code>h</code>) must be provided together or omitted.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Extract frame 1000 (full frame)</span>
|
||||
<h4>Auto-detect Algorithm</h4>
|
||||
<p>When <code>frame</code> is not provided, the endpoint finds the best frame using this fallback chain:</p>
|
||||
<ol>
|
||||
<li><strong>Main characters</strong>: find the two identities with the most face detections (TMDb source)</li>
|
||||
<li><strong>Mutual gaze</strong>: if their face traces have a TKG <code>CO_OCCURS_WITH</code> edge with <code>mutual_gaze=true</code>, take <code>first_frame</code></li>
|
||||
<li><strong>Co-occurrence</strong>: fallback to the first frame where both identities appear together</li>
|
||||
<li><strong>Single identity</strong>: if only one main identity exists, take its highest-quality face frame</li>
|
||||
<li><strong>Any identity</strong>: fallback to the best-quality face frame across all identities</li>
|
||||
<li><strong>Error</strong>: if no face exists, returns <code>404</code></li>
|
||||
</ol>
|
||||
<p>The selected frame is constrained to the <strong>first half of the video</strong> (<code>total_frames / 2</code>).</p>
|
||||
<h4>Examples</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Auto-detect best representative frame</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/thumbnail"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-o<span class="w"> </span>representative.jpg
|
||||
|
||||
<span class="c1"># Extract frame 1000 (full frame)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/bd80fec92b0b6963d177a2c55bf713e2/thumbnail?frame=1000"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span>-o<span class="w"> </span>frame_1000.jpg
|
||||
|
||||
@@ -359,10 +375,185 @@ curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</s
|
||||
<h4>Response</h4>
|
||||
<ul>
|
||||
<li><strong>200</strong>: <code>image/jpeg</code> binary data</li>
|
||||
<li><strong>404</strong>: File not found</li>
|
||||
<li><strong>404</strong>: File not found / No faces in file (auto-detect)</li>
|
||||
<li><strong>500</strong>: FFmpeg error (e.g., frame number exceeds video duration)</li>
|
||||
</ul>
|
||||
<h3><code>GET /api/v1/file/:file_uuid/clip</code></h3>
|
||||
<h4>Technical Details</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Detail</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Backend</strong></td>
|
||||
<td>FFmpeg (<code>ffmpeg-full</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Filter</strong></td>
|
||||
<td><code>select=eq(n\,FRAME)</code> to select frame, optional <code>crop=W:H:X:Y</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Output</strong></td>
|
||||
<td>Single JPEG via pipe (<code>image2pipe</code>, <code>mjpeg</code> codec)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Cache</strong></td>
|
||||
<td><code>Cache-Control: public, max-age=86400</code> (24h)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Frame number</strong></td>
|
||||
<td>Zero-based (<code>frame=0</code> = first frame of video)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/file/:file_uuid/representative-frame</code></h3>
|
||||
<p>Return JSON metadata about the best representative frame for the video. Uses the same auto-detect algorithm as <code>GET /thumbnail</code> (without crop support).</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/representative-frame"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'.'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aeed71342a899fe4b4c57b7d41bcb692"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"frame_number"</span><span class="p">:</span><span class="w"> </span><span class="mi">38165</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"timestamp_secs"</span><span class="p">:</span><span class="w"> </span><span class="mf">1526.6</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"face_quality"</span><span class="p">:</span><span class="w"> </span><span class="mf">37292.97</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"main_identities"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c3545906-c82d-4b66-aa1d-150bc02decce"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"face_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">16456</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2b0ddefe-e2a9-4533-9308-b375594604d5"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"face_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">10643</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"traces"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"trace_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">919</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2b0ddefe-e2a9-4533-9308-b375594604d5"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"x"</span><span class="p">:</span><span class="w"> </span><span class="mi">764</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"y"</span><span class="p">:</span><span class="w"> </span><span class="mi">237</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"width"</span><span class="p">:</span><span class="w"> </span><span class="mi">199</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"height"</span><span class="p">:</span><span class="w"> </span><span class="mi">199</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.8426</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"trace_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">920</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c3545906-c82d-4b66-aa1d-150bc02decce"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"x"</span><span class="p">:</span><span class="w"> </span><span class="mi">1143</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"y"</span><span class="p">:</span><span class="w"> </span><span class="mi">312</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"width"</span><span class="p">:</span><span class="w"> </span><span class="mi">215</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"height"</span><span class="p">:</span><span class="w"> </span><span class="mi">215</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.8068</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response Fields</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>frame_number</code></td>
|
||||
<td>integer</td>
|
||||
<td>Selected representative frame number (primary coordinate)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>timestamp_secs</code></td>
|
||||
<td>float</td>
|
||||
<td>Time in seconds (derived from <code>frame_number / fps</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>face_quality</code></td>
|
||||
<td>float</td>
|
||||
<td>Quality score <code>area × confidence</code> of the best face at this frame</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>main_identities</code></td>
|
||||
<td>array</td>
|
||||
<td>Top 2 most frequent TMDb identities in the file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>main_identities[].name</code></td>
|
||||
<td>string</td>
|
||||
<td>Identity display name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>main_identities[].face_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total face detections count</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces</code></td>
|
||||
<td>array</td>
|
||||
<td>All face traces present at the selected frame</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces[].trace_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Face trace ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces[].identity_uuid</code></td>
|
||||
<td>string or null</td>
|
||||
<td>Matched identity UUID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces[].name</code></td>
|
||||
<td>string or null</td>
|
||||
<td>Identity name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces[].x, y, width, height</code></td>
|
||||
<td>integer</td>
|
||||
<td>Bounding box coordinates</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>traces[].confidence</code></td>
|
||||
<td>float</td>
|
||||
<td>Detection confidence (0.0–1.0)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Error Responses</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>404</code></td>
|
||||
<td>File not found / No faces in file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>500</code></td>
|
||||
<td>Database error</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Extract a video clip (time range) as MPEG-TS stream. Uses FFmpeg <code>-ss</code> fast seek.</p>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
|
||||
@@ -209,7 +209,191 @@ a { color: #0066cc; }
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<p><em>Updated: 2026-05-19 12:49:24</em></p>
|
||||
<h2>POST /api/v1/agents/search</h2>
|
||||
<p>Conversational search assistant. Uses Gemma4 function calling to automatically decide which tools to call based on the user's natural language query. Supports multi-turn conversation.</p>
|
||||
<h3>Request</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn 和 Cary Grant 第一次同框在哪個 frame?"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"conversation_id"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>query</code></td>
|
||||
<td>string</td>
|
||||
<td>✅</td>
|
||||
<td>自然語言查詢</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>conversation_id</code></td>
|
||||
<td>string</td>
|
||||
<td>❌</td>
|
||||
<td>延續對話時傳入;新對話不傳</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>❌</td>
|
||||
<td>Portal 有選中檔案時可指定</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Response</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"conversation_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"conv_abc123"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"answer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"在 Charade (1963) 中,Audrey Hepburn 與 Cary Grant 第一次同框在第 38619 幀(約 1544.76 秒)。"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"need_input"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"sources"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"tool"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tkg_query"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"result"</span><span class="p">:</span><span class="w"> </span><span class="s2">"{\"first_cooccurrence\":{\"frame\":38619,\"timestamp_secs\":1544.76}}"</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>conversation_id</code></td>
|
||||
<td>string</td>
|
||||
<td>後續對話需要傳入此 ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>answer</code></td>
|
||||
<td>string</td>
|
||||
<td>Agent 的自然語言回答(或反問)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>need_input</code></td>
|
||||
<td>boolean</td>
|
||||
<td><code>true</code> 表示 agent 需要更多資訊才能回答</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>suggestions</code></td>
|
||||
<td>string[]</td>
|
||||
<td>建議用戶提供的線索(當 <code>need_input=true</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sources</code></td>
|
||||
<td>array</td>
|
||||
<td>引用的工具執行結果</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Conversation Flow</h3>
|
||||
<div class="codehilite"><pre><span></span><code>Round 1: POST /agents/search { query: "我想看男女主角同框" }
|
||||
→ need_input: true, suggestions: ["片名", "演員", "年代"]
|
||||
→ answer: "請問是哪部電影?請提供更多線索"
|
||||
|
||||
Round 2: POST /agents/search { query: "奧黛麗赫本", conversation_id: "..." }
|
||||
→ need_input: false
|
||||
→ answer: "找到 Charade (1963),Audrey Hepburn 和 Cary Grant..."
|
||||
</code></pre></div>
|
||||
|
||||
<h3>Available Tools</h3>
|
||||
<p>Agent 內部使用 Gemma4 function calling 自動調用以下工具:</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tool</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>find_file</code></td>
|
||||
<td>透過片名/演員/年份關鍵字搜尋影片,回傳 file_uuid + has_data 狀態</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>list_files</code></td>
|
||||
<td>列出近期註冊的影片</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>tkg_query</code></td>
|
||||
<td>查詢人物互動資料(7 種子類型:top_identities、first_cooccurrence、identity_details、mutual_gaze、interaction_network、identity_traces、file_info)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>smart_search</code></td>
|
||||
<td>文字內容 ILIKE 搜尋 chunk(可指定 file_uuid 限制範圍)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>get_identity_detail</code></td>
|
||||
<td>查詢單一身份的詳細資料(角色、TMDb 資訊)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>get_file_info</code></td>
|
||||
<td>查詢影片基本資訊(片長、解析度)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>get_representative_frame</code></td>
|
||||
<td>查詢影片最具代表性的 frame 資訊</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Design Principles</h3>
|
||||
<ul>
|
||||
<li><strong>用戶不需要知道 file_uuid</strong> — Agent 會自動用 <code>find_file</code> 搜尋或反問</li>
|
||||
<li><strong>不推薦無資料的影片</strong> — <code>has_data=false</code> 的影片不會被推薦給用戶</li>
|
||||
<li><strong>多輪對話</strong> — 透過 <code>conversation_id</code> 延續上下文,agent 會記得之前的交流</li>
|
||||
<li><strong>並行工具呼叫</strong> — Gemma4 可以一次呼叫多個工具再綜合回答</li>
|
||||
</ul>
|
||||
<h3>Model</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Detail</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>LLM</strong></td>
|
||||
<td>Gemma4 26B (Q5_K_M)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Engine</strong></td>
|
||||
<td>llama.cpp at <code>localhost:8082</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Endpoint</strong></td>
|
||||
<td><code>/v1/chat/completions</code> (OpenAI-compatible)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Temperature</strong></td>
|
||||
<td>0.1</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Max rounds</strong></td>
|
||||
<td>5 (tool call iterations)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Conversation TTL</strong></td>
|
||||
<td>30 minutes</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<p><em>Updated: 2026-05-22</em></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
470
docs_v1.0/doc_developer/14_identity_history.html
Normal file
470
docs_v1.0/doc_developer/14_identity_history.html
Normal file
@@ -0,0 +1,470 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>14 Identity History - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
.topbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; }
|
||||
.logout-btn { font-size: 13px; color: #999; text-decoration: none; }
|
||||
.logout-btn:hover { color: #cc0000; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="topbar">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{method:'POST'}).then(()=>window.location.reload());return false">Logout</a>
|
||||
</div>
|
||||
<!-- module: identity_history -->
|
||||
<!-- description: Identity PATCH operation history, undo, and redo -->
|
||||
<!-- depends: 01_auth, 07_identity -->
|
||||
|
||||
<h2>Identity Operation History</h2>
|
||||
<p>Every <code>PATCH /api/v1/identity/:identity_uuid</code> automatically records a before/after snapshot in the <code>identity_history</code> table. Use undo/redo to revert or reapply changes, and history to inspect the operation log.</p>
|
||||
<h3>History System Overview</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Storage</td>
|
||||
<td>PostgreSQL <code>identity_history</code> table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Snapshot</td>
|
||||
<td>Full identity record (all fields) before and after each PATCH</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Max records</td>
|
||||
<td>256 per identity (oldest auto-deleted when limit exceeded)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Undo steps</td>
|
||||
<td>Unlimited (no expiry, no step limit)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Redo stack</td>
|
||||
<td>Cleared on new PATCH (<code>is_undone=true</code> records are deleted)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Stack Model</h4>
|
||||
<div class="codehilite"><pre><span></span><code>PATCH 1 → PATCH 2 → PATCH 3 (undo stack, is_undone=false)
|
||||
↓ undo
|
||||
PATCH 1 → PATCH 2 (undo stack)
|
||||
PATCH 3 (redo stack, is_undone=true)
|
||||
↓ redo
|
||||
PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
|
||||
</code></pre></div>
|
||||
|
||||
<p>A new PATCH after undo clears the redo stack (PATCH 3 is lost).</p>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/identity/:identity_uuid/undo</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Undo the most recent PATCH operations. Restores the identity's <code>before_snapshot</code> and marks the history records as undone.</p>
|
||||
<h4>Request (JSON)</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>steps</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td><code>1</code></td>
|
||||
<td>Number of undo steps to apply (max records undone in one call)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Behavior</h4>
|
||||
<ul>
|
||||
<li>Queries <code>is_undone=false</code> records, ordered by <code>created_at DESC</code></li>
|
||||
<li>Restores <code>name</code>, <code>identity_type</code>, <code>source</code>, <code>status</code>, <code>metadata</code>, <code>tmdb_id</code>, <code>tmdb_profile</code> from the last record's <code>before_snapshot</code></li>
|
||||
<li>Marks the undone records as <code>is_undone=true</code> with <code>undone_at=NOW()</code></li>
|
||||
<li>Syncs <code>identity.json</code> to disk</li>
|
||||
<li>Updates <code>_index.json</code> if name changed</li>
|
||||
</ul>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/undo"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"steps": 1}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"undone_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"current_state"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"people"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tmdb"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"confirmed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{},</span>
|
||||
<span class="w"> </span><span class="nt">"tmdb_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">112</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"tmdb_profile"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>undone_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Number of history records undone</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>current_state</code></td>
|
||||
<td>object</td>
|
||||
<td>Full identity state after undo</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Error Responses</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>400</code></td>
|
||||
<td>No undo operations available</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>404</code></td>
|
||||
<td>Identity not found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>500</code></td>
|
||||
<td>Database error</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>POST /api/v1/identity/:identity_uuid/redo</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Redo previously undone PATCH operations. Restores the identity's <code>after_snapshot</code> and marks the history records as no longer undone.</p>
|
||||
<h4>Request (JSON)</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>steps</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td><code>1</code></td>
|
||||
<td>Number of redo steps to apply</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Behavior</h4>
|
||||
<ul>
|
||||
<li>Queries <code>is_undone=true</code> records, ordered by <code>created_at DESC</code></li>
|
||||
<li>Restores all identity fields from the last record's <code>after_snapshot</code></li>
|
||||
<li>Marks records as <code>is_undone=false</code> with <code>undone_at=NULL</code></li>
|
||||
<li>Syncs <code>identity.json</code> to disk</li>
|
||||
<li>Updates <code>_index.json</code> if name changed</li>
|
||||
</ul>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/redo"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"steps": 1}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"redone_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"current_state"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">9</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"John Smith"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"people"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tmdb"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"confirmed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nt">"aliases"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="err">...</span><span class="p">]</span><span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"tmdb_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">112</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"tmdb_profile"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>redone_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Number of history records redone</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>current_state</code></td>
|
||||
<td>object</td>
|
||||
<td>Full identity state after redo</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Error Responses</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>400</code></td>
|
||||
<td>No redo operations available</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>404</code></td>
|
||||
<td>Identity not found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>500</code></td>
|
||||
<td>Database error</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/identity/:identity_uuid/history</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: identity-level</p>
|
||||
<p>Query the operation history for an identity. Returns paginated records with undo/redo stack counts.</p>
|
||||
<h4>Query Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td><code>1</code></td>
|
||||
<td>Page number (1-indexed)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td><code>20</code></td>
|
||||
<td>Items per page (max 100)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identity_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a9a901056d6b46ff92da0c3c1a57dff4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"undo_stack_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"redo_stack_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"results"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"history_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"operation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"update"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"is_undone"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-27T12:00:00Z"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"undone_at"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"history_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">41</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"operation"</span><span class="p">:</span><span class="w"> </span><span class="s2">"update"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"is_undone"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-27T11:30:00Z"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"undone_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-27T13:00:00Z"</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>total</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total history records for this identity</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>undo_stack_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Records available for undo (<code>is_undone=false</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>redo_stack_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Records available for redo (<code>is_undone=true</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].history_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>History record ID</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].operation</code></td>
|
||||
<td>string</td>
|
||||
<td>Operation type (<code>"update"</code> for PATCH)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].is_undone</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Whether the operation has been undone</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].created_at</code></td>
|
||||
<td>string</td>
|
||||
<td>When the PATCH was applied</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>results[].undone_at</code></td>
|
||||
<td>string</td>
|
||||
<td>When the undo occurred (null if not undone)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="nv">$IDENTITY_UUID</span><span class="s2">/history?page=1&limit=10"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Error Responses</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>404</code></td>
|
||||
<td>Identity not found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>500</code></td>
|
||||
<td>Database error</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3>Comparison: PATCH Undo vs Merge Undo</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Aspect</th>
|
||||
<th>PATCH Undo/Redo</th>
|
||||
<th>Merge Undo</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Storage</td>
|
||||
<td>PostgreSQL <code>identity_history</code></td>
|
||||
<td>MongoDB <code>identity_merge_history</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Trigger</td>
|
||||
<td>Every PATCH</td>
|
||||
<td>Every mergeinto with <code>keep_history=true</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Undo deadline</td>
|
||||
<td>None (unlimited)</td>
|
||||
<td>24 hours</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Redo support</td>
|
||||
<td>Yes</td>
|
||||
<td>No</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Step undo</td>
|
||||
<td>Yes (<code>steps</code> param)</td>
|
||||
<td>No (full undo only)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Max records</td>
|
||||
<td>256 per identity</td>
|
||||
<td>Unlimited</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<p><em>Updated: 2026-05-28</em></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -29,7 +29,7 @@ a:hover td { background: #f8f8f8; border-radius: 4px; }
|
||||
<a class="logout-btn" href="#" onclick="fetch('/api/v1/auth/logout',{method:'POST'}).then(()=>window.location.reload());return false">Logout</a>
|
||||
</div>
|
||||
<p class="subtitle">API 參考手冊 — 登入後可瀏覽各模組文件</p>
|
||||
<table><tr onclick="window.location='11_error_codes.html'" style="cursor:pointer"><td class="cn">錯誤碼</td><td class="en">Error Codes</td></tr></table>
|
||||
<table><tr onclick="window.location='11_error_codes.html'" style="cursor:pointer"><td class="cn">錯誤碼</td><td class="en">Error Codes</td></tr><tr onclick="window.location='14_identity_history.html'" style="cursor:pointer"><td class="cn">14 Identity History</td><td class="en"></td></tr></table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
358
docs_v1.0/doc_user/API_ACCESS.html
Normal file
358
docs_v1.0/doc_user/API_ACCESS.html
Normal file
@@ -0,0 +1,358 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Api Access - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<h1>Momentry Core API 存取指南</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>項目</th>
|
||||
<th>內容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>版本</td>
|
||||
<td>V1.3</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>日期</td>
|
||||
<td>2026-03-25</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>用途</td>
|
||||
<td>API 存取方式、端點與整合指南</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>版本歷史</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>版本</th>
|
||||
<th>日期</th>
|
||||
<th>目的</th>
|
||||
<th>操作人</th>
|
||||
<th>工具/模型</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>V1.3</td>
|
||||
<td>2026-03-25</td>
|
||||
<td>更新: n8n 搜尋回傳 <code>file_path</code> 取代 <code>media_url</code>,新增 API Key 驗證說明</td>
|
||||
<td>OpenCode</td>
|
||||
<td>deepseek-reasoner</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>V1.2</td>
|
||||
<td>2026-03-24</td>
|
||||
<td>更新網址與服務列表</td>
|
||||
<td>Warren</td>
|
||||
<td>OpenCode / MiniMax M2.5</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>V1.1</td>
|
||||
<td>2026-03-23</td>
|
||||
<td>初始版本</td>
|
||||
<td>Warren</td>
|
||||
<td>OpenCode / MiniMax M2.5</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>基本網址</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>環境</th>
|
||||
<th>URL</th>
|
||||
<th>說明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>本地開發</strong></td>
|
||||
<td><code>http://localhost:3002</code></td>
|
||||
<td>直接訪問 API,繞過反向代理</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>外部訪問</strong></td>
|
||||
<td><code>https://m5api.momentry.ddns.net</code></td>
|
||||
<td>通過 Caddy 反向代理訪問,需網路可達</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>何時使用哪個 URL</h3>
|
||||
<p><strong>使用 <code>localhost:3002</code>:</strong>
|
||||
- 開發/測試環境
|
||||
- 直接在伺服器上操作
|
||||
- 當反向代理有問題時</p>
|
||||
<p><strong>使用 <code>m5api.momentry.ddns.net</code>:</strong>
|
||||
- n8n workflow 中呼叫 API
|
||||
- 外部系統整合
|
||||
- 生產環境</p>
|
||||
<h2>認證</h2>
|
||||
<p>所有 <code>/api/v1/*</code> 端點(除了健康檢查 <code>/health</code> 與 <code>/health/detailed</code>)都需要 API Key 認證。</p>
|
||||
<p>請在請求標頭中加入:</p>
|
||||
<div class="codehilite"><pre><span></span><code>X-API-Key: YOUR_API_KEY
|
||||
</code></pre></div>
|
||||
|
||||
<p><strong>目前示範使用的 API Key</strong>: <code>demo_api_key_12345</code></p>
|
||||
<blockquote>
|
||||
<p><strong>注意</strong>: 正式環境請使用安全的 API Key 管理機制,避免在客戶端暴露 API Key。</p>
|
||||
</blockquote>
|
||||
<hr />
|
||||
<h2>影片搜尋 API</h2>
|
||||
<h3>語意搜尋</h3>
|
||||
<p><strong>端點:</strong> <code>POST /api/v1/search</code></p>
|
||||
<p><strong>請求:</strong></p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"charade"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"limit"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a1b10138a6bbb0cd"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>欄位</th>
|
||||
<th>類型</th>
|
||||
<th>必填</th>
|
||||
<th>說明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>query</code></td>
|
||||
<td>字串</td>
|
||||
<td>是</td>
|
||||
<td>搜尋文字</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>整數</td>
|
||||
<td>否</td>
|
||||
<td>最大回傳結果數(預設 10)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>uuid</code></td>
|
||||
<td>字串</td>
|
||||
<td>否</td>
|
||||
<td>依影片 UUID 過濾</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p><strong>回應:</strong></p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"results"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a1b10138a6bbb0cd"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sentence_0006"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sentence"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">48.8</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_time"</span><span class="p">:</span><span class="w"> </span><span class="mf">55.44</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"fun plot twists, Woody Dialog and charming performances..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.526</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"charade"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>n8n 整合搜尋</h3>
|
||||
<p><strong>端點:</strong> <code>POST /api/v1/n8n/search</code></p>
|
||||
<p><strong>請求:</strong></p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"charade"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"limit"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p><strong>回應:</strong></p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"query"</span><span class="p">:</span><span class="w"> </span><span class="s2">"charade"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"count"</span><span class="p">:</span><span class="w"> </span><span class="mi">5</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"hits"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sentence_0006"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"vid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a1b10138a6bbb0cd"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start"</span><span class="p">:</span><span class="w"> </span><span class="mf">48.8</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end"</span><span class="p">:</span><span class="w"> </span><span class="mf">55.44</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Chunk sentence_0006"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"fun plot twists..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.526</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov"</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<blockquote>
|
||||
<p><strong>注意</strong>: API 現在返回 <code>file_path</code>(檔案系統路徑)而非 <code>media_url</code>(網頁 URL)。如需在網頁中播放影片,請將檔案路徑轉換為可訪問的 URL(例如透過 SFTPGo 分享連結)。</p>
|
||||
</blockquote>
|
||||
<hr />
|
||||
<h2>影片管理 API</h2>
|
||||
<h3>列出所有影片</h3>
|
||||
<p><strong>端點:</strong> <code>GET /api/v1/videos</code></p>
|
||||
<h3>查詢影片資訊</h3>
|
||||
<p><strong>端點:</strong> <code>GET /api/v1/lookup?uuid={uuid}</code> 或 <code>GET /api/v1/lookup?path={path}</code></p>
|
||||
<h3>取得處理進度</h3>
|
||||
<p><strong>端點:</strong> <code>GET /api/v1/progress/{uuid}</code></p>
|
||||
<hr />
|
||||
<h2>區塊資料結構</h2>
|
||||
<p>每個搜尋結果包含影片播放的時間資訊:</p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>欄位</th>
|
||||
<th>說明</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>uuid</code></td>
|
||||
<td>影片識別碼</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>chunk_id</code></td>
|
||||
<td>區塊唯一識別碼</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>chunk_type</code></td>
|
||||
<td>類型:<code>sentence</code>、<code>cut</code>、<code>time_based</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>start_time</code></td>
|
||||
<td>開始時間(秒)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>end_time</code></td>
|
||||
<td>結束時間(秒)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>text</code></td>
|
||||
<td>語音轉文字內容</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>score</code></td>
|
||||
<td>相關性分數(0-1)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>整合範例</h2>
|
||||
<h3>JavaScript/fetch</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="kd">const</span><span class="w"> </span><span class="nx">response</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">fetch</span><span class="p">(</span><span class="s1">'http://localhost:3002/api/v1/search'</span><span class="p">,</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nx">method</span><span class="o">:</span><span class="w"> </span><span class="s1">'POST'</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nx">headers</span><span class="o">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span>
|
||||
<span class="w"> </span><span class="s1">'Content-Type'</span><span class="o">:</span><span class="w"> </span><span class="s1">'application/json'</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s1">'X-API-Key'</span><span class="o">:</span><span class="w"> </span><span class="s1">'YOUR_API_KEY'</span><span class="w"> </span><span class="c1">// 替換為實際的 API Key</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nx">body</span><span class="o">:</span><span class="w"> </span><span class="nb">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">({</span><span class="w"> </span><span class="nx">query</span><span class="o">:</span><span class="w"> </span><span class="s1">'charade'</span><span class="p">,</span><span class="w"> </span><span class="nx">limit</span><span class="o">:</span><span class="w"> </span><span class="mf">5</span><span class="w"> </span><span class="p">})</span>
|
||||
<span class="p">});</span>
|
||||
<span class="kd">const</span><span class="w"> </span><span class="nx">data</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">response</span><span class="p">.</span><span class="nx">json</span><span class="p">();</span>
|
||||
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">data</span><span class="p">.</span><span class="nx">results</span><span class="p">);</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3>PHP/cURL</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="x">$ch = curl_init('http://localhost:3002/api/v1/search');</span>
|
||||
<span class="x">curl_setopt($ch, CURLOPT_POST, true);</span>
|
||||
<span class="x">curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([</span>
|
||||
<span class="x"> 'query' => 'charade',</span>
|
||||
<span class="x"> 'limit' => 5</span>
|
||||
<span class="x">]));</span>
|
||||
<span class="x">curl_setopt($ch, CURLOPT_HTTPHEADER, [</span>
|
||||
<span class="x"> 'Content-Type: application/json',</span>
|
||||
<span class="x"> 'X-API-Key: YOUR_API_KEY' // 替換為實際的 API Key</span>
|
||||
<span class="x">]);</span>
|
||||
<span class="x">$response = curl_exec($ch);</span>
|
||||
<span class="x">$data = json_decode($response, true);</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>影片嵌入網址</h2>
|
||||
<blockquote>
|
||||
<p><strong>重要</strong>: API 現在返回 <code>file_path</code>(檔案系統路徑),而非直接可訪問的網址。您需要將檔案路徑轉換為 SFTPGo 分享連結才能嵌入影片。</p>
|
||||
</blockquote>
|
||||
<p><strong>檔案路徑轉換為網址:</strong>
|
||||
- API 返回的 <code>file_path</code> 範例:<code>/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4</code>
|
||||
- 對應的 SFTPGo 分享連結:<code>https://wp.momentry.ddns.net/demo/video.mp4</code>
|
||||
- 轉換方式:移除 <code>/Users/accusys/momentry/var/sftpgo/data/</code> 前綴,將剩餘路徑附加到 <code>https://wp.momentry.ddns.net/</code></p>
|
||||
<p><strong>手動建立分享連結:</strong>
|
||||
1. 開啟 SFTPGo Web UI:<code>http://localhost:8080</code>
|
||||
2. 使用帳號 <code>demo</code> / 密碼 <code>demopassword123</code> 登入
|
||||
3. 導航至 <code>Files</code> → 選擇影片檔案
|
||||
4. 點擊 <code>Share</code> → <code>Create Link</code>
|
||||
5. 複製產生的分享連結</p>
|
||||
<p>使用搜尋結果中的 <code>start_time</code> 和 <code>end_time</code> 來嵌入影片片段。</p>
|
||||
<hr />
|
||||
<h2>服務列表</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>服務</th>
|
||||
<th>網址</th>
|
||||
<th>用途</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Momentry API</td>
|
||||
<td><code>http://localhost:3002</code></td>
|
||||
<td>核心 API</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>SFTPGo</td>
|
||||
<td><code>http://localhost:8080</code></td>
|
||||
<td>檔案儲存</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Qdrant</td>
|
||||
<td><code>http://localhost:6333</code></td>
|
||||
<td>向量搜尋</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>PostgreSQL</td>
|
||||
<td><code>localhost:5432</code></td>
|
||||
<td>資料庫</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>示範影片</h2>
|
||||
<ul>
|
||||
<li><strong>檔案:</strong> <code>Old_Time_Movie_Show_-_Charade_1963.HD.mov</code></li>
|
||||
<li><strong>UUID:</strong> <code>a1b10138a6bbb0cd</code></li>
|
||||
<li><strong>長度:</strong> 約 6879 秒(約 1.9 小時)</li>
|
||||
<li><strong>區塊數:</strong> 3886 個(句子 + 場景 + 時間)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
3537
docs_v1.0/doc_user/API_ENDPOINTS.html
Normal file
3537
docs_v1.0/doc_user/API_ENDPOINTS.html
Normal file
File diff suppressed because it is too large
Load Diff
207
docs_v1.0/doc_user/API_ERROR_CODES.html
Normal file
207
docs_v1.0/doc_user/API_ERROR_CODES.html
Normal file
@@ -0,0 +1,207 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Api Error Codes - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<hr />
|
||||
<p>document_type: "api_reference"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "API Error Codes (API 標準錯誤碼)"
|
||||
date: "2026-05-17"
|
||||
version: "V1.1"
|
||||
status: "active"
|
||||
owner: "M5"
|
||||
created_by: "OpenCode"</p>
|
||||
<hr />
|
||||
<h1>API Error Codes (API 標準錯誤碼)</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>項目</th>
|
||||
<th>內容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>目標讀者</td>
|
||||
<td>developer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>預備知識</td>
|
||||
<td>需有 API Key</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>Error Response Format</h2>
|
||||
<p>All API errors follow this JSON structure:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"error"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"code"</span><span class="p">:</span><span class="w"> </span><span class="s2">"E001_NOT_FOUND"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Resource not found"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"details"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"resource"</span><span class="p">:</span><span class="w"> </span><span class="s2">"file_uuid"</span><span class="p">,</span><span class="w"> </span><span class="nt">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"abc"</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h2>Error Code List</h2>
|
||||
<h3>Generic Errors (E0xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E001_NOT_FOUND</code></td>
|
||||
<td>404</td>
|
||||
<td>Resource not found (file, identity, chunk)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E002_DUPLICATE</code></td>
|
||||
<td>409</td>
|
||||
<td>Resource already exists</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E003_VALIDATION</code></td>
|
||||
<td>400</td>
|
||||
<td>Request parameter validation failed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E004_UNAUTHORIZED</code></td>
|
||||
<td>401</td>
|
||||
<td>Invalid API key or token</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E005_INTERNAL</code></td>
|
||||
<td>500</td>
|
||||
<td>Internal server error</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Processor Errors (E1xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E101_PROCESSOR_FAIL</code></td>
|
||||
<td>500</td>
|
||||
<td>Python script execution failed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E102_TIMEOUT</code></td>
|
||||
<td>504</td>
|
||||
<td>Processing timeout</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E103_RESUME_FAIL</code></td>
|
||||
<td>500</td>
|
||||
<td>Resume failed (checkpoint not found)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E104_NO_VIDEO</code></td>
|
||||
<td>400</td>
|
||||
<td>Video file path not found</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>Identity Errors (E2xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E201_FACE_NOT_FOUND</code></td>
|
||||
<td>404</td>
|
||||
<td>Face detection not found</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E202_MERGE_CONFLICT</code></td>
|
||||
<td>409</td>
|
||||
<td>Identity merge conflict</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E203_CANDIDATE_EMPTY</code></td>
|
||||
<td>404</td>
|
||||
<td>No candidates available for confirmation</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h3>TMDb Errors (E3xx)</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Code</th>
|
||||
<th>HTTP</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>E301_TMDB_NO_KEY</code></td>
|
||||
<td>400</td>
|
||||
<td><code>TMDB_API_KEY</code> environment variable not set</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E302_TMDB_UNREACHABLE</code></td>
|
||||
<td>502</td>
|
||||
<td>TMDb API unreachable or timed out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E303_TMDB_CACHE_NOT_FOUND</code></td>
|
||||
<td>200</td>
|
||||
<td>No local TMDb cache; run prefetch first</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E304_TMDB_PROBE_FAILED</code></td>
|
||||
<td>500</td>
|
||||
<td>TMDb probe execution failed</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>E305_TMDB_MOVIE_NOT_FOUND</code></td>
|
||||
<td>404</td>
|
||||
<td>No matching TMDb movie found from filename</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
125
docs_v1.0/doc_user/API_INDEX.html
Normal file
125
docs_v1.0/doc_user/API_INDEX.html
Normal file
@@ -0,0 +1,125 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Api Index - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<hr />
|
||||
<p>document_type: "api_reference"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "Momentry Core API 文件總覽"
|
||||
date: "2026-05-17"
|
||||
version: "V1.0"
|
||||
status: "active"
|
||||
owner: "M5"
|
||||
created_by: "OpenCode"</p>
|
||||
<hr />
|
||||
<h1>Momentry Core API 文件總覽</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>項目</th>
|
||||
<th>內容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>目標讀者</td>
|
||||
<td>developer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>預備知識</td>
|
||||
<td>需有 API Key</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>📁 文件結構</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="n">API_WORKSPACE</span><span class="o">/</span>
|
||||
<span class="err">└──</span><span class="w"> </span><span class="n">modules</span><span class="o">/</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="n">_template</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">One</span><span class="o">-</span><span class="n">line</span><span class="w"> </span><span class="n">description</span><span class="w"> </span><span class="n">of</span><span class="w"> </span><span class="n">what</span><span class="w"> </span><span class="n">this</span><span class="w"> </span><span class="k">module</span><span class="w"> </span><span class="n">covers</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">01</span><span class="n">_auth</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Authentication</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">login</span><span class="p">,</span><span class="w"> </span><span class="n">logout</span><span class="p">,</span><span class="w"> </span><span class="n">JWT</span><span class="p">,</span><span class="w"> </span><span class="n">session</span><span class="w"> </span><span class="n">cookie</span><span class="p">,</span><span class="w"> </span><span class="n">API</span><span class="w"> </span><span class="n">key</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">02</span><span class="n">_health</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Health</span><span class="w"> </span><span class="n">check</span><span class="w"> </span><span class="n">endpoints</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">03</span><span class="n">_register</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">File</span><span class="w"> </span><span class="n">registration</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">register</span><span class="p">,</span><span class="w"> </span><span class="n">scan</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">04</span><span class="n">_lookup</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">File</span><span class="w"> </span><span class="n">lookup</span><span class="w"> </span><span class="n">by</span><span class="w"> </span><span class="n">name</span><span class="w"> </span><span class="k">and</span><span class="w"> </span><span class="n">unregistration</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">05</span><span class="n">_process</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Processing</span><span class="w"> </span><span class="n">pipeline</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">trigger</span><span class="p">,</span><span class="w"> </span><span class="n">probe</span><span class="p">,</span><span class="w"> </span><span class="n">progress</span><span class="p">,</span><span class="w"> </span><span class="n">jobs</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">06</span><span class="n">_search</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Vector</span><span class="w"> </span><span class="n">search</span><span class="p">,</span><span class="w"> </span><span class="n">hybrid</span><span class="w"> </span><span class="n">search</span><span class="p">,</span><span class="w"> </span><span class="n">BM25</span><span class="p">,</span><span class="w"> </span><span class="n">n8n</span><span class="p">,</span><span class="w"> </span><span class="n">visual</span><span class="p">,</span><span class="w"> </span><span class="n">identity</span><span class="w"> </span><span class="n">text</span><span class="w"> </span><span class="n">search</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">07</span><span class="n">_identity</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Global</span><span class="w"> </span><span class="n">identities</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">CRUD</span><span class="p">,</span><span class="w"> </span><span class="n">detail</span><span class="p">,</span><span class="w"> </span><span class="n">files</span><span class="p">,</span><span class="w"> </span><span class="n">faces</span><span class="p">,</span><span class="w"> </span><span class="n">bind</span><span class="p">,</span><span class="w"> </span><span class="n">unbind</span><span class="p">,</span><span class="w"> </span><span class="n">search</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">08</span><span class="n">_identity_agent</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Identity</span><span class="w"> </span><span class="n">agent</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">analyze</span><span class="p">,</span><span class="w"> </span><span class="n">suggest</span><span class="p">,</span><span class="w"> </span><span class="n">merge</span><span class="p">,</span><span class="w"> </span><span class="n">clustering</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">08</span><span class="n">_media</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Video</span><span class="w"> </span><span class="n">streaming</span><span class="w"> </span><span class="o">&</span><span class="w"> </span><span class="n">frame</span><span class="w"> </span><span class="n">extraction</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">09</span><span class="n">_tmdb</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">TMDb</span><span class="w"> </span><span class="n">enrichment</span><span class="w"> </span><span class="n">endpoints</span><span class="w"> </span><span class="err">—</span><span class="w"> </span><span class="n">prefetch</span><span class="p">,</span><span class="w"> </span><span class="n">probe</span><span class="p">,</span><span class="w"> </span><span class="n">resource</span><span class="p">,</span><span class="w"> </span><span class="n">check</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">10</span><span class="n">_pipeline</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Stats</span><span class="w"> </span><span class="n">endpoints</span><span class="p">,</span><span class="w"> </span><span class="n">inference</span><span class="w"> </span><span class="n">health</span><span class="p">,</span><span class="w"> </span><span class="n">stfpgo</span><span class="w"> </span><span class="n">status</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">11</span><span class="n">_error_codes</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span><span class="n">Standard</span><span class="w"> </span><span class="n">API</span><span class="w"> </span><span class="n">error</span><span class="w"> </span><span class="n">codes</span>
|
||||
<span class="err">│</span><span class="w"> </span><span class="err">├──</span><span class="w"> </span><span class="mh">12</span><span class="n">_agent</span><span class="p">.</span><span class="n">md</span><span class="w"> </span><span class="err">←</span><span class="w"> </span>
|
||||
<span class="err">└──</span><span class="w"> </span><span class="p">(</span><span class="n">generated</span><span class="w"> </span><span class="n">files</span><span class="w"> </span><span class="err">→</span><span class="w"> </span><span class="n">GUIDES</span><span class="o">/</span><span class="p">)</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h2>快速選擇指南</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>需求</th>
|
||||
<th>閱讀文件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>查看所有 API 端點(curl 範例版)</td>
|
||||
<td><code>GUIDES/API_ENDPOINTS.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>查看快速端點摘要</td>
|
||||
<td><code>GUIDES/API_QUICK_REFERENCE.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>執行 TMDb Enrichment</td>
|
||||
<td><code>GUIDES/TMDb_User_Guide.md</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>查看錯誤碼</td>
|
||||
<td><code>GUIDES/API_ERROR_CODES.md</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>文件模組清單</h2>
|
||||
<ul>
|
||||
<li><code>_template</code> — One-line description of what this module covers</li>
|
||||
<li><code>01_auth</code> — Authentication — login, logout, JWT, session cookie, API key</li>
|
||||
<li><code>02_health</code> — Health check endpoints</li>
|
||||
<li><code>03_register</code> — File registration — register, scan</li>
|
||||
<li><code>04_lookup</code> — File lookup by name and unregistration</li>
|
||||
<li><code>05_process</code> — Processing pipeline — trigger, probe, progress, jobs</li>
|
||||
<li><code>06_search</code> — Vector search, hybrid search, BM25, n8n, visual, identity text search</li>
|
||||
<li><code>07_identity</code> — Global identities — CRUD, detail, files, faces, bind, unbind, search</li>
|
||||
<li><code>08_identity_agent</code> — Identity agent — analyze, suggest, merge, clustering</li>
|
||||
<li><code>08_media</code> — Video streaming & frame extraction</li>
|
||||
<li><code>09_tmdb</code> — TMDb enrichment endpoints — prefetch, probe, resource, check</li>
|
||||
<li><code>10_pipeline</code> — Stats endpoints, inference health, stfpgo status</li>
|
||||
<li><code>11_error_codes</code> — Standard API error codes</li>
|
||||
<li><code>12_agent</code> — </li>
|
||||
</ul>
|
||||
<hr />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
2105
docs_v1.0/doc_user/API_QUICK_REFERENCE.html
Normal file
2105
docs_v1.0/doc_user/API_QUICK_REFERENCE.html
Normal file
File diff suppressed because it is too large
Load Diff
3684
docs_v1.0/doc_user/API_REFERENCE.html
Normal file
3684
docs_v1.0/doc_user/API_REFERENCE.html
Normal file
File diff suppressed because it is too large
Load Diff
1603
docs_v1.0/doc_user/API_TRAINING_MARCOM.html
Normal file
1603
docs_v1.0/doc_user/API_TRAINING_MARCOM.html
Normal file
File diff suppressed because it is too large
Load Diff
1084
docs_v1.0/doc_user/Demo_EndToEnd.html
Normal file
1084
docs_v1.0/doc_user/Demo_EndToEnd.html
Normal file
File diff suppressed because it is too large
Load Diff
472
docs_v1.0/doc_user/M5API_Pipeline_Demo.html
Normal file
472
docs_v1.0/doc_user/M5API_Pipeline_Demo.html
Normal file
@@ -0,0 +1,472 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>M5Api Pipeline Demo - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<hr />
|
||||
<p>document_type: "demo_guide"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "M5API Pipeline Demo"
|
||||
date: "2026-05-16"
|
||||
version: "V1.0"
|
||||
status: "active"
|
||||
owner: "M5"
|
||||
created_by: "OpenCode"
|
||||
tags:
|
||||
- "demo"
|
||||
- "pipeline"
|
||||
- "api"
|
||||
- "m5api"
|
||||
ai_query_hints:
|
||||
- "M5API Pipeline demo"
|
||||
- "如何透過 M5 的 API 執行 Pipeline"
|
||||
related_documents:
|
||||
- "GUIDES/Demo_EndToEnd.md"
|
||||
- "GUIDES/API_ENDPOINTS.md"</p>
|
||||
<hr />
|
||||
<h1>Momentry Core — M5API Pipeline Demo</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>項目</th>
|
||||
<th>內容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>建立者</td>
|
||||
<td>OpenCode</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>建立時間</td>
|
||||
<td>2026-05-16</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>文件版本</td>
|
||||
<td>V1.0</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>目標讀者</td>
|
||||
<td>developer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>預備知識</td>
|
||||
<td>需有 API Key、M5 服務已啟動</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>Prerequisites</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nv">API</span><span class="o">=</span><span class="s2">"https://m5api.momentry.ddns.net"</span>
|
||||
<span class="nv">KEY</span><span class="o">=</span><span class="s2">"muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 1: System Health Check</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/health"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{ip, port, status, version, build_git_hash}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"ip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.110.201"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">3002</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"build_git_hash"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c41f7e0c"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>All core services verified:</p>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/health/detailed"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{</span>
|
||||
<span class="s1"> services, schema: .schema.ok,</span>
|
||||
<span class="s1"> scripts: .pipeline.scripts_count,</span>
|
||||
<span class="s1"> integrity: .pipeline.scripts_integrity,</span>
|
||||
<span class="s1"> procs: [.pipeline.processors | to_entries[] | select(.value==true and .key!="total_py_files") | .key]</span>
|
||||
<span class="s1">}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"services"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"postgres"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"redis"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"qdrant"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"mongodb"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ok"</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"schema"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"scripts"</span><span class="p">:</span><span class="w"> </span><span class="mi">286</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"integrity"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"matched"</span><span class="p">:</span><span class="w"> </span><span class="mi">345</span><span class="p">,</span><span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">345</span><span class="p">,</span><span class="w"> </span><span class="nt">"ok"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"procs"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"asr"</span><span class="p">,</span><span class="s2">"yolo"</span><span class="p">,</span><span class="s2">"face"</span><span class="p">,</span><span class="s2">"pose"</span><span class="p">,</span><span class="s2">"ocr"</span><span class="p">,</span><span class="s2">"cut"</span><span class="p">,</span><span class="s2">"caption"</span><span class="p">,</span><span class="s2">"scene"</span><span class="p">,</span><span class="s2">"story"</span><span class="p">,</span><span class="s2">"asrx"</span><span class="p">,</span><span class="s2">"probe"</span><span class="p">,</span><span class="s2">"visual_chunk"</span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 2: List Registered Files</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files?page=1&page_size=5"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, files: [.data[]? | {name: .file_name[0:50], status}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">56</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"files"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade (1963) Cary Grant & Audrey Hepburn ..."</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ExaSAN PCIe series - Director Ou Yu-Zhi ..."</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Old_Time_Movie_Show_-_Charade_1963.HD.mov"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Old Felix the Cat Cartoon.mp4"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"unregistered"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"short_clip.mov"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 3: Register a New File</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># POST with file_path (must exist on server filesystem)</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_path": "/path/to/video.mp4"}'</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/register"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{success, file_uuid, file_name, file_type, duration, fps, already_exists}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response (new registration):</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade (1963) Cary Grant & Audrey Hepburn ...mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</span><span class="p">:</span><span class="w"> </span><span class="mf">6785.014</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">23.976</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"already_exists"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response (duplicate content — SHA256 dedup):</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"already_exists"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Content already registered (identical file)"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 4: Probe (ffprobe Metadata)</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nv">UUID</span><span class="o">=</span><span class="s2">"3abeee81d94597629ed8cb943f182e94"</span>
|
||||
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/probe"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{name: .file_name, video: "\(.width)x\(.height)", fps, duration, cached, streams: [.streams[] | {type: .codec_type, codec: .codec_name}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade (1963) Cary Grant & Audrey Hepburn ...mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"video"</span><span class="p">:</span><span class="w"> </span><span class="s2">"720x304"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">23.976</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</span><span class="p">:</span><span class="w"> </span><span class="mf">6785.014</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"cached"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"streams"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span><span class="w"> </span><span class="nt">"codec"</span><span class="p">:</span><span class="w"> </span><span class="s2">"h264"</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"audio"</span><span class="p">,</span><span class="w"> </span><span class="nt">"codec"</span><span class="p">:</span><span class="w"> </span><span class="s2">"aac"</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Error cases:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Non-existent UUID</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span><span class="s2">"https://m5api.momentry.ddns.net/api/v1/file/bad_uuid/probe"</span>
|
||||
<span class="c1"># → {"error":"Video not found","file_uuid":"bad_uuid"} HTTP 404</span>
|
||||
|
||||
<span class="c1"># File deleted from disk</span>
|
||||
<span class="c1"># → {"error":"File does not exist at registered path","file_uuid":"...","file_path":"..."} HTTP 404</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 5: Submit Processing Job</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Specific processors</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"processors":["asr","cut","yolo","face","pose","ocr"]}'</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/process"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{job_id, file_uuid: .file_uuid[0:16], status}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"job_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">167</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d9459762"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"PENDING"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<blockquote>
|
||||
<p><strong>All processors</strong>: Send <code>{}</code> (empty body) to run all 12 processors.
|
||||
Available: <code>asr</code>, <code>cut</code>, <code>yolo</code>, <code>face</code>, <code>pose</code>, <code>ocr</code>, <code>asrx</code>, <code>visual_chunk</code>, <code>scene</code>, <code>story</code>, <code>caption</code></p>
|
||||
</blockquote>
|
||||
<hr />
|
||||
<h2>Step 6: Monitor Progress</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="k">while</span><span class="w"> </span>true<span class="p">;</span><span class="w"> </span><span class="k">do</span>
|
||||
<span class="w"> </span><span class="nv">PROGRESS</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/progress/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">"</span><span class="k">)</span>
|
||||
<span class="w"> </span><span class="nv">STATUS</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROGRESS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">'.status // "?"'</span><span class="k">)</span>
|
||||
<span class="w"> </span><span class="nv">PROCS</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROGRESS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">'[.processors[]? | "\(.name)=\(.status)(\(.frames_processed))"] | join(" ")'</span><span class="k">)</span>
|
||||
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="k">$(</span>date<span class="w"> </span>+%H:%M:%S<span class="k">)</span><span class="s2">: </span><span class="nv">$PROCS</span><span class="s2">"</span>
|
||||
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROCS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span>-q<span class="w"> </span><span class="s2">"completed"</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="k">break</span>
|
||||
<span class="w"> </span>sleep<span class="w"> </span><span class="m">10</span>
|
||||
<span class="k">done</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Typical output:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">01</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">ocr</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">11</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">ocr</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">21</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">ocr</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">31</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">pending</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">41</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">30</span><span class="p">:</span><span class="mf">51</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">ocr</span><span class="o">=</span><span class="kr">run</span><span class="n">ning</span><span class="p">(</span><span class="mf">0</span><span class="p">)</span>
|
||||
<span class="mf">12</span><span class="p">:</span><span class="mf">31</span><span class="p">:</span><span class="mf">01</span><span class="p">:</span><span class="w"> </span><span class="n">asr</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">cut</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">yolo</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">face</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="nb">pos</span><span class="n">e</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span><span class="w"> </span><span class="n">ocr</span><span class="o">=</span><span class="n">completed</span><span class="p">(</span><span class="mf">8951</span><span class="p">)</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p><strong>Status transition chain</strong>: <code>pending → running → completed</code></p>
|
||||
<p>Check job state:</p>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/jobs?uuid=</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.jobs[]? | {id, status}]'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 7: Verify Results</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/progress/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{processors: [.processors[] | {name, status, frames: .frames_processed}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"processors"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"asr"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cut"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"yolo"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"face"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pose"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ocr"</span><span class="p">,</span><span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span><span class="w"> </span><span class="nt">"frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">162568</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 8: Universal Search</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Search for a person name</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s2">"{\"query\":\"Audrey\",\"uuid\":\"</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">\",\"limit\":3}"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, hits: [.results[]? | {chunk_id: .chunk_id[0:40], text: .text[0:80], score}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"hits"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94_998192"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94_998193"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"score"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.9</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Search Chinese text</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s2">"{\"query\":\"導演\",\"uuid\":\"</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">\",\"limit\":3}"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p><strong>Search modes</strong>: The universal search endpoint supports:
|
||||
- Text match (ILIKE on <code>text_content</code> and <code>content</code> columns)
|
||||
- Time range filtering (<code>time_range: [start, end]</code>)
|
||||
- Speaker/person ID filtering
|
||||
- Chunk type filtering
|
||||
- Visual content filtering (objects, density, classes)</p>
|
||||
<hr />
|
||||
<h2>Step 9: Get Chunk Detail</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nv">CHUNK_ID</span><span class="o">=</span><span class="s2">"3abeee81d94597629ed8cb943f182e94_998192"</span>
|
||||
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/chunk/</span><span class="si">${</span><span class="nv">CHUNK_ID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{chunk_id, chunk_type, text: .text_content, fps, start_frame, end_frame}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94_998192"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sentence"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">23.976</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"start_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">2395281</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"end_frame"</span><span class="p">:</span><span class="w"> </span><span class="mi">2395341</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 10: Chunk Fallback (Stale Qdrant Compatibility)</h2>
|
||||
<p>Old integer-format chunk_ids from stale Qdrant payloads are automatically resolved via <code>WHERE id = int(chunk_id)</code>:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Integer format (old Qdrant payload)</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/chunk/998192"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{chunk_id, text: .text_content}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response (same chunk as above):</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"chunk_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3abeee81d94597629ed8cb943f182e94_998192"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"text"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p><strong>Both formats work:</strong>
|
||||
- <code>chunk/{uuid}_{id}</code> → exact <code>chunk_id</code> match
|
||||
- <code>chunk/{id}</code> → fallback by primary key <code>id</code></p>
|
||||
<hr />
|
||||
<h2>Step 11: File Detail</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{file_name, status, file_type, file_path}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade (1963) Cary Grant & Audrey Hepburn ...mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/Users/accusys/momentry/var/sftpgo/data/demo/Charade..."</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 12: File Identities</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/identities"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, identities: [.data[]? | {name, face_count, confidence}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"identities"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Audrey Hepburn"</span><span class="p">,</span><span class="w"> </span><span class="nt">"face_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">22082</span><span class="p">,</span><span class="w"> </span><span class="nt">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.93</span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="p">{</span><span class="nt">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Cary Grant"</span><span class="p">,</span><span class="w"> </span><span class="nt">"face_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">15334</span><span class="p">,</span><span class="w"> </span><span class="nt">"confidence"</span><span class="p">:</span><span class="w"> </span><span class="mf">0.91</span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">]</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 13: Identity Detail</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># List all global identities</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identities?page=1&page_size=3"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, identities: [.data[]? | {name, type: .identity_type, source}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Get identity files (cross-file faces)</span>
|
||||
<span class="nv">IDENTITY_UUID</span><span class="o">=</span><span class="s2">"c3545906-c82d-4b66-aa1d-150bc02decce"</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/identity/</span><span class="si">${</span><span class="nv">IDENTITY_UUID</span><span class="si">}</span><span class="s2">/files"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, files: [.data[]? | {file_uuid: .file_uuid[0:16], face_count}]}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Step 14: Schema & Integrity Verification</h2>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-sf<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/health/detailed"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{</span>
|
||||
<span class="s1"> ip, port,</span>
|
||||
<span class="s1"> schema: .schema.ok,</span>
|
||||
<span class="s1"> migrations: [.schema.applied[]?.filename],</span>
|
||||
<span class="s1"> integrity: .pipeline.scripts_integrity</span>
|
||||
<span class="s1">}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>Response:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"ip"</span><span class="p">:</span><span class="w"> </span><span class="s2">"192.168.110.201"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"port"</span><span class="p">:</span><span class="w"> </span><span class="mi">3002</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"schema"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"migrations"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_add_content_hash.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_add_registered_status.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_add_schema_version.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_cleanup_inactive_identities.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_public_schema_v4_tables.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_public_schema_v4.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_public_v4_complete.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_fix_chunk_id_format.sql"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="s2">"migrate_add_identity_indexes.sql"</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"integrity"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="nt">"matched"</span><span class="p">:</span><span class="w"> </span><span class="mi">345</span><span class="p">,</span><span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">345</span><span class="p">,</span><span class="w"> </span><span class="nt">"ok"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">}</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h2>Full Automation Script</h2>
|
||||
<div class="codehilite"><pre><span></span><code><span class="ch">#!/bin/bash</span>
|
||||
<span class="nb">set</span><span class="w"> </span>-euo<span class="w"> </span>pipefail
|
||||
|
||||
<span class="nv">API</span><span class="o">=</span><span class="s2">"</span><span class="si">${</span><span class="nv">API</span><span class="k">:-</span><span class="nv">https</span><span class="p">://m5api.momentry.ddns.net</span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="nv">KEY</span><span class="o">=</span><span class="s2">"</span><span class="si">${</span><span class="nv">KEY</span><span class="k">:-</span><span class="nv">muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69</span><span class="si">}</span><span class="s2">"</span>
|
||||
|
||||
<span class="c1"># 1. Health</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Health ==="</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/health"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{status, version, build_git_hash}'</span>
|
||||
|
||||
<span class="c1"># 2. Register file (argument: file path)</span>
|
||||
<span class="nv">FILE_PATH</span><span class="o">=</span><span class="s2">"</span><span class="si">${</span><span class="nv">1</span><span class="p">:?Usage: </span><span class="nv">$0</span><span class="p"> <file_path></span><span class="si">}</span><span class="s2">"</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Register ==="</span>
|
||||
<span class="nv">REG</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s2">"{\"file_path\":\"</span><span class="nv">$FILE_PATH</span><span class="s2">\"}"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/register"</span><span class="k">)</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$REG</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{success, file_uuid, file_name}'</span>
|
||||
<span class="nv">UUID</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$REG</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">'.file_uuid'</span><span class="k">)</span>
|
||||
<span class="o">[</span><span class="w"> </span>-z<span class="w"> </span><span class="s2">"</span><span class="nv">$UUID</span><span class="s2">"</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="o">{</span><span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"Registration failed"</span><span class="p">;</span><span class="w"> </span><span class="nb">exit</span><span class="w"> </span><span class="m">1</span><span class="p">;</span><span class="w"> </span><span class="o">}</span>
|
||||
|
||||
<span class="c1"># 3. Probe</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Probe ==="</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/probe"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>jq<span class="w"> </span><span class="s1">'{name, fps, duration}'</span>
|
||||
|
||||
<span class="c1"># 4. Submit job</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Process ==="</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{}'</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">/process"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{job_id, status}'</span>
|
||||
|
||||
<span class="c1"># 5. Poll progress</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Waiting for pipeline... ==="</span>
|
||||
<span class="k">while</span><span class="w"> </span>true<span class="p">;</span><span class="w"> </span><span class="k">do</span>
|
||||
<span class="w"> </span><span class="nv">PROGRESS</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-sf<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/progress/</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">"</span><span class="k">)</span>
|
||||
<span class="w"> </span><span class="nv">STATUS</span><span class="o">=</span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROGRESS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">'.status // "?"'</span><span class="k">)</span>
|
||||
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="k">$(</span>date<span class="w"> </span>+%H:%M:%S<span class="k">)</span><span class="s2">: </span><span class="k">$(</span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROGRESS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-r<span class="w"> </span><span class="s1">'[.processors[]? | "\(.name)=\(.status)(\(.frames_processed))"] | join(" ")'</span><span class="k">)</span><span class="s2">"</span>
|
||||
<span class="w"> </span><span class="nb">echo</span><span class="w"> </span><span class="s2">"</span><span class="nv">$PROGRESS</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span>-e<span class="w"> </span><span class="s1">'[.processors[]? | select(.status == "pending")] | length == 0'</span><span class="w"> </span>>/dev/null<span class="w"> </span><span class="o">&&</span><span class="w"> </span><span class="k">break</span>
|
||||
<span class="w"> </span>sleep<span class="w"> </span><span class="m">10</span>
|
||||
<span class="k">done</span>
|
||||
|
||||
<span class="c1"># 6. Search</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"=== Search ==="</span>
|
||||
curl<span class="w"> </span>-sf<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s2">"{\"query\":\"test\",\"uuid\":\"</span><span class="si">${</span><span class="nv">UUID</span><span class="si">}</span><span class="s2">\",\"limit\":3}"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/search/universal"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, hits: [.results[]? | {chunk_id: .chunk_id[0:30], text: .text[0:60]}]}'</span>
|
||||
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">""</span>
|
||||
<span class="nb">echo</span><span class="w"> </span><span class="s2">"✅ Done: </span><span class="nv">$UUID</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
923
docs_v1.0/doc_user/TMDb_User_Guide.html
Normal file
923
docs_v1.0/doc_user/TMDb_User_Guide.html
Normal file
@@ -0,0 +1,923 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Tmdb User Guide - Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 960px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 24px; margin: 24px 0 12px; }
|
||||
h2 { font-size: 20px; margin: 20px 0 10px; color: #222; }
|
||||
h3 { font-size: 16px; margin: 16px 0 8px; color: #444; }
|
||||
p { line-height: 1.6; margin: 8px 0; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 12px 0; font-size: 14px; }
|
||||
th, td { border: 1px solid #ddd; padding: 8px 12px; text-align: left; }
|
||||
th { background: #f0f0f0; font-weight: 600; }
|
||||
code { background: #f0f0f0; padding: 2px 6px; border-radius: 3px; font-size: 13px; }
|
||||
pre { background: #f8f8f8; border: 1px solid #ddd; border-radius: 6px; padding: 12px; overflow-x: auto; margin: 12px 0; }
|
||||
pre code { background: none; padding: 0; }
|
||||
a { color: #0066cc; }
|
||||
.back { display: inline-block; margin-bottom: 20px; color: #666; }
|
||||
.back:hover { color: #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<a class="back" href="index.html">← Back to index</a>
|
||||
<hr />
|
||||
<p>document_type: "user_manual"
|
||||
service: "MOMENTRY_CORE"
|
||||
title: "TMDb Enrichment 使用指南"
|
||||
date: "2026-05-17"
|
||||
version: "V1.0"
|
||||
status: "active"
|
||||
owner: "M5"
|
||||
created_by: "OpenCode"</p>
|
||||
<hr />
|
||||
<h1>TMDb Enrichment 使用指南</h1>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>項目</th>
|
||||
<th>內容</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>目標讀者</td>
|
||||
<td>developer</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>預備知識</td>
|
||||
<td>需有 API Key</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>Base URL</h2>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment</th>
|
||||
<th>URL</th>
|
||||
<th>Purpose</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Playground (Dev)</td>
|
||||
<td><code>http://localhost:3003</code></td>
|
||||
<td>Development and testing</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Production</td>
|
||||
<td><code>http://localhost:3002</code></td>
|
||||
<td>Production deployment</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>External (M5)</td>
|
||||
<td><code>https://m5api.momentry.ddns.net</code></td>
|
||||
<td>Remote access</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h2>Variables</h2>
|
||||
<p>All examples in this documentation use these environment variables:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nv">API</span><span class="o">=</span><span class="s2">"http://localhost:3003"</span>
|
||||
<span class="nv">KEY</span><span class="o">=</span><span class="s2">"your-api-key-here"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h2>Authentication</h2>
|
||||
<p>All endpoints under <code>/api/v1/*</code> require authentication.
|
||||
The following endpoints are public (no auth needed):</p>
|
||||
<ul>
|
||||
<li><code>GET /health</code></li>
|
||||
<li><code>POST /api/v1/auth/login</code></li>
|
||||
<li><code>POST /api/v1/auth/logout</code></li>
|
||||
</ul>
|
||||
<h3>Three Authentication Modes</h3>
|
||||
<p>The system supports three authentication methods, checked in <strong>priority order</strong> by the middleware:</p>
|
||||
<div class="codehilite"><pre><span></span><code>Middleware priority:
|
||||
1. Session Cookie (Portal/browser)
|
||||
2. JWT Bearer (API clients: n8n, CLI)
|
||||
3. API Key Header (legacy compatibility)
|
||||
4. API Key Query Param (?api_key=)
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mode</th>
|
||||
<th>Transport</th>
|
||||
<th>Expiry</th>
|
||||
<th>Scope</th>
|
||||
<th>Best for</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Session Cookie</strong></td>
|
||||
<td><code>Cookie: session_id=<uuid></code></td>
|
||||
<td>24h</td>
|
||||
<td>per-browser session</td>
|
||||
<td>Portal (browser)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>JWT</strong></td>
|
||||
<td><code>Authorization: Bearer <token></code></td>
|
||||
<td>1h</td>
|
||||
<td>per-login token</td>
|
||||
<td>API clients (n8n, CLI, scripts)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>API Key</strong></td>
|
||||
<td><code>X-API-Key: <key></code></td>
|
||||
<td>90d</td>
|
||||
<td>fixed key for automation</td>
|
||||
<td>Legacy scripts, WordPress</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3>Login</h3>
|
||||
<p><strong>Default accounts & API keys:</strong></p>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Password</th>
|
||||
<th>API Key</th>
|
||||
<th>Role</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>admin</code></td>
|
||||
<td><code>admin</code></td>
|
||||
<td>—</td>
|
||||
<td>admin</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>demo</code></td>
|
||||
<td><code>demo</code></td>
|
||||
<td><code>muser_demo_key_32chars_abcdef1234567890</code></td>
|
||||
<td>user</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>The demo API key is set via <code>MOMENTRY_DEMO_API_KEY</code> env var and can be used in place of JWT for marcom integrations:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Using API key instead of JWT</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: muser_demo_key_32chars_abcdef1234567890"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Login as admin</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username": "admin", "password": "admin"}'</span>
|
||||
|
||||
<span class="c1"># Login as demo user</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username": "demo", "password": "demo"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Success Response</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"jwt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"eyJhbGciOiJIUzI1NiIs..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"api_key"</span><span class="p">:</span><span class="w"> </span><span class="s2">"muser_..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"user"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"username"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"role"</span><span class="p">:</span><span class="w"> </span><span class="s2">"admin"</span>
|
||||
<span class="w"> </span><span class="p">},</span>
|
||||
<span class="w"> </span><span class="nt">"expires_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-18T13:00:00Z"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>jwt</code></td>
|
||||
<td>string</td>
|
||||
<td>JWT access token. Use as <code>Authorization: Bearer <jwt></code>. Expires in 1 hour.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>api_key</code></td>
|
||||
<td>string</td>
|
||||
<td>Legacy API key. Use as <code>X-API-Key: <key></code>. Good for 90 days.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user.username</code></td>
|
||||
<td>string</td>
|
||||
<td>Username</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user.role</code></td>
|
||||
<td>string</td>
|
||||
<td>Role: <code>admin</code>, <code>user</code>, or <code>readonly</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>expires_at</code></td>
|
||||
<td>string</td>
|
||||
<td>ISO8601 timestamp of JWT expiration</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>The login endpoint also sets a <code>Set-Cookie</code> header for browser-based clients:</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="nt">Set-Cookie</span><span class="o">:</span><span class="w"> </span><span class="nt">session_id</span><span class="o">=<</span><span class="nt">uuid</span><span class="o">>;</span><span class="w"> </span><span class="nt">Path</span><span class="o">=/</span><span class="nt">api</span><span class="o">;</span><span class="w"> </span><span class="nt">HttpOnly</span><span class="o">;</span><span class="w"> </span><span class="nt">SameSite</span><span class="o">=</span><span class="nt">Strict</span><span class="o">;</span><span class="w"> </span><span class="nt">Max-Age</span><span class="o">=</span><span class="nt">86400</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Error Response (401)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Invalid username or password"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Using JWT</h3>
|
||||
<p>JWT is preferred for API clients (n8n, CLI scripts, WordPress). It is validated by the middleware without a database lookup (stateless).</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Login and capture JWT</span>
|
||||
<span class="nv">JWT</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username":"admin","password":"admin"}'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>python3<span class="w"> </span>-c<span class="w"> </span><span class="s2">"import json,sys;print(json.load(sys.stdin)['jwt'])"</span><span class="k">)</span>
|
||||
|
||||
<span class="c1"># Use JWT for all subsequent requests</span>
|
||||
curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span>
|
||||
curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$JWT</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>JWT is short-lived (1 hour). When it expires, request a new one via login.</p>
|
||||
<hr />
|
||||
<h3>Using Session Cookie (Browser)</h3>
|
||||
<p>Browser-based clients (Portal) get a session cookie automatically after login. The browser sends the cookie with every request—no manual header needed.</p>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Login captures the session cookie from Set-Cookie header</span>
|
||||
curl<span class="w"> </span>-v<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username":"admin","password":"admin"}'</span><span class="w"> </span><span class="m">2</span>><span class="p">&</span><span class="m">1</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span><span class="s2">"Set-Cookie"</span>
|
||||
|
||||
<span class="c1"># Browser automatically sends: Cookie: session_id=<uuid></span>
|
||||
<span class="c1"># No manual header needed for subsequent requests</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>The session cookie is HttpOnly (not accessible from JavaScript) and SameSite=Strict (protected against CSRF).</p>
|
||||
<hr />
|
||||
<h3>Using Legacy API Key</h3>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span>
|
||||
|
||||
<span class="c1"># Also accepted via Bearer header (non-JWT format) or query parameter:</span>
|
||||
curl<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Authorization: Bearer </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span>
|
||||
curl<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?api_key=</span><span class="nv">$KEY</span><span class="s2">"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<p>API keys are validated via SHA256 hash lookup in the database. They are long-lived (90 days) and intended for automation.</p>
|
||||
<h3>Obtaining an API Key (CLI)</h3>
|
||||
<div class="codehilite"><pre><span></span><code>momentry<span class="w"> </span>api-key<span class="w"> </span>create<span class="w"> </span><span class="s2">"My API Key"</span><span class="w"> </span>--key-type<span class="w"> </span>user
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Logout</h3>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Logout using the session cookie (browser)</span>
|
||||
curl<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/logout"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=<uuid>"</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>What logout does</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Auth mode</th>
|
||||
<th>Effect</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Session Cookie</strong></td>
|
||||
<td>Session deleted from database. Same cookie returns 401 on subsequent requests.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>JWT</strong></td>
|
||||
<td>JWT remains valid until expiry. (JWT is stateless — logout adds JWT to a blacklist only if API key mode is used.)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>API Key</strong></td>
|
||||
<td>API key remains valid. (Legacy keys are shared across sessions — revoking would break other clients.)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example: full session lifecycle</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># 1. Login</span>
|
||||
<span class="nv">SESSION_ID</span><span class="o">=</span><span class="k">$(</span>curl<span class="w"> </span>-s<span class="w"> </span>-D<span class="w"> </span>-<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/login"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"username":"admin","password":"admin"}'</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>grep<span class="w"> </span><span class="s2">"Set-Cookie"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span><span class="s1">'s/.*session_id=\([^;]*\).*/\1/'</span><span class="k">)</span>
|
||||
|
||||
<span class="c1"># 2. Use session (works)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-o<span class="w"> </span>/dev/null<span class="w"> </span>-w<span class="w"> </span><span class="s2">"HTTP %{http_code}\n"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=</span><span class="nv">$SESSION_ID</span><span class="s2">"</span>
|
||||
<span class="c1"># → HTTP 200</span>
|
||||
|
||||
<span class="c1"># 3. Logout</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/auth/logout"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=</span><span class="nv">$SESSION_ID</span><span class="s2">"</span>
|
||||
<span class="c1"># → {"success": true}</span>
|
||||
|
||||
<span class="c1"># 4. Use session again (rejected)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-o<span class="w"> </span>/dev/null<span class="w"> </span>-w<span class="w"> </span><span class="s2">"HTTP %{http_code}\n"</span><span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Cookie: session_id=</span><span class="nv">$SESSION_ID</span><span class="s2">"</span>
|
||||
<span class="c1"># → HTTP 401</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Authentication Flow Summary</h3>
|
||||
<div class="codehilite"><pre><span></span><code>Login Request
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 1. Check users │ ← users table (argon2 password verify)
|
||||
│ table │
|
||||
└──────┬───────────┘
|
||||
│
|
||||
┌───┴───┐
|
||||
│ match │
|
||||
└───┬───┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ 2. Create JWT │ ← 1h expiry, signed with JWT_SECRET
|
||||
├──────────────────┤
|
||||
│ 3. Create │ ← 24h expiry, stored in sessions table
|
||||
│ session │
|
||||
├──────────────────┤
|
||||
│ 4. Set-Cookie │ ← HttpOnly, SameSite=Strict, Path=/api
|
||||
├──────────────────┤
|
||||
│ 5. Return │ ← JWT + api_key + user info to client
|
||||
└──────────────────┘
|
||||
</code></pre></div>
|
||||
|
||||
<div class="codehilite"><pre><span></span><code>Protected Request
|
||||
│
|
||||
▼
|
||||
┌──────────────────────┐
|
||||
│ Middleware checks: │
|
||||
│ │
|
||||
│ 1. Cookie session? │ → DB lookup session → get api_key → verify
|
||||
│ │
|
||||
│ 2. JWT Bearer? │ → verify JWT signature → decode claims
|
||||
│ │
|
||||
│ 3. X-API-Key? │ → SHA256 hash → DB lookup → verify
|
||||
│ │
|
||||
│ 4. ?api_key=? │ → same as #3
|
||||
│ │
|
||||
│ 5. None → 401 │
|
||||
└──────────────────────┘
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
<h3>Error Responses</h3>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>Missing or invalid authentication</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>Session expired or logged out</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>JWT expired</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>API key revoked or inactive</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3>Related</h3>
|
||||
<ul>
|
||||
<li><code>POST /api/v1/resource/tmdb/check</code> — test authentication + TMDb API connectivity</li>
|
||||
<li><code>GET /health/detailed</code> — view auth status (integrations section)</li>
|
||||
</ul>
|
||||
<hr />
|
||||
<h2>File Registration</h2>
|
||||
<h3><code>POST /api/v1/files/register</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Register a video file for processing. Returns the file's metadata and UUID.</p>
|
||||
<p><strong>New in v0.1.2</strong>: Registration now <strong>automatically triggers the processing pipeline</strong> — no need to call <code>POST /api/v1/file/:uuid/process</code> separately. The system will:
|
||||
1. Register the file and run ffprobe
|
||||
2. Auto-run offline TMDb probe (reads local identity files, no API calls)
|
||||
3. Create a monitor job for the worker
|
||||
4. Worker starts all 10 processors (Cut → ASR → ASRX → YOLO → OCR → Face → Pose → VisualChunk → Story → 5W1H)</p>
|
||||
<p>If the file already exists (same content hash), returns the existing record with <code>already_exists: true</code>.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>—</td>
|
||||
<td>Path to video file on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pattern</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Regex pattern for batch register (requires <code>file_path</code> to be a directory)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>user_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>User ID to associate with registration</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>content_hash</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Pre-computed SHA-256 hash (skips computation)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Register a single file</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/register"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_path": "/path/to/video.mp4"}'</span>
|
||||
|
||||
<span class="c1"># Batch register files matching a pattern in a directory</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/register"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_path": "/path/to/dir", "pattern": ".*\\.mp4$"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/path/to/video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"duration"</span><span class="p">:</span><span class="w"> </span><span class="mf">120.5</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"width"</span><span class="p">:</span><span class="w"> </span><span class="mi">1920</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"height"</span><span class="p">:</span><span class="w"> </span><span class="mi">1080</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"fps"</span><span class="p">:</span><span class="w"> </span><span class="mf">24.0</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"total_frames"</span><span class="p">:</span><span class="w"> </span><span class="mi">2892</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"already_exists"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"File registered successfully"</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>success</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Always true on 200</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>32-char hex UUID of the registered file</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_name</code></td>
|
||||
<td>string</td>
|
||||
<td>File name (auto-renamed if name conflict)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Canonical path on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>file_type</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"video"</code>, <code>"audio"</code>, or <code>"unknown"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>duration</code></td>
|
||||
<td>float</td>
|
||||
<td>Duration in seconds</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>width</code></td>
|
||||
<td>integer</td>
|
||||
<td>Video width in pixels</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>height</code></td>
|
||||
<td>integer</td>
|
||||
<td>Video height in pixels</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>fps</code></td>
|
||||
<td>float</td>
|
||||
<td>Frames per second</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>total_frames</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total frame count</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>already_exists</code></td>
|
||||
<td>boolean</td>
|
||||
<td>True if same content was already registered</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>message</code></td>
|
||||
<td>string</td>
|
||||
<td>Human-readable status</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Error Responses</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>HTTP</th>
|
||||
<th>When</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>401</code></td>
|
||||
<td>Missing or invalid API key</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>400</code></td>
|
||||
<td>Invalid request body</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>404</code></td>
|
||||
<td>File path does not exist</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h3><code>GET /api/v1/files/scan</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Scan the filesystem directory and list all media files, showing which are registered, processing, or unregistered.</p>
|
||||
<h4>Query Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Default</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>1</td>
|
||||
<td>Page number (1-based)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>all</td>
|
||||
<td>Items per page (alias: <code>limit</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>limit</code></td>
|
||||
<td>integer</td>
|
||||
<td>No</td>
|
||||
<td>all</td>
|
||||
<td>Max items (alias for <code>page_size</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>pattern</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td>—</td>
|
||||
<td>Regex filter on file name (e.g., <code>.*\\.mp4$</code>)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sort_by</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>name</code></td>
|
||||
<td>Sort field: <code>name</code>, <code>size</code>, <code>modified</code>, <code>status</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>sort_order</code></td>
|
||||
<td>string</td>
|
||||
<td>No</td>
|
||||
<td><code>asc</code></td>
|
||||
<td>Sort direction: <code>asc</code> or <code>desc</code></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="c1"># Full scan</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{total, registered_count, unregistered_count}'</span>
|
||||
|
||||
<span class="c1"># Paginated (page 1, 5 per page)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?page=1&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{page, total_pages, files: [.files[].file_name]}'</span>
|
||||
|
||||
<span class="c1"># Regex filter: only mp4 files</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?pattern=.*\\.mp4</span>$<span class="s2">"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{filtered_total, files: [.files[].file_name]}'</span>
|
||||
|
||||
<span class="c1"># Sort by file size (largest first)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?sort_by=size&sort_order=desc&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.files[] | {file_name, file_size}]'</span>
|
||||
|
||||
<span class="c1"># Sort by modified time (most recent first)</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?sort_by=modified&sort_order=desc&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.files[] | {file_name, modified_time}]'</span>
|
||||
|
||||
<span class="c1"># Sort by status</span>
|
||||
curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/files/scan?sort_by=status&page_size=5"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'[.files[] | {file_name, status}]'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"files"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span>
|
||||
<span class="w"> </span><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"file_name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"video.mp4"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">12345678</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"is_registered"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3a6c1865..."</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"completed"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"registration_time"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-05-16T12:00:00Z"</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"job_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">42</span>
|
||||
<span class="w"> </span><span class="p">}</span>
|
||||
<span class="w"> </span><span class="p">],</span>
|
||||
<span class="w"> </span><span class="nt">"total"</span><span class="p">:</span><span class="w"> </span><span class="mi">107</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"filtered_total"</span><span class="p">:</span><span class="w"> </span><span class="mi">80</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"page"</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"page_size"</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"total_pages"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"registered_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">26</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"unregistered_count"</span><span class="p">:</span><span class="w"> </span><span class="mi">81</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>files</code></td>
|
||||
<td>array</td>
|
||||
<td>Array of file info objects (paginated)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_name</code></td>
|
||||
<td>string</td>
|
||||
<td>File name</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].relative_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Path relative to scan root</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_path</code></td>
|
||||
<td>string</td>
|
||||
<td>Absolute path on disk</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>File size in bytes</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].modified_time</code></td>
|
||||
<td>string</td>
|
||||
<td>Last modified timestamp (ISO8601)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].is_registered</code></td>
|
||||
<td>boolean</td>
|
||||
<td>Whether file is registered in DB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>32-char hex UUID (only if registered)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].status</code></td>
|
||||
<td>string</td>
|
||||
<td><code>"completed"</code>, <code>"processing"</code>, <code>"registered"</code>, <code>"unregistered"</code>, or <code>null</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].registration_time</code></td>
|
||||
<td>string</td>
|
||||
<td>DB registration timestamp (only if registered)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>files[].job_id</code></td>
|
||||
<td>integer</td>
|
||||
<td>Processing job ID (only if a job exists)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>total</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total files found on disk (unfiltered)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>filtered_total</code></td>
|
||||
<td>integer</td>
|
||||
<td>Files matching regex filter</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page</code></td>
|
||||
<td>integer</td>
|
||||
<td>Current page number</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>page_size</code></td>
|
||||
<td>integer</td>
|
||||
<td>Items per page</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>total_pages</code></td>
|
||||
<td>integer</td>
|
||||
<td>Total pages</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>registered_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Files registered in DB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>unregistered_count</code></td>
|
||||
<td>integer</td>
|
||||
<td>Files not yet registered</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Notes</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Feature</th>
|
||||
<th>Behavior</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Regex</strong></td>
|
||||
<td>Case-insensitive (<code>(?i)</code> prefix auto-applied). Applied to <code>file_name</code>.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Sort order</strong></td>
|
||||
<td>Default (<code>sort_by=name</code>): registered files first, then alphabetically. <code>sort_by=status</code>: alphabetical by status string.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Pagination</strong></td>
|
||||
<td><code>page_size</code> and <code>limit</code> are aliases. Default: show all results.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Processing order</strong></td>
|
||||
<td><code>pattern</code> regex filter → <code>sort_by</code>/<code>sort_order</code> → <code>page</code>/<code>page_size</code> slice.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<hr />
|
||||
<h2>TMDb Enrichment</h2>
|
||||
<blockquote>
|
||||
<p>⚠️ <strong>External resource</strong>: TMDb requires internet access, violating Momentry's local-only principle.
|
||||
All core processing (ASR, YOLO, Face, OCR, Pose, embeddings) runs fully offline.
|
||||
TMDb enrichment is <strong>optional</strong> and gated behind <code>TMDB_API_KEY</code> + <code>MOMENTRY_TMDB_PROBE_ENABLED</code>.</p>
|
||||
</blockquote>
|
||||
<h3>Overview</h3>
|
||||
<p>TMDb enrichment is an optional identity enrichment step that can be run after Pipeline face detection completes. The workflow is:</p>
|
||||
<ol>
|
||||
<li><strong>Prefetch</strong> (requires internet): Download movie cast data from TMDb API → cache to <code>{file_uuid}.tmdb.json</code></li>
|
||||
<li><strong>Probe</strong>: Read local cache → create identities for <strong>all</strong> cast members (<code>source='tmdb'</code>) + save <code>identity.json</code> + download profile image to <code>{OUTPUT}/identities/{uuid}/profile.jpg</code></li>
|
||||
<li><strong>Match</strong>: The worker automatically matches video faces against TMDb identities when <code>MOMENTRY_TMDB_PROBE_ENABLED=true</code></li>
|
||||
</ol>
|
||||
<h3><code>POST /api/v1/agents/tmdb/prefetch</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Fetch TMDb cast data for a registered file and cache it locally. This is the only step requiring internet access.</p>
|
||||
<h4>Request Parameters</h4>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Field</th>
|
||||
<th>Type</th>
|
||||
<th>Required</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>file_uuid</code></td>
|
||||
<td>string</td>
|
||||
<td>Yes</td>
|
||||
<td>File UUID to enrich</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/agents/tmdb/prefetch"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"Content-Type: application/json"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-d<span class="w"> </span><span class="s1">'{"file_uuid": "'</span><span class="s2">"</span><span class="nv">$FILE_UUID</span><span class="s2">"</span><span class="s1">'"}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"> </span><span class="nt">"file_uuid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"..."</span><span class="p">,</span><span class="w"> </span><span class="nt">"cache_path"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/output/...tmdb.json"</span><span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3><code>POST /api/v1/file/:file_uuid/tmdb-probe</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: file-level</p>
|
||||
<p>Read local TMDb cache and create/update identities. Requires prefetch to have been run first.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/file/</span><span class="nv">$FILE_UUID</span><span class="s2">/tmdb-probe"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{identities_created, movie_title}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200 — identities created)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w"> </span><span class="nt">"identities_created"</span><span class="p">:</span><span class="w"> </span><span class="mi">15</span><span class="p">,</span><span class="w"> </span><span class="nt">"movie_title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Charade"</span><span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response (200 — no cache)</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span><span class="nt">"success"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w"> </span><span class="nt">"message"</span><span class="p">:</span><span class="w"> </span><span class="s2">"No TMDb cache found. Run tmdb-prefetch first."</span><span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3><code>GET /api/v1/resource/tmdb</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: system-level</p>
|
||||
<p>View TMDb resource status including configuration, identity counts, and cache file count.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb"</span><span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'{identities_seeded, cache_files}'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h3><code>POST /api/v1/resource/tmdb/check</code></h3>
|
||||
<p><strong>Auth</strong>: Required
|
||||
<strong>Scope</strong>: system-level</p>
|
||||
<p>Ping the TMDb API to verify connectivity and measure latency.</p>
|
||||
<h4>Example</h4>
|
||||
<div class="codehilite"><pre><span></span><code>curl<span class="w"> </span>-s<span class="w"> </span>-X<span class="w"> </span>POST<span class="w"> </span><span class="s2">"</span><span class="nv">$API</span><span class="s2">/api/v1/resource/tmdb/check"</span><span class="w"> </span><span class="se">\</span>
|
||||
<span class="w"> </span>-H<span class="w"> </span><span class="s2">"X-API-Key: </span><span class="nv">$KEY</span><span class="s2">"</span><span class="w"> </span><span class="p">|</span><span class="w"> </span>jq<span class="w"> </span><span class="s1">'.status'</span>
|
||||
</code></pre></div>
|
||||
|
||||
<h4>Response</h4>
|
||||
<div class="codehilite"><pre><span></span><code><span class="p">{</span>
|
||||
<span class="w"> </span><span class="nt">"api_key_configured"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"enabled"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"api_reachable"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span>
|
||||
<span class="w"> </span><span class="nt">"api_latency_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">120</span>
|
||||
<span class="p">}</span>
|
||||
</code></pre></div>
|
||||
|
||||
<hr />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
26
docs_v1.0/doc_user/index.html
Normal file
26
docs_v1.0/doc_user/index.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Momentry API Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; color: #333; padding: 40px; }
|
||||
.container { max-width: 900px; margin: 0 auto; background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; }
|
||||
h1 { font-size: 28px; margin-bottom: 8px; }
|
||||
p.subtitle { color: #666; margin-bottom: 24px; }
|
||||
ul { list-style: none; }
|
||||
li { padding: 8px 0; border-bottom: 1px solid #eee; }
|
||||
li:last-child { border: none; }
|
||||
a { color: #0066cc; text-decoration: none; font-size: 16px; }
|
||||
a:hover { text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Momentry API Documentation</h1>
|
||||
<p class="subtitle">Generated from API_WORKSPACE modules</p>
|
||||
<ul><li><a href="API_ACCESS.html">Api Access</a></li><li><a href="API_ENDPOINTS.html">Api Endpoints</a></li><li><a href="API_ERROR_CODES.html">Api Error Codes</a></li><li><a href="API_INDEX.html">Api Index</a></li><li><a href="API_QUICK_REFERENCE.html">Api Quick Reference</a></li><li><a href="API_REFERENCE.html">Api Reference</a></li><li><a href="API_TRAINING_MARCOM.html">Api Training Marcom</a></li><li><a href="Demo_EndToEnd.html">Demo Endtoend</a></li><li><a href="M5API_Pipeline_Demo.html">M5Api Pipeline Demo</a></li><li><a href="TMDb_User_Guide.html">Tmdb User Guide</a></li></ul>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
46
docs_v1.0/doc_user/login.html
Normal file
46
docs_v1.0/doc_user/login.html
Normal file
@@ -0,0 +1,46 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Login - Momentry Docs</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #f5f5f5; display: flex; justify-content: center; align-items: center; height: 100vh; }
|
||||
.card { background: white; border-radius: 12px; box-shadow: 0 2px 12px rgba(0,0,0,0.08); padding: 40px; width: 360px; }
|
||||
h1 { font-size: 24px; margin-bottom: 24px; text-align: center; }
|
||||
input { width: 100%; padding: 10px 12px; margin-bottom: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; }
|
||||
button { width: 100%; padding: 10px; background: #0066cc; color: white; border: none; border-radius: 6px; font-size: 16px; cursor: pointer; }
|
||||
button:hover { background: #0052a3; }
|
||||
.error { color: #cc0000; font-size: 13px; margin-bottom: 12px; display: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h1>Momentry Docs</h1>
|
||||
<form id="loginForm">
|
||||
<input type="text" id="username" placeholder="Username" value="demo" required>
|
||||
<input type="password" id="password" placeholder="Password" value="demo" required>
|
||||
<div class="error" id="error">Invalid credentials</div>
|
||||
<button type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
<script>
|
||||
document.getElementById('loginForm').onsubmit = async function(e) {
|
||||
e.preventDefault();
|
||||
const resp = await fetch('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify({
|
||||
username: document.getElementById('username').value,
|
||||
password: document.getElementById('password').value
|
||||
})
|
||||
});
|
||||
if (resp.ok) {
|
||||
window.location.href = '/doc/index.html';
|
||||
} else {
|
||||
document.getElementById('error').style.display = 'block';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -66,6 +66,7 @@ const MODULES = [
|
||||
["10_pipeline","生產線","Pipeline"],
|
||||
["12_agent","智慧代理","AI Agents"],
|
||||
["13_config","系統設定","System Config"],
|
||||
["14_identity_history","操作歷史","Operation History (Undo/Redo)"],
|
||||
];
|
||||
|
||||
const el = document.getElementById('content');
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
### `POST /api/v1/search/smart`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Semantic vector search using EmbeddingGemma-300m. Generates a query embedding via EmbeddingGemma (port 11436), then searches pgvector `story_parent` and `llm_parent` chunks by cosine similarity.
|
||||
|
||||
@@ -15,13 +15,22 @@ Semantic vector search using EmbeddingGemma-300m. Generates a query embedding vi
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `file_uuid` | string | Yes | — | File UUID to search within |
|
||||
| `query` | string | Yes | — | Search text |
|
||||
| `file_uuid` | string | No | — | File UUID to search within. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 5 | Max results to return |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 5 | Items per page |
|
||||
|
||||
#### Example
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/smart" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-d '{"query": "Audrey Hepburn"}'
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/smart" \
|
||||
@@ -37,6 +46,7 @@ curl -s -X POST "$API/api/v1/search/smart" \
|
||||
"query": "Audrey Hepburn",
|
||||
"results": [
|
||||
{
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"parent_id": 1087822,
|
||||
"scene_order": 1087822,
|
||||
"start_frame": 104438,
|
||||
@@ -54,12 +64,16 @@ curl -s -X POST "$API/api/v1/search/smart" \
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].file_uuid` | string | File UUID where result was found |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/universal`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Multi-type BM25 full-text search across chunks, frames, and persons. Uses PostgreSQL `tsvector`.
|
||||
|
||||
@@ -68,13 +82,22 @@ Multi-type BM25 full-text search across chunks, frames, and persons. Uses Postgr
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `query` | string | Yes | — | Search text |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `types` | string[] | No | `["chunk","frame","person"]` | Search types |
|
||||
| `limit` | integer | No | 10 | Max results per type |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 20 | Items per page |
|
||||
|
||||
#### Example
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/universal" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer $JWT" \
|
||||
-d '{"query": "Cary Grant"}'
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/search/universal" \
|
||||
@@ -90,6 +113,7 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
"results": [
|
||||
{
|
||||
"type": "chunk",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"chunk_id": "bd80fec92b0b6963d177a2c55bf713e2_2",
|
||||
"chunk_type": "story_child",
|
||||
"start_frame": 5103,
|
||||
@@ -98,6 +122,25 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
"end_time": 213.64,
|
||||
"text": "[213s-214s] Cary Grant: \"Olá!\"",
|
||||
"score": 0.9
|
||||
},
|
||||
{
|
||||
"type": "frame",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"frame_number": 5105,
|
||||
"timestamp": 212.72,
|
||||
"score": 0.7,
|
||||
"objects": null,
|
||||
"ocr_texts": null,
|
||||
"faces": null
|
||||
},
|
||||
{
|
||||
"type": "person",
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"identity_id": 12,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"appearance_count": 542,
|
||||
"score": 0.95
|
||||
}
|
||||
],
|
||||
"total": 20,
|
||||
@@ -105,23 +148,78 @@ curl -s -X POST "$API/api/v1/search/universal" \
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].type` | string | Result type: `chunk`, `frame`, or `person` |
|
||||
| `results[].file_uuid` | string | File UUID where result was found (all types) |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/frames`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search face detection frames by identity name or trace ID.
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/search/identity_text`
|
||||
### `GET /api/v1/search/identity_text`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: file-level
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search text chunks spoken by a specific identity.
|
||||
Search text chunks → find associated identities. Returns chunks where face detections overlap with text content.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `q` | string | Yes | — | Search text (ILIKE match) |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 50 | Max results |
|
||||
| `page` | integer | No | 1 | Page number |
|
||||
| `page_size` | integer | No | 50 | Items per page |
|
||||
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/search/identity_text?q=love" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/search/identity_text?file_uuid=$FILE_UUID&q=love" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"chunk_id": "llm_parent_..._256_270",
|
||||
"start_time": 256.256,
|
||||
"end_time": 270.228,
|
||||
"text_content": "...lack of affection...",
|
||||
"identity_id": 9,
|
||||
"identity_name": "Audrey Hepburn",
|
||||
"identity_source": "tmdb",
|
||||
"trace_id": 94
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].file_uuid` | string | File UUID where chunk was found |
|
||||
| `results[].identity_id` | integer | Identity ID if face was detected |
|
||||
| `results[].trace_id` | integer | Face trace ID |
|
||||
|
||||
---
|
||||
|
||||
@@ -145,4 +243,4 @@ Search text chunks spoken by a specific identity.
|
||||
| **Storage** | pgvector (`chunk.embedding` column) |
|
||||
|
||||
---
|
||||
*Updated: 2026-05-19 12:49:24*
|
||||
*Updated: 2026-05-27 — Added global search support for smart, universal, identity_text APIs*
|
||||
|
||||
@@ -70,7 +70,16 @@ curl -s "$API/api/v1/identity/$IDENTITY_UUID" -H "X-API-Key: $KEY"
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Delete an identity permanently.
|
||||
Delete an identity permanently. All face detections bound to this identity are unbound (`identity_id` set to `NULL`). The identity JSON file is deleted from disk.
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Every DELETE records a full snapshot of the identity and its unbound faces. See [`14_identity_history.md`](14_identity_history.md#4-delete-history--undoredo) for:
|
||||
|
||||
- Undo via `POST /api/v1/identity/:identity_uuid/undo` — recreates identity and re-binds faces
|
||||
- Redo via `POST /api/v1/identity/:identity_uuid/redo` — re-deletes the identity
|
||||
|
||||
**Note**: Delete undo/redo reuses the same endpoints as PATCH undo/redo. The endpoint automatically detects whether the identity was deleted (undo) or needs to be re-deleted (redo) based on the history record.
|
||||
|
||||
---
|
||||
|
||||
@@ -129,124 +138,75 @@ curl -s -X PATCH "$API/api/v1/identity/$IDENTITY_UUID" \
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No fields to update or invalid UUID format |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Every bind records a before/after snapshot. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for:
|
||||
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/undo` — Revert a bind
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/redo` — Reapply an undone bind
|
||||
- `GET /api/v1/identity/:identity_uuid/bind/history` — Query bind operations
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/files`
|
||||
## Metadata (Embedded JSON)
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
The `identities.metadata` column is a **JSONB** field that stores arbitrary structured data alongside the identity's core fields (name, status, identity_type). No schema is enforced — any valid JSON object is accepted.
|
||||
|
||||
Get all files where this identity appears. Returns per-file summary including face count, confidence, and appearance time range.
|
||||
### Merge Behavior
|
||||
|
||||
#### Example
|
||||
| Operation | Strategy | Example |
|
||||
|-----------|----------|---------|
|
||||
| **PATCH** | Shallow top-level merge: `COALESCE(metadata,'{}'::jsonb) \|\| $1::jsonb` | Sending `{"tmdb_rating": 8.5}` only adds/overwrites `tmdb_rating`; all other existing keys are preserved. |
|
||||
| **mergeinto** | Recursive deep merge — nested sub-keys are merged individually, not replaced wholesale | Target has `{"tmdb": {"biography": "..."}}`, source has `{"tmdb": {"birthday": "1904-01-18"}}` → result is `{"tmdb": {"biography": "...", "birthday": "1904-01-18"}}`. |
|
||||
| **Upload (`POST`)** | Direct overwrite — the entire `metadata` field is replaced with the request value. | |
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/files" -H "X-API-Key: $KEY"
|
||||
```
|
||||
### Validation
|
||||
|
||||
---
|
||||
| Scenario | Result |
|
||||
|----------|--------|
|
||||
| PATCH with non-object metadata (`string`, `array`, `number`, `null`) | `400 Bad Request: "metadata must be a JSON object"` |
|
||||
| mergeinto with non-object metadata | Accepted (mergeinto validates at application level) |
|
||||
| Upload with non-object metadata | Accepted (upload replaces directly) |
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/faces`
|
||||
### Conventional Keys
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
| Key | Type | Writer | Purpose |
|
||||
|-----|------|--------|---------|
|
||||
| `aliases` | `[{locale, name}]` | PATCH, mergeinto | Multilingual display names (see [Alias System](#alias-system-bcp-47-locale-tags)) |
|
||||
| `merged_into` | `{uuid, at}` | mergeinto | Marks an identity as merged (undo mechanism reads this) |
|
||||
| `tmdb_*` | various | TMDb probe | Movie metadata (biography, birthday, known_for, etc.). Written only when `MOMENTRY_TMDB_PROBE_ENABLED=true`. |
|
||||
| `source` | string | mergeinto | Tagged on aliases/metadata when added by merge (`"merge"` value) |
|
||||
|
||||
Get all face detection records associated with this identity.
|
||||
Custom keys are fully supported — no registration required.
|
||||
|
||||
#### Example
|
||||
### Search Coverage
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/faces" -H "X-API-Key: $KEY"
|
||||
```
|
||||
The identity search endpoint (`GET /api/v1/identity/search`) matches across three scopes:
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_uuid` | string | File where face was detected |
|
||||
| `frame_number` | integer | Frame number of detection |
|
||||
| `face_id` | string | Face ID (format: `face_{frame_number}`) |
|
||||
| `confidence` | float | Detection confidence |
|
||||
1. `i.name` — exact and ILIKE against display name
|
||||
2. `jsonb_array_elements(i.metadata->'aliases')->>'name'` — locale-tagged alias names
|
||||
3. `i.metadata::text ILIKE $1` — raw string search across the entire JSON blob (all keys, all values)
|
||||
|
||||
---
|
||||
This means searching for `"1904-01-18"` or `"biography"` will match identities whose metadata contains those strings anywhere.
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/chunks`
|
||||
### History Snapshots
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
Every `identity_history` record captures the **full metadata** in both `before_snapshot` and `after_snapshot` (as part of the complete identity JSONB dump). Undo restores the identity row — including metadata — to the `before_snapshot` state.
|
||||
|
||||
Get all text chunks (sentences) spoken while this identity's face was on screen. Useful for finding what a person said.
|
||||
For merge operations, the MongoDB merge history records `metadata_fields_added` and `metadata_fields_added_paths` (dot-separated paths like `"tmdb.biography"`). Merge undo removes only those specific paths, preserving subsequent manual edits to other metadata keys.
|
||||
|
||||
#### Example
|
||||
### Best Practices
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/chunks" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"data": [
|
||||
{
|
||||
"id": 0,
|
||||
"file_uuid": "bd80fec92b0b6963d177a2c55bf713e2",
|
||||
"chunk_id": "bd80fec92b0b6963d177a2c55bf713e2_2",
|
||||
"chunk_type": "sentence",
|
||||
"start_frame": 5103,
|
||||
"end_frame": 5127,
|
||||
"fps": 24.0,
|
||||
"start_time": 212.64,
|
||||
"end_time": 213.64,
|
||||
"text_content": "[213s-214s] Cary Grant: \"Olá!\""
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `file_uuid` | string | File identifier |
|
||||
| `chunk_id` | string | Sentence chunk identifier |
|
||||
| `start_frame` | integer | Frame-accurate start position |
|
||||
| `end_frame` | integer | Frame-accurate end position |
|
||||
| `fps` | float | Frames per second |
|
||||
| `start_time` | float | Start time in seconds |
|
||||
| `end_time` | float | End time in seconds |
|
||||
| `text_content` | string | Spoken text content |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/:identity_uuid/bind`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Bind a face detection to an identity. Associates the face trace with the identity for future search and recognition.
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `file_uuid` | string | Yes | File where face is detected |
|
||||
| `face_id` | string | Yes | Face ID (format: `{frame}_{idx}`) |
|
||||
|
||||
#### Side Effects
|
||||
|
||||
- 清除該 face detection row 的 `stranger_id`(設為 NULL)
|
||||
- 不影響 `identities` 表中原有的 stranger auto-identity 記錄
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"file_uuid": "'"$FILE_UUID"'", "face_id": "1_5"}'
|
||||
```
|
||||
| Guideline | Reason |
|
||||
|-----------|--------|
|
||||
| Deep nesting is allowed in metadata | All metadata merge operations use `jsonb_deep_merge()` — nested sub-keys are merged recursively, not replaced wholesale |
|
||||
| Use `aliases` for display names | Frontend has built-in locale fallback logic (see [Alias System](#alias-system-bcp-47-locale-tags)) |
|
||||
| Avoid >1MB per identity | Metadata is included in search indexing (`metadata::text ILIKE`); large blobs degrade query performance |
|
||||
| Don't rely on metadata ordering | JSONB preserves insertion order but PostgreSQL does not guarantee it across operations |
|
||||
| No LLM/Gemma4 agent writes to metadata | Only API endpoints (PATCH, mergeinto, upload) and TMDb probe modify `identities.metadata` |
|
||||
|
||||
---
|
||||
|
||||
@@ -295,6 +255,10 @@ curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/trace" \
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Trace bind operations share the same history/undo/redo system as single-face binds. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for endpoints.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/:identity_uuid/traces`
|
||||
@@ -382,6 +346,13 @@ Unbind a face detection from an identity. Removes the identity association from
|
||||
- 被 unbind 的 face 不會自動成為 stranger
|
||||
- 要重新標記為 stranger 需重新跑 Agent API(`identity/analyze`)
|
||||
|
||||
#### History & Undo/Redo
|
||||
|
||||
Unbind records a before/after snapshot. See [`14_identity_history.md`](14_identity_history.md#2-bindunbindtrace-history--undoredo) for:
|
||||
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/undo` — Revert an unbind
|
||||
- `POST /api/v1/identity/:identity_uuid/bind/redo` — Reapply an undone unbind
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/:identity_uuid/mergeinto`
|
||||
@@ -391,6 +362,13 @@ Unbind a face detection from an identity. Removes the identity association from
|
||||
|
||||
Transfer all face bindings from this identity to another identity, then optionally delete or mark the source as merged.
|
||||
|
||||
#### Two Merge Cases
|
||||
|
||||
| Case | Description | Undo/Redo Support |
|
||||
|------|-------------|-------------------|
|
||||
| **stranger → identity** | Merge an auto-generated stranger identity into a known identity (TMDb or user-defined) | ✅ 24hr undo/redo |
|
||||
| **identity A → identity B** | Merge two known identities (e.g., duplicate entries) | ✅ 24hr undo/redo |
|
||||
|
||||
#### Request Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
@@ -402,8 +380,12 @@ Transfer all face bindings from this identity to another identity, then optional
|
||||
|
||||
- 轉移所有 `face_detections.identity_id` 到目標 identity
|
||||
- 同時清除所有被轉移 rows 的 `stranger_id`
|
||||
- 將 source name 加入 target aliases (with `source: "merge"` tag)
|
||||
- 將 source aliases 加入 target aliases (if not already present)
|
||||
- 將 source metadata fields 加入 target metadata (if not already present)
|
||||
- `keep_history: true`(預設):source identity 設為 `status='merged'`,保留記錄
|
||||
- `keep_history: false`:**刪除** source identity 及其 identity JSON 檔案
|
||||
- **記錄 merge history 到 MongoDB**(支援 undo/redo)
|
||||
|
||||
#### Example
|
||||
|
||||
@@ -411,7 +393,7 @@ Transfer all face bindings from this identity to another identity, then optional
|
||||
curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"into_uuid": "'"$TARGET_UUID"'", "keep_history": false}'
|
||||
-d '{"into_uuid": "'"$TARGET_UUID"'", "keep_history": true}'
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
@@ -419,11 +401,23 @@ curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Merged 'stranger_13894' into 'Louis Viret' (52 faces transferred, source deleted)",
|
||||
"data": { "faces_transferred": 52 }
|
||||
"message": "Merged 'stranger_13894' into 'Louis Viret' (52 faces transferred, history kept)",
|
||||
"data": {
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"faces_transferred": 52,
|
||||
"aliases_added": 1,
|
||||
"metadata_fields_added": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merge_id` | string | Unique merge operation ID (for undo) |
|
||||
| `faces_transferred` | integer | Number of face detections transferred |
|
||||
| `aliases_added` | integer | Number of aliases added to target |
|
||||
| `metadata_fields_added` | integer | Number of metadata fields added to target |
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
@@ -433,25 +427,189 @@ curl -s -X POST "$API/api/v1/identity/$SOURCE_UUID/mergeinto" \
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identities/search`
|
||||
### `POST /api/v1/identity/merge/:merge_id/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Search identities by name (ILIKE search). Returns matching identity records.
|
||||
Undo a merge operation within 24 hours. Restores the source identity and reverts face bindings.
|
||||
|
||||
#### Undo Behavior
|
||||
|
||||
| Action | Description |
|
||||
|--------|-------------|
|
||||
| Restore source identity | If `keep_history=true`: restore status to `confirmed`<br>If `keep_history=false`: recreate identity from MongoDB snapshot |
|
||||
| Restore faces | Transfer faces back to source identity |
|
||||
| Remove aliases from target | Remove aliases with `source: "merge"` tag |
|
||||
| Remove metadata fields from target | Remove fields that were added from source |
|
||||
| **Preserve manual changes** | Keep aliases/metadata manually added after merge |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Cary" -H "X-API-Key: $KEY"
|
||||
curl -s -X POST "$API/api/v1/identity/merge/550e8400-e29b-41d4-a716-446655440000/undo" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Undo merge completed: 'stranger_13894' restored, 52 faces reverted",
|
||||
"data": {
|
||||
"source_identity_restored": {
|
||||
"uuid": "a9a90105...",
|
||||
"name": "stranger_13894",
|
||||
"status": "confirmed"
|
||||
},
|
||||
"faces_reverted": 52,
|
||||
"aliases_removed_from_target": 1,
|
||||
"metadata_fields_removed_from_target": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | Undo deadline expired (>24hr) or already undone |
|
||||
| `404` | Merge record not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### `POST /api/v1/identity/merge/:merge_id/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo a previously undone merge operation. See [`14_identity_history.md`](14_identity_history.md#post-apiv1identitymergemerge_idredo) for full details.
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identity/merge/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query merge history records from MongoDB.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `source_uuid` | string | No | — | Filter by source identity UUID |
|
||||
| `target_uuid` | string | No | — | Filter by target identity UUID |
|
||||
| `merge_id` | string | No | — | Filter by specific merge ID |
|
||||
| `undone` | bool | No | — | Filter by undone status |
|
||||
| `page` | int | No | 1 | Page number |
|
||||
| `page_size` | int | No | 20 | Items per page |
|
||||
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/merge/history?page=1&page_size=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"page": 1,
|
||||
"page_size": 10,
|
||||
"results": [
|
||||
{
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"source_name": "stranger_13894",
|
||||
"target_name": "Louis Viret",
|
||||
"faces_transferred": 52,
|
||||
"merged_at": "2026-05-27T10:00:00Z",
|
||||
"undo_deadline": "2026-05-28T10:00:00Z",
|
||||
"undone": false,
|
||||
"undo_expired": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `name` | string | Identity name |
|
||||
| `source` | string | Identity source |
|
||||
| `tmdb_id` | integer | TMDb ID (if source = tmdb) |
|
||||
| `file_uuid` | string | Associated file |
|
||||
| `merge_id` | string | Unique merge operation ID |
|
||||
| `source_name` | string | Source identity name |
|
||||
| `target_name` | string | Target identity name |
|
||||
| `faces_transferred` | integer | Number of faces transferred |
|
||||
| `merged_at` | datetime | When merge occurred |
|
||||
| `undo_deadline` | datetime | 24hr deadline for undo |
|
||||
| `undone` | bool | Whether merge was undone |
|
||||
| `undo_expired` | bool | Whether undo deadline passed |
|
||||
|
||||
---
|
||||
|
||||
### `GET /api/v1/identities/search`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: global / file-level
|
||||
|
||||
Search identity name → find associated chunks. Searches identity name and aliases, returns identities with their associated text chunks.
|
||||
|
||||
#### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `q` | string | Yes | — | Search text (ILIKE match on name and aliases) |
|
||||
| `file_uuid` | string | No | — | Restrict to specific file. If omitted, searches all files (global search) |
|
||||
| `limit` | integer | No | 50 | Max results |
|
||||
|
||||
#### Example (Global Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Audrey" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Example (File-specific Search)
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identities/search?q=Audrey&file_uuid=$FILE_UUID" -H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
#### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"total": 5,
|
||||
"results": [
|
||||
{
|
||||
"identity_id": 9,
|
||||
"name": "Audrey Hepburn",
|
||||
"source": "tmdb",
|
||||
"tmdb_id": 1932,
|
||||
"file_uuid": "a6fb22eebefaef17e62af874997c5944",
|
||||
"trace_id": 41,
|
||||
"chunk_id": "llm_parent_..._204_207",
|
||||
"start_time": 204.162,
|
||||
"text_content": "...confrontation..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `results[].identity_id` | integer | Identity ID |
|
||||
| `results[].name` | string | Identity name |
|
||||
| `results[].source` | string | Identity source (`tmdb`, `user_defined`, etc.) |
|
||||
| `results[].tmdb_id` | integer | TMDb person ID (if source = tmdb) |
|
||||
| `results[].file_uuid` | string | File where identity appears |
|
||||
| `results[].trace_id` | integer | Face trace ID |
|
||||
| `results[].chunk_id` | string | Associated chunk ID |
|
||||
| `results[].start_time` | float | Chunk start time |
|
||||
| `results[].text_content` | string | Chunk text content |
|
||||
|
||||
---
|
||||
|
||||
@@ -628,4 +786,4 @@ PATCH /api/v1/identity/:identity_uuid
|
||||
This **replaces** the entire `aliases` array. To add to existing aliases, include all existing entries in the request.
|
||||
|
||||
---
|
||||
*Updated: 2026-05-25
|
||||
*Updated: 2026-05-25 — Added `GET /api/v1/file/:file_uuid/faces` with 4 binding states, filters, strangers table split
|
||||
|
||||
696
docs_v1.0/doc_wasm/modules/14_identity_history.md
Normal file
696
docs_v1.0/doc_wasm/modules/14_identity_history.md
Normal file
@@ -0,0 +1,696 @@
|
||||
<!-- module: identity_history -->
|
||||
<!-- description: Identity operation history, undo, and redo (PATCH, bind, unbind, bind_trace, mergeinto) -->
|
||||
<!-- depends: 01_auth, 07_identity -->
|
||||
|
||||
## Identity Operation History
|
||||
|
||||
Every mutation on an identity automatically records a before/after snapshot. Use undo/redo to revert or reapply changes, and history to inspect the operation log.
|
||||
|
||||
Three independent undo/redo systems exist:
|
||||
|
||||
| System | Storage | Operations Covered |
|
||||
|--------|---------|-------------------|
|
||||
| **PATCH** | PostgreSQL `identity_history` | `update` |
|
||||
| **Bind** | PostgreSQL `identity_history` | `bind`, `unbind`, `bind_trace` |
|
||||
| **Merge** | MongoDB `identity_merge_history` | mergeinto |
|
||||
| **Delete** | PostgreSQL `identity_history` | `delete` |
|
||||
|
||||
---
|
||||
|
||||
### 1. PATCH History & Undo/Redo
|
||||
|
||||
#### Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table |
|
||||
| Snapshot | Full identity record (all fields) before and after each PATCH |
|
||||
| Max records | 256 per identity (oldest auto-deleted when limit exceeded) |
|
||||
| Undo steps | Unlimited (no expiry, no step limit) |
|
||||
| Redo stack | Cleared on new PATCH (`is_undone=true` + `operation='update'` records are deleted) |
|
||||
|
||||
##### Stack Model
|
||||
|
||||
```
|
||||
PATCH 1 → PATCH 2 → PATCH 3 (undo stack, is_undone=false)
|
||||
↓ undo
|
||||
PATCH 1 → PATCH 2 (undo stack)
|
||||
PATCH 3 (redo stack, is_undone=true)
|
||||
↓ redo
|
||||
PATCH 1 → PATCH 2 → PATCH 3 (undo stack)
|
||||
```
|
||||
|
||||
A new PATCH after undo clears only the operation='update' redo stack (PATCH 3 is lost). Bind/merge redo stacks are not affected.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Undo the most recent PATCH operations. Restores the identity's `before_snapshot` and marks the history records as undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of undo steps to apply (max records undone in one call) |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=false` records with `operation='update'`, ordered by `created_at DESC`
|
||||
- Restores `name`, `identity_type`, `source`, `status`, `metadata`, `tmdb_id`, `tmdb_profile` from the last record's `before_snapshot`
|
||||
- Marks the undone records as `is_undone=true` with `undone_at=NOW()`
|
||||
- Syncs `identity.json` to disk
|
||||
- Updates `_index.json` if name changed
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/undo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"undone_count": 1,
|
||||
"current_state": {
|
||||
"id": 9,
|
||||
"uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": {},
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `undone_count` | integer | Number of history records undone |
|
||||
| `current_state` | object | Full identity state after undo |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No undo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo previously undone PATCH operations. Restores the identity's `after_snapshot` and marks the history records as no longer undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of redo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=true` records with `operation='update'`, ordered by `created_at DESC`
|
||||
- Restores all identity fields from the last record's `after_snapshot`
|
||||
- Marks records as `is_undone=false` with `undone_at=NULL`
|
||||
- Syncs `identity.json` to disk
|
||||
- Updates `_index.json` if name changed
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/redo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"redone_count": 1,
|
||||
"current_state": {
|
||||
"id": 9,
|
||||
"uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"name": "John Smith",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": { "aliases": [...] },
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `redone_count` | integer | Number of history records redone |
|
||||
| `current_state` | object | Full identity state after redo |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No redo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `GET /api/v1/identity/:identity_uuid/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query the PATCH operation history for an identity. Returns paginated records with undo/redo stack counts (filtered to `operation='update'`).
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `page` | integer | No | `1` | Page number (1-indexed) |
|
||||
| `limit` | integer | No | `20` | Items per page (max 100) |
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"total": 5,
|
||||
"undo_stack_count": 3,
|
||||
"redo_stack_count": 2,
|
||||
"results": [
|
||||
{
|
||||
"history_id": 42,
|
||||
"operation": "update",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T12:00:00Z",
|
||||
"undone_at": null
|
||||
},
|
||||
{
|
||||
"history_id": 41,
|
||||
"operation": "update",
|
||||
"is_undone": true,
|
||||
"created_at": "2026-05-27T11:30:00Z",
|
||||
"undone_at": "2026-05-27T13:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total PATCH history records for this identity |
|
||||
| `undo_stack_count` | integer | Records available for undo (`is_undone=false`) |
|
||||
| `redo_stack_count` | integer | Records available for redo (`is_undone=true`) |
|
||||
| `results[].history_id` | integer | History record ID |
|
||||
| `results[].operation` | string | Operation type (`"update"` for PATCH) |
|
||||
| `results[].is_undone` | boolean | Whether the operation has been undone |
|
||||
| `results[].created_at` | string | When the PATCH was applied |
|
||||
| `results[].undone_at` | string | When the undo occurred (null if not undone) |
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/history?page=1&limit=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 2. Bind/Unbind/Trace History & Undo/Redo
|
||||
|
||||
All three operations (`bind`, `unbind`, `bind_trace`) share a single history table and undo/redo stack.
|
||||
|
||||
#### Bind Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table (same table as PATCH) |
|
||||
| Snapshot | `{"file_uuid", "face_id" (or "trace_id"), "identity_id_before/after"}` |
|
||||
| Max records | 256 per identity (shared limit across all operation types) |
|
||||
| Undo steps | Unlimited (`steps` param) |
|
||||
| Redo stack | Cleared on new bind/unbind/bind_trace (`operation IN ('bind','unbind','bind_trace')` + `is_undone=true` records deleted) |
|
||||
| Stack isolation | Bind redo stack is **independent** from PATCH redo stack — clearing one does not affect the other |
|
||||
|
||||
##### Stack Model
|
||||
|
||||
```
|
||||
bind face_1 (to id=9) → unbind face_1 → bind trace 906 (to id=9)
|
||||
(undo stack, is_undone=false) (undo stack) (undo stack)
|
||||
↓ undo (first undone: bind_trace)
|
||||
bind trace 906 (is_undone=true)
|
||||
(redo stack)
|
||||
↓ redo
|
||||
bind face_1 → unbind face_1 → bind trace 906
|
||||
(undo stack)
|
||||
```
|
||||
|
||||
A new bind/unbind/trace after undo clears only the bind redo stack (operations with `IN ('bind','unbind','bind_trace')`).
|
||||
|
||||
##### Snapshot Format
|
||||
|
||||
**Before (bind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_before": null
|
||||
}
|
||||
```
|
||||
|
||||
**After (bind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_after": 9
|
||||
}
|
||||
```
|
||||
|
||||
**Before (unbind) — binding existed before:**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_before": 9
|
||||
}
|
||||
```
|
||||
|
||||
**After (unbind):**
|
||||
```json
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"identity_id_after": null
|
||||
}
|
||||
```
|
||||
|
||||
For `bind_trace`, the snapshot uses `trace_id` instead of `face_id`, with `identity_id_before` capturing the first face's identity in that trace.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/bind/undo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Undo the most recent bind/unbind/bind_trace operations. Restores `identity_id_before` from the snapshot and marks records as undone.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of undo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=false` records with `operation IN ('bind','unbind','bind_trace')`, ordered by `created_at DESC`
|
||||
- Restores `identity_id_before` — for bind this is `null` (face was unbound), for unbind this is the original identity (face goes back), for bind_trace this is the trace's previous identity
|
||||
- Marks the undone records as `is_undone=true` with `undone_at=NOW()`
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/undo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"operation": "bind",
|
||||
"undone_count": 1,
|
||||
"affected_rows": 53
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `operation` | string | The actual operation undone (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `undone_count` | integer | Number of history records undone |
|
||||
| `affected_rows` | integer | Number of `face_detections` rows updated |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No bind undo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/:identity_uuid/bind/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo previously undone bind/unbind/bind_trace operations. Restores `identity_id_after` from the snapshot.
|
||||
|
||||
##### Request (JSON)
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `steps` | integer | No | `1` | Number of redo steps to apply |
|
||||
|
||||
##### Behavior
|
||||
|
||||
- Queries `is_undone=true` records with `operation IN ('bind','unbind','bind_trace')`, ordered by `created_at DESC`
|
||||
- Restores `identity_id_after` — for bind this is the identity the face was bound to, for unbind this is `null`
|
||||
- Marks records as `is_undone=false` with `undone_at=NULL`
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/$IDENTITY_UUID/bind/redo" \
|
||||
-H "X-API-Key: $KEY" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"steps": 1}'
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"operation": "unbind",
|
||||
"redone_count": 1,
|
||||
"affected_rows": 1
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `operation` | string | The actual operation redone (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `redone_count` | integer | Number of history records redone |
|
||||
| `affected_rows` | integer | Number of `face_detections` rows updated |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | No bind redo operations available |
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
#### `GET /api/v1/identity/:identity_uuid/bind/history`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Query the bind/unbind/bind_trace operation history for an identity. Returns paginated records with undo/redo stack counts.
|
||||
|
||||
##### Query Parameters
|
||||
|
||||
| Field | Type | Required | Default | Description |
|
||||
|-------|------|----------|---------|-------------|
|
||||
| `page` | integer | No | `1` | Page number (1-indexed) |
|
||||
| `limit` | integer | No | `20` | Items per page (max 100) |
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
|
||||
"total": 3,
|
||||
"undo_stack_count": 2,
|
||||
"redo_stack_count": 1,
|
||||
"results": [
|
||||
{
|
||||
"history_id": 52,
|
||||
"operation": "bind_trace",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T14:00:00Z",
|
||||
"undone_at": null
|
||||
},
|
||||
{
|
||||
"history_id": 51,
|
||||
"operation": "unbind",
|
||||
"is_undone": true,
|
||||
"created_at": "2026-05-27T13:00:00Z",
|
||||
"undone_at": "2026-05-27T14:30:00Z"
|
||||
},
|
||||
{
|
||||
"history_id": 50,
|
||||
"operation": "bind",
|
||||
"is_undone": false,
|
||||
"created_at": "2026-05-27T12:00:00Z",
|
||||
"undone_at": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `total` | integer | Total bind history records for this identity |
|
||||
| `undo_stack_count` | integer | Records available for undo (`is_undone=false`) |
|
||||
| `redo_stack_count` | integer | Records available for redo (`is_undone=true`) |
|
||||
| `results[].history_id` | integer | History record ID |
|
||||
| `results[].operation` | string | Operation type (`bind`, `unbind`, or `bind_trace`) |
|
||||
| `results[].is_undone` | boolean | Whether the operation has been undone |
|
||||
| `results[].created_at` | string | When the operation was applied |
|
||||
| `results[].undone_at` | string | When the undo occurred (null if not undone) |
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s "$API/api/v1/identity/$IDENTITY_UUID/bind/history?page=1&limit=10" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `404` | Identity not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 3. Merge History & Undo/Redo
|
||||
|
||||
Merge operations use MongoDB for richer record-keeping, with a 24-hour undo deadline.
|
||||
|
||||
#### Merge Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | MongoDB `identity_merge_history` collection |
|
||||
| Snapshot | Full source identity state + target identity state + aliases/metadata diffs |
|
||||
| Trigger | Every mergeinto with `keep_history=true` |
|
||||
| Undo deadline | 24 hours (renewed on redo) |
|
||||
| Redo support | Yes — restores undone merges with new 24hr deadline |
|
||||
| Max records | Unlimited |
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/merge/:merge_id/undo`
|
||||
|
||||
Already documented in [`07_identity.md`](07_identity.md#post-apiv1identitymergemerge_idundo). See that document for full details.
|
||||
|
||||
---
|
||||
|
||||
#### `POST /api/v1/identity/merge/:merge_id/redo`
|
||||
|
||||
**Auth**: Required
|
||||
**Scope**: identity-level
|
||||
|
||||
Redo a previously undone merge operation within the renewed 24-hour deadline.
|
||||
|
||||
##### Request
|
||||
|
||||
No body required. The merge ID is taken from the URL path.
|
||||
|
||||
##### Behavior
|
||||
|
||||
1. Validates the merge record exists and `undone=true` (not already active)
|
||||
2. Checks the 24-hour undo deadline (if expired, the redo is rejected)
|
||||
3. Restores face bindings: moves all faces from `target_identity` back to `source_identity`
|
||||
4. Re-adds aliases that were removed by the undo (aliases with `source: "merge"` tag)
|
||||
5. Re-adds metadata fields that were removed by the undo
|
||||
6. If `keep_history=true`: sets `source_identity.status = 'merged'` again
|
||||
7. If `keep_history=false`: recreates source identity from the `undone_snapshot` stored at undo time
|
||||
8. Syncs both identity JSON files to disk
|
||||
9. Sets `undone=false`, clears `undone_snapshot`, renews `undo_deadline = NOW() + 24h`
|
||||
10. Records `redone_by` user for audit
|
||||
|
||||
##### Example
|
||||
|
||||
```bash
|
||||
curl -s -X POST "$API/api/v1/identity/merge/550e8400-e29b-41d4-a716-446655440000/redo" \
|
||||
-H "X-API-Key: $KEY"
|
||||
```
|
||||
|
||||
##### Response (200)
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": "Redo merge completed: merged 'stranger_13894' into 'Louis Viret' (52 faces transferred)",
|
||||
"data": {
|
||||
"merge_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"faces_transferred": 52,
|
||||
"aliases_re_added": 1,
|
||||
"metadata_fields_re_added": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| `merge_id` | string | The merge operation ID |
|
||||
| `faces_transferred` | integer | Number of faces transferred from source to target |
|
||||
| `aliases_re_added` | integer | Number of aliases restored to target |
|
||||
| `metadata_fields_re_added` | integer | Number of metadata fields restored to target |
|
||||
|
||||
##### Error Responses
|
||||
|
||||
| HTTP | When |
|
||||
|------|------|
|
||||
| `400` | Merge not undone, deadline expired, or cannot redo |
|
||||
| `404` | Merge record not found |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### 4. Delete History & Undo/Redo
|
||||
|
||||
#### Delete Operation Overview
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| Storage | PostgreSQL `identity_history` table |
|
||||
| Snapshot | `{"identity": {...full row...}, "unbound_faces": [{file_uuid, face_id, trace_id}, ...]}` |
|
||||
| Max records | 1 active delete record per identity (redo stack cleared on new delete) |
|
||||
| Undo support | Yes — recreates identity row, re-binds faces |
|
||||
| Redo support | Yes — re-deletes the identity |
|
||||
| Identity file | Deleted on delete, recreated on undo |
|
||||
|
||||
#### Snapshot Format
|
||||
|
||||
```json
|
||||
{
|
||||
"identity": {
|
||||
"id": 9,
|
||||
"uuid": "a9a90105-6d6b-46ff-92da-0c3c1a57dff4",
|
||||
"name": "Cary Grant",
|
||||
"identity_type": "people",
|
||||
"source": "tmdb",
|
||||
"status": "confirmed",
|
||||
"metadata": {},
|
||||
"tmdb_id": 112,
|
||||
"tmdb_profile": null
|
||||
},
|
||||
"unbound_faces": [
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_5",
|
||||
"trace_id": null
|
||||
},
|
||||
{
|
||||
"file_uuid": "aeed71342a899fe4b4c57b7d41bcb692",
|
||||
"face_id": "1_6",
|
||||
"trace_id": 906
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Stack Model
|
||||
|
||||
```
|
||||
DELETE identity (undo stack, is_undone=false)
|
||||
↓ undo
|
||||
Identity recreated, faces re-bound
|
||||
→ delete history marked is_undone=true
|
||||
↓ redo (re-delete)
|
||||
Identity deleted again, faces unbound
|
||||
→ delete history marked is_undone=false
|
||||
```
|
||||
|
||||
A new delete after an undo clears the delete redo stack (no redo possible for the old delete).
|
||||
|
||||
#### Undo Behavior (via existing `POST /api/v1/identity/:identity_uuid/undo`)
|
||||
|
||||
1. Normal identity lookup fails (row was deleted)
|
||||
2. Checks `identity_history` for `operation='delete' AND is_undone=false` matching the UUID in the snapshot
|
||||
3. Recreates the identity row (new internal `id`, same UUID)
|
||||
4. Re-binds all faces listed in `unbound_faces` to the new identity
|
||||
5. Deletes the `identity_history` delete record as `is_undone=true` with `undone_at=NOW()`
|
||||
6. Syncs `identity.json` to disk
|
||||
7. Updates `_index.json`
|
||||
|
||||
#### Redo Behavior (via existing `POST /api/v1/identity/:identity_uuid/redo`)
|
||||
|
||||
1. Identity lookup succeeds (identity was restored by prior undo)
|
||||
2. Checks `identity_history` for `operation='delete' AND is_undone=true` matching the identity_id
|
||||
3. Deletes `identity.json` from disk
|
||||
4. Unbinds all faces (`identity_id = NULL`)
|
||||
5. Deletes the identity row
|
||||
6. Marks the delete history record as `is_undone=false`
|
||||
7. Returns success
|
||||
|
||||
#### Error Responses (delete undo/redo)
|
||||
|
||||
| HTTP | Scenario |
|
||||
|------|----------|
|
||||
| `400` | No delete history available (either no delete or already undone/redone) |
|
||||
| `404` | Identity not found (for redo — identity wasn't restored) |
|
||||
| `500` | Database error |
|
||||
|
||||
---
|
||||
|
||||
### Comparison: PATCH vs Bind vs Merge vs Delete Undo/Redo
|
||||
|
||||
| Aspect | PATCH Undo/Redo | Bind Undo/Redo | Merge Undo/Redo | Delete Undo/Redo |
|
||||
|--------|----------------|----------------|-----------------|------------------|
|
||||
| Storage | PostgreSQL `identity_history` | PostgreSQL `identity_history` | MongoDB `identity_merge_history` | PostgreSQL `identity_history` |
|
||||
| Operation filter | `operation='update'` | `operation IN ('bind','unbind','bind_trace')` | — | `operation='delete'` |
|
||||
| Trigger | Every PATCH | Every bind/unbind/bind_trace | Every mergeinto with `keep_history=true` | Every DELETE |
|
||||
| Undo deadline | None (unlimited) | None (unlimited) | 24 hours (renewed on redo) | None (unlimited) |
|
||||
| Redo support | Yes | Yes | Yes | Yes |
|
||||
| Step undo | Yes (`steps` param) | Yes (`steps` param) | No (full undo/redo only) | No (single record) |
|
||||
| Max records | 256 per identity | 256 per identity (shared) | Unlimited | 256 per identity (shared) |
|
||||
| User tracking | `user_id` + `user_source` | `user_id` + `user_source` | `performed_by_user` + `undone_by` / `redone_by` | `user_id` + `user_source` |
|
||||
|
||||
---
|
||||
|
||||
*Updated: 2026-05-28*
|
||||
Binary file not shown.
28
migrations/033_create_identity_history_table.sql
Normal file
28
migrations/033_create_identity_history_table.sql
Normal file
@@ -0,0 +1,28 @@
|
||||
-- Migration: Create identity_history table for undo/redo support
|
||||
-- Description: Stores PATCH operation history for identity undo/redo functionality
|
||||
-- Date: 2026-05-28
|
||||
|
||||
-- Create identity_history table
|
||||
CREATE TABLE IF NOT EXISTS identity_history (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
identity_id INTEGER NOT NULL REFERENCES identities(id) ON DELETE CASCADE,
|
||||
operation VARCHAR(20) NOT NULL, -- 'update', 'create', 'delete'
|
||||
before_snapshot JSONB, -- 操作前完整狀態
|
||||
after_snapshot JSONB, -- 操作後完整狀態
|
||||
is_undone BOOLEAN DEFAULT FALSE, -- 是否已被 undo
|
||||
undone_at TIMESTAMPTZ, -- undo 時間
|
||||
user_id VARCHAR(100), -- 操作者
|
||||
user_source VARCHAR(50), -- 'wordpress', 'api', 'cli'
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create indexes for efficient undo/redo operations
|
||||
CREATE INDEX IF NOT EXISTS idx_identity_history_identity_time
|
||||
ON identity_history(identity_id, created_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_identity_history_not_undone
|
||||
ON identity_history(identity_id, created_at DESC)
|
||||
WHERE NOT is_undone;
|
||||
|
||||
-- Add comment
|
||||
COMMENT ON TABLE identity_history IS 'Stores identity PATCH operation history for undo/redo support. Max 256 records per identity.';
|
||||
23
migrations/034_processor_state_machine.sql
Normal file
23
migrations/034_processor_state_machine.sql
Normal file
@@ -0,0 +1,23 @@
|
||||
-- Migration: 034_processor_state_machine
|
||||
-- Purpose: Add processor_alerts table for State Machine alert mechanism
|
||||
-- Date: 2026-05-30
|
||||
|
||||
-- Create processor_alerts table
|
||||
CREATE TABLE IF NOT EXISTS processor_alerts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
file_uuid VARCHAR(32),
|
||||
processor_type VARCHAR(32) NOT NULL,
|
||||
alert_type VARCHAR(32) NOT NULL,
|
||||
message TEXT,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Create indexes for efficient querying
|
||||
CREATE INDEX IF NOT EXISTS idx_alerts_file_uuid ON processor_alerts(file_uuid);
|
||||
CREATE INDEX IF NOT EXISTS idx_alerts_processor_type ON processor_alerts(processor_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_alerts_alert_type ON processor_alerts(alert_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_alerts_created_at ON processor_alerts(created_at);
|
||||
|
||||
-- Add comments
|
||||
COMMENT ON TABLE processor_alerts IS 'Processor state machine alerts for dependency/resource/output issues';
|
||||
COMMENT ON COLUMN processor_alerts.alert_type IS 'Alert types: dependency_not_met, resource_exhausted, output_invalid, timeout';
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user