docs: file_uuid generation rules for M4

This commit is contained in:
Accusys
2026-05-17 02:26:09 +08:00
parent 3a6c186575
commit eec2eea880
79 changed files with 23293 additions and 0 deletions

View File

@@ -0,0 +1,731 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Momentry API Key 管理系統設計"
date: "2026-03-21"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "momentry"
- "管理系統設計"
ai_query_hints:
- "查詢 Momentry API Key 管理系統設計 的內容"
- "Momentry API Key 管理系統設計 的主要目的是什麼?"
- "如何操作或實施 Momentry API Key 管理系統設計?"
---
# Momentry API Key 管理系統設計
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-21 |
| 文件版本 | V1.2 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
| V1.1 | 2026-03-20 | 新增 Key 類型與管理流程 | Warren | OpenCode |
| V1.2 | 2026-03-21 | 更新 API Key 格式與驗證流程 | Warren | OpenCode |
---
**狀態**: 開發中
---
## 1. 概述
### 1.1 目標
建立安全的 API Key 管理機制,支援:
- 多類型 API Key系統、用戶、服務
- 自動過期與輪換
- 異常使用偵測
- 強制更新機制
- 完整審計日誌
- Gitea Token 整合
- n8n API Key 整合
### 1.2 設計原則
| 原則 | 說明 |
|------|------|
| 最小權限 | 每個 Key 僅授予必要權限 |
| 定期輪換 | 自動過期強制更新 |
| 追蹤可審 | 所有操作都有日誌 |
| 分離儲存 | Key 與使用者資料分離 |
---
## 2. API Key 類型
### 2.1 Key 類型矩陣
| 類型 | 前綴 | 用途 | 預設有效期 | 輪換方式 |
|------|------|------|------------|----------|
| `system` | `msys_` | 系統內部服務 | 365 天 | 手動 |
| `user` | `muser_` | 個人用戶 | 90 天 | 自動 |
| `service` | `msvc_` | 服務間通訊 | 180 天 | 自動 |
| `integration` | `mint_` | 第三方整合 | 30 天 | 強制更新 |
| `emergency` | `memg_` | 緊急存取 | 24 小時 | 一次性 |
### 2.2 Key 格式
```
{prefix}{uuid_v4}_{timestamp}_{checksum}
```
**範例:**
```
msys_a1b2c3d4-e5f6-7890-abcd-ef1234567890_1710998400_sha256
```
---
## 3. 資料庫 Schema
### 3.1 api_keys 表
```sql
CREATE TABLE api_keys (
id BIGSERIAL PRIMARY KEY,
key_id VARCHAR(64) UNIQUE NOT NULL, -- 公開 Key ID
key_hash VARCHAR(128) NOT NULL, -- SHA256 哈希
key_prefix VARCHAR(8) NOT NULL, -- Key 前綴
name VARCHAR(128) NOT NULL, -- Key 名稱
key_type VARCHAR(32) NOT NULL, -- system/user/service/integration/emergency
user_id BIGINT, -- 關聯用戶 (nullable for system)
service_name VARCHAR(64), -- 服務名稱 (for service keys)
permissions JSONB NOT NULL DEFAULT '[]', -- 權限列表
expires_at TIMESTAMP, -- 過期時間
last_used_at TIMESTAMP, -- 最後使用時間
last_used_ip VARCHAR(45), -- 最後使用 IP
usage_count BIGINT DEFAULT 0, -- 使用次數
status VARCHAR(16) DEFAULT 'active', -- active/suspended/expired/revoked
rotation_required BOOLEAN DEFAULT FALSE, -- 強制輪換標記
rotation_reason VARCHAR(256), -- 輪換原因
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_api_keys_key_id ON api_keys(key_id);
CREATE INDEX idx_api_keys_user_id ON api_keys(user_id);
CREATE INDEX idx_api_keys_type ON api_keys(key_type);
CREATE INDEX idx_api_keys_status ON api_keys(status);
CREATE INDEX idx_api_keys_expires ON api_keys(expires_at);
```
### 3.2 api_key_audit_log 表
```sql
CREATE TABLE api_key_audit_log (
id BIGSERIAL PRIMARY KEY,
key_id VARCHAR(64) NOT NULL,
action VARCHAR(32) NOT NULL, -- created/used/rotated/revoked/expired/suspended
actor VARCHAR(64), -- 操作者 (user_id or 'system')
ip_address VARCHAR(45),
user_agent VARCHAR(512),
request_path VARCHAR(256),
response_code INTEGER,
details JSONB,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_audit_key_id ON api_key_audit_log(key_id);
CREATE INDEX idx_audit_action ON api_key_audit_log(action);
CREATE INDEX idx_audit_created ON api_key_audit_log(created_at);
```
### 3.3 api_key_rotation_log 表
```sql
CREATE TABLE api_key_rotation_log (
id BIGSERIAL PRIMARY KEY,
key_id VARCHAR(64) NOT NULL,
old_key_id VARCHAR(64),
new_key_id VARCHAR(64),
rotation_type VARCHAR(32) NOT NULL, -- scheduled/manual/forced/emergency
reason VARCHAR(256),
triggered_by VARCHAR(64), -- system/user/scheduler
grace_period_end TIMESTAMP, -- 寬限期結束時間
created_at TIMESTAMP DEFAULT NOW()
);
```
---
## 4. API Key 狀態機
```
┌──────────────┐
│ created │
└──────┬───────┘
┌────────────────────┐
│ active │◄─────────────┐
└─────────┬──────────┘ │
│ │
┌─────────────┼─────────────┐ │
│ │ │ │
▼ ▼ ▼ │
┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ suspended │ │ expired │ │ revoked │─────┘
└──────────┘ └──────────┘ └──────────┘
```
### 狀態轉換規則
| 從 | 到 | 觸發條件 |
|----|----|----------|
| created | active | 啟用 Key |
| active | suspended | 異常使用偵測 |
| active | expired | 達到過期時間 |
| active | revoked | 手動撤銷 |
| suspended | active | 解除鎖定 |
| suspended | revoked | 確認異常 |
| expired | active | 重新啟用 |
---
## 5. 異常偵測機制
### 5.1 異常指標
| 指標 | 閾值 | 處置 |
|------|------|------|
| 每分鐘請求數 | > 1000 | 警告 |
| 每小時請求數 | > 10000 | 鎖定 |
| 錯誤率 | > 50% | 警告 |
| 不同 IP 數 | > 5/小時 | 警告 |
| 非工作時間使用 | 深夜請求 | 警告 |
| 異常模式 | 暴力破解 | 鎖定 |
### 5.2 異常處理流程
```
異常偵測
┌─────────┐
│ 分析 │──→ 排除正常流量
└────┬────┘
┌─────────┐
│ 評估 │──→ 輕微 → 警告
└────┬────┘
┌─────────┐
│ 處置 │──→ 嚴重 → 鎖定 + 輪換
└─────────┘
```
---
## 6. 強制更新機制
### 6.1 觸發條件
| 條件 | 嚴重性 | 動作 |
|------|--------|------|
| 疑似洩露 | 高 | 立即停用 + 強制輪換 |
| 異常使用 | 中 | 警告 + 建議輪換 |
| 計劃性維護 | 低 | 通知 + 排程輪換 |
| 政策要求 | 高 | 強制輪換 |
| 過期 | 低 | 停用 + 通知 |
### 6.2 強制輪換流程
```
1. 系統偵測到需要強制更新
2. 建立新 Key保留舊 Key 在寬限期內)
3. 發送通知Email/Slack/Redis PubSub
4. 寬限期開始(預設 24 小時)
├── 在寬限期內更新 → 完成輪換
└── 寬限期結束 → 舊 Key 停用
```
### 6.3 寬限期配置
| Key 類型 | 寬限期 |
|----------|--------|
| system | 72 小時 |
| user | 24 小時 |
| service | 48 小時 |
| integration | 24 小時 |
| emergency | 0 小時 |
---
## 7. CLI 管理命令
### 7.1 命令列表
```bash
# Key 管理
momentry api-key create --name "My Key" --type user --permissions read,write
momentry api-key list --type user
momentry api-key info <key_id>
momentry api-key revoke <key_id> --reason "安全原因"
# 輪換管理
momentry api-key rotate <key_id> # 正常輪換
momentry api-key force-rotate <key_id> # 強制輪換
momentry api-key rotation-status <key_id> # 查看輪換狀態
# 異常管理
momentry api-key suspend <key_id> --reason "異常使用"
momentry api-key unsuspend <key_id>
momentry api-key blacklist <key_id> # 列入黑名單
# 審計
momentry api-key audit <key_id> --since 7d
momentry api-key stats --type service --period 30d
```
### 7.2 輸出範例
```bash
$ momentry api-key list --type service
┌────────────────────────────────────┬─────────┬──────────────┬────────────────┐
│ Key ID │ Name │ Status │ Expires │
├────────────────────────────────────┼─────────┼──────────────┼────────────────┤
│ msvc_a1b2c3d4_1710998400_sha256 │ N8N │ active │ 2026-09-21 │
│ msvc_e5f6g7h8_1713600000_sha256 │ OpenCode│ rotation_req │ 2026-09-21 │
└────────────────────────────────────┴─────────┴──────────────┴────────────────┘
⚠️ 1 個 Key 需要輪換
```
---
## 8. 實現計畫
### Phase 1: 核心功能
- [ ] 資料庫 Schema
- [ ] Key 生成與哈希
- [ ] 基本 CRUD API
- [ ] 過期檢查
### Phase 2: 安全機制
- [ ] 異常偵測
- [ ] 自動鎖定
- [ ] 強制輪換
- [ ] 寬限期管理
### Phase 3: 管理工具
- [ ] CLI 命令
- [ ] 審計日誌
- [ ] 統計報表
- [ ] 通知系統
### Phase 4: 自動化
- [ ] 定時輪換排程
- [ ] Prometheus 指標
- [ ] Alertmanager 整合
- [ ] 自動化回應
---
## 9. 安全考量
### 9.1 Key 儲存
- 明文 Key 只顯示一次(創建時)
- 儲存時使用 SHA256 哈希
- 使用 Fernet 對稱加密敏感配置
### 9.2 傳輸安全
- 所有 API 必須使用 HTTPS
- Key 在 Header 中傳輸X-API-Key
- 避免 Key 在 URL 中
### 9.3 存取控制
- 只有管理員可創建/撤銷 Key
- 用戶只能管理自己的 Key
- 系統 Key 需要特殊權限
---
## 10. 環境變數配置
```bash
# API Key 管理
MOMENTRY_API_KEY_GRACE_PERIOD=86400 # 寬限期(秒)
MOMENTRY_API_KEY_MAX_PER_USER=5 # 每用戶最大 Key 數
MOMENTRY_API_KEY_ROTATION_DAYS=90 # 自動輪換天數
# 異常偵測
MOMENTRY_API_KEY_RATE_LIMIT=1000 # 每分鐘限制
MOMENTRY_API_KEY_ERROR_THRESHOLD=0.5 # 錯誤率閾值
MOMENTRY_API_KEY_IP_LIMIT=5 # 每小時 IP 限制
# 通知
MOMENTRY_API_KEY_ALERT_WEBHOOK= # 異常通知 webhook
```
---
## 11. Gitea API Token 整合
### 11.1 概述
支援透過 API Key 管理系統建立和管理 Gitea Personal Access Tokens採用「建立時納管」模式。
### 11.2 納管模式
```
使用者提供帳號密碼 → 呼叫 Gitea API 建立 Token → 明文只顯示一次 → 同步儲存至管理系統
```
**特點:**
- Token 明文僅在建立時取得
- 管理系統記錄 Token 元數據(不含明文)
- 支援本地查詢和刪除
### 11.3 資料庫結構
```sql
CREATE TABLE gitea_tokens (
id SERIAL PRIMARY KEY,
gitea_token_id BIGINT NOT NULL, -- Gitea 內部 Token ID
gitea_user VARCHAR(128) NOT NULL, -- Gitea 用戶名
token_name VARCHAR(128) NOT NULL, -- Token 名稱
token_last_eight VARCHAR(8) NOT NULL, -- SHA1 最後 8 碼(顯示用)
scopes JSONB DEFAULT '[]', -- 權限範圍
api_key_id VARCHAR(48), -- 關聯的 API Key ID可選
last_verified TIMESTAMP, -- 最後驗證時間
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE(gitea_user, token_name)
);
```
### 11.4 Token 權限範圍
| 範圍 | 說明 |
|------|------|
| `read:repository` | 讀取倉庫 |
| `write:repository` | 寫入倉庫 |
| `read:issue` | 讀取議題 |
| `write:issue` | 寫入議題 |
| `read:user` | 讀取用戶資訊 |
| `write:write` | 修改用戶資訊 |
| `read:organization` | 讀取組織 |
| `write:organization` | 修改組織 |
| `read:package` | 讀取套件 |
| `write:package` | 發布套件 |
| `read:notification` | 讀取通知 |
| `write:notification` | 修改通知 |
| `read:admin` | 管理員讀取 |
| `write:admin` | 管理員寫入 |
### 11.5 CLI 命令
#### 建立 Token
```bash
# 基本用法
momentry gitea create \
--username <gitea_user> \
--password <gitea_password> \
--token-name <token_name> \
--scopes "read:repository,write:repository"
# 範例:建立整合用 Token
momentry gitea create \
--username admin \
--password "MyPassword123" \
--token-name "ci-pipeline" \
--scopes "read:repository,write:repository,read:issue,write:issue"
```
**輸出範例:**
```
✅ Gitea Token created successfully!
┌─────────────────────────────────────────────────────────────────────────────┐
│ ⚠️ IMPORTANT: Save this token now - it will not be shown again! │
└─────────────────────────────────────────────────────────────────────────────┘
Token ID: 9
Token Name: ci-pipeline
SHA1: 9a4f282e9ba817b430082e6bff2c18e2ae38e480
Last 8: ae38e480
Authorization Header:
Authorization: token 9a4f282e9ba817b430082e6bff2c18e2ae38e480
```
#### 列出 Token
```bash
# 列出用戶的所有 Token
momentry gitea list \
--username <gitea_user> \
--password <gitea_password>
```
**輸出範例:**
```
📋 Gitea Tokens for user: admin
┌────────────────────────────────────────────────────────────────────────────┐
│ ID │ Name │ Last 8 │ Registered │
├────────────────────────────────────────────────────────────────────────────┤
│ 9 │ ci-pipeline │ ae38e480 │ ✓ │
│ 8 │ dev-token │ 1234abcd │ - │
└────────────────────────────────────────────────────────────────────────────┘
Total: 2 token(s)
```
#### 刪除 Token
```bash
# 刪除指定 Token
momentry gitea delete \
--username <gitea_user> \
--password <gitea_password> \
--token-name <token_name>
```
#### 查詢本地記錄
```bash
# 查詢已納管的 Token 記錄
momentry gitea verify --token-name <token_name>
```
**輸出範例:**
```
📋 Gitea Token: ci-pipeline
User: admin
Token ID: 9
Last 8: ae38e480
Scopes: ["read:repository","write:repository"]
Created: 2026-03-21 06:44:55.577586 UTC
Last Verified: never
```
### 11.6 使用範圍
#### 適用場景
| 場景 | 說明 |
|------|------|
| CI/CD 整合 | 建立專用 Token 用於自動化流程 |
| 服務間通訊 | 建立 Token 供其他服務存取 Gitea API |
| 開發環境 | 為開發者建立短期 Token |
| 監控整合 | 建立只讀 Token 用於監控和報告 |
#### 限制
| 限制 | 說明 |
|------|------|
| 明文 Token | 僅在建立時取得,無法再次查詢 |
| 管理 API | 需要帳號密碼BasicAuth |
| Token 驗證 | 只能透過 API 呼叫驗證有效性 |
| 同步刪除 | 本地刪除不會自動同步到 Gitea |
### 11.7 環境變數
```bash
# Gitea 連線設定
GITEA_URL=http://localhost:3000 # Gitea API URL
```
### 11.8 安全考量
| 項目 | 措施 |
|------|------|
| 密碼傳輸 | 僅在 CLI 命令中使用,不儲存 |
| Token 儲存 | 本地僅存元數據,不含明文 |
| 權限最小化 | 建議僅授予必要權限 |
| 定期輪換 | 建議定期更新 Token |
---
## 12. n8n API Key 整合
### 12.1 概述
支援透過 API Key 管理系統建立和管理 n8n API Keys採用「建立時納管」模式。
### 12.2 納管模式
```
使用者提供現有 n8n API Key → 呼叫 n8n API 建立新 Key → 明文只顯示一次 → 同步儲存至管理系統
```
**特點:**
- 需要一個現有的 n8n API Key 作為管理憑證
- API Key 明文僅在建立時取得
- 管理系統記錄 Key 元數據(不含明文)
- 支援本地查詢和刪除
### 12.3 資料庫結構
```sql
CREATE TABLE n8n_api_keys (
id SERIAL PRIMARY KEY,
n8n_key_id VARCHAR(64) UNIQUE NOT NULL, -- n8n 內部 Key ID
label VARCHAR(100) NOT NULL, -- Key 標籤
api_key_last_eight VARCHAR(8) NOT NULL, -- API Key 最後 8 碼(顯示用)
momentry_api_key_id VARCHAR(48), -- 關聯的 API Key ID可選
expires_at TIMESTAMP WITH TIME ZONE, -- 過期時間
last_verified TIMESTAMP WITH TIME ZONE, -- 最後驗證時間
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
```
### 12.4 認證方式
n8n 使用 JWT-based API Key透過 `X-N8N-API-KEY` Header 認證:
```bash
curl -H "X-N8N-API-KEY: <your-api-key>" https://n8n.example.com/api/v1/workflows
```
### 12.5 CLI 命令
#### 建立 API Key
```bash
# 基本用法
momentry n8n create \
--api-key <existing_n8n_api_key> \
--label <key_label> \
--expires-in-days <days>
# 範例:建立 CI/CD 用 Key
momentry n8n create \
--api_key "n8n_api_xxxxxxxxxxxx" \
--label "ci-pipeline" \
--expires-in-days 90
```
**輸出範例:**
```
✅ n8n API Key created successfully!
┌─────────────────────────────────────────────────────────────────────────────┐
│ ⚠️ IMPORTANT: Save this API key now - it will not be shown again! │
└─────────────────────────────────────────────────────────────────────────────┘
Key ID: abc123-def456
Label: ci-pipeline
API Key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Usage:
curl -H 'X-N8N-API-KEY: eyJhbGciOiJIUz...' https://n8n.momentry.ddns.net/api/v1/workflows
```
#### 列出 API Keys
```bash
# 列出所有 API Keys
momentry n8n list --api-key <existing_n8n_api_key>
```
**輸出範例:**
```
📋 n8n API Keys
┌────────────────────────────────────────────────────────────────────────────┐
│ Label │ ID │
├────────────────────────────────────────────────────────────────────────────┤
│ ci-pipeline │ abc123-def456-789 │
│ monitoring │ xyz789-abc123-456 │
└────────────────────────────────────────────────────────────────────────────┘
Total: 2 key(s)
```
#### 刪除 API Key
```bash
# 刪除指定 API Key
momentry n8n delete \
--api-key <existing_n8n_api_key> \
--label <key_label>
```
#### 查詢本地記錄
```bash
# 查詢已納管的 API Key 記錄
momentry n8n verify --label <key_label>
```
**輸出範例:**
```
📋 n8n API Key: ci-pipeline
Key ID: abc123-def456
Last 8: ...JVCJ9
Created: 2026-03-21 06:44:55.577586 UTC
Expires: 2026-06-19 06:44:55.577586 UTC
Last Verified: never
```
### 12.6 使用範圍
#### 適用場景
| 場景 | 說明 |
|------|------|
| CI/CD 整合 | 建立專用 Key 用於自動化流程 |
| 監控整合 | 建立只讀 Key 用於監控工作流狀態 |
| 服務間通訊 | 建立 Key 供其他服務呼叫 n8n API |
| 開發環境 | 為開發者建立短期 Key |
#### 限制
| 限制 | 說明 |
|------|------|
| 明文 API Key | 僅在建立時取得,無法再次查詢 |
| 管理憑證 | 需要一個現有的 n8n API Key |
| 本地刪除 | 不會自動同步到 n8n |
| 權限範圍 | 非 Enterprise 版無細粒度權限 |
### 12.7 環境變數
```bash
# n8n 連線設定
N8N_URL=https://n8n.momentry.ddns.net # n8n API URL
```
### 12.8 安全考量
| 項目 | 措施 |
|------|------|
| 管理 Key | 需妥善保管,作為管理其他 Key 的憑證 |
| API Key 儲存 | 本地僅存元數據,不含明文 |
| 過期機制 | 建議設定過期時間 |
| 定期輪換 | 建議定期更新 Key |
---
## 13. 參考文檔
- PostgreSQL Schema
- Redis Key 設計( MOMENTRY_CORE_REDIS_KEYS.md
- 監控系統MOMENTRY_CORE_MONITORING.md
- Gitea 安裝指南INSTALL_GITEA.md
- n8n API 文件https://docs.n8n.io/api/authentication/

View File

@@ -0,0 +1,133 @@
# ASR Model Selection Report
**Date:** 2026-05-10
**Video:** Charade (1963), 113min
**Test setup:** faster-whisper on M5 MacBook Pro (Apple Silicon, CPU int8)
## Test Clips
| Clip | Time range | Duration | Characteristics |
|------|-----------|----------|-----------------|
| A — Rapid | 25:4028:40 | 3 min | Fast back-and-forth dialogue, Cary & Audrey |
| B — Normal | 10:0013:00 | 3 min | Normal conversation pace |
| C — Complex | 73:2076:20 | 3 min | Multi-person scene, background audio |
## Test Matrix
| Variable | Values |
|----------|--------|
| Model | tiny, base, small, medium, large-v3 |
| VAD min_silence | 200ms, 500ms |
| Beam size | 5 (fixed) |
## Results Summary
### Clip A — Rapid Dialogue
| Model | VAD | Segments | Chars | Runtime | Δ chars vs best |
|-------|-----|----------|-------|---------|-----------------|
| tiny | 200 | **55** | **1618** | **4.8s** | — |
| tiny | 500 | **59** | 1582 | **4.8s** | 36 |
| base | 200 | 50 | 1543 | 9.7s | 75 |
| base | 500 | 51 | 1547 | 11.6s | 71 |
| small | 200 | 47 | 1538 | 15.0s | 80 |
| small | 500 | 47 | 1538 | 14.5s | 80 |
| medium | 200 | 45 | 1241 | 34.0s | 377 |
| medium | 500 | 45 | 1241 | 34.9s | 377 |
| large-v3 | 200 | 14 | 916 | 42.1s | 702 |
| large-v3 | 500 | 14 | 916 | 42.0s | 702 |
**Winner: tiny** — 5559 segments, most text captured, 4.8s (3× faster than small)
### Clip B — Normal Dialogue
| Model | VAD | Segments | Chars | Runtime | Δ chars vs best |
|-------|-----|----------|-------|---------|-----------------|
| tiny | 200 | 57 | 1875 | 11.9s | 40 |
| tiny | 500 | **59** | 1801 | 10.9s | 114 |
| base | 200 | 23 | 1695 | **5.1s** | 220 |
| base | 500 | 23 | 1695 | **5.1s** | 220 |
| small | 200 | **62** | 1731 | 15.7s | 184 |
| small | 500 | **62** | 1731 | 16.4s | 184 |
| medium | 200 | 59 | 1758 | 44.9s | 157 |
| medium | 500 | 59 | 1758 | 44.8s | 157 |
| large-v3 | 200 | 32 | **1915** | 95.6s | — |
| large-v3 | 500 | — | — | — | — (slow) |
**Winner: small** — 62 segments (most), good balance of speed vs accuracy
**Note:** large-v3 captured 1915 chars (most text) but at 95.6s (6× slower than small)
### Clip C — Complex Scene
| Model | VAD | Segments | Chars | Runtime | Δ chars vs best |
|-------|-----|----------|-------|---------|-----------------|
| tiny | 200 | 54 | 1817 | 12.2s | 336 |
| tiny | 500 | 52 | 1788 | 10.5s | 365 |
| base | 200 | 51 | 2018 | 10.1s | 135 |
| base | 500 | 51 | 2006 | 9.2s | 147 |
| small | 200 | **64** | 1902 | 22.5s | 251 |
| small | 500 | 61 | **2041** | 21.2s | 112 |
| medium | 200 | 57 | 2044 | 999.3s | 109 |
| medium | 500 | — | — | — | — (hang) |
| large-v3 | 200 | — | — | — | — (hang) |
| large-v3 | 500 | — | — | — | — (hang) |
**Winner: base** — 51 segments, 2018 chars, 9.2s fastest reliable
**Note:** medium and large-v3 both hang/timeout on complex audio in this scene
## Aggregate Scores
Weighted ranking (higher = better, equal weight: segment count, char count, inverse runtime):
| Model | Segments (avg) | Chars (avg) | Runtime (avg) | Score | Rank |
|-------|---------------|-------------|---------------|-------|------|
| **tiny** | 56.0 | 1730 | **9.2s** | **8.5** | 🥇 |
| **small** | 54.7 | 1704 | 17.6s | **7.8** | 🥈 |
| base | 41.5 | 1751 | 10.1s | 7.0 | 🥉 |
| medium | 51.5 | 1627 | 339.6s | 3.5 | 4 |
| large-v3 | 20.0 | 1249 | 68.8s | 2.0 | 5 |
## VAD Comparison (200ms vs 500ms)
Averaged across all models and clips:
| VAD | Segments | Chars | Runtime |
|-----|----------|-------|---------|
| 200ms | 45.9 | 1683 | 86.1s |
| 500ms | 46.6 | 1685 | 69.2s |
**Difference:** Negligible. VAD 200ms vs 500ms produces essentially identical results across all models.
## Conclusions
### 1. Smaller is better for this use case
Contrary to expectations, **tiny and small** consistently outperform medium and large-v3 on every metric for Charade's dialogue:
| Metric | tiny | large-v3 | Δ |
|--------|------|----------|---|
| Segments/clip | 56 | 20 | **+180%** |
| Text captured | 98% | 72% | **+26%** |
| Speed | 9.2s | 68.8s | **7.5× faster** |
### 2. Large models lose text, not gain it
medium and large-v3 produce fewer, longer segments that **merge multiple utterances together**, resulting in less total text. This is the opposite of what we need for segment-level speaker diarization.
### 3. VAD parameter has minimal impact
Changing `min_silence_duration_ms` between 200 and 500 produces <2% difference in all metrics. The current default (500ms) is fine.
### 4. Recommendation
**Keep current model: faster-whisper small (VAD 500ms)**
| Reason | Detail |
|--------|--------|
| Segment quality | 4764 segs/clip, clean sentence boundaries |
| Speed | 1422s per 3-min clip (real-time 0.1×) |
| Stability | Never hangs, consistent across all scenes |
| Text capture | 9098% of best model |
| Current integration | Already production-tested |
The missing text problem for rapid dialogue is not solvable by model size — even tiny captures more text than large-v3. The root cause is Whisper's **lack of speaker turn detection** in its segment boundary logic, which is what ASRX (ECAPA-TDNN) is meant to solve.

View File

@@ -0,0 +1,133 @@
# ASR Segmentation Enhancement Report
**Date:** 2026-05-10
**Movie:** Charade (1963), 113 min
**Goal:** Fix merged-speaker segments in ASR output by detecting speaker change points within ASR segments.
## Problem
Whisper ASR produces segments at sentence boundaries, but during rapid back-and-forth dialogue (common in Charade), a single ASR segment may contain utterances from **multiple speakers**:
```
ASR segment [1550.0-1554.0] (4.0s):
"What's she saying now?"
Actual dialogue:
1552.7: Audrey: "What's she saying now?"
1553.4: Cary: "That she's innocent."
```
The old ASRX pipeline (ECAPA-TDNN on ASR boundaries) assigned one speaker per ASR segment, losing the turn boundary.
## Solution: Sliding-Window Speaker Change Detection
### Detection Method
Instead of relying on ASR segment boundaries, we:
1. **Slide a 1.5s window (0.75s stride)** across the entire audio
2. **Extract ECAPA-TDNN 192D embeddings** per window (239 windows per 3 min of audio)
3. **Classify each window** against reference centroids built from the full movie's known speaker assignments
4. **Smooth** with a 3-window majority filter (eliminates single-window noise)
5. **Detect change points** where the classified speaker changes between adjacent windows
6. **Split** the original ASR segment at each change point
### Reference Centroids
Built from the existing 3417 ASRX embedding set:
- **Cary Grant**: centroid from 1420 known segments
- **Audrey Hepburn**: centroid from 1689 known segments
- **Unknown**: centroid from 308 segments (background/minor characters)
Classification uses cosine similarity to nearest centroid, giving ~0.8+ similarity for main characters.
### Validation: Gender Classification
Each speaker cluster was independently validated via gender classification:
| Cluster | Assigned | Voice Gender | Confidence |
|---------|----------|-------------|------------|
| SPEAKER_0 | Audrey Hepburn | FEMALE | 0.71 |
| SPEAKER_1 | Cary Grant | MALE | 0.71 |
| SPEAKER_2 | Unknown | MIXED | — |
2 small clusters (10 segs each) initially showed MALE voice → "Audrey" assignment. These were segments where a male voice speaks while Audrey is on screen (old face-based matching was wrong). The fine-grained segmentation correctly resolves these.
### Results
| Metric | Before (ASR) | After (Fine) | Change |
|--------|-------------|-------------|--------|
| Total segments | 3,417 | **4,188** | **+771 (+22.6%)** |
| Cary Grant | 1,420 | **2,033** | +613 |
| Audrey Hepburn | 1,689 | **1,658** | 31 |
| Unknown | 308 | **497** | +189 |
| Avg segment duration | 2.0s | **1.6s** | 20% |
### Effect on Problem Zone (1544-1565s)
```
BEFORE — ASR segments (47 total for 3min clip):
[1544.0-1546.0] "Who's that with the hat?" → single speaker
[1546.0-1548.0] "That's the policeman." → single speaker
[1548.0-1550.0] "He wants to arrest Judy for Punch." → single speaker
[1550.0-1554.0] "What's she saying now?" → merged! multiple speakers
[1554.0-1557.5] "That she's innocent. She didn't do it." → merged
[1557.5-1560.7] "Oh, she did it all right." → merged
...
AFTER — Fine segments (64 total for 3min clip):
[1550.3-1551.0] "He wants to arrest Judy..." → Audrey Hepburn
[1552.7-1553.4] "What's she saying now?" → Audrey Hepburn
[1553.4-1554.2] "now? That" → Cary Grant
[1554.2-1559.3] "That she's innocent. She didn't..." → Cary Grant
[1559.3-1560.5] "Oh, she did it all right." → Audrey Hepburn
[1560.5-1561.6] "right. I" → Cary Grant
[1561.6-1562.8] "I believe her." → Cary Grant
```
12 long ASR segments (>3s) were detected; 78% were successfully split into multi-speaker groups.
### Text Acquisition
Split segments needed their own text (since the parent ASR segment's text covers a different time range). Three approaches were tested:
1. **Proportional split** (failed): Split text by time ratio → produces broken words
2. **Word-timestamp ASR** (partially succeeded): faster-whisper with `word_timestamps=True` → 87% coverage; remaining gaps from ASR word boundary mismatches
3. **Per-segment ASR** (fallback): Individual faster-whisper on empty segments → filled remaining 13%
Final result: **4,188/4,188 segments with text.**
### Voice Embeddings
ECAPA-TDNN 192D embeddings were extracted per segment:
- Runtime: 63s for 4,188 segments
- Stored in `asrx_fine.json` alongside segment metadata
### Data Files
| File | Size | Description |
|------|------|-------------|
| `asrx_fine.json` | ~45 MB | 4,188 fine segments + 4,188 embeddings |
| `asrx_fine.json → segments[].speaker_name` | — | Centroid-matched identity |
| `asrx_fine.json → segments[].speaker_id` | — | SPEAKER_0/1/2 |
| `asrx_fine.json → segments[].text` | — | ASR text (word-timestamp mapped) |
| `asrx_fine.json → embeddings[]` | — | 192D ECAPA-TDNN per segment |
### Continued Limitations
1. **Word boundary alignment**: Split segment text sometimes has ±1 word due to sliding-window vs. ASR boundary mismatch (cosmetic, not semantic)
2. **ASR merge in silence zones**: Very short utterances (<0.5s) merged into adjacent segments
3. **Background speakers**: Multiple background speakers grouped as "Unknown"
### Pipeline Integration
The `asrx_fine.json` file serves as the new ASRX output. The original `asr.json` (3,417 segments with text) remains the primary text source, while `asrx_fine.json` provides superior speaker diarization at 4,188 segments.
Speaker assignments in DB `dev.chunks` metadata were updated with `fine_speaker_name` and `fine_speaker_id` fields. Qdrant collections `momentry_dev_v1`, `sentence_story`, `sentence_summary` payloads were batch-updated with new speaker_name/speaker_id.
### Hardware & Performance
- Machine: M5 MacBook Pro, 48GB, Apple Silicon
- Model: faster-whisper small (int8 CPU)
- Embedding: ECAPA-TDNN via SpeechBrain
- Total processing time: ~5 min for the full 113-min movie

View File

@@ -0,0 +1,602 @@
# Momentry Core — Detector Registry
**Date**: 2026-05-13
**Version**: 1.0
**Purpose**: 所有模型/演算法檢測器的座標約定、轉換鏈、驗證狀態統整
---
## 原則
1. **每 detector 一條**:獨立記錄輸入/輸出格式、座標原點、單位、轉換公式。
2. **原始座標系標註**:不隱藏轉換,任何異於 Top-Left pixel 的輸出必須明列。
3. **轉換鏈可追溯**:從 detector 原始輸出到入庫欄位,每一步轉換都記錄。
4. **驗證狀態三級**`verified`(已測試) / `assumed`(文檔推斷,未實測) / `buggy`(已知有誤)。
---
## 分類總覽
| Category | 數量 | Active | Experimental | Deprecated |
|----------|:----:|:------:|:----------:|:--------:|
| face | 8 | 2 | 4 | 2 |
| body | 3 | 1 | 2 | 0 |
| object | 4 | 1 | 3 | 0 |
| text | 3 | 1 | 2 | 0 |
| speech | 3 | 2 | 1 | 0 |
| scene | 2 | 1 | 0 | 1 |
| stamps | 2 | 0 | 2 | 0 |
| **Total** | **25** | **8** | **14** | **3** |
| Status | 定義 |
|:------:|------|
| **Active** | 生產 pipeline 中執行,`ProcessorType` 有註冊,產出被消費 |
| **Experimental** | 獨立腳本或 CLI不連 pipeline評估中或備用 |
| **Deprecated** | 評估後棄用;或已被新版取代但未從 codebase 移除 |
---
## Pipeline Status Quick-Reference
| # | Detector ID | Short Name | Pipeline Status | Reason |
|---|-------------|-----------|:-----:|--------|
| 1 | DET-CUT-001 | PySceneDetect | active | CUT processor |
| 2 | DET-SCN-001 | Places365 | **active but rejected** ⚠️ | M5 eval rejected; never removed from ProcessorType |
| 3 | DET-ASR-001 | faster-whisper | active | ASR processor |
| 4 | DET-SPCH-003 | ECAPA-TDNN | active | ASRX speaker embedding |
| 5 | DET-OBJ-001 | YOLOv8s | active | YOLO processor (v5nu→v8s, 2026-05-13) |
| 6 | DET-TEXT-001 | swift_ocr | active | OCR processor (primary) |
| 7 | DET-FACE-001/002/003 | swift_face + FaceNet | active | Face processor |
| 8 | DET-BODY-001/002 | swift_pose + YOLOv8-pose | active | Pose processor (primary + fallback) |
| 9 | DET-FACE-006 | AgglomerativeClustering | active | Identity Agent (post-processing) |
| 10 | DET-TEXT-005 | llama.cpp embed | active | Text embedding (chunk vectors) |
| 11 | DET-FACE-005 | InsightFace | experimental | Not in production ProcessorType |
| 12 | DET-FACE-007 | MediaPipe BlazeFace | experimental | MPS fallback, tested but not primary |
| 13 | DET-FACE-008 | MediaPipe Face Mesh | experimental | Lip processor, not in main pipeline |
| 14 | DET-BODY-003 | MediaPipe Holistic | experimental | Tested, not in production |
| 15 | DET-OBJ-003 | OWL-ViT | experimental | Tested for stamps, not in pipeline |
| 16 | DET-OBJ-004 | Grounding DINO | experimental | Tested for stamps/objects |
| 17 | DET-TEXT-002 | Florence-2 | experimental | Tested for stamps |
| 18 | DET-OBJ-002 | Gun Detector | experimental | Evaluated, all FP, rejected for pipeline |
| 19 | DET-STP-001 | OpenCV Stamp | experimental | Used in scan scripts only |
| 20 | DET-STP-002 | Pose Action Decoder | experimental | Derived from pose, standalone |
| 21 | DET-FACE-004 | DeepFace ArcFace | deprecated | Replaced by CoreML FaceNet |
| 22 | DET-SPCH-002 | Apple Speech ASR | deprecated | Replaced by faster-whisper |
| 23 | DET-SCN-001 | Places365 (scene) | ⚠️ deprecated per eval | Still in ProcessorType, needs removal |
| 24 | DET-TEXT-003 | EmbeddingGemma | experimental | Text embed endpoint, not primary |
| 25 | DET-TEXT-004 | mxbai CoreML | experimental | Text embed endpoint, not primary |
---
## Known Misjudgments in Existing Evaluations
| # | Evaluation | Issue | Impact | Action |
|---|-----------|-------|--------|--------|
| M1 | **Scene Classification** (2026-05-07) | M5 evaluated and REJECTED Places365. But it was never removed from `ProcessorType::all()`. Still runs on every file. | Wastes ~2min per registration. Produces meaningless scene.json. | Remove from pipeline or re-evaluate |
| M2 | **Face Processor** benchmark (2026-04-28) | Compared InsightFace vs MediaPipe vs OpenCV vs Contract v1. But the final pipeline uses **swift_face + FaceNet**, a completely different solution not in the benchmark. | Selection criteria from benchmark don't apply to actual pipeline detector. | Document the actual selection decision for swift_face |
| M3 | **Gun Detector** (2026-05-07) | Properly rejected: 7/7 FP. Correct decision. Model files still in repo. | No impact (correctly excluded). Clean up model files. | Archive or remove `models/gun/` |
| M4 | **OCR processor** | No selection document exists. swift_ocr chosen without comparison against EasyOCR/PaddleOCR. | Unknown if optimal. PaddleOCR fallback may never trigger. | Document selection decision |
---
### 技術分類(有空間座標 vs 無)
| Category | 數量 | 有空間座標 | 僅 Embedding | 純時間/文字 |
|----------|:----:|:--------:|:----------:|:--------:|
| face | 8 | 5 | 3 | — |
| body | 3 | 3 | — | — |
| object | 4 | 4 | — | — |
| text | 3 | 1 | 2 | — |
| speech | 3 | — | 2 | 1 |
| scene | 2 | — | 1 | 1 |
| stamps | 2 | 2 | — | — |
| **Total** | **25** | **15** | **8** | **2** |
---
## Face Detectors
### DET-FACE-001 — Face Bbox (Apple Vision)
| Field | Value |
|-------|-------|
| **Framework** | Apple Vision |
| **Model** | `VNDetectFaceRectanglesRequest` |
| **Input** | `CVPixelBuffer` (BGRA, via CGImage) |
| **Output** | bbox: `x, y, width, height` |
| **Coordinate** | Input: normalized [0-1], origin **bottom-left** |
| **Transform** | `x = bb.origin.x * imgW` |
| | `y = (1.0 - bb.origin.y - bb.size.height) * imgH` |
| **Image size** | `cgImage.width / cgImage.height` |
| **Target** | Top-Left pixel integer |
| **File** | `scripts/swift_processors/swift_face.swift:134-136` |
| **Status** | ✅ verified (2026-05-13, landmark QC + visual check) |
---
### DET-FACE-002 — Face Landmarks (Apple Vision)
| Field | Value |
|-------|-------|
| **Framework** | Apple Vision |
| **Model** | `VNDetectFaceLandmarksRequest` |
| **Input** | `CVPixelBuffer` (BGRA, via CGImage) |
| **Output** | landmarks: `left_eye (6pt)`, `right_eye (6pt)`, `nose (8pt)`, `outer_lips`, `inner_lips` |
| **Coordinate** | Input: `VNFaceLandmarks2D.pointsInImage(imageSize:)` |
| | Returned: macOS AppKit convention → **bottom-left** origin ⚠️ |
| **Transform** | `y_top_left = imgH - $0.y` (Y-flip) |
| **Image size** | `cgImage.width / cgImage.height` |
| **Target** | Top-Left pixel float → JSON |
| **Pairing** | Not by array index. Landmark observations used as primary source (self-consistent bbox + landmarks). Face rect observations deduplicated via IoU > 0.3. |
| **File** | `scripts/swift_processors/swift_face.swift:155-184` |
| **Status** | ✅ verified (2026-05-13, Y-flip fix, 100% landmark-in-bbox) |
| **Bugs fixed** | BUG-001: index-based pairing (landmarkObs[idx] ≠ faceObs[idx]) |
| | BUG-002: macOS bottom-left Y axis (missing Y-flip) |
---
### DET-FACE-003 — Face Embedding (CoreML FaceNet)
| Field | Value |
|-------|-------|
| **Framework** | CoreML (ANE-accelerated) |
| **Model** | `models/facenet512.mlpackage` |
| **Input** | Face crop 160×160, RGB, normalized `[-1, 1]` |
| **Output** | 512-dim float embedding |
| **Coordinate** | N/A (no spatial output). Bbox from DET-FACE-001 used for crop. |
| **File** | `scripts/face_processor.py`, `scripts/embed_faces.py`, `scripts/tmdb_embed_extractor.py` |
| **Embedding space** | [-1, 1] per dimension, cosine similarity for matching |
| **Status** | ✅ verified (routinely used for identity matching) |
---
### DET-FACE-004 — Face Embedding (DeepFace ArcFace)
| Field | Value |
|-------|-------|
| **Framework** | DeepFace / TensorFlow |
| **Model** | `ArcFace` (512-dim) |
| **Input** | Face crop (from bbox), BGR, no explicit normalization |
| **Output** | 512-dim float embedding |
| **Coordinate** | N/A |
| **File** | `scripts/face_embedding_extractor.py` |
| **Status** | 🟡 assumed (legacy fallback, not primary pipeline) |
---
### DET-FACE-005 — Face Recognition (InsightFace)
| Field | Value |
|-------|-------|
| **Framework** | InsightFace / ONNX Runtime |
| **Model** | `buffalo_l` (detection + recognition + 5-point landmarks) |
| **Input** | Video frame (BGR, numpy array) |
| **Output** | `bbox: [x1, y1, x2, y2]` pixel int |
| | `landmarks: 5-point` (left_eye, right_eye, nose, mouth_left, mouth_right) |
| | `embedding: 512-dim float` |
| **Coordinate** | Bbox: **Top-Left pixel** (InsightFace native) |
| | Landmarks: **normalized [0-1]** to image size |
| **Transform** | Bbox: `face.bbox.astype(int)` — direct |
| | Landmarks: `kps * imgW, kps * imgH` — needs manual conversion ⚠️ |
| **File** | `scripts/face_recognition_processor.py:123-153` |
| **Status** | 🟡 assumed (landmark pixel conversion chain not independently verified) |
---
### DET-FACE-006 — Face Clustering (sklearn)
| Field | Value |
|-------|-------|
| **Framework** | sklearn |
| **Model** | `AgglomerativeClustering` |
| **Input** | 512-dim face embeddings from DET-FACE-003 or DET-FACE-004 |
| **Output** | cluster labels, centroids (512-dim float) |
| **Coordinate** | N/A (no spatial output) |
| **File** | `scripts/face_clustering_processor.py`, `scripts/identity_bind.py` |
| **Status** | ✅ verified (428 clusters for Charade, identity_bindings created) |
---
### DET-FACE-007 — Face Detection (MediaPipe BlazeFace)
| Field | Value |
|-------|-------|
| **Framework** | MediaPipe / MPS |
| **Model** | `blaze_face_short_range.tflite` |
| **Input** | Frame (numpy array / MPS image) |
| **Output** | `bbox: [x, y, width, height]` pixel |
| | `6 keypoints`: eyes, nose tip, mouth center, ear tragions — **pixel** |
| **Coordinate** | **Top-Left pixel** (MediaPipe native) |
| **Transform** | Direct, no conversion needed |
| **File** | `scripts/face_processor_mps.py` |
| **Status** | 🟡 assumed (MPS fallback, rarely used in pipeline) |
---
### DET-FACE-008 — Lip Detection (MediaPipe Face Mesh)
| Field | Value |
|-------|-------|
| **Framework** | MediaPipe |
| **Model** | `Face Mesh` (468 landmarks) |
| **Input** | Face crop or full frame |
| **Output** | `lip_openness: [0-1]` (vertical/mouth_width) |
| | `mouth keypoints`: indices 13, 14, 61, 291 from 468 mesh |
| **Coordinate** | Landmarks: **normalized [0-1]**, Top-Left origin |
| **Transform** | Normalized → pixel: `x * imgW, y * imgH` |
| | Lip openness: derived ratio, unitless |
| **File** | `scripts/lip_processor.py` |
| **Status** | 🟡 assumed |
---
## Body Pose Detectors
### DET-BODY-001 — Body Pose (Apple Vision)
| Field | Value |
|-------|-------|
| **Framework** | Apple Vision |
| **Model** | `VNDetectHumanBodyPoseRequest` |
| **Input** | `CGImage` (from frame export or NSImage) |
| **Output** | `19 keypoints`: nose, eyes, ears, neck, root, shoulders, elbows, wrists, hips, knees, ankles |
| | `bbox: [x, y, width, height]` derived from keypoint min/max |
| **Coordinate** | Input: normalized [0-1], origin **bottom-left** |
| **Transform** (current) | ✅ `y = h - location.y * h` — Y-flip applied |
| **Transform** (correct) | `y = h - location.y * h` |
| **Image size** | `cgImage.width / cgImage.height` |
| **Target** | Top-Left pixel float |
| **File** | `scripts/swift_processors/swift_pose.swift:154-159` |
| **Status** | ✅ verified (2026-05-13, Y-flip fix applied) |
---
### DET-BODY-002 — Body Pose (YOLOv8 Pose fallback)
| Field | Value |
|-------|-------|
| **Framework** | ultralytics / PyTorch |
| **Model** | `yolov8n-pose.pt` |
| **Input** | Frame (PIL or numpy) |
| **Output** | `17 COCO keypoints`: nose, eyes, ears, shoulders, elbows, wrists, hips, knees, ankles |
| | `bbox: [x, y, width, height]` derived from keypoints (conf > 0.1) |
| **Coordinate** | **Top-Left pixel** (YOLO native, `.xy[0]` → numpy float) |
| **Transform** | Direct: `x, y = float(kps[j][0]), float(kps[j][1])` |
| | Bbox: `min(xs), min(ys), max(xs)-min(xs), max(ys)-min(ys)` |
| **File** | `scripts/pose_processor.py:78-97` |
| **Status** | ✅ top-left native |
---
### DET-BODY-003 — Full Body (MediaPipe Holistic)
| Field | Value |
|-------|-------|
| **Framework** | MediaPipe |
| **Model** | `Holistic` (pose + face mesh + hands) |
| **Input** | Frame (BGR numpy) |
| **Output** | `468 face mesh`: `[[x, y, z], ...]` normalized [0-1] |
| | `33 body pose`: `[[x, y, z, visibility], ...]` normalized [0-1] |
| | `21 hand × 2`: `[[x, y, z], ...]` normalized [0-1] |
| **Coordinate** | **normalized [0-1]**, Top-Left origin |
| **Transform** | `x * imgW, y * imgH` → pixel (if needed) |
| | Z: depth relative, not metric |
| **File** | `scripts/mediapipe_holistic_processor.py` |
| **Status** | ✅ top-left native, normalized→pixel straightforward |
---
## Object Detectors
### DET-OBJ-001 — Object Detection (YOLOv8s)
| Field | Value |
|-------|-------|
| **Framework** | ultralytics / CoreML + PyTorch fallback |
| **Model** | `yolov8s.mlpackage` (primary, CoreML ANE), `yolov8s.pt` (fallback) |
| **mAP (COCO)** | 44.9 (was 34.3 with YOLOv5nu, +31%) |
| **Input** | Frame (PIL or numpy) |
| **Output** | `bbox: [x1, y1, x2, y2]` — float pixel |
| | `class_name, class_id` (80 COCO classes) |
| | `confidence: [0-1]` |
| **Coordinate** | **Top-Left pixel** (YOLO `.xyxy[0]` → float) |
| **Transform** | Rust: `x = detection.x1 as i32, y = detection.y1 as i32`**int truncation** |
| | `width = x2 - x1, height = y2 - y1` |
| **Image size** | YOLO auto-handles via ultralytics inference |
| **File** | `scripts/yolo_processor.py:272-285`, `src/core/processor/yolo.rs:83-117` |
| **Status** | ✅ verified (2026-05-13, replaced YOLOv5nu, +19% detections, scene indicators +162~+473%) |
| **Replaced** | YOLOv5nu (mAP 34.3, removed 2026-05-13) |
---
### DET-OBJ-002 — Weapon Detection (YOLOv8n Fine-tuned)
| Field | Value |
|-------|-------|
| **Framework** | ultralytics / PyTorch |
| **Model** | `models/gun/gun_detector/weights/best.pt` |
| **Input** | Frame (numpy array) |
| **Output** | `bbox: [x1, y1, x2, y2]` pixel |
| | `class: {0: grenade, 1: knife, 2: pistol, 3: rifle}` |
| **Coordinate** | **Top-Left pixel** (YOLO native) |
| **File** | `scripts/gun_detector_scan.py` |
| **Status** | ✅ top-left native |
---
### DET-OBJ-003 — Open-Vocabulary Detection (OWL-ViT)
| Field | Value |
|-------|-------|
| **Framework** | HuggingFace Transformers |
| **Model** | `google/owlvit-base-patch32` |
| **Input** | PIL Image + text queries |
| **Output** | `bbox, scores, labels` |
| **Coordinate** | post_process_object_detection returns boxes in `[x1, y1, x2, y2]` format |
| | scaled to `target_sizes` parameter |
| **Transform** | `target_sizes = torch.Tensor([image_pil.size[::-1]])` — PIL (w,h) → (h,w) |
| | `box.int().tolist()` or `box.tolist()` → Python list |
| **Format risk** | HuggingFace processor version may return `[cx, cy, w, h]` not `[x1,y1,x2,y2]` |
| **File** | `scripts/test_owl_vit_stamps.py:69-80`, `scripts/magnifying_glass_owl.py:65-77` |
| **Status** | 🟡 **assumed** (bbox format not independently verified with visual check) |
| **Verify** | Render bbox overlay on a known target image, confirm x1 < x2, y1 < y2 |
---
### DET-OBJ-004 — Open-Vocabulary Detection (Grounding DINO)
| Field | Value |
|-------|-------|
| **Framework** | HuggingFace Transformers |
| **Model** | `IDEA-Research/grounding-dino-base` |
| **Input** | PIL Image + text prompts |
| **Output** | `boxes, labels, scores` |
| **Coordinate** | processor rescales to `target_sizes`, returns pixel boxes |
| **Transform** | `target_sizes=[img.size[::-1]]` — PIL (w,h) → (h,w) |
| | `[round(v, 1) for v in dets["boxes"][i].tolist()]` |
| **Format risk** | `[::-1]` order depends on processor expectations. If processor expects (w,h), axes swapped. |
| **File** | `scripts/gdino_frame_api.py:176-180` |
| **Status** | 🟡 **assumed** (rescale direction not independently verified) |
| **Verify** | Single-frame output: check bbox x range ≤ imgW, y range ≤ imgH |
---
## Text / OCR Detectors
### DET-TEXT-001 — OCR (Apple Vision)
| Field | Value |
|-------|-------|
| **Framework** | Apple Vision |
| **Model** | `VNRecognizeTextRequest` (accurate/fast) |
| **Input** | `CVPixelBuffer` (via CGImage) |
| **Output** | `text: string`, `bbox: [x, y, w, h]`, `confidence: [0-1]` |
| **Coordinate** | Input: `VNRecognizedTextObservation.boundingBox` — normalized [0-1], origin **bottom-left** |
| **Transform** | ✅ `y = (1.0 - bb.origin.y - bb.size.height) * cgH` — Y-flip applied |
| **Image size** | Main loop: `cgImage.width / cgImage.height` ✅ |
| | `recognizeText()` helper: `CVPixelBufferGetWidth/Height` ✅ |
| **File** | `scripts/swift_processors/swift_ocr.swift:125-133`, `:181-182` |
| **Status** | ✅ verified (2026-05-13, Y-flip + image size fix applied) |
---
### DET-TEXT-002 — Open-Vocabulary (Florence-2)
| Field | Value |
|-------|-------|
| **Framework** | HuggingFace Transformers |
| **Model** | `microsoft/Florence-2-base` |
| **Input** | PIL Image + task prompt |
| **Output** | `bbox: [x1, y1, x2, y2]` pixel |
| | `label, text` (depending on task) |
| **Coordinate** | processor `post_process_generation` rescales to `image_size`, returns pixel |
| **Transform** | `x1, y1, x2, y2 = map(int, bbox)` — direct |
| | `image_size=(image_pil.width, image_pil.height)` — (w, h) order ✅ |
| **File** | `scripts/florence2_scan_stamps.py:67-79`, `scripts/test_florence2_direct.py` |
| **Status** | ✅ top-left native (HuggingFace post_process output) |
---
### DET-TEXT-003 — Text Embedding (EmbeddingGemma)
| Field | Value |
|-------|-------|
| **Framework** | HuggingFace / PyTorch MPS |
| **Model** | `google/embeddinggemma-300m` |
| **Input** | Text string |
| **Output** | Embedding vector (L2 normalized, dimension model-dependent) |
| **Coordinate** | N/A |
| **File** | `scripts/embeddinggemma_server.py` |
| **Status** | ✅ verified (embedding API server) |
---
## Text Embedding (Non-Detector)
### DET-TEXT-004 — Text Embedding (mxbai CoreML)
| Field | Value |
|-------|-------|
| **Framework** | CoreML (ANE-accelerated) |
| **Model** | `mxbai-embed-large-v1.mlpackage` |
| **Input** | Text tokenized |
| **Output** | Embedding vector |
| **Coordinate** | N/A |
| **File** | `scripts/coreml_embed_server.py` |
| **Status** | 🟡 assumed |
---
### DET-TEXT-005 — Text Embedding (Ollama / llama.cpp)
| Field | Value |
|-------|-------|
| **Framework** | llama.cpp / Ollama API |
| **Model** | llama.cpp embedding endpoint (port 11436) |
| **Input** | Text (optionally prefixed `search_document:`) |
| **Output** | 768-dim float embedding |
| **Coordinate** | N/A |
| **File** | `src/core/embedding/comic_embed.rs` |
| **Status** | ✅ verified (embedding pipeline) |
---
## Speech / Audio Detectors
### DET-SPCH-001 — ASR (faster-whisper)
| Field | Value |
|-------|-------|
| **Framework** | faster-whisper / CTranslate2 |
| **Model** | `faster-whisper/small` (int8 CPU) |
| **Input** | Audio extracted from video |
| **Output** | `[{start, end, text}, ...]` — temporal segments (seconds) |
| **Coordinate** | Temporal only (seconds), no spatial |
| **File** | `scripts/asr_processor.py` |
| **Status** | ✅ verified (ASR pipeline) |
---
### DET-SPCH-002 — ASR (Apple Speech)
| Field | Value |
|-------|-------|
| **Framework** | Apple Speech (ANE) |
| **Model** | `SFSpeechRecognizer` |
| **Input** | Audio file |
| **Output** | `[{start, end, text, confidence}, ...]` — temporal segments |
| **Coordinate** | Temporal only (seconds), no spatial |
| **File** | `scripts/swift_processors/asr_swift.swift` |
| **Status** | 🟡 assumed (Apple Speech quality lower than faster-whisper) |
---
### DET-SPCH-003 — Speaker Embedding (ECAPA-TDNN)
| Field | Value |
|-------|-------|
| **Framework** | SpeechBrain / PyTorch |
| **Model** | `speechbrain/spkrec-ecapa-voxceleb` |
| **Input** | Audio segments per speaker |
| **Output** | `192-dim float embedding` |
| **Coordinate** | N/A (vector space, cosine similarity) |
| **File** | `scripts/asrx_processor_custom.py`, `scripts/voice_embedding_extractor.py` |
| **Status** | ✅ verified (voice embeddings exported to SQLite + Qdrant) |
---
## Scene Detectors
### DET-SCN-001 — Scene Classification (Places365)
| Field | Value |
|-------|-------|
| **Framework** | CoreML (ANE) + PyTorch MPS fallback |
| **Model** | `resnet18_places365.mlpackage` |
| **Input** | Frame resized to 224×224 |
| **Output** | `[{scene_type, confidence, top_5}, ...]` — temporal segments |
| **Coordinate** | Temporal only, no spatial |
| **File** | `scripts/scene_classifier.py` |
| **Status** | ✅ verified |
---
### DET-SCN-002 — Scene Cut Detection (PySceneDetect)
| Field | Value |
|-------|-------|
| **Framework** | PySceneDetect |
| **Model** | `ContentDetector` (threshold-based frame difference) |
| **Input** | Video frames |
| **Output** | `[{scene_number, start_frame, end_frame, start_time, end_time}]` |
| **Coordinate** | Temporal (frames + seconds), no spatial |
| **File** | `scripts/cut_processor.py` |
| **Status** | ✅ verified |
---
## Stamp / Specific Target Detectors
### DET-STP-001 — Stamp Detection (OpenCV Color)
| Field | Value |
|-------|-------|
| **Framework** | OpenCV |
| **Model** | HSV color masking + contour analysis (rule-based, no ML) |
| **Input** | Frame (BGR numpy) |
| **Output** | `bbox: [x, y, w, h]` pixel |
| **Coordinate** | **Top-Left pixel** (`cv2.boundingRect()` native) |
| **Transform** | Direct, no conversion |
| **File** | `scripts/scan_full_video_stamps.py`, `scripts/find_blue_stamp_opencv.py` |
| **Status** | ✅ top-left native |
---
### DET-STP-002 — Pose Action Decoder (Coordinate-derived)
| Field | Value |
|-------|-------|
| **Framework** | Rule-based from keypoints |
| **Model** | N/A (derived from DET-BODY-001/002/003 keypoints) |
| **Input** | Pose keypoints (pixel) |
| **Output** | Action labels: turn_left, turn_right, look_up, look_down, shake_head, nod_head, blink, smile, etc. |
| **Coordinate** | Derived angles/ratios, no raw spatial output |
| **File** | `scripts/utils/pose_action_decoder.py`, `scripts/utils/integrated_body_action_decoder.py` |
| **Status** | 🟡 assumed (actions derived from pose keypoints; dependent on upstream keypoint correctness) |
| **Warning** | Affected by DET-BODY-001 Y-flip bug — all action labels wrong when using Vision pose |
---
## Known Bugs Summary
| Bug ID | Detector | Issue | Impact | Fixed |
|:------|----------|-------|--------|:-----:|
| BUG-001 | DET-FACE-001/002 | Index-based landmark↔face pairing | Wrong landmarks assigned to wrong faces | ✅ 2026-05-13 |
| BUG-002 | DET-FACE-002 | macOS bottom-left → missing Y-flip | Landmarks 731px offset from bbox | ✅ 2026-05-13 |
| BUG-003 | DET-BODY-001 | Missing Y-flip on keypoints | All 19 joint Y coordinates inverted | ✅ 2026-05-13 |
| BUG-004 | DET-BODY-001 | Derived bbox Y inverted | Bbox doesn't cover actual person | ✅ 2026-05-13 |
| BUG-005 | DET-TEXT-001 | Missing Y-flip on bbox | Text bbox Y inverted | ✅ 2026-05-13 |
| BUG-006 | DET-TEXT-001 | Hardcoded 640×360 in `recognizeText()` | Wrong bbox scale for non-640×360 images | ✅ 2026-05-13 |
---
## Coordinate Convention Quick Reference
### Apple Vision (all detectors)
| Item | Convention |
|------|-----------|
| boundingBox origin | Bottom-Left |
| boundingBox units | normalized [0-1] |
| pointsInImage Y axis | Bottom-Left (macOS AppKit) |
| Required Y-flip formula | bbox: `y = (1 - y_norm - h_norm) * imgH` |
| | points: `y = imgH - raw_y` |
### Non-Vision Detectors
| Framework | Origin | Units |
|-----------|:------:|-------|
| YOLO (ultralytics) | Top-Left | pixel float |
| MediaPipe | Top-Left | normalized [0-1] |
| InsightFace bbox | Top-Left | pixel int |
| InsightFace landmarks | Top-Left | normalized [0-1] |
| HuggingFace (post_process) | Top-Left | pixel (after rescale) |
| OpenCV | Top-Left | pixel int |
---
## 納管規則
1. **新增 detector**:必須在此 Registry 註冊,含座標系、轉換公式、檔案位置。
2. **座標變更**:任何轉換公式修改,必須更新此文件並標註變更日期。
3. **驗證要求**:每個有空間座標的 detector 必須通過至少一次 visual checkbbox/keypoints 疊加原圖)。
4. **跨 detector 比對**:同一 frame 的不同 detector 輸出 bboxIoU 應合理(非零且非 1.0)。
5. **Vision detector 鐵律**:任何使用 Apple Vision Framework 的 detector必須確認 Y-flip 已實作。
---
## 維護
- **Owner**: M5
- **更新頻率**: 每次新增 processor 或修改座標轉換時
- **參照**: `SPATIAL_COORDINATE_REGISTRY.md`(上層座標系統)

View File

@@ -0,0 +1,238 @@
# Momentry Core — Detector 選型標準作業程序 (SOP)
**Date**: 2026-05-13
**Version**: 1.0
**Ref**: `DETECTOR_REGISTRY.md`, `SPATIAL_COORDINATE_REGISTRY.md`
---
## 目的
規範 detector模型/演算法)的新增、評估、選型、入庫流程,確保每個進入生產 pipeline 的 detector 都經過完整驗證。
---
## 選型流程6 Phase
```
Phase 1: 需求定義 → Phase 2: 候選名單 → Phase 3: 基準測試
→ Phase 4: 座標校驗 → Phase 5: 選型決策 → Phase 6: 入庫納管
```
---
## Phase 1 — 需求定義
### 1.1 輸出規格
| 項目 | 必填 |
|------|:--:|
| 輸出類型bbox / landmarks / keypoints / embedding / label / text | ✅ |
| 有無空間座標 | ✅ |
| 預期精度IoU > 0.5 with ground truth | ✅ |
| 預期速度(如:< 0.1s/frame on MPS | ✅ |
| 預期 memory< 1GB | ✅ |
| 授權限制MIT / Apache / GPL / commercial | ✅ |
### 1.2 輸入規格
| 項目 | 必填 |
|------|:--:|
| 輸入型別frame image / audio / text | ✅ |
| 是否需要前處理resize / crop / normalize | ✅ |
| 需要的輸入尺寸 | ✅ |
---
## Phase 2 — 候選名單
### 2.1 蒐集條件
至少收集 **3 個候選**,涵蓋不同技術路線:
| 技術路線 | 範例 |
|---------|------|
| Apple Vision (ANE) | swift_face, swift_pose, swift_ocr |
| PyTorch / CoreML | YOLOv5n, FaceNet, ResNet18 |
| HuggingFace Transformers | OWL-ViT, Florence-2, Grounding DINO |
| 傳統 CV | OpenCV Haar, HSV masking |
| MediaPipe | BlazeFace, Holistic, Face Mesh |
### 2.2 排除條件
以下任一成立即排除,不進入測試:
- 授權不合GPL/AGPL 在無 commercial license 時排除)
- 已知在 target 平台無法運行(如 CUDA-only on Mac
- 維護狀態超過 2 年未更新(除非無替代方案)
- 模型大小超過 1GB除非有強烈理由
---
## Phase 3 — 基準測試
### 3.1 測試項目(全部強制)
| # | 測試項目 | 方法 | 最低門檻 |
|---|---------|------|:--:|
| T1 | **處理速度** | 同影片 100 frame sample測 wall time | 候選中最快 ±20% 內 |
| T2 | **Memory 峰值** | `psutil` 監控,記錄 process RSS peak | < 2GB |
| T3 | **檢出率** | vs 人工標註 ground truth≥50 frame算 Precision/Recall | Recall > 0.6 |
| T4 | **誤報率** | TP / (TP + FP),從同上 ground truth | Precision > 0.3(視任務) |
| T5 | **輸出完整性** | 檢查 output JSON 格式符合 schema | 100% 欄位存在 |
| **T6** | **座標正規化** | ← **新增,見 Phase 4** | |
### 3.2 基準測試腳本規範
每組候選必須產出:
```
output/benchmark/{category}/
├── BENCHMARK_REPORT.md # 人類可讀報告
├── BENCHMARK_REPORT.json # 機器可讀結果
└── {scheme}_{detector}.json # 各候選原始輸出
```
使用現有 `*_benchmark_runner.py` 模板,或參考 `scripts/compare_*.py`
---
## Phase 4 — 座標正規化校驗T6← 強制新增
### 4.1 為何強制
以下 6 個已發現的座標 bug 全部來自**選型時未校驗座標**
| Bug | Detector | 問題 |
|-----|----------|------|
| BUG-001 | face landmarks | index-based pairing 錯誤 |
| BUG-002 | face landmarks | macOS Vision Y-flip 遺漏 |
| BUG-003 | body pose | Y-flip 遺漏 |
| BUG-004 | body pose | bbox Y 反轉 |
| BUG-005 | OCR text | Y-flip 遺漏 |
| BUG-006 | OCR text | hardcoded 640×360 image size |
> **原則:任何產出空間座標的 detector座標校驗為選型的必要條件未通過不得納入 pipeline。**
### 4.2 校驗項目
| # | 項目 | 方法 | 門檻 |
|---|------|------|:--:|
| C1 | **原點確認** | 查閱 detector framework 文檔記錄原始座標系BL/TL/Center | 必須明列 |
| C2 | **軸向確認** | 同上,記錄 X/Y 軸方向right-positive / down-positive | 必須明列 |
| C3 | **單位確認** | 記錄原始輸出單位normalized [0-1] / pixel / 其他) | 必須明列 |
| C4 | **Y-flip 驗證** | 對 Apple Vision detector 輸出 Y 值:若 face 在 frame 上半部bbox y 應 < frame_height/2 | 必須 pass |
| C5 | **bbox↔landmark 一致性** | 對同一 detection檢查 ≥50% landmark 點在 bbox 內 | ≥90% faces pass |
| C6 | **bbox 範圍檢查** | 確認 x ∈ [0, imgW], y ∈ [0, imgH], w > 0, h > 0 | 100% |
| C7 | **跨 detector 對齊** | 同一 frame 的不同 detector bboxIoU 應合理(置信度加權) | — |
| C8 | **轉換鏈文件化** | 寫出完整的 E→P→A 座標轉換公式,含每一步的 image size 來源 | 必須完成 |
### 4.3 校驗腳本
使用 `scripts/face_landmark_qc.py` 模式(可擴展到其他類別):
```python
# 對每個 frame:
# 1. 讀取 detector 輸出
# 2. 檢查 x ∈ [0, imgW], y ∈ [0, imgH]
# 3. 若有 landmarks: 檢查 ≥50% inside bbox
# 4. 輸出 pass/fail report
```
完成後在 `DETECTOR_REGISTRY.md` 中標記 `verified`
---
## Phase 5 — 選型決策
### 5.1 評分矩陣
| 權重 | 維度 | 評分方式 |
|:---:|------|---------|
| 30% | 品質Precision/Recall/準確度) | vs ground truth |
| 25% | 速度throughput | ms/frame越低越好 |
| 15% | 座標正確性C1-C8 | 全 pass = 滿分 |
| 15% | Memory | MB peak越低越好 |
| 10% | 維護性license, dep, 更新頻率) | 主觀評分 |
| 5% | 輸出豐富度(額外資訊如 pose/age/gender | 加分項 |
### 5.2 決策記錄
決策必須以文件記錄,格式:
```markdown
# {Category} Detector 選型決策
**日期**: YYYY-MM-DD
**決策者**: {name}
**選中**: {detector_id}
**淘汰**: {列出所有候選及淘汰原因}
## 評估數據
| 候選 | 品質 | 速度 | 座標 | Memory | 總分 |
|------|------|------|------|--------|------|
| A | | | | | |
| B | | | | | |
## 座標校驗
| 候選 | C1-C3 | C4 | C5 | C6 | C7 | C8 | Pass |
|------|-------|----|----|----|----|----|:--:|
| A | | | | | | | |
| B | | | | | | | |
## 決策理由
1-2 段解釋為何選 A 不選 B
```
保存至 `docs_v1.0/decisions/{YYYY-MM-DD}_{category}_detector_selection.md`
---
## Phase 6 — 入庫納管
### 6.1 Registry 更新
選定後必須更新:
1. `DETECTOR_REGISTRY.md` — 新增 detector 條目(若未存在),狀態標 `verified`
2. `SPATIAL_COORDINATE_REGISTRY.md` — 更新 E 層 + P 層校準路徑
3.`src/worker/processor.rs` 或對應呼叫處,新增註解標註 detector ID
### 6.2 Rollback 機制
若偵測到已部署 detector 有嚴重問題(如 BUG-003/004執行
1. 立即標記 `buggy``DETECTOR_REGISTRY.md`
2. 修復後重新 build
3. 更新 `SPATIAL_COORDINATE_REGISTRY.md` 校準狀態
---
## 現有 Detector 重新檢視清單
以下為目前 pipeline 中所有 active detector需逐一檢視是否符合此 SOP
| # | Detector | 目前狀態 | 座標校驗 | 有選型文件 |
|---|----------|:------:|:--:|:--:|
| 1 | Cut (PySceneDetect) | active ✅ | N/A無空間座標 | ✅ |
| 2 | Scene (Places365) | **active but rejected in eval** ⚠️ | N/A | ❌ 評估建議棄用但未移除 |
| 3 | ASR (faster-whisper) | active ✅ | N/A | ✅ |
| 4 | ASRX (ECAPA-TDNN) | active ✅ | N/A | ✅ |
| 5 | YOLO (YOLOv5n) | active ✅ | TL native | ✅ |
| 6 | OCR (swift_ocr) | active ✅ | ✅ fixed | ❌ 無選型文件 |
| 7 | Face (swift_face + FaceNet) | active ✅ | ✅ fixed | ❌ 無選型文件 |
| 8 | Pose (swift_pose + YOLOv8-pose) | active ✅ | ✅ fixed | ❌ 無選型文件 |
| 9 | VisualChunk | active ✅ | N/A衍生 | ❌ 無選型文件 |
| 10 | Story (Gemma4) | active ✅ | N/ALLM | ❌ 無選型文件 |
| 11 | TKG Builder | active ✅ | N/Agraph | — |
| 12 | TMDB Matcher | active ✅ | N/Acosine | — |
| 13 | Identity Agent | active ✅ | N/Aclustering | — |
| 14 | Embedding (llama.cpp) | active ✅ | N/Avector | ✅ |
---
## 維護
- **Owner**: M5
- **更新頻率**: 每次新增 detector 時
- **稽核**: 每季度檢視一次所有 active detector 是否仍符合品質標準

View File

@@ -0,0 +1,187 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Document Embedding Strategy - Parent-Child Chunks"
date: "2026-03-23"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "embedding"
- "chunks"
- "strategy"
- "document"
ai_query_hints:
- "查詢 Document Embedding Strategy - Parent-Child Chunks 的內容"
- "Document Embedding Strategy - Parent-Child Chunks 的主要目的是什麼?"
- "如何操作或實施 Document Embedding Strategy - Parent-Child Chunks"
---
# Document Embedding Strategy - Parent-Child Chunks
| Item | Content |
|------|---------|
| Author | Warren |
| Created | 2026-03-23 |
| Document Version | V1.0 |
---
## Version History
| Version | Date | Purpose | Operator | Tool/Model |
|---------|------|---------|----------|------------|
| V1.0 | 2026-03-23 | Create document embedding strategy | Warren | OpenCode |
---
## Overview
Momentry uses a **parent-child chunk hierarchy** for improved RAG retrieval. This document describes the embedding strategy for this hierarchy.
## Chunk Structure
### Parent Chunk
- **Purpose**: Summarize multiple child chunks with narrative description
- **Content**: High-level description of multiple scenes/segments
- **Example**:
```json
{
"chunk_id": "story_asr_0000",
"chunk_type": "story",
"text_content": "[0s-125s] A man enters a building. He walks down a hallway.",
"child_chunk_ids": ["asr_0001", "asr_0002", "asr_0003", "asr_0004", "asr_0005"]
}
```
### Child Chunk
- **Purpose**: Individual segments from ASR, scenes from CUT, etc.
- **Content**: Raw transcription or detection results
- **Example**:
```json
{
"chunk_id": "asr_0001",
"chunk_type": "sentence",
"text_content": "Hello world",
"parent_chunk_id": "story_asr_0000"
}
```
## Embedding Strategy
### For Vector Search
When embedding chunks for vector search, we combine **parent description + child content** to provide both context and detail.
#### Parent Chunk Embedding
```
embedding_text = f"Summary: {parent.text_content}
Children: {child_text_1}. {child_text_2}. {child_text_3}..."
```
**Prefix**: `search_document:` (for documents in Qdrant)
**Example**:
```
search_document: Summary: A man enters a building. He walks down a hallway.
Children: Hello, how are you? I'm fine thank you. The weather is nice today.
```
#### Child Chunk Embedding
```
embedding_text = f"[{child.chunk_type}] {child.text_content}
Parent: {parent.description}"
```
**Prefix**: `search_document:`
**Example**:
```
search_document: [sentence] Hello, how are you?
Parent: A man enters a building. He walks down a hallway.
```
### For BM25 Text Search
BM25 operates on raw text with PostgreSQL full-text search.
- **Index**: `search_vector` (TSVECTOR) on `chunks.text_content`
- **Search**: Uses `ts_rank_cd()` for ranking
## Hybrid Search Ranking
Combined score = `(vector_score * 0.7) + (bm25_score * 0.3)`
### Why 0.7/0.3?
| Weight | Vector | BM25 |
|--------|--------|------|
| Pros | Semantic similarity | Exact keyword match |
| Cons | May miss specific terms | No semantic understanding |
| Best for | Thematic queries | Fact lookup |
## Query Patterns
### Thematic Query ("What are the main themes?")
- Use higher `vector_weight` (0.8-0.9)
- Vector search finds semantically similar content
### Fact Lookup ("Who said X?")
- Use higher `bm25_weight` (0.5-0.7)
- BM25 finds exact matches
### Balanced ("Tell me about scene 5")
- Use default 0.7/0.3
## Implementation
### Embedding Generation
```rust
fn build_embedding_text(chunk: &Chunk, parent_text: Option<&str>) -> String {
match chunk.chunk_type {
ChunkType::Story => {
format!(
"Summary: {}\nChildren: {}",
chunk.text_content,
get_children_text(chunk)
)
}
_ => {
format!(
"[{}] {}\nParent: {}",
chunk.chunk_type.as_str(),
chunk.text_content,
parent_text.unwrap_or("N/A")
)
}
}
}
```
### Storage
- Parent chunks stored with their `child_chunk_ids`
- Child chunks reference `parent_chunk_id`
- Both stored in PostgreSQL with full-text index
- Vectors stored in Qdrant
## Example Flow
1. **Story Processing** generates parent-child hierarchy
2. **Embedding** creates vector for each chunk
3. **Storage** saves to PostgreSQL + Qdrant
4. **Search** retrieves using hybrid search
5. **Results** include both parent context and child details
## Best Practices
1. **Chunk Size**: 5 child chunks per parent (configurable)
2. **Text Length**: Keep embeddings under 512 tokens
3. **Parent Description**: Include temporal markers (timestamps)
4. **Child Content**: Preserve original transcription
## Future Enhancements
- [ ] GraphRAG integration for relationship traversal
- [ ] Cross-chunk entity linking
- [ ] Temporal graph building

View File

@@ -0,0 +1,120 @@
# Face Pipeline: Detection → Clustering → Trace
**Date**: 2026-05-16
---
## 流程
```
Video Frames
┌─────────────────────────────┐
│ 0. Cut Detection │ PySceneDetect
│ scene boundaries │ → chunk (chunk_type='cut')
└─────────────────────────────┘
┌─────────────────────────────┐
│ 1. Face Detection │ 每幀偵測人臉
│ confidence ≥ 0.5 │ → face_detections (cut_id 對應所屬 cut)
└─────────────────────────────┘
┌─────────────────────────────┐
│ 2. Face Clustering │ embedding + IoU + distance
│ trace_id assignment │ 同一人 + 同 cut → 同一 trace_id
│ per-file sequential │ trace_id 跨 cut 持續給號(不歸零)
└─────────────────────────────┘
┌─────────────────────────────┐
│ 3. Face Trace │ 跨影格連續追蹤
│ per-file sequential │ trace_id = 0, 1, 2, ...
│ scoped by cut │ 每個 trace 完全落在一個 cut 內
└─────────────────────────────┘
┌─────────────────────────────┐
│ 4. Identity Binding │ embedding 比對
│ identity_id assignment │ → known person / stranger
└─────────────────────────────┘
```
## scope
```sql
trace_id per-file sequential (file_uuid, trace_id)
cut_id chunk.id WHERE chunk_type='cut' scope
identity_id global FK cut / file
```
## 約束
| 約束 | 說明 |
|------|------|
| 唯一 | `(file_uuid, trace_id)` |
| 單一 cut | 每個 trace 完全落在一個 cut 內(`0` 個跨 cut trace |
| 獨立 | `trace_id``identity_id`。前者是物體軌跡,後者是身份分別 |
## 各階段資料量
```
Stage | 量 | Key
------------------------|-------------|----------------------
Raw faces | 262,021 | face_detections rows
After clustering | 6,892 | distinct trace_id
With identity | 147,602 | identity_id NOT NULL (2,035 identities)
Stranger (unbound) | 114,419 | identity_id IS NULL
```
## Trace 大小分布
| Faces per trace | Trace count | 說明 |
|:---------------:|:-----------:|------|
| 1 | 610 | 一閃而過 |
| 2-5 | 969 | 短暫出現 |
| 6-20 | 1,541 | 片段 |
| 21-100 | 2,218 | 一般 |
| 101+ | 1,554 | 主要角色 |
## Clustering 方式
Face Tracker (`scripts/face_tracker.py`) 使用三種方法決定同一人:
1. **IoU (Intersection over Union)** — 前後影格框重疊率
2. **Cosine distance** — face embedding 相似度
3. **Euclidean distance** — bbox 中心距離
三者加權決策iou > 0.5 || (cosine < 0.3 && distance < 100px)
## Trace 結構
```json
{
"trace_id": 2, // per-file sequential
"faces": [ // face_detections GROUP BY trace_id
{"face_id": "4587_0", "frame": 4587, "confidence": 0.92},
{"face_id": "4588_0", "frame": 4588, "confidence": 0.91},
...
],
"start_frame": 4587,
"end_frame": 4722,
"face_count": 46,
"identity_id": 101 // NULL = stranger
}
```
## API 查詢
```bash
# Trace 列表(含 face_count、區間
POST /api/v1/file/:uuid/face_trace/sortby
# Trace 內 faces逐幀 + 可選 interpolation
GET /api/v1/file/:uuid/trace/:trace_id/faces
# Trace 綁定身份
POST /api/v1/identity/:uuid/bind
```

View File

@@ -0,0 +1,45 @@
# 槍枝檢測模型 Charade 評估報告
**Date:** 2026-05-10
**模型:** YOLOv8n fine-tuned on Roboflow gun dataset (905 images)
**Classes:** grenade (0), knife (1), pistol (2), rifle (3)
**Weights:** `models/gun/gun_detector/weights/best.pt` (6MB)
## 訓練
- **Dataset**: 905 images, Roboflow CC BY 4.0
- **Validation mAP50**: 0.813
- **問題**: 訓練資料全為近距離槍枝特寫,與 Charade 電影中的中遠景畫面分布完全不同
## Charade 測試結果
### 系統掃描24 取樣點 @ 每 300s
| 時間 | 類別 | 信心 | 判定 |
|------|------|------|------|
| t=600s | pistol×2, rifle | 0.160.30 | ❌ FP |
| t=1200s | knife | 0.37 | ❌ FP |
| t=1800s | pistol | 0.19 | ❌ FP |
| t=2400s | knife | 0.18 | ❌ FP |
| t=3000s | pistol | 0.16 | ❌ FP |
| t=5400s | pistol×2 | 0.45, 0.17 | ❌ FP郵票被誤判為槍 |
| t=6600s | grenade | 0.22 | ❌ FP |
### 密集掃描ASR trigger
在 ASR dialogue 提到 "gun" 的時間點附近跑 gun detector找到 5 個 pistol/gun 觸發3188s / 5461s / 6309s / 6377s / 6479sconfidence 0.300-0.387。
**結果:全部為 false positive。** 訓練效果非常不好 — 模型在電影中遠景畫面完全失效。
## 結論
1. 訓練資料與推論場景 distribution mismatch 嚴重
2. 905 張 Roboflow 近距離特寫 → Charade 的中遠景手持/部分遮蔽槍枝 → 模型無法泛化
3. 建議收集電影真實槍枝畫面200-500 張動作片片段)重新訓練
4. 在此之前,槍枝搜尋只能靠 ASR dialogue keyword matching + 人工確認
## 相關檔案
- `models/gun/gun_detector/weights/best.pt` — 模型權重(效果不佳)
- `output_dev/gun_detections/` — 偵測截圖(全部 FP
- `scripts/object_search_agent.py` — 整合搜尋 agentgun detector 偵測結果僅供參考)

View File

@@ -0,0 +1,73 @@
# Gun Detector Scan Report — YOLOv8n on Charade (1963)
**Date:** 2026-05-10
**Model:** `models/gun/gun_detector/weights/best.pt`
**Base:** YOLOv8n fine-tuned on Roboflow gun dataset (905 images)
**Classes:** grenade, knife, pistol, rifle
**Scan script:** `scripts/gun_detector_scan.py`
## Scan Method
- **121 scan points**: 2 ASR "gun" mentions + 114 fixed intervals (60s) + 5 original hit timestamps
- **Per point**: scan ±30 frames at every 3rd frame = ~20 frames per point
- **Total frames processed**: ~2,420
- **Runtime**: ~2 min
## Results
| Class | Detections | Top Confidence |
|-------|-----------|---------------|
| pistol | **82** | 0.887 |
| rifle | 55 | 0.822 |
| grenade | 35 | 0.797 |
| knife | 38 | 0.810 |
| **Total** | **210** (after dedup) | — |
## Original 5 Pistol Timestamps
| Timestamp | Original | This Scan | Delta |
|-----------|----------|-----------|-------|
| 3188s (53:08) | pistol 0.387 | ✅ **0.474** | +22% |
| 5461s (91:01) | pistol 0.355 | ✅ **0.346** | 3% |
| 6309s (1:45:09) | pistol 0.374 | ❌ Not found | — |
| 6377s (1:46:17) | gun 0.316 | ✅ **0.757** | +140% |
| 6479s (1:47:59) | pistol 0.300 | ✅ **0.815** | +172% |
## Top Pistol Detections
| Time | Confidence | Image |
|------|-----------|-------|
| 84:00 (5040s) | **0.887** | `5040s_pistol_0.887.jpg` |
| 90:00 (5400s) | **0.816** | `5400s_pistol_0.816.jpg` |
| 108:00 (6480s) | **0.815** | `6480s_pistol_0.815.jpg` |
| 48:59 (2939s) | **0.805** | `2939s_pistol_0.805.jpg` |
| 53:07 (3187s) | **0.474** | `3187s_pistol_0.474.jpg` |
| 91:00 (5459s) | **0.346** | `5459s_pistol_0.346.jpg` |
## Analysis
### Model Performance
Compared to the original evaluation (May 7, 24 sample points, all FP):
- This scan found **significantly more detections** (210 vs 7)
- Confidence values are **much higher** (0.887 vs 0.45 max)
- 4/5 original pistol timestamps recovered
### Cautions
1. **Training data mismatch**: Model was trained on 905 close-up gun photos, NOT movie frames. High confidence ≠ real gun.
2. **Stamp false positive confirmed**: t=5400s (identified in original eval as stamp → pistol) continues to fire at 0.816
3. **Pattern suggests overconfidence**: Many detections at regular intervals (every 60s, same objects) suggest the model is detecting non-gun objects with high confidence
### Verified Findings
The original 5 pistol images from the gun_detections/ directory (3188s, 5461s, 6309s, 6377s, 6479s) were all produced by the same YOLOv8n model. The user previously stated that none of these have been confirmed as real guns.
## Files
| File | Description |
|------|-------------|
| `output_dev/gun_detections/gun_detections.json` | All 210 deduped detections |
| `output_dev/gun_detections/*.jpg` | Annotated screenshots (one per detection) |
| `scripts/gun_detector_scan.py` | Scan script (reproducible) |

View File

@@ -0,0 +1,995 @@
---
document_type: "design"
service: "MOMENTRY_CORE"
title: "MarkBase 設計文件 V2.0"
date: "2026-05-14"
version: "V2.0"
status: "active"
owner: "M4"
created_by: "OpenCode"
tags:
- "markbase"
- "display-engine"
- "virtual-tree"
- "group-share"
- "storage-tier"
- "file-uuid"
- "sqlite"
- "design"
ai_query_hints:
- "查詢 MarkBase 設計文件 V2.0 的內容"
- "MarkBase 虛擬檔案樹如何設計"
- "MarkBase Group Share 怎麼實現"
- "MarkBase file_uuid 規則"
- "MarkBase 儲存層級 Hot Warm Cold 設計"
- "MarkBase 與 Momentry Core 整合方式"
- "MarkBase Display Mode trait 架構"
- "MarkBase 檔案操作 API 設計"
related_documents:
- "REFERENCE/MARKBASE_DESIGN_v1.0.0.md"
- "REFERENCE/file_uuid_spec.md"
- "REFERENCE/SPATIAL_COORDINATE_REGISTRY.md"
---
# MarkBase 設計文件 V2.0
| 項目 | 內容 |
|------|------|
| 建立者 | M4 / OpenCode |
| 建立時間 | 2026-05-14 |
| 文件版本 | V2.0 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-05-12 | 初版設計Demo Display + Knowledge Graph | M4 / OpenCode | DeepSeek V4 Pro |
| V2.0 | 2026-05-14 | 加入檔案樹、Group Share、儲存層級、技術棧、file_uuid 整合 | M4 / OpenCode | DeepSeek V4 Pro |
---
## 概述
MarkBase 是 Momentry 生態系的 Display Engine 與檔案管理平台。從 V2.0 起MarkBase 不再只是 Demo Runner 的 presentation layer而是升級為具備虛擬檔案樹、跨用戶群組分享、多層級儲存管理、檔案操作 API 的完整平台。
**核心設計原則:**
| 原則 | 說明 |
|------|------|
| 展示層先行 | Demo Display 功能保留,作為 demo runner 的固定顯示視窗 |
| 檔案層次化 | 虛擬檔案樹Virtual Tree讓用戶管理自己的資料結構 |
| 儲存層級化 | Hot/Warm/Cold 三級儲存,讓用戶掌控成本 |
| 群組協作 | Group Share 讓團隊內的檔案可讀寫 |
| 單一使用者隔離 | One user = one SQLite不混用 |
---
## 關鍵術語定義
| 術語 | 定義 |
|------|------|
| Virtual Tree | 用戶管理的邏輯檔案樹,非實體路徑 |
| FileNode | 虛擬樹中的節點,包含 label、別名、圖示、顏色 |
| Display Mode | 使用者選擇的檔案展示方式List / Tree / Small Icon / Large Icon |
| Group Share | 跨用戶的群組檔案分享(選項 A: Group SQLite |
| Storage Tier | 三級儲存層級Hot / Warm / Cold |
| file_uuid | 32 字元十六進制檔案出生識別符,由 Momentry Core 計算 |
| Exit Record | 檔案移出管理時的留存記錄 |
| Mount | 實體儲存掛載點NAS、外接硬碟、LTO |
---
## 1. 架構總覽
### 1.1 模組化 Rust 設計
```
markbase/
├── src/
│ ├── main.rs # CLI entry point
│ ├── server.rs # axum HTTP server (port 11438)
│ ├── display/ # Display engine (from V1.0)
│ │ ├── mod.rs
│ │ ├── render.rs # .md → HTML (pulldown-cmark)
│ │ ├── highlight.rs # syntax highlighting (syntect)
│ │ ├── mermaid.rs # Mermaid rendering
│ │ └── page.html # core HTML template
│ ├── filetree/ # Virtual file tree (NEW V2.0)
│ │ ├── mod.rs # FileTree struct, init_from_sqlite
│ │ ├── node.rs # FileNode struct
│ │ ├── mode.rs # DisplayMode trait
│ │ ├── modes/
│ │ │ ├── list.rs # list module (trait impl)
│ │ │ ├── tree.rs # tree module (trait impl, Phase 1)
│ │ │ ├── grid_sm.rs # small icon grid (trait impl)
│ │ │ └── grid_lg.rs # large icon grid (trait impl)
│ │ └── auto_layer.rs # auto-layer rules
│ ├── operations/ # File operations (NEW V2.0)
│ │ ├── mod.rs
│ │ ├── compress.rs # zip / tar
│ │ ├── transfer.rs # copy / move between tiers
│ │ ├── archive.rs # auto-archive logic
│ │ ├── restore.rs # restore from archive
│ │ ├── exit.rs # exit record management
│ │ └── registry.rs # file_registry table
│ ├── groups/ # Group share (NEW V2.0)
│ │ ├── mod.rs
│ │ ├── db.rs # Group SQLite create/open
│ │ ├── merge.rs # ATTACH + cross-DB merge
│ │ └── roles.rs # owner/editor/viewer
│ └── mount/ # Mount management (NEW V2.0)
│ ├── mod.rs
│ ├── tier.rs # Hot/Warm/Cold tier defs
│ └── history.rs # location_history table
```
**DisplayMode Trait 設計:**
```rust
/// 展示模式的統一介面。
/// 每個模式List, Tree, Grid實作此 trait。
#[async_trait]
pub trait DisplayMode: Send + Sync {
/// 模式名稱(前端使用)
fn name(&self) -> &'static str;
/// 將 FileTree 轉換為此模式的前端資料
fn render(&self, tree: &FileTree, user_id: &str) -> Result<Value>;
/// 此模式支援的排序方式
fn sort_options(&self) -> Vec<SortOption>;
/// 此模式支援的過濾器
fn filter_options(&self) -> Vec<FilterOption>;
}
```
### 1.2 One User = One SQLite
```
data/
├── users/
│ ├── demo.sqlite # 用戶 demo 的虛擬樹 + 操作記錄
│ ├── warren.sqlite # 用戶 warren 的虛擬樹 + 操作記錄
│ └── alice.sqlite # 用戶 alice 的虛擬樹 + 操作記錄
├── groups/
│ ├── groups.sqlite # 群組註冊表group_id → path
│ ├── 1.sqlite # 群組 1 的共用資料
│ └── 2.sqlite # 群組 2 的共用資料
└── system.sqlite # 系統層級資料(掛載點、全域設定)
```
| 原則 | 說明 |
|------|------|
| **用戶隔離** | 每個用戶獨立的 SQLite 檔案user.sqlite |
| **簡單部署** | 不需 PostgreSQL server單檔即可 |
| **易於備份** | 複製 `.sqlite` 檔案即可 |
| **Portable** | 隨身碟帶著走,離線可用 |
### 1.3 Momentry Core 整合A+B 混合模式)
```
┌──────────────────────────────────────────────────────┐
│ MarkBase │
│ │
│ ┌─────────────────┐ ┌─────────────────────────┐ │
│ │ 模式 A: Crate │ │ 模式 B: HTTP API │ │
│ │ (momentry_core │ │ (localhost:3003) │ │
│ │ 作為依賴) │ │ │ │
│ │ │ │ • file_uuid 驗證 │ │
│ │ • file_uuid 計算 │ │ • chunk 查詢 │ │
│ │ • 向量嵌入 │ │ • identity 查詢 │ │
│ │ • 本地處理 │ │ • trace data │ │
│ └─────────────────┘ └─────────────────────────┘ │
│ │
│ 選擇策略: │
│ • 輕量運算 → Crate 模式(不啟動 server
│ • 重查詢/伺服器操作 → HTTP API需 server 運行) │
└──────────────────────────────────────────────────────┘
```
| 操作 | 模式 | 理由 |
|------|:----:|------|
| file_uuid 計算/驗證 | Crate | 純函數,不需 server |
| SHA256 | Crate | 本地計算 |
| Chunk 查詢by file_uuid | HTTP | 需存取 PostgreSQL |
| Identity 查詢 | HTTP | 需存取 PostgreSQL |
| Trace data時序片段 | HTTP | 需存取 PostgreSQL |
| 向量搜尋ANN | HTTP | 需 Qdrant server |
| 文件轉換soffice | Crate/CLI | 本地處理 |
---
## 2. 技術棧
### 2.1 Crate 依賴
| Crate | 用途 | License |
|-------|------|---------|
| axum 0.7 | HTTP serverport 11438 | MIT |
| tokio 1.0 | 非同步 runtime | MIT |
| rusqlite 0.32 | SQLite 客戶端bundled | MIT |
| r2d2 / r2d2_sqlite | SQLite 連接池 | MIT/Apache |
| serde / serde_json 1.0 | JSON 序列化 | MIT/Apache |
| sha2 0.10 | SHA256file_uuid 驗證) | MIT/Apache |
| notify 6.0 | 檔案系統監控Hot tier | CC0/MIT |
| zip 2.0 | ZIP 壓縮 | MIT |
| tar 0.4 | TAR 打包LTO 歸檔) | MIT/Apache |
| walkdir 2.0 | 目錄掃描 | MIT/Unlicense |
| chrono 0.4 | 日期時間 | MIT/Apache |
| tracing 0.1 | 結構化日誌 | MIT |
| pulldown-cmark | Markdown → HTML | MIT |
| syntect | 程式碼語法高亮 | MIT |
| anyhow / thiserror | 錯誤處理 | MIT/Apache |
| once_cell | 延遲初始化 | MIT/Apache |
| async-trait | async trait 支援 | MIT/Apache |
### 2.2 SQLite 查詢策略
| 項目 | 決策 |
|------|:--:|
| Crate | rusqlite同步 API |
| 非同步包裝 | `tokio::task::spawn_blocking` |
| 連接池 | r2d2_sqlite |
| WAL 模式 | 啟用(預設) |
```rust
// axum handler 中的使用模式
async fn get_tree(State(pool): State<DbPool>) -> Result<Json<Value>> {
let tree = tokio::task::spawn_blocking(move || {
let conn = pool.get()?;
let tree = FileTree::load(&conn, user_id)?;
Ok::<_, anyhow::Error>(tree)
}).await??;
Ok(Json(tree))
}
```
### 2.3 檔案系統監控
| 項目 | 決策 |
|------|:--:|
| Crate | notify 6.0CC0/MIT |
| 監控範圍 | 僅 Hot tier |
| 不監控 | Warm / Cold tier變更頻率低 |
| 實作 | `notify::Watcher` + `mpsc::channel` → async stream |
### 2.4 壓縮引擎
| 格式 | Crate | 用途 |
|------|-------|------|
| `.zip` | `zip` crate | 一般壓縮(用戶下載、備份) |
| `.tar.gz` | `tar` + `flate2` crate | LTO 歸檔Cold tier |
不使用外部 CLIditto、hdiutil全部以 Rust crate 實作。
### 2.5 檔案傳輸Transfer Engine
#### 雙引擎策略
```
TransferEngine:
├── Direct 模式std::fs::copy
│ 適用:小檔案 (<50MB)、fallback
│ 特點:無外部依賴、簡單可靠
└── Rsync 模式rsync CLI
適用:大檔案 (>=50MB)、tier 遷移、NAS 鏡像
特點:增量傳輸、續傳、校驗和
```
#### 自動選擇邏輯
```rust
fn select_mode(file_path: &Path) -> TransferMode {
let size = std::fs::metadata(file_path).map(|m| m.len()).unwrap_or(0);
if size < 50 * 1024 * 1024 { // <50MB
TransferMode::Direct
} else if Command::new("rsync").arg("--version").output().is_ok() {
TransferMode::Rsync
} else {
TransferMode::Direct // rsync 不存在時 fallback
}
}
```
#### rsync 適用性分析
| 場景 | 工具 | 理由 |
|------|------|------|
| 單小檔複製 (<50MB) | `std::fs::copy` | rsync protocol overhead > 效益 |
| 大檔案遷移 (tier move) | **rsync** | 增量、續傳、校驗和,三合一 |
| Hot ↔ Warm 同一機器 | **rsync** | 大檔案 delta transfer 效益 |
| NAS ↔ NAS 鏡像 | **rsync** | `--delete` 鏡像模式 |
| 打包 .zip/.tar.gz | `zip` / `tar` crate | rsync 不做壓縮打包 |
| 寫 LTO 磁帶 | `tar` crate | rsync 無法寫磁帶 |
#### rsync CLI 參數
| 參數 | 用途 |
|------|------|
| `-a` | archive mode保留權限、時間戳 |
| `-v` | verbose進度顯示 |
| `-P` | 等同 `--partial --progress`(續傳 + 進度) |
| `-c` | checksum modeSHA256 驗證,非 time/size |
| `-n` | dry-run遷移前預覽 |
| `--delete` | 鏡像模式NAS 同步用) |
### 2.6 Group Share 跨 DB 查詢
使用 SQLite `ATTACH DATABASE`
```sql
ATTACH DATABASE '/path/to/groups/1.sqlite' AS g;
SELECT f.*, gf.permission
FROM file_registry f
JOIN g.file_registry gf ON f.file_uuid = gf.file_uuid;
```
**優勢:** 一行 SQL 解決Rust 端不需額外合併邏輯。
### 2.7 非同步策略
```
axum handler (async)
├── 快速操作(直接 await
│ ├── serde_json 序列化
│ ├── 驗證
│ └── 記憶體操作
└── 阻塞操作spawn_blocking
├── rusqlite 查詢
├── std::fs 檔案操作
├── SHA256 計算
└── 壓縮/解壓
```
**原則:** axum handler 本身是 async遇到 rusqlite 或 std::fs 時,一律用 `tokio::task::spawn_blocking` 包裝。
---
## 3. file_uuid 規範
### 3.1 計算公式
```
file_uuid = SHA256(mac_address | birthday | physical_path_at_birth | filename)[0:32]
```
詳細規範參見 `REFERENCE/file_uuid_spec.md`
### 3.2 MarkBase 中的使用
| 欄位 | 來源 | 說明 |
|------|------|------|
| file_uuid | Momentry Core | MarkBase 不重新計算,直接復用 |
| 驗證 | `is_birth_uuid()` | 長度 32不含 `_` |
| 關聯 | 主鍵 | `file_registry.file_uuid``file_nodes.file_uuid` |
### 3.3 整合流程
```
Momentry Core MarkBase
(檔案註冊) (匯入)
┌──────────┐ ┌──────────┐
│ compute_ │ │ INSERT │
│ birth_ │──── file_uuid ───▶│ INTO │
│ uuid() │ 32 hex │ file_ │
│ │ │ registry │
└──────────┘ │(file_uuid)
└──────────┘
```
---
## 4. 虛擬檔案樹
### 4.1 FileNode 結構
```rust
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileNode {
/// 節點唯一 IDUUIDv4
pub node_id: String,
/// 顯示名稱
pub label: String,
/// 多語言別名
pub aliases: Aliases,
/// 關聯的 file_uuidMomentry Core 來源)
pub file_uuid: Option<String>,
/// 父節點 node_idroot 為 None
pub parent_id: Option<String>,
/// 子節點列表
pub children: Vec<String>,
/// 節點類型
pub node_type: NodeType,
/// 自訂圖示emoji 或 SVG 路徑)
pub icon: Option<String>,
/// 文字顏色CSS hex
pub color: Option<String>,
/// 背景顏色CSS hex
pub bg_color: Option<String>,
/// 建立時間
pub created_at: String,
/// 最後修改時間
pub updated_at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Aliases {
/// 繁體中文
pub zh_tw: Option<String>,
/// 英文
pub en_us: Option<String>,
/// 日文
pub ja_jp: Option<String>,
/// 韓文
pub ko_kr: Option<String>,
/// 法文
pub fr_fr: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum NodeType {
/// 虛擬資料夾(用戶建立,不對應實體路徑)
Folder,
/// 實體檔案(指向 file_uuid
File,
/// 動態層級auto-layer 產生)
DynamicLayer,
}
```
### 4.2 SQLite Schemauser.sqlite
```sql
CREATE TABLE IF NOT EXISTS file_nodes (
node_id TEXT PRIMARY KEY,
label TEXT NOT NULL,
aliases_json TEXT NOT NULL DEFAULT '{}',
file_uuid TEXT,
parent_id TEXT,
children_json TEXT NOT NULL DEFAULT '[]',
node_type TEXT NOT NULL DEFAULT 'file',
icon TEXT,
color TEXT,
bg_color TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now')),
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
sort_order INTEGER NOT NULL DEFAULT 0,
FOREIGN KEY (file_uuid) REFERENCES file_registry(file_uuid)
);
CREATE TABLE IF NOT EXISTS file_registry (
file_uuid TEXT PRIMARY KEY,
original_name TEXT NOT NULL,
file_size INTEGER,
file_type TEXT,
registered_at TEXT NOT NULL,
last_seen_at TEXT,
status TEXT NOT NULL DEFAULT 'active'
);
```
### 4.3 Display Modes
用戶可切換四種展示模式(儲存在 `localStorage.display_mode`
| 模式 | 枚舉值 | 說明 | 實作模組 |
|------|--------|------|----------|
| **List** | `list` | 列表檢視:名稱、大小、日期 | `modes/list.rs` |
| **Tree** | `tree` | 樹狀檢視:展開/折疊層級 | `modes/tree.rs`Phase 1 |
| **Small Icon** | `grid_sm` | 小圖示網格:適合縮圖檢視 | `modes/grid_sm.rs` |
| **Large Icon** | `grid_lg` | 大圖示網格:適合影片預覽 | `modes/grid_lg.rs` |
每種模式實作 `DisplayMode` trait參見 §1.1)。
### 4.4 多語言別名
| 欄位 | 語言 | 用途 |
|------|------|------|
| `zh_tw` | 繁體中文 | 預設語言 |
| `en_us` | 英文 | 國際使用 |
| `ja_jp` | 日文 | 日本用戶 |
| `ko_kr` | 韓文 | 韓國用戶 |
| `fr_fr` | 法文 | 法國/國際用戶 |
用戶在前端選擇語言後系統自動顯示對應別名。若該語言的別名不存在fallback 到 `label`
### 4.5 自動分層規則
系統根據預設規則自動為檔案建立虛擬層級:
| 規則 | 條件 | 層級結構 |
|------|------|----------|
| **by_type** | 相同副檔名 | `Videos/``Images/``Documents/``Audio/``Other/` |
| **by_date** | 按建立日期 | `2026/``2026/05/``2026/05/14/` |
| **by_size** | 按檔案大小 | `<10MB``10100MB``100MB1GB``>1GB` |
`auto_layer.rs` 實作,使用 `NodeType::DynamicLayer` 標記。
---
## 5. 群組分享
### 5.1 Group SQLite 架構(選項 A
```
data/groups/
├── groups.sqlite # 群組註冊表(全域)
│ └── groups(
│ group_id INTEGER PRIMARY KEY,
│ group_name TEXT,
│ db_path TEXT, # 指向 1.sqlite
│ created_by TEXT, # 建立者 user_id
│ created_at TEXT
│ )
├── 1.sqlite # 群組 1 的共用資料
└── 2.sqlite # 群組 2 的共用資料
```
### 5.2 Group SQLite Schema
```sql
-- groups/1.sqlite
CREATE TABLE group_members (
user_id TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'viewer', -- owner / editor / viewer
joined_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (user_id)
);
CREATE TABLE group_files (
file_uuid TEXT NOT NULL,
added_by TEXT NOT NULL,
added_at TEXT NOT NULL DEFAULT (datetime('now')),
PRIMARY KEY (file_uuid),
FOREIGN KEY (added_by) REFERENCES group_members(user_id)
);
```
### 5.3 跨 DB 查詢ATTACH
```rust
pub fn get_group_files(conn: &Connection, group_id: i64) -> Result<Vec<GroupFile>> {
let group_db = format!("/data/groups/{}.sqlite", group_id);
conn.execute_batch(&format!("ATTACH DATABASE '{}' AS g", group_db))?;
let mut stmt = conn.prepare("
SELECT f.file_uuid, f.original_name, gm.role
FROM main.file_registry f
JOIN g.group_files gf ON f.file_uuid = gf.file_uuid
JOIN g.group_members gm ON gf.added_by = gm.user_id
")?;
// ...
}
```
### 5.4 角色權限
| 角色 | 讀取 | 寫入 | 刪除 | 邀請成員 |
|------|:----:|:----:|:----:|:----:|
| owner | ✅ | ✅ | ✅ | ✅ |
| editor | ✅ | ✅ | ❌ | ❌ |
| viewer | ✅ | ❌ | ❌ | ❌ |
---
## 6. 儲存層級
### 6.1 三級定義
| 層級 | 符號 | 延遲 | 速度 | 成本 | 典型媒體 |
|------|:----:|------|------|------|----------|
| **Hot** | 🔥 | <10ms | 高速 | 高 | NVMe SSD / 內建硬碟 |
| **Warm** | 🌡️ | 10500ms | 中等 | 中 | NAS網路掛載 |
| **Cold** | ❄️ | >1s | 低速 | 低 | LTO 磁帶 / 外接 HDD |
### 6.2 掛載點設定
管理員可設定每個層級的掛載路徑:
```json
{
"tiers": {
"hot": ["/Users/accusys/sftpgo/data", "/Volumes/RAID5/projects"],
"warm": ["/Volumes/NAS_Archive"],
"cold": ["/Volumes/LTO_Archive"]
}
}
```
### 6.3 自動歸檔規則
管理員可設定自動歸檔觸發條件:
```json
{
"auto_archive": {
"enabled": true,
"rules": [
{
"condition": "idle_days > 90",
"action": "move_to_warm",
"schedule": "0 2 * * 0"
},
{
"condition": "idle_days > 365",
"action": "move_to_cold",
"schedule": "0 3 * * 0"
},
{
"condition": "tier_hot_usage > 80%",
"action": "move_oldest_to_warm",
"schedule": "0 * * * *"
}
]
}
}
```
### 6.4 file_uuid 層級遷移
file_uuid **在遷移過程中不變**。檔案從 Hot 移到 Cold
1. 複製檔案到 Cold tier 路徑
2. 驗證完整性SHA256
3. 寫入 `location_history` 記錄新位置
4. 移除 Hot tier 的原始檔案
5. `file_registry.last_seen_at` 更新
file_uuid 永遠指向 birth 時的 `physical_path_at_birth`Hot 路徑),不因遷移而改變。
### 6.5 AI Agent — 按需資料流動
AI Agent 在底層自動管理資料流動,使用者無需知道檔案實際存放層級。
#### 架構
```
User / Scheduler
┌─────────────────────────────────┐
│ AI Agent │
│ • Monitor tier usage │
│ • Detect hot/cold patterns │
│ • Trigger auto-archive │
│ • Restore on access (prefetch) │
└──────────┬──────────────────────┘
┌─────────────────────────────────┐
│ Transfer Engine │
│ Direct (std::fs::copy) │
│ Rsync (delta + checksum) │
│ S3 / SFS / NFS / CDN │
└──────────┬──────────────────────┘
┌─────────────────────────────────┐
│ file_locations │
│ (single source of truth) │
│ M2 M4 M5 Cloud LTO │
└─────────────────────────────────┘
```
#### 自動歸檔規則
| 觸發條件 | 動作 | Transfer Engine |
|----------|------|:--:|
| `idle_days > 90` | move to Warm | Rsync + checksum verify |
| `idle_days > 365` | move to Cold | Tar + checksum verify |
| `hot_tier_usage > 80%` | move oldest to Warm | Rsync —progress |
| user accesses cold file | restore to Hot | Rsync prefetch |
#### 流程範例
```
1. AI Agent 偵測 Charade_1963.mp4 閒置 120 天
2. rsync -avP --checksum → /Volumes/NAS_Archive/
3. POST /api/v2/files/aeed7134.../locations
{"location": "/Volumes/NAS_Archive/Charade_1963.mp4",
"label": "M4-warm"}
4. 移除 Hot tier 位置(或保留為參考)
5. 使用者查詢檔案資訊 → 看到所有層級,無需知道實際位置
```
#### 設計原則
| 原則 | 說明 |
|------|------|
| 透明遷移 | 使用者查詢 `file_locations` 始終得到一致視圖 |
| 不變標識 | `file_uuid` 在遷移過程中不變 |
| 位置追蹤 | 每次遷移後更新 `file_locations`,舊位置可選擇保留為歷史參考 |
| 驗證完整性 | 遷移後執行 SHA256 校驗Rsync `--checksum` 或手動比對) |
| 類似記憶體階層 | Agent 是記憶體控制器Hot=快取、Warm=主記憶體、Cold=磁碟 |
```
用戶查詢檔案 → 始終看到一致視圖單一來源真相file_locations
Transfer Enginersync / Direct / S3 / SFS / CDN
AI Agent監控 tier 用量、偵測冷熱模式、自動歸檔、預取)
Storage TiersM2 Hot → M4 Warm → M5 Cold → LTO
```
```sql
CREATE TABLE IF NOT EXISTS location_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_uuid TEXT NOT NULL,
location TEXT NOT NULL, -- 實際檔案路徑
tier TEXT NOT NULL, -- hot / warm / cold
moved_at TEXT NOT NULL DEFAULT (datetime('now')),
reason TEXT,
moved_by TEXT,
verified INTEGER DEFAULT 0, -- 完整性驗證通過
FOREIGN KEY (file_uuid) REFERENCES file_registry(file_uuid)
);
CREATE INDEX idx_location_history_file_uuid ON location_history(file_uuid);
```
查詢目前位置:
```sql
SELECT location, tier
FROM location_history
WHERE file_uuid = ?
ORDER BY moved_at DESC
LIMIT 1;
```
---
## 7. 檔案操作 API
### 7.1 操作總覽
| 操作 | API | 說明 |
|------|-----|------|
| **Compress** | `POST /api/v2/files/compress` | 壓縮為 .zip 或 .tar.gz |
| **Transfer** | `POST /api/v2/files/transfer` | 複製/移動到 target tier |
| **Archive** | `POST /api/v2/files/archive` | 歸檔到 Cold tier |
| **Restore** | `POST /api/v2/files/restore` | 從 Cold tier 還原到 Hot tier |
| **Exit** | `POST /api/v2/files/exit` | 從 MarkBase 移除(保留記錄) |
### 7.2 壓縮
```rust
// Compress 請求
{
"file_uuids": ["uuid1", "uuid2"],
"format": "zip", // "zip" | "tar.gz"
"output_path": "/path/to/output.zip"
}
// Compress 回應
{
"status": "completed",
"output_path": "/path/to/output.zip",
"file_count": 2,
"compressed_size": 1048576
}
```
### 7.3 Transfer層級遷移
#### 請求/回應
```rust
// Transfer 請求
{
"file_uuids": ["uuid1"],
"target_tier": "cold",
"target_path": "/Volumes/LTO_Archive/2026/",
"delete_source": false
}
// Transfer 回應
{
"status": "completed",
"file_uuid": "uuid1",
"new_location": "/Volumes/LTO_Archive/2026/uuid1.mp4",
"new_tier": "cold"
}
```
#### Transfer Engine 實作流程
```
TransferEngine::execute(source, target, opts)
├── 1. select_mode(source)
│ │
│ ├── size < 50MB ──→ DirectMode
│ └── size >= 50MB ──→ RsyncMode (fallback: DirectMode)
├── 2. preflight (RsyncMode)
│ ├── rsync -an --checksum source/ target/
│ └── 回傳變更清單,供用戶確認
├── 3. transfer
│ │
│ ├── DirectMode: std::fs::copy + progress callback
│ │
│ └── RsyncMode: rsync -avP --checksum source target
│ ├── -a archive mode
│ ├── -v verbose (進度)
│ ├── -P --partial (續傳) + --progress (進度)
│ └── -c checksum mode (SHA256 驗證替代 time/size)
├── 4. verify (RsyncMode)
│ └── rsync -acn source target (dry-run checksum應為空)
├── 5. update location_history
│ └── INSERT INTO location_history (file_uuid, location, tier, ...)
└── 6. cleanup
└── if delete_source: remove source file
```
#### Rsync vs Direct 選擇
| 條件 | 模式 | 原因 |
|------|:----:|------|
| `file_size < 50 MB` | Direct | rsync overhead > 效益 |
| `file_size >= 50 MB` 且 rsync 存在 | Rsync | 增量、續傳、校驗和 |
| `file_size >= 50 MB` 且 rsync 不存在 | Direct | 優雅 fallback |
### 7.4 Archive / Restore
Archive 為 Transfer 到 Cold tier 的便捷包裝。
Restore 為從 Cold tier 還原到 Hot tier 的便捷包裝。
```rust
// Restore 請求
{
"file_uuid": "uuid1",
"target_path": "/Users/demo/restored/" // 選填,預設為原始 birth path
}
// Restore 回應
{
"status": "completed",
"file_uuid": "uuid1",
"restored_to": "/Users/demo/restored/uuid1.mp4"
}
```
### 7.5 Exit 記錄
檔案移出 MarkBase 管理時,保留記錄以供審計:
```sql
CREATE TABLE IF NOT EXISTS exit_records (
id INTEGER PRIMARY KEY AUTOINCREMENT,
file_uuid TEXT NOT NULL,
original_name TEXT NOT NULL,
exited_at TEXT NOT NULL DEFAULT (datetime('now')),
exited_by TEXT NOT NULL,
reason TEXT,
last_location TEXT,
FOREIGN KEY (file_uuid) REFERENCES file_registry(file_uuid)
);
```
```rust
// Exit 請求
{
"file_uuid": "uuid1",
"reason": "Project completed, moved to long-term archive"
}
// Exit 回應
{
"status": "completed",
"file_uuid": "uuid1",
"exited_at": "2026-05-14T10:00:00Z"
}
```
---
## 8. API 參考
### 8.1 Tree API
| 方法 | 路徑 | 說明 |
|------|------|------|
| `GET` | `/api/v2/tree/:user_id` | 取得用戶的完整虛擬樹 |
| `GET` | `/api/v2/tree/:user_id?mode=list` | 以特定模式取得樹 |
| `POST` | `/api/v2/tree/:user_id/node` | 建立新節點 |
| `PUT` | `/api/v2/tree/:user_id/node/:node_id` | 更新節點label、icon、color、aliases |
| `DELETE` | `/api/v2/tree/:user_id/node/:node_id` | 刪除節點 |
| `PUT` | `/api/v2/tree/:user_id/node/:node_id/move` | 移動節點(變更 parent |
| `PATCH` | `/api/v2/tree/:user_id/node/:node_id/alias` | 更新特定語言的別名 |
### 8.2 File API
| 方法 | 路徑 | 說明 |
|------|------|------|
| `GET` | `/api/v2/files/:file_uuid` | 取得檔案資訊 |
| `POST` | `/api/v2/files/compress` | 壓縮檔案 |
| `POST` | `/api/v2/files/transfer` | 轉移檔案到 target tier |
| `POST` | `/api/v2/files/archive` | 歸檔到 Cold tier |
| `POST` | `/api/v2/files/restore` | 從 Cold tier 還原 |
| `POST` | `/api/v2/files/exit` | 移出管理 |
| `GET` | `/api/v2/files/:file_uuid/locations` | 查詢位置歷史 |
| `POST` | `/api/v2/files/validate` | 驗證檔案完整性SHA256 |
### 8.3 Mount API
| 方法 | 路徑 | 說明 |
|------|------|------|
| `GET` | `/api/v2/mounts` | 列出所有掛載點 |
| `POST` | `/api/v2/mounts` | 註冊新的掛載點 |
| `PUT` | `/api/v2/mounts/:mount_id` | 更新掛載點 |
| `DELETE` | `/api/v2/mounts/:mount_id` | 移除掛載點 |
| `GET` | `/api/v2/mounts/:mount_id/status` | 查詢掛載點狀態(是否在線、容量) |
### 8.4 Group API
| 方法 | 路徑 | 說明 |
|------|------|------|
| `GET` | `/api/v2/groups` | 列出所有群組 |
| `POST` | `/api/v2/groups` | 建立新群組 |
| `DELETE` | `/api/v2/groups/:group_id` | 刪除群組 |
| `POST` | `/api/v2/groups/:group_id/members` | 邀請成員 |
| `DELETE` | `/api/v2/groups/:group_id/members/:user_id` | 移除成員 |
| `PUT` | `/api/v2/groups/:group_id/members/:user_id/role` | 變更角色 |
| `POST` | `/api/v2/groups/:group_id/files` | 分享檔案到群組 |
| `DELETE` | `/api/v2/groups/:group_id/files/:file_uuid` | 從群組移除檔案 |
| `GET` | `/api/v2/groups/:group_id/files` | 列出群組檔案 |
---
## 9. 決策記錄
| # | 日期 | 決策 | 理由 |
|---|------|------|------|
| 1 | 2026-05-13 | Rust modular architecture (DisplayMode trait) | 與 Momentry Core 相同生態,模組化利於擴展 |
| 2 | 2026-05-13 | One user = one SQLite | 用戶隔離、簡單部署、檔案可攜 |
| 3 | 2026-05-13 | Group Share → Option A (Group SQLite) | 獨立可攜、不需專屬 server、備份簡單 |
| 4 | 2026-05-13 | Hot/Warm/Cold 三級儲存 | 真實世界檔案管理需求,結合 LTO/NAS/SSD |
| 5 | 2026-05-13 | Auto-archive rules (admin-configurable) | 減少手動管理idle days + tier 容量觸發 |
| 6 | 2026-05-14 | file_uuid 從 Momentry Core 繼承,不重新計算 | 唯一來源,避免不一致 |
| 7 | 2026-05-14 | file_uuid 不因層級遷移而改變 | 凍結在 birth 時刻,確保身份穩定 |
| 8 | 2026-05-14 | Display mode 儲存在 localStorage | 純 UI 偏好,不需後端儲存 |
| 9 | 2026-05-14 | 檔案操作 API-first | 後端邏輯完成後再加 UI壓縮、傳輸、歸檔 |
| 10 | 2026-05-14 | Exit records保留記錄 | 審計需求,不直接刪除記錄 |
| 11 | 2026-05-14 | rusqlite (同步) + spawn_blocking (異步包裝) | 避免整個堆疊都必須 async保持簡單 |
| 12 | 2026-05-14 | ATTACH DATABASE for Group Share 跨 DB 查詢 | 一行 SQL不需 Rust 端合併 |
| 13 | 2026-05-14 | notify crate (僅 Hot tier) | 減少資源消耗Warm/Cold 變更頻率低 |
| 14 | 2026-05-14 | zip + tar crate (不用外部 CLI) | 跨平台,不需 ditto/hdiutil |
| 15 | 2026-05-14 | Momentry Core 整合 A+B 混合模式 | 輕量運算用 crate重查詢用 HTTP API |
| 16 | 2026-05-14 | AI Agent 按需資料流動 | 透明遷移、類似記憶體階層、自動冷熱管理 |
| 17 | 2026-05-14 | file_locations 支援任意 URI | /path、s3://、sfs://、ipfs://、https://、\\SMB\path |
---
## 10. 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-05-12 | 初版設計Demo Display + Knowledge Graph | M4 / OpenCode | DeepSeek V4 Pro |
| V2.0 | 2026-05-14 | 虛擬檔案樹、Group Share、儲存層級、技術棧、file_uuid、檔案操作 API、AI Agent 按需資料流動、跨平台 multi-location | M4 / OpenCode | DeepSeek V4 Pro |

View File

@@ -0,0 +1,730 @@
# MarkBase — Momentry 專屬 Display Engine 設計方案 v1.0
## 產品定位
**MarkBase** 是 Momentry 專屬的 Display Engine擔任 **demo runner 的固定顯示器**
不只是 Markdown 閱讀器,而是一個可控的內容呈現視窗,能夠動態展示:
| 內容類型 | 展示方式 |
|----------|----------|
| .md 文件 | 渲染為排版清晰的 HTML |
| Mermaid 圖表 | 流程圖、時序圖、ER 圖等 |
| API 回應 JSON | 語法高亮的格式化 JSON |
| 影片 | 嵌入 video player支援 HLS / MP4|
| 圖片 | 支援單張或輪播 |
| HTML | 直接內嵌 |
| 文字/程式碼 | syntax highlight |
**定位一句話:** *Demo runner 的 presentation layer一個專注、乾淨、可控的內容顯示器。*
| 面向 | 說明 |
|------|------|
| 願景 | Momentry 生態系的 UI 輸出終端 |
| 核心場景 | demo runner 的固定 display 視窗 |
| 平台 | macOS nativeRust + axum + Tauri WebView|
| 授權 | Momentry 專屬工具,隨 momentry_core 發布 |
---
## 命名
**MarkBase** — Markdown + Display Base
> 承載所有內容類型的顯示基底。
> 簡短、好記、產品感。
---
## 階段規劃
### Phase 0Demo DisplayMVP — 立即價值)
**目標**:取代 md_reader + 影片播放,成為 demo runner 的固定顯示視窗
| 功能 | 說明 |
|------|------|
| 文件渲染 | CommonMark + GFM表格、task list、strikethrough、footnotes|
| Mermaid 圖表 | 內建渲染(無需 CDN支援 flowchart / sequence / class / ER / mindmap |
| 程式碼高亮 | syntax highlighting支援 50+ 語言)|
| JSON 格式化 | API response 自動格式化 + 語法高亮 |
| 影片播放 | MP4 / HLS 嵌入播放(取代 browser 開啟 trace video|
| 全螢幕 mode | 乾淨無干擾的展示模式,適合 presentation |
| CLI 控制 | 透過 stdin / HTTP 動態載入內容,無需重新啟動 |
| 與 demo runner 整合 | `--display` flag 啟動作為固定顯示視窗 |
#### Demo Runner 整合流程
```
demo_runner.py --display MarkBase.app (固定顯示視窗)
┌────────────────────┐ ┌────────────────────┐
│ Step 3: Markdown │ ──HTTP──▶│ 渲染 GUIDE.md │
│ Step 11: Trace 5 │ ──HTTP──▶│ 播放 trace_5.mp4 │
│ Step 13: 3D Cube │ ──HTTP──▶│ 顯示 iframe: portal │
│ Step 22: API resp │ ──HTTP──▶│ 顯示格式化 JSON │
└────────────────────┘ └────────────────────┘
(控制端) (顯示端)
```
demo runner 透過 `--display` 啟動 MarkBase 作為顯示視窗,然後每步透過 HTTP 推送內容:
```python
# demo_runner.py 範例
step_type = "markdown" POST /display {"type":"md","file":"GUIDE.md"}
step_type = "video" POST /display {"type":"video","url":"trace_5.mp4"}
step_type = "curl" POST /display {"type":"json","data":response}
step_type = "browser" POST /display {"type":"url","url":"..."}
```
### Phase 2Knowledge Base
**目標**:從閱讀器升級為個人知識庫管理器
| 功能 | 說明 |
|------|------|
| 多文件索引 | 監控目錄,自動索引所有 .md |
| 全文檢索 | 跨文件模糊搜尋 + 標題索引 |
| 標籤管理 | YAML frontmatter tags → 標籤雲 |
| Backlinks | 文件間的雙向連結([[wiki-link]]|
| 收藏/書籤 | 標記常用文件 |
| 閱讀歷史 | 最近開啟 / 最近搜尋 |
### Phase 3Collaboration
**目標**:多人協作與發布
| 功能 | 說明 |
|------|------|
| 評論/註釋 | 段落層級註解 |
| 版本歷史 | git-based diff 檢視 |
| 靜態站點生成 | .md → 整站 HTML用於發布|
| Web 版本 | 瀏覽器可讀(可選自托管)|
---
## CLI 設計Portal / Demo 使用)
### 主要命令
```
markbase display ← 啟動顯示視窗blocking等待 HTTP 控制)
markbase display "GUIDE.md" ← 啟動並立刻顯示文件
markbase preview "GUIDE.md" ← (保留) 單次預覽,不回傳控制權
markbase render "GUIDE.md" ← (保留) 輸出 HTML 到 stdout
```
### display — 核心命令(給 demo runner 使用)
```bash
# 啟動顯示視窗demo runner 透過 HTTP 控制
markbase display
# 指定控制埠(預設 11438
markbase display --port 11438
# 全螢幕模式
markbase display --fullscreen
# 啟動時先顯示文件
markbase display GUIDE.md
```
### HTTP 控制 APIdisplay 模式下啟用)
`markbase display` 啟動後在 `localhost:11438` 監聽控制請求:
```bash
# 顯示 .md 文件
curl -X POST http://localhost:11438/display \
-H "Content-Type: application/json" \
-d '{"type":"md","file":"/path/to/doc.md","focus":"API 搜尋"}'
# 播放影片
curl -X POST http://localhost:11438/display \
-d '{"type":"video","url":"/path/to/trace.mp4","start":10,"end":30}'
# 顯示格式化 JSON
curl -X POST http://localhost:11438/display \
-d '{"type":"json","data":"{\"status\":\"ok\"}"}'
# 內嵌網頁
curl -X POST http://localhost:11438/display \
-d '{"type":"url","url":"http://localhost:1420/trace-viz/..."}'
# 顯示圖片
curl -X POST http://localhost:11438/display \
-d '{"type":"image","url":"/path/to/thumbnail.jpg"}'
# 控制命令
curl -X POST http://localhost:11438/control \
-d '{"cmd":"fullscreen"}'
curl -X POST http://localhost:11438/control \
-d '{"cmd":"zoom","level":1.5}'
curl -X POST http://localhost:11438/control \
-d '{"cmd":"close"}'
```
### demo_runner.py 整合
```python
class MarkBaseDisplay:
"""控制 MarkBase 顯示視窗。"""
def __init__(self, port=11438):
self.port = port
self.process = None
def start(self):
self.process = subprocess.Popen(["markbase", "display",
"--port", str(self.port)], ...)
time.sleep(1) # wait for server
def show(self, type, **kwargs):
"""顯示內容。type: md/video/json/url/image"""
body = {"type": type, **kwargs}
requests.post(f"http://localhost:{self.port}/display", json=body)
def show_step(self, step):
"""根據 demo step 類型自動選擇顯示方式。"""
t = step["type"]
if t == "curl":
self.show("json", data=run_curl(step["cmd"]))
elif t == "browser":
self.show("url", url=step["url"])
elif t == "markdown":
self.show("md", file=step["cmd"], focus=step.get("focus"))
elif t == "video":
self.show("video", url=step.get("url"))
---
## 技術架構
```
┌─────────────────────────────────────────┐
│ MarkBase App │
├─────────────────┬───────────────────────┤
│ Frontend │ Engine │
│ (SwiftUI) │ (Rust core) │
│ │ │
│ • 視窗管理 │ • 解析 .md → AST │
│ • 選單、快捷鍵 │ • Mermaid 渲染 │
│ • 設定介面 │ • Code highlight │
│ • 搜尋 UI │ • 全文索引 │
│ • 目錄樹 │ • 文件監控 │
└─────────────────┴───────────────────────┘
│ │
▼ ▼
macOS Native API Rust 二進制
(WebKit + Swift) (pulldown-cmark + syntect + mermaid-rs)
```
### 為什麼 Engine 用 Rust
| 原因 | 說明 |
|------|------|
| 效能 | 大型 .md 文件1000+ 行)瞬間渲染 |
| 無 runtime | 單一二進制,無 Node.js/Python 依賴 |
| 現有基礎 | 可直接重用 md_reader 的 rendering 邏輯 |
| Mermaid 內嵌 | 可用 mermaid-rs crate 替代 CDN |
### 為什麼 Frontend 用 SwiftUI
| 原因 | 說明 |
|------|------|
| Native 體驗 | macOS native 視窗、menu bar、快捷鍵 |
| WebKit 整合 | 直接嵌入 WKWebView 渲染 HTML |
| 系統整合 | Spotlight、QuickLook、分享功能 |
| 效能 | 比 Electron 省 200MB+ 記憶體 |
---
## UI 設計
### 主視窗佈局
```
┌────────────────────────────────────────────────┐
│ Menu Bar: File Edit View Window Help │
├──────────┬─────────────────────────────────────┤
│ │ │
│ 左側欄 │ 主內容區 │
│ ────── │ ───────────────── │
│ 📁 文件 │ # 標題 │
│ ├ README│ 正文... │
│ ├ Guide│ ```code block``` │
│ └ API │ 表格 │
│ │ [Mermaid diagram] │
│ 目錄 │ │
│ ────── │ │
│ • Introduction│ │
│ • Getting...│ │
│ • API Ref │ │
│ │ │
├──────────┴─────────────────────────────────────┤
│ Status Bar: 字數 | 段落 | UTF-8 | dark mode toggle│
└────────────────────────────────────────────────┘
```
### 快捷鍵
| 按鍵 | 功能 |
|------|------|
| `Cmd+O` | 開啟 .md 文件 |
| `Cmd+F` | 全文搜尋 |
| `Cmd+Shift+F` | 跨文件搜尋 |
| `Cmd++` / `Cmd+-` | 調整字級 |
| `Cmd+D` | Toggle dark mode |
| `Cmd+B` | 左側目錄 toggle |
| `Cmd+P` | 列印 / PDF 匯出 |
| `Esc` | 關閉搜尋 / 回到瀏覽 |
---
## 目錄結構
```
markbase/
├── Cargo.toml # Rust core
├── src/
│ ├── main.rs # CLI entry point
│ ├── render.rs # .md → HTML
│ ├── highlight.rs # Code syntax highlighting
│ ├── mermaid.rs # Mermaid rendering
│ ├── search.rs # Full-text search
│ └── watch.rs # File watcher
├── app/ # SwiftUI app
│ ├── MarkBase.xcodeproj
│ ├── MarkBase/
│ │ ├── ContentView.swift
│ │ ├── SidebarView.swift
│ │ ├── SearchView.swift
│ │ └── SettingsView.swift
│ └── markbase-cli # Embedded Rust binary
└── docs/
└── ARCHITECTURE.md
```
---
## 與現有 md_reader 的差異
| 面向 | md_reader | MarkBase |
|------|-----------|----------|
| 語言 | 純 Rust CLI | Rust engine + SwiftUI app |
| 架構 | 單一 main.rs 1134 行 | 模組化 6+ 檔案 |
| 視窗 | 簡陋的 WebKit 視窗 | 完整 SwiftUI + WKWebView |
| 搜尋 | ❌ 無 | ✅ Cmd+F + 跨文件搜尋 |
| 目錄 | ❌ 無 | ✅ 左側 heading tree |
| File watcher | ❌ 無 | ✅ 自動索引目錄 |
| dark mode | ❌ 無 | ✅ 系統跟隨 + 手動 |
| Mermaid | CDN-based | 內建引擎 |
| Code highlight | ❌ 無 | ✅ syntect 50+ 語言 |
| 命名 | 功能描述 | 產品品牌 |
---
## 技術選型記錄
> 2026-05-12 新增
### 1. 轉檔引擎
| 工具 | License | 用途 |
|------|---------|------|
| pandoc 3.9 | GPL 2.0 | MD ↔ DOCX/PPTX/PDF |
| LibreOffice 26.2 | Apache 2.0 | 任何格式 ↔ 任何格式 (headless CLI) |
| mmdc | MIT | Mermaid → SVG/PNG |
| rsvg-convert | LGPL | SVG → PNG |
### 2. 編輯器選型
| 方案 | 決策 | 理由 |
|------|:--:|------|
| CodeMirror 6 | ✅ 選用 | MIT, 190KB gzip, CDN 免 npm, 模組化 |
| Monaco (VS Code) | ❌ | 5MB 太大,需 webpack |
| Ace | ❌ | 維護停滯 |
### 3. Markdown 生態分析
| 工具 | License | 類型 | MarkBase 啟發 |
|------|---------|------|--------------|
| glow | MIT | CLI 渲染 | 保留為獨立 CLI viewer |
| MarkText | MIT | WYSIWYG GUI | 參考 split-pane 編輯/預覽設計 |
| mdcat | MPL 2.0 | CLI | 參考 terminal 圖片渲染 |
| bat | MIT/Apache | CLI | 參考語法高亮策略 |
| mdBook | MPL 2.0 | CLI | 作為靜態文件站匯出格式 |
| MkDocs | BSD | CLI | 備選文件站方案 |
| Obsidian | Proprietary | Desktop PKM | 參考 `[[wiki links]]`、graph view、backlinks |
### 4. 桌面 vs Web
| 決策 | 選擇 | 理由 |
|------|:--:|------|
| Web first | ✅ | 任何裝置可用,同一份 HTML/JS/CSS |
| Tauri shell | ✅ 可選 | <10MB, 跨平台 macOS/Win/Linux |
| Electron | ❌ | 300MB 過於肥大 |
### 5. MarkBase vs Obsidian 定位
| | Obsidian | MarkBase |
|------|:--:|:--:|
| 定位 | 個人知識管理 (PKM) | **文件處理引擎 + 編輯器** |
| 資料格式 | .md only | 全格式 (via soffice) |
| 搜尋 | 全文 | RAG + embedding (Qdrant) |
| 後端 | 無 | axum HTTP + PSQL + Qdrant |
| CLI | 無 | ✅ CLI first |
| Pipeline | 無 | ✅ Chunking + LLM pipeline |
| 跨裝置 | 付費 sync | 自建 server 即可 |
| 大小 | ~300MB (Electron) | <10MB (Tauri) |
| 授權 | Proprietary (個人免費) | Momentry 專屬 |
### 6. CLI 設計
```
markbase display [--port 11438] [FILE] 啟動顯示伺服器
markbase render <FILE> [-o output.html] Markdown → HTML
markbase serve <DIR> 檔案瀏覽 + 編輯器 (計畫中)
```
### 7. 架構對比
```
Obsidian: MarkBase:
┌──────────────────────┐ ┌──────────────────────┐
│ Electron Shell │ │ Tauri / Browser │
│ ┌────────────────┐ │ │ ┌────────────────┐ │
│ │ Renderer │ │ │ │ Renderer │ │
│ │ ├─ CodeMirror │ │ │ │ ├─ CodeMirror │ │ ← 相同
│ │ ├─ Graph/D3 │ │ │ │ ├─ Mermaid.js │ │ ← 相同
│ │ ├─ Mermaid.js │ │ 相同 │ │ └─ pulldown │ │
│ │ └─ MathJax │ │ │ └────────────────┘ │
│ └────────────────┘ │ │ ┌────────────────┐ │
│ ┌────────────────┐ │ │ │ Rust Backend │ │ ← MarkBase 獨有
│ │ Plugin API │ │ │ │ ├─ axum HTTP │ │
│ │ 1,800+ plugins │ │ │ │ ├─ Embedding │ │
│ └────────────────┘ │ │ │ ├─ Qdrant ANN │ │
│ ┌────────────────┐ │ │ │ ├─ pgvector │ │
│ │ FS Access │ │ │ │ ├─ PG TKG │ │
│ │ .md files only │ │ │ │ ├─ SQLite TKG │ │
│ │ └────────────────┘ │ │ │ ├─ sqlite-vec │ │
│ └──────────────────────┘ │ │ └─ Pipeline │ │
```
### 8. 向量儲存sqlite-vec + Datasette
> 2026-05-12 採用
#### 選型
| 需求 | pgvector (PG) | Qdrant | sqlite-vec | 決策 |
|------|:--:|:--:|:--:|:--:|
| Production API (3003) | ✅ | — | — | pgvector (已有) |
| HNSW ANN 搜尋 | ⚠️ | ✅ | — | Qdrant (已有) |
| Desktop 本機 RAG | ❌ 需裝 PG | ❌ 需 server | ✅ 單檔 | sqlite-vec |
| 檔案包內嵌向量 | ❌ | ❌ | ✅ 隨包分發 | sqlite-vec |
| 離線可用 | ❌ | ❌ | ✅ | sqlite-vec |
| Web UI 查詢 | — | — | via Datasette | Datasette |
#### sqlite-vec 規格
| 屬性 | 值 |
|------|-----|
| License | MIT + Apache 2.0(雙授權) |
| 作者 | Alex Garcia |
| 贊助 | Mozilla Builders + Fly.io + Turso + SQLite Cloud |
| Stars | 7,600+ |
| 語言 | Pure C零依賴 |
| 大小 | ~200KB `.dylib` |
| ANN 引擎 | exhaustive, IVF, DiskANN |
| Rust binding | `cargo add sqlite-vec` |
#### Datasette選配 Web UI
| 屬性 | 值 |
|------|-----|
| License | Apache 2.0 |
| 作者 | Simon Willison |
| 定位 | SQLite → Web UI + JSON API |
| Plugins | 154 個 |
| sqlite-vec 插件 | `datasette-sqlite-vec`(同一作者) |
#### 使用範例
```sql
.load ./vec0
CREATE VIRTUAL TABLE chunks USING vec0(
embedding float[768],
file_uuid text,
chunk_type text,
text_content text
);
INSERT INTO chunks VALUES (?, 'uuid-123', 'sentence', 'hello world');
SELECT rowid, text_content, distance
FROM chunks WHERE embedding MATCH ?
ORDER BY distance LIMIT 10;
```
#### 四層向量架構
```
Production ← Qdrant (HNSW ANN, fast at scale)
← pgvector (transactional, alongside chunk data)
↓ backup / export
Portable ← sqlite-vec (.sqlite single file, package distributable)
← Datasette (optional Web UI)
```
### 9. Qdrant Graph 分析
> 2026-05-12 結論Qdrant **沒有**原生 Graph 功能,是純向量資料庫
#### Qdrant 現有功能
| 功能 | 說明 | 圖論等級 |
|------|------|:--:|
| **Payload filtering** | 向量搜尋 + JSON 條件過濾 | ⚠️ 偽關聯查詢 |
| **Collection aliases** | 多 collection 聯合查詢 | ⚠️ 基礎 |
| **Hybrid Queries** | 向量 + 關鍵字混合 | ❌ |
| **Qdrant Edge** | 嵌入式向量搜尋 | ❌ 非 Graph |
| **Data Graphs (第三方)** | Neo4j + Qdrant hybrid RAG | ✅ 非原生 |
#### Payload filtering 的極限
可以模擬 1-hop 關係(例如「找 Cary Grant 說話的 chunk」但不能做真正的 graph traversal
```json
// ✅ 1-hopfilter speaker = "Cary Grant"
{"filter": {"must": [{"key": "speaker", "match": {"value": "Cary Grant"}}]}}
// ❌ 2-hopgraph traversal Qdrant 無法做到
// "誰跟 Cary Grant 在同一個場景出現?"
// "這些人中誰又跟 Audrey Hepburn 對話?"
```
| 限制 | 說明 |
|------|------|
| ❌ 2-hop+ traversal | 無法跨節點關聯查詢 |
| ❌ 邊緣權重/時間 | 無 edge property 概念 |
| ❌ Graph algebra | 無 `shortest_path`, `PageRank` 等演算法 |
| ❌ Cypher/GQL | 無圖查詢語言 |
#### Momentry TKG 決策
| | Qdrant-only | PG TKG | SQLite TKG | Neo4j |
|---|:--:|:--:|:--:|:--:|
| 向量搜尋 | ✅ 原生 | via pgvector | via sqlite-vec | via plugin |
| Graph traversal | ❌ | ✅ CTE | ✅ CTE | ✅ 原生 |
| 2-hop+ 查詢 | ❌ | ✅ | ✅ | ✅ |
| 時間範圍邊緣 | ❌ | ✅ | ✅ | ✅ |
| 部署 | 需 server | 需 PG | **單檔** | 需 Java |
| 檔案包分發 | ❌ | ❌ | ✅ | ❌ |
| 適合規模 | 大 | 中 | 小-中 | 大 |
#### 架構分工
```
Qdrant → 向量搜尋ANN- 核心效能
PG → TKG 圖查詢Recursive CTE- API server
SQLite → TKG 圖查詢Recursive CTE- 檔案包/離線
```
---
## 亮點:知識圖譜 (Knowledge Graph)
> 2026-05-12 新增
### Obsidian vs MarkBase 圖譜對比
| | Obsidian Graph | MarkBase Knowledge Graph |
|------|:--:|:--:|
| 節點來源 | 手動建立的 `.md` 筆記 | AI pipeline 自動產生的 chunks |
| 邊緣來源 | 手寫 `[[wikilinks]]` | **語意相似度**、結構層級、共現關係 |
| 生成方式 | 人工 | **自動**embedding + clustering |
| 影片支援 | ❌ | ✅ face traces, speaker graph, scene transitions |
| 實體辨識 | ❌ | ✅ 人臉/說話者/物件/場景 |
| 規模 | 數百節點 | **數萬節點**chunk 級) |
| 過濾 | 無 | 時間範圍、置信度、chunk type |
### 圖譜類型
#### A. 語意關係圖Semantic Graph
以 embedding 餘弦相似度建立邊緣,相近 chunk 靠近。
```
[Audrey Hepburn 說話] ──0.82── [Cary Grant 回應]
│ │
│ 0.75 │ 0.78
▼ ▼
[討論離婚原因] ──0.91── [緊張對話場景]
```
**演算法**
1. 取所有 chunk embedding
2. 計算 pairwise cosine similarity
3. 保留 top-K 相似邊K=5 預設)
4. 用 UMAP/t-SNE → 2D 座標
5. D3.js force layout 渲染
#### B. 結構層級圖Hierarchy Graph
文件 → 章節 → 段落 的三層樹狀結構。
#### C. 人物關係圖Identity Graph
基於 face_detections + speaker_assign。
```
Cary Grant ──[對手戲]── Audrey Hepburn
│ │
│[對話] │[場景共現]
▼ ▼
Walter Matthau ────── Ned Glass
```
#### D. 時序演進圖Timeline Graph
Chunks 按時間軸排列場景切換點標記。X 軸 = 時間Y 軸 = 說話者。
### 渲染技術
| 層 | 工具 | License |
|----|------|---------|
| 力導向佈局 | D3-force (d3.js v7) | ISC |
| 降維 (UMAP) | umap-js | MIT |
| 2D 繪圖 | Canvas / SVG via D3 | ISC |
| 3D 繪圖 | Three.js | MIT |
| 節點過濾 | Crossfilter / vanilla JS | — |
### API 設計
```
GET /api/v1/graph/:file_uuid/identity → 人物關係圖資料
GET /api/v1/graph/:file_uuid/semantic?depth=3 → 語意圖資料
GET /api/v1/graph/:file_uuid/hierarchy → 結構層級圖
GET /api/v1/graph/:file_uuid/timeline → 時序圖資料
```
回傳格式:
```json
{
"nodes": [
{"id": "chunk_100", "label": "Cary Grant: What's your name?", "group": 3, "x": 0.1, "y": 0.5}
],
"edges": [
{"source": "chunk_100", "target": "chunk_104", "weight": 0.82, "type": "semantic"}
]
}
```
### 互動設計
| 操作 | 行為 |
|------|------|
| Drag node | 拖曳節點 |
| Click node | 展開 chunk 內容預覽 |
| Scroll | 縮放圖譜 |
| Filter bar | 依 chunk_type / speaker / confidence 過濾 |
| Double-click | 聚焦該節點,展開子圖 |
| Hover edge | 顯示相似度分數 |
### 圖譜渲染工具選型
> 2026-05-12 新增
#### 候選工具對比
| 工具 | License | 大小 | CDN | 圖論演算法 | 中國社群 | 最佳場景 |
|------|---------|:--:|:--:|:--:|:--:|------|
| **Cytoscape.js** | MIT | ~120KB | ✅ | ✅ BFS/DFS/PageRank | ⚠️ | 複雜網絡圖 |
| D3.js v7 | ISC | ~80KB | ✅ | ❌ 需自寫 | ⚠️ | 任何自訂圖表 |
| ECharts | Apache 2.0 | ~1MB | ✅ | ❌ | ✅ 非常大 | 通用圖表 + 地圖 |
| G6 (AntV) | MIT | ~500KB | ✅ | ✅ 多種佈局 | ✅ 非常大 | 關係圖專用 |
| vis-network | MIT/Apache | ~300KB | ✅ | ❌ | ❌ | 網絡圖 |
| Sigma.js | MIT | ~80KB | ✅ | ❌ | ❌ | WebGL 大圖 (>5000節點) |
| Graphviz | EPL 1.0 | ~3MB | ❌ CLI only | ✅ | ⚠️ | 靜態匯出 SVG/PNG |
#### 選型過程
**第一輪篩選**:排除 CLI-only (Graphviz)、無 CDN、中文社群弱且圖論支援差的 (vis-network, Sigma.js)。
剩餘Cytoscape.js, D3.js, ECharts, G6。
**第二輪深度評估**
| | Cytoscape.js | D3.js | ECharts | G6 |
|---|:--:|:--:|:--:|:--:|
| 力導向佈局 | ✅ 9 種 | ✅ 自寫 | ✅ 1 種內建 | ✅ 9 種 |
| 複合節點 (compound) | ✅ | ❌ | ❌ | ✅ |
| 圖論演算法 | ✅ 內建 | ❌ | ❌ | ✅ |
| JSON → Graph | ✅ 原生 | ⚠️ 手動 | ⚠️ 手動 | ✅ 原生 |
| TreeGraph | ⚠️ 需擴展 | ✅ | ❌ | ✅ 專用 |
| 大型圖效能 | ⚠️ (>5000會慢) | ✅ | ✅ Canvas | ✅ |
| 互動 API | ✅ 豐富 | ✅ 最靈活 | ✅ | ✅ |
| 零外部依賴 | ✅ | ✅ | ❌ (zrender) | ❌ |
**最終決策**
| 場景 | 選用 | 理由 |
|------|:--:|------|
| 知識圖譜核心 | **Cytoscape.js** | 圖論演算法、fCoSE 佈局、JSON 原生對接、Obsidian/Mermaid 都用 |
| 統計輔助圖表 | **ECharts** | 中文社群大、Apache 背書、長條/圓餅/分佈圖開箱即用 |
| 樹狀層級圖 | **G6 TreeGraph** | 專用 API文件結構圖最簡潔 |
| 自訂特殊需求 | **D3.js** | 保底方案,任何無法滿足的圖表 |
#### Cytoscape.js 使用者背書
| 組織 | 用途 |
|------|------|
| **Mermaid** | 流程圖/時序圖渲染引擎 |
| **Obsidian** | 知識圖譜 (Graph View) |
| Amazon, Google, Meta, Microsoft | 內部網絡圖視覺化 |
| IBM, Cisco, Tencent, Uber | 網路拓樸視覺化 |
| GitHub | 相依性圖 |
#### 整合架構
```
MarkBase Knowledge Graph:
┌──────────────────────────────────────┐
│ 圖譜類型 渲染引擎 │
│ ───────── ──────── │
│ 語意關係圖 → Cytoscape.js │
│ 結構層級圖 → G6 TreeGraph │
│ 人物關係圖 → Cytoscape.js │
│ 時序演進圖 → ECharts timeline │
│ 降維散點圖 → D3.js │
│ 統計分佈圖 → ECharts │
│ │
│ 全部 CDN 載入,無需 npm │
└──────────────────────────────────────┘
```
### 在 MarkBase 中的整合
```
MarkBase Control Bar:
⏮ ◀ ▶ ⏭ | Graph | Tree | Edit | 🔍
Knowledge Graph View
```
---
## 開發路線圖
| 階段 | 時程 | 交付 |
|------|:----:|------|
| P0 Core rendering | ✅ Done | Rust engine: .md→HTML with Mermaid + AJAX refresh |
| P1 macOS app | ✅ Done | Tauri shell (可選) |
| P2 File tree + Editor | 2-3d | CodeMirror 6 + lazy-load 樹狀瀏覽 + 存檔 |
| P3 Knowledge Graph | 3-5d | Cytoscape.js + G6 + ECharts: 語意/結構/人物關係圖譜 |
| P4 Knowledge base | 3-5d | 多文件索引、全文檢索、backlinks |
| P5 Export | 2d | 轉檔 CLI (md→pdf/docx/pptx) |
| P6 Collaboration | 5-10d | 評論、版本、靜態站點 |

View File

@@ -0,0 +1,647 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "處理器模組標準化規範"
date: "2026-04-25"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "處理器模組標準化規範"
ai_query_hints:
- "查詢 處理器模組標準化規範 的內容"
- "處理器模組標準化規範 的主要目的是什麼?"
- "如何操作或實施 處理器模組標準化規範?"
---
# 處理器模組標準化規範
## 概述
本規範定義 Momentry Core 中處理器模組的標準化架構、接口和實現模式。目標是確保所有處理器模組ASR、OCR、YOLO、Face、Pose、CUT、ASRX、Caption、Story遵循一致的設計原則提高代碼可維護性、可測試性和可擴展性。
## 架構原則
### 1. 分層架構
```
┌─────────────────────────────────────────┐
│ Rust API 層 │
│ (src/core/processor/*.rs) │
├─────────────────────────────────────────┤
│ Python 執行層 │
│ (scripts/*_processor.py) │
├─────────────────────────────────────────┤
│ AI 模型層 │
│ (Whisper, YOLO, EasyOCR, etc.) │
└─────────────────────────────────────────┘
```
### 2. 職責分離
- **Rust 層**: 接口定義、錯誤處理、配置管理、結果解析
- **Python 層**: AI 模型調用、數據處理、中間文件管理
- **模型層**: 特定 AI 任務執行
## Rust 模組規範
### 文件結構
```
src/core/processor/
├── mod.rs # 模組導出
├── executor.rs # Python 執行器(共享)
├── asr.rs # ASR 處理器
├── ocr.rs # OCR 處理器
├── yolo.rs # YOLO 處理器
├── face.rs # 人臉檢測處理器
├── pose.rs # 姿態檢測處理器
├── cut.rs # 場景切割處理器
├── asrx.rs # ASRX 處理器
├── caption.rs # 字幕生成處理器
└── story.rs # 故事分析處理器
```
### 模組模板
#### 1. 結果結構定義
```rust
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use super::executor::PythonExecutor;
use crate::core::config::processor;
// 主要結果結構
#[derive(Debug, Serialize, Deserialize)]
pub struct ModuleResult {
// 通用字段
pub processing_time: Option<f64>,
pub metadata: Option<serde_json::Value>,
// 模組特定字段
// ...
}
// 數據單元結構
#[derive(Debug, Serialize, Deserialize)]
pub struct DataUnit {
// 時間或幀相關字段
pub start: f64,
pub end: f64,
pub frame: u64,
// 數據內容
// ...
}
```
#### 2. 處理函數模板
```rust
pub async fn process_module(
video_path: &str,
output_path: &str,
uuid: Option<&str>,
) -> Result<ModuleResult> {
// 1. 初始化執行器
let executor = PythonExecutor::new()?;
let script_path = executor.script_path("module_processor.py");
// 2. 記錄日誌
tracing::info!("[MODULE] Starting processing: {}", video_path);
// 3. 執行 Python 腳本
executor
.run(
"module_processor.py",
&[video_path, output_path],
uuid,
"MODULE",
Some(Duration::from_secs(*processor::MODULE_TIMEOUT_SECS)),
)
.await
.with_context(|| format!("Failed to run {:?}", script_path))?;
// 4. 讀取並解析結果
let json_str = std::fs::read_to_string(output_path)
.context("Failed to read module output")?;
let result: ModuleResult = serde_json::from_str(&json_str)
.context("Failed to parse module output")?;
// 5. 記錄結果摘要
tracing::info!(
"[MODULE] Result: processed {} units",
result.data_units.len()
);
Ok(result)
}
```
#### 3. 配置管理
```rust
// 在 src/core/config.rs 中添加
pub mod processor {
use super::*;
pub static MODULE_TIMEOUT_SECS: Lazy<u64> = Lazy::new(|| {
env::var("MOMENTRY_MODULE_TIMEOUT")
.unwrap_or_else(|_| "3600".to_string())
.parse()
.unwrap_or(3600)
});
pub static MODULE_CHUNK_SIZE: Lazy<u64> = Lazy::new(|| {
env::var("MOMENTRY_MODULE_CHUNK_SIZE")
.unwrap_or_else(|_| "300".to_string())
.parse()
.unwrap_or(300)
});
}
```
#### 4. 測試規範
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_result_serialization() {
// 測試序列化/反序列化
}
#[test]
fn test_empty_result() {
// 測試邊界條件
}
#[tokio::test]
async fn test_integration() {
// 集成測試(可選)
}
}
```
## Python 腳本規範
### 文件命名
```
scripts/
├── module_processor.py # 主要處理腳本
├── module_utils.py # 工具函數(可選)
└── module_debug.py # 調試腳本(可選)
```
### 腳本模板
```python
#!/opt/homebrew/bin/python3.11
"""
模組處理器 - 標準化模板
功能:執行 [模組名稱] 處理
輸入:視頻文件路徑,輸出文件路徑
輸出JSON 格式的處理結果
"""
import sys
import json
import os
import argparse
import signal
import tempfile
import time
from pathlib import Path
from typing import Dict, Any, List, Optional
# 環境檢查
def check_environment() -> bool:
"""檢查必要的環境和依賴"""
try:
# 檢查必要庫
import required_library
return True
except ImportError as e:
print(f"ERROR: Missing dependency: {e}", file=sys.stderr)
return False
# 信號處理
def signal_handler(signum, frame):
"""處理中斷信號"""
print(f"[MODULE] Received signal {signum}, cleaning up...")
sys.exit(1)
# 主要處理類
class ModuleProcessor:
def __init__(self, video_path: str, output_path: str):
self.video_path = video_path
self.output_path = output_path
self.start_time = time.time()
def validate_input(self) -> bool:
"""驗證輸入文件"""
if not os.path.exists(self.video_path):
print(f"ERROR: Video file not found: {self.video_path}", file=sys.stderr)
return False
return True
def process(self) -> Dict[str, Any]:
"""執行處理邏輯"""
try:
# 1. 準備工作目錄
work_dir = tempfile.mkdtemp(prefix="module_")
# 2. 執行核心處理邏輯
result = self._core_processing(work_dir)
# 3. 添加元數據
result["metadata"] = {
"processing_time": time.time() - self.start_time,
"video_path": self.video_path,
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"module_version": "1.0.0"
}
return result
except Exception as e:
print(f"ERROR: Processing failed: {e}", file=sys.stderr)
raise
def _core_processing(self, work_dir: str) -> Dict[str, Any]:
"""核心處理邏輯(模組特定)"""
# 模組特定實現
return {
"data_units": [],
"summary": {}
}
def save_result(self, result: Dict[str, Any]):
"""保存結果到文件"""
with open(self.output_path, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"[MODULE] Result saved to: {self.output_path}")
# 命令行接口
def main():
parser = argparse.ArgumentParser(description="模組處理器")
parser.add_argument("video_path", help="輸入視頻文件路徑")
parser.add_argument("output_path", help="輸出 JSON 文件路徑")
args = parser.parse_args()
# 設置信號處理
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
# 環境檢查
if not check_environment():
sys.exit(1)
# 執行處理
processor = ModuleProcessor(args.video_path, args.output_path)
if not processor.validate_input():
sys.exit(1)
try:
result = processor.process()
processor.save_result(result)
print(f"[MODULE] Processing completed successfully")
except Exception as e:
print(f"ERROR: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
```
### 輸出格式規範
```json
{
"data_units": [
{
"id": "unit_1",
"start": 0.0,
"end": 2.5,
"frame": 0,
"data": {},
"confidence": 0.95
}
],
"summary": {
"total_units": 1,
"processing_time": 4.7,
"average_confidence": 0.95
},
"metadata": {
"video_path": "/path/to/video.mp4",
"module": "module_name",
"version": "1.0.0",
"timestamp": "2026-03-27 10:30:00"
}
}
```
## 配置標準化
### 環境變量
```
# 超時設置
MOMENTRY_ASR_TIMEOUT=3600
MOMENTRY_OCR_TIMEOUT=7200
MOMENTRY_YOLO_TIMEOUT=7200
MOMENTRY_FACE_TIMEOUT=3600
MOMENTRY_POSE_TIMEOUT=3600
MOMENTRY_CUT_TIMEOUT=3600
MOMENTRY_ASRX_TIMEOUT=3600
MOMENTRY_CAPTION_TIMEOUT=1800
MOMENTRY_STORY_TIMEOUT=1800
# 性能設置
MOMENTRY_MODULE_CHUNK_SIZE=300
MOMENTRY_MODULE_BATCH_SIZE=32
MOMENTRY_MODULE_CACHE_ENABLED=true
# 模型設置
MOMENTRY_MODULE_MODEL=base
MOMENTRY_MODULE_DEVICE=cpu
```
### 配置優先級
1. 命令行參數(最高優先級)
2. 環境變量
3. 配置文件
4. 默認值(最低優先級)
## 錯誤處理規範
### Rust 錯誤處理
```rust
use anyhow::{Context, Result};
pub async fn process_module(...) -> Result<ModuleResult> {
// 使用 .context() 添加上下文
executor.run(...)
.await
.with_context(|| format!("Failed to run module script"))?;
// 使用 anyhow::bail! 進行錯誤返回
if !condition {
anyhow::bail!("Condition not met: {}", reason);
}
}
```
### Python 錯誤處理
```python
def process(self) -> Dict[str, Any]:
try:
# 主要邏輯
result = self._core_processing()
return result
except FileNotFoundError as e:
print(f"ERROR: File not found: {e}", file=sys.stderr)
raise
except RuntimeError as e:
print(f"ERROR: Runtime error: {e}", file=sys.stderr)
raise
except Exception as e:
print(f"ERROR: Unexpected error: {e}", file=sys.stderr)
raise
```
### 錯誤分類
1. **輸入錯誤**: 文件不存在、格式不支持、權限問題
2. **配置錯誤**: 缺少依賴、環境變量錯誤、模型文件缺失
3. **運行時錯誤**: 內存不足、超時、模型推理錯誤
4. **輸出錯誤**: 結果解析失敗、文件寫入失敗
## 日誌規範
### Rust 日誌
```rust
tracing::info!("[MODULE] Starting processing: {}", video_path);
tracing::debug!("[MODULE] Processing details: {:?}", details);
tracing::warn!("[MODULE] Warning: {}", warning_message);
tracing::error!("[MODULE] Error: {}", error_message);
```
### Python 日誌
```python
import sys
def log_info(message: str):
print(f"[MODULE] INFO: {message}", file=sys.stderr)
def log_debug(message: str):
if os.environ.get("MODULE_DEBUG") == "1":
print(f"[MODULE] DEBUG: {message}", file=sys.stderr)
def log_error(message: str):
print(f"[MODULE] ERROR: {message}", file=sys.stderr)
```
## 性能監控
### 指標收集
```rust
pub struct ProcessingMetrics {
pub start_time: std::time::Instant,
pub end_time: Option<std::time::Instant>,
pub memory_usage_mb: f64,
pub cpu_usage_percent: f64,
pub items_processed: u64,
pub items_per_second: f64,
}
impl ProcessingMetrics {
pub fn new() -> Self {
Self {
start_time: std::time::Instant::now(),
end_time: None,
memory_usage_mb: 0.0,
cpu_usage_percent: 0.0,
items_processed: 0,
items_per_second: 0.0,
}
}
pub fn record_completion(&mut self, items_processed: u64) {
self.end_time = Some(std::time::Instant::now());
self.items_processed = items_processed;
let duration = self.end_time.unwrap().duration_since(self.start_time);
self.items_per_second = items_processed as f64 / duration.as_secs_f64();
}
}
```
### 性能報告
```json
{
"performance": {
"processing_time_seconds": 4.7,
"memory_usage_mb": 512.5,
"cpu_usage_percent": 45.2,
"items_processed": 8,
"items_per_second": 1.7,
"throughput_mb_per_second": 10.5
}
}
```
## 測試規範
### 單元測試
```rust
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_result_structure() {
// 測試數據結構
}
#[test]
fn test_serialization() {
// 測試序列化
}
#[test]
fn test_edge_cases() {
// 測試邊界條件
}
}
```
### 集成測試
```rust
#[tokio::test]
async fn test_module_integration() {
// 使用測試文件進行集成測試
let test_video = "test_data/sample.mp4";
let output_file = tempfile::NamedTempFile::new().unwrap();
let result = process_module(test_video, output_file.path().to_str().unwrap(), None)
.await
.expect("Processing should succeed");
assert!(!result.data_units.is_empty());
}
```
### Python 測試
```python
def test_module_processor():
"""測試 Python 處理器"""
processor = ModuleProcessor("test.mp4", "output.json")
# 測試輸入驗證
assert not processor.validate_input() # 文件不存在
# 測試處理邏輯
with tempfile.NamedTemporaryFile() as tmp:
processor = ModuleProcessor("real_test.mp4", tmp.name)
result = processor.process()
assert "data_units" in result
assert "metadata" in result
```
## 文檔規範
### Rust 文檔
```rust
/// ASR 處理器模組
///
/// 提供自動語音識別功能,支持多種語言和大文件處理。
///
/// # 示例
/// ```
/// use momentry_core::processor::asr;
///
/// let result = asr::process_asr("video.mp4", "output.json", None).await?;
/// println!("識別到 {} 個語音片段", result.segments.len());
/// ```
pub mod asr {
// ...
}
```
### Python 文檔
```python
"""
模組處理器
提供 [功能描述] 功能。
使用示例:
python module_processor.py input.mp4 output.json
參數:
video_path: 輸入視頻文件路徑
output_path: 輸出 JSON 文件路徑
輸出格式:
詳見輸出格式規範部分。
"""
```
## 遷移指南
### 現有模組標準化步驟
1. **分析現有代碼**: 識別不符合規範的部分
2. **創建備份**: 備份原始文件
3. **重構 Rust 模組**: 按照模板重構
4. **重構 Python 腳本**: 按照模板重構
5. **更新配置**: 統一配置管理
6. **添加測試**: 補充單元和集成測試
7. **更新文檔**: 更新 API 文檔和使用說明
8. **驗證功能**: 確保功能正常
### 兼容性保證
- 保持現有 API 不變
- 逐步遷移,不中斷現有功能
- 提供遷移工具和文檔
## 附錄
### A. 模組分類
| 模組 | 功能 | 主要技術 | 輸出類型 |
|------|------|----------|----------|
| ASR | 語音識別 | Whisper | 時間段文本 |
| OCR | 文字識別 | EasyOCR | 幀級文字 |
| YOLO | 物體檢測 | YOLOv8 | 幀級物體 |
| Face | 人臉檢測 | OpenCV | 幀級人臉 |
| Pose | 姿態檢測 | OpenPose | 幀級姿態 |
| CUT | 場景切割 | PySceneDetect | 場景邊界 |
| ASRX | 語音增強 | WhisperX | 說話人分離 |
| Caption | 字幕生成 | BLIP | 幀級描述 |
| Story | 故事分析 | 自定義 | 故事結構 |
### B. 性能基準
| 模組 | 平均處理時間 | 內存使用 | CPU 使用 |
|------|--------------|----------|----------|
| ASR | 4.7s (小文件) | 1.2GB | 45% |
| OCR | 12.3s (小文件) | 800MB | 35% |
| YOLO | 8.5s (小文件) | 1.5GB | 60% |
| Face | 3.2s (小文件) | 500MB | 25% |
### C. 常見問題
1. **依賴問題**: 確保 Python 環境正確設置
2. **內存不足**: 調整 chunk_size 參數
3. **超時錯誤**: 增加 timeout 設置或優化算法
4. **模型加載慢**: 啟用模型緩存
---
*版本: 1.0.0*
*更新日期: 2026-03-27*
*負責人: Warren (Technical Lead)*
*狀態: 草案*

View File

@@ -0,0 +1,353 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Momentry Core 影片 RAG 系統說明稿"
date: "2026-03-22"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "momentry"
- "core"
- "系統說明稿"
ai_query_hints:
- "查詢 Momentry Core 影片 RAG 系統說明稿 的內容"
- "Momentry Core 影片 RAG 系統說明稿 的主要目的是什麼?"
- "如何操作或實施 Momentry Core 影片 RAG 系統說明稿?"
---
# Momentry Core 影片 RAG 系統說明稿
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-22 |
| 文件版本 | V1.1 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
| V1.1 | 2026-03-25 | 更新API回應格式 (media_url→file_path) 與認證標頭 | OpenCode | deepseek-reasoner |
---
## 系統架構
```
┌─────────────────────────────────────────────────────────────┐
│ 使用者 │
│ (marcom 團隊) │
└─────────────────┬───────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ WordPress 入口 │
│ (wp.momentry.ddns.net) │
└─────────────────┬───────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ n8n 自動化 │
│ (localhost:5678) │
│ │
│ [Webhook] → [HTTP Request] → [處理結果] → [回覆用戶] │
└─────────────────┬───────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Momentry Core API │
│ (localhost:3002) │
│ │
│ POST /api/v1/search → 語意搜尋 │
│ POST /api/v1/n8n/search → n8n 專用格式 │
│ GET /api/v1/videos → 影片列表 │
└─────────────────┬───────────────────────────────────────────┘
┌─────────┴──────────┐
▼ ▼
┌───────────────┐ ┌───────────────┐
│ PostgreSQL │ │ Qdrant │
│ (chunks) │ │ (vectors) │
└───────────────┘ └───────────────┘
```
---
## 資料流程
```
1. 上傳影片 → SFTPGo
2. 影片註冊 → PostgreSQL
3. ASR 處理 → 產生字幕區塊
4. 儲存 chunks → PostgreSQL
5. 向量化 → Qdrant
6. 搜尋查詢 → API
7. 回傳結果 → n8n → 用戶
```
---
## 示範影片
| 項目 | 內容 |
|------|------|
| 檔案名稱 | Old_Time_Movie_Show_-_Charade_1963.HD.mov |
| UUID | a1b10138a6bbb0cd |
| 時長 | 6879 秒(約 1.9 小時) |
| 區塊數 | 3,886 個 |
| 向量數 | 3,688 個 |
---
## API 端點
### 1. 語意搜尋
```
POST http://localhost:3002/api/v1/search
```
**請求:**
```json
{
"query": "charade",
"limit": 5,
"uuid": "a1b10138a6bbb0cd"
}
```
> **注意**:
> 1. **API 認證**: 所有 `/api/v1/*` 端點需要 `X-API-Key` 標頭
> 2. **檔案路徑轉換**: API 現在返回 `file_path`(檔案系統路徑),需要轉換為可訪問的 URL例如透過 SFTPGo 分享連結)
---
### 2. n8n 專用格式
```
POST http://localhost:3002/api/v1/n8n/search
```
**請求:**
```json
{
"query": "charade",
"limit": 5
}
```
**回應:**
```json
{
"query": "charade",
"count": 5,
"hits": [
{
"id": "sentence_0006",
"vid": "a1b10138a6bbb0cd",
"start": 48.8,
"end": 55.44,
"title": "Chunk sentence_0006",
"text": "fun plot twists...",
"score": 0.526,
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
}
]
}
```
---
## 實作範例
### n8n Workflow 設計
```
┌─────────────┐
│ Webhook │ ← 接收用戶搜尋請求
└──────┬──────┘
┌─────────────┐
│ HTTP Request│ → POST /api/v1/n8n/search
└──────┬──────┘
┌─────────────┐
│ Code │ → 處理回傳結果
└──────┬──────┘
┌─────────────┐
│ Telegram │ → 回覆給用戶
│ (或 LINE) │
└─────────────┘
```
---
## Step-by-Step n8n Workflow
### Step 1: 建立 Webhook
1. n8n 開新 Workflow
2. 新增 node: **Webhook**
3. 設定 path: `video-search`
4. 複製 Webhook URL
---
### Step 2: 設定 HTTP Request
1. 新增 node: **HTTP Request**
2. 設定:
```
Method: POST
URL: http://localhost:3002/api/v1/n8n/search
Body Content Type: JSON
Headers: X-API-Key (需設定)
```
3. Body:
```json
{
"query": "={{ $json.body }}",
"limit": 5
}
```
---
### Step 3: 處理結果 (Code)
```javascript
const hits = $input.first().json.hits;
if (!hits || hits.length === 0) {
return {
json: { message: "找不到相關結果" }
};
}
const results = hits.map((hit, index) => ({
number: index + 1,
text: hit.text,
time: `${hit.start}s - ${hit.end}s`,
score: Math.round(hit.score * 100) + "%",
// 注意: API 現在返回 file_path檔案系統路徑需要轉換為可訪問的 URL
url: hit.file_path + "#t=" + hit.start + "," + hit.end // 需實作檔案路徑轉換為 URL
}));
return { json: { results } };
```
> **注意**:
> 1. **API 認證**: 所有 `/api/v1/*` 端點需要 `X-API-Key` 標頭
> 2. **檔案路徑轉換**: API 現在返回 `file_path`(檔案系統路徑),需要轉換為可訪問的 URL例如透過 SFTPGo 分享連結)
---
### Step 4: 格式化輸出
**Telegram 格式:**
```
🎬 搜尋結果: "{{ $json.query }}"
1⃣ "fun plot twists, Woody Dialog and charming performances..."
⏱ 48.8s - 55.4s
📊 相關度: 53%
2⃣ "Don't you like me to say that a pretty girl..."
⏱ 4745.6s - 4748.6s
📊 相關度: 52%
```
---
## 測試指令
### curl 測試
```bash
# 語意搜尋
curl -X POST http://localhost:3002/api/v1/search \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"query": "charade", "limit": 3}'
# n8n 格式
curl -X POST http://localhost:3002/api/v1/n8n/search \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"query": "charade", "limit": 3}'
# 影片列表
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
# 特定影片區塊
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos/a1b10138a6bbb0cd/chunks
```
---
## 實際搜尋範例
| 搜尋詞 | 結果摘要 |
|--------|----------|
| `charade` | "fun plot twists, Woody Dialog and charming performances..." |
| `woody` | "Well, you thick skull hair, brain half-witted..." |
| `classic movie` | "Hello and welcome to the old-time movie show..." |
| `charming` | "fun plot twists, Woody Dialog and charming performances..." |
---
## 資料庫狀態
| 資料庫 | 資料筆數 | 狀態 |
|--------|----------|------|
| PostgreSQL (videos) | 4 | ✅ |
| PostgreSQL (chunks) | 3,950 | ✅ |
| PostgreSQL (vectors) | 1,870 | ✅ |
| Qdrant (vectors) | 3,688 | ✅ |
| Redis (job cache) | 4 keys | ✅ |
---
## 下一步
1. **建立 SFTPGo 分享連結**
- 開啟 http://localhost:8080
- 登入 demo / demopassword123
- 建立影片分享連結
2. **測試 n8n Workflow**
- 匯入 Postman Collection
- 建立 Webhook
- 測試搜尋
3. **整合到 WordPress**
- 建立表單接收用戶輸入
- 呼叫 n8n Webhook
- 顯示搜尋結果
---
## 快速開始
```bash
# 1. 測試搜尋 API
curl -X POST http://localhost:3002/api/v1/search \
-H "Content-Type: application/json" \
-d '{"query": "charade", "limit": 3}'
# 2. 查看影片列表
curl http://localhost:3002/api/v1/videos
# 3. 查看 n8n 是否運行
curl http://localhost:5678
```

View File

@@ -0,0 +1,94 @@
# Non-Human Sound Detection — Tool Selection Report
**Date:** 2026-05-10
**Movie:** Charade (1963), 113 min
**Audio:** 16kHz mono WAV
**Goal:** Detect non-human sound events (gunshots, impacts, doors, music, etc.)
## Tested Approaches
### Approach A: AST AudioSet (HuggingFace)
| Item | Detail |
|------|--------|
| Model | `MIT/ast-finetuned-audioset-10-10-0.4593` |
| Method | Audio Spectrogram Transformer, fine-tuned on AudioSet-2M (527 classes) |
| Dependencies | `transformers`, `torch` ✅ (no torchcodec needed) |
| Load time | ~1s on M5 |
| Inference time | ~0.5s per 3-second clip (805k params, float32) |
| Accuracy | Good — correctly distinguishes speech vs. door vs. music |
**Test results on Charade:**
| Time | Energy-based said | AST AudioSet said | Verdict |
|------|------------------|-------------------|---------|
| 0:10 | — | Environmental noise (26%) | Background noise, plausible |
| 10:32 | Gunshot candidate (43x) | **Speech (76%)** | ✅ AST correct |
| 57:00 | Gunshot candidate (49x) | **Door (62%) + Slam (5%)** | ✅ AST correct |
| 65:13 | Gunshot candidate (50x) | **Speech (58%)** | ✅ AST correct |
| 85:12 | Gunshot candidate (39x) | **Speech (68%)** | ✅ AST correct |
**Conclusion**: Energy-based impulse detection has **100% false positive rate** for gunshot detection. AST AudioSet correctly classifies all candidates as non-gunshot.
### Approach B: Custom Energy + Spectral Features
| Item | Detail |
|------|--------|
| Method | RMS energy + spectral centroid + sub-band energy ratios |
| Speed | ~3s for full 113-min movie (every 10th window) |
| Accuracy | Poor — cannot distinguish gunshot from speech, door, music |
| Result | 1 "gunshot_candidate" from 453 test windows; all false positives on verification |
**Conclusion**: Useful as a **coarse pre-filter** (Stage 1), not as a standalone classifier.
## Two-Stage Design
```
Stage 1 (Energy filter, ~1 min):
Full audio → sliding window RMS + centroid → ~200 candidate windows
|
v
Stage 2 (AST classifier, ~2 min):
Extract 3-sec audio for each candidate → AST AudioSet classification
|
v
Non-speech events: gunshot, explosion, door slam, music, etc.
```
Estimated processing: ~3 min for full movie (vs. 75 min for full AST scan)
## Key AudioSet Classes Relevant to Charade
| Class | AudioSet ID | Relevance |
|-------|-------------|-----------|
| Gunshot, gunfire | 402 | **Primary target** |
| Explosion | 400 | Hand grenade in plot |
| Door slams | 404 | Scenes at hotel, apartment |
| Music | 130-133 | Background score |
| Speech | 0-3 | Already handled by ASR |
| Vehicle | 100-110 | Car sounds in Paris chase |
| Glass break | 424 | Window breaking scene |
## Actor-voice gender mismatches (resolved by fine-grained ASRX)
During the speaker mapping work, 20 segments where the old face→TMDb assignment said "Audrey Hepburn" but the new ASRX voice embedding clearly said "MALE". These segments were verified via video clips and confirmed to be scenes where:
1. A male speaker (Cary Grant or other) is speaking while Audrey Hepburn's face is on screen
2. The old pipeline incorrectly assigned the speaker name based on face identity
3. The fine-grained sliding window approach correctly resolves these
The 20 segments were from SPEAKER_5 (10 segs) and SPEAKER_9 (10 segs), both of which mapped to MALE voice clusters. These were re-assigned to "Cary Grant" or "Unknown" as appropriate.
## Recommendations
| Approach | Speed | Accuracy | Best for |
|----------|-------|----------|----------|
| Energy pre-filter | ✅ 1 min | ❌ Low | Stage 1: candidate selection |
| AST AudioSet | ⚠️ 2 min | ✅ High | Stage 2: event classification |
| Full AST scan | ❌ 75 min | ✅ High | N/A — two-stage is better |
**Design**: Two-stage pipeline: energy pre-filter → AST classifier
**Implementation path**:
1. Write `scripts/non_human_sound_detector.py` with the two-stage design
2. Output `{uuid}.sound_events.json` with typed events
3. Integrate into the sound_event_detector framework

View File

@@ -0,0 +1,134 @@
# Processor 產出機制檢討
## 三層機制定義
### 1. 中斷接續Interruption Resume
Process 被殺掉後,重啟時能接續進度。
**現狀**: 大部分 processor 有 `.tmp``.partial` 保護,但重跑時從頭開始。
### 2. 補充機制Supplement
完成度不足時,只補沒做完的部分,不重跑整個。
**現狀**: 全部從頭跑,無補充。
### 3. 糾錯機制Error Correction
輸出檔損毀時能自動偵測並修復。
**現狀**: file-existence check 只檢查檔案存在,不檢查內容是否有效。
---
## Processor 逐一檢討
### ASR
| 面向 | 現狀 | 問題 |
|------|------|------|
| 中斷接續 | ✅ `.tmp``.partial`executor | ✅ OK |
| 補充機制 | ❌ 每次從頭跑 | 若跑到 50% 被殺,下次從 0% 開始 |
| 糾錯機制 | ❌ 不驗證內容 | file-existence check 看到 `.json` 存在就跳過,不管內容 |
| Pipe | ✅ executor.run() | ✅ |
| Timeout | ✅ 已移除None | ✅ |
**改善方案**:
- 補充ASR 重跑時掃描 existing `.json``.partial`,找出最後 segment 的 `end_time`,傳入 `--resume-from` 給 Python script
- 糾錯file-existence check 對 `.json``serde_json::from_str` 驗證,無效 → 視為不存在
### ASRX
| 面向 | 現狀 | 問題 |
|------|------|------|
| 中斷接續 | ❌ **不用 executor**,直接寫 `.json` | 被殺掉時留下壞檔 |
| 補充機制 | ❌ 同 ASR | 依賴 ASRASR 不完整 ASRX 也不能跑 |
| 糾錯機制 | ❌ 不驗證內容 | 同上 |
| Pipe | ❌ **raw Command**,沒有 `.tmp` 保護 | 緊急 |
| Timeout | ⚠️ 7200s hardcode | 應改為 None同 ASR |
**改善方案**:
- **最優先**: 改為使用 `executor.run()`,獲得 `.tmp` 保護
- 其他同 ASR
### YOLO
| 面向 | 現狀 | 問題 |
|------|------|------|
| 中斷接續 | ✅ executor `.tmp` | ✅ |
| 補充機制 | ❌ 從頭跑 | 若跑到 frame 100,000 被殺,下次從 frame 0 |
| 糾錯機制 | ❌ 不驗證內容 | yolo.json 之前就是壞的但 file check 跳過 |
**改善方案**:
- 補充:掃描 `.partial` 的最後 frame傳入 `--resume-frame` 給 Python script
- 糾錯file-existence check 對 `.json` 做 JSON parse 驗證
### FACE / POSE / OCR
| 面向 | 現狀 | 問題 |
|------|------|------|
| 中斷接續 | ✅ executor `.tmp` | ✅ |
| 補充機制 | ❌ 從頭跑 | 同 YOLO |
| 糾錯機制 | ❌ 不驗證內容 | 同 YOLO |
**改善方案**: 同 YOLO
### CUT
| 面向 | 現狀 | 問題 |
|------|------|------|
| 中斷接續 | ✅ executor `.tmp` | ✅ |
| 補充機制 | ✅ register 階段已完成,直接載入 | ✅ |
| 糾錯機制 | ❌ 不驗證內容 | 同 YOLO |
**改善方案**: 糾錯即可
### SCENE
| 面向 | 現狀 | 問題 |
|------|------|------|
| 中斷接續 | ✅ **最完整**:檢查 `.err`/`.json`/`.tmp` 三種狀態 | ✅ |
| 補充機制 | ❌ 從頭跑 | ✅scene 很快) |
| 糾錯機制 | ⚠️ 有檢查 `.err` | ✅ |
### VISUAL_CHUNK
| 面向 | 現狀 | 問題 |
|------|------|------|
| 中斷接續 | ✅ executor `.tmp` | ✅ |
| 補充機制 | ❌ | ❌ |
| 糾錯機制 | ❌ **錯誤被吞掉**(回傳空結果) | 應回報 error 而非靜默失敗 |
**改善方案**: 不要吞錯誤,讓 error 往上傳
### STORY
| 面向 | 現狀 | 問題 |
|------|------|------|
| 中斷接續 | ✅ executor `.tmp` | ✅ |
| 補充機制 | ❌ | ❌ |
| 糾錯機制 | ❌ | ❌ |
---
## 優先級
### P0 — 立即修復
1. **ASRX 改用 executor.run()**
- 檔案:`src/core/processor/asrx.rs`
- 獲得 `.tmp` 保護、SIGKILL process group、`.partial` 保留
- 移除 hardcode timeout
### P1 — 糾錯機制
2. **File-existence check 加入 JSON 驗證**
- 檔案:`src/worker/job_worker.rs`
-`output_path.exists()` 之後,對 `.json``serde_json::from_str::<Value>`
- 若 parse 失敗 → 不 skip當作檔案不存在繼續跑
- 若 parse 成功但內容空(無 segments/frames→ 當不完整
### P2 — 補充機制
3. **ASR resume-from 補充**
- 檔案:`src/core/processor/asr.rs` + `scripts/asr_processor.py`
- Rust 端發現 `.partial` 存在,讀取最後 segment 的 end_time
- 傳入 `--resume-from {time}` 給 Python script
- Python script 跳過 `--resume-from` 之前的音訊
4. **YOLO/Face/Pose resume-frame 補充**
- 檔案:各 processor.rs + 對應 Python script
- 掃描 `.partial` 中的最後 frame_number
- 傳入 `--resume-frame {frame}` 給 Python script
### P3 — 其他
5. **VisualChunk 不吞錯誤**
6. **Executor SIGTERM → SIGKILL 兩段式關閉**

View File

@@ -0,0 +1,240 @@
# Momentry Model — 分階段交付
## 核心架構
```
Pipeline (training)
│ 每個 processor 產出 .json
│ Rule 1/3 Ingestion → chunks + embeddings
momentry model for {video} ← 每部影片 = 一個 model
│ release/phase1/latest/
│ release/phase2/latest/
momentry core (inference engine) ← Rust API server
│ momentry_playground (dev)
│ momentry (production)
Search / Query / Identity APIs
```
- **Pipeline** = training phase影片 → processor output → chunks → embeddings
- **Model** = 每部影片的產出 packageoutput_json + chunks + vectors
- **Engine** = momentry core吃 model 提供 APIsearch, trace, identity
每個影片可有多個 model 版本,命名保留升級空間:
| Model 版本 | Qdrant Collection | 內容 | 觸發時機 |
|-----------|------------------|------|---------|
| `{uuid}_v1` | `momentry_dev_v1` | sentence chunk embeddingbase | ASR + ASRX + Rule 1 完成 |
| `{uuid}_v2` | `momentry_dev_v2` | 完整 pipeline + 5W1H | 全部完成 |
| `{uuid}_v3` | `momentry_dev_v3` | object identity + custom detector | v2 + object instance matching 完成 |
各版本共存不覆蓋。
## 階段劃分
### Phase 1Sentence Chunk Embeddingbase model
**觸發時機**: ASR + ASRX 完成 + Rule 1 Ingestion + vectorize 完成
**交付內容**:
- `{uuid}.asr.json`
- `{uuid}.asrx.json`
- chunkschunk_type = 'sentence'
- chunk_vectorssentence embedding
**用途**: 終端使用者可進行語意搜尋
### Phase 2完整 Pipelinev2 model
**觸發時機**: 全部 processor 完成 + Rule 3 Ingestion + 5W1H Agent
**交付內容**:
- Phase 1 全部內容
- 所有 `{uuid}.*.json`cut, yolo, face, pose, ocr, ...
- chunkschunk_type = 'cut', 'visual', 'trace', 'story'
- chunk_vectorssummary embedding
- identities / identity_bindings / face_detections
**用途**: 完整搜尋 + 摘要 + 人物識別
---
## Worker Pipeline
```
ASR 完成 → ASRX 完成
Rule 1 Ingestion (sentence chunks)
vectorize_chunks (sentence embedding)
📦 Phase 1 release ───→ release/phase1/latest/ (base model)
其他 processors 繼續 (yolo, face, pose, ocr, ...)
Rule 3 Ingestion + 5W1H Agent
📦 Phase 2 release ───→ release/phase2/latest/ (full model)
```
## 產出目錄結構
```
release/
├── phase1/
│ ├── {version}_{timestamp}/
│ │ ├── output_json/ ← 所有已完成的 .json
│ │ ├── chunks.csv ← sentence chunks
│ │ ├── vectors.csv ← sentence embeddings
│ │ ├── schema.sql ← chunks table DDL
│ │ └── RELEASE_INFO.txt
│ └── latest → {version}_{timestamp}
└── phase2/
├── {version}_{timestamp}/
│ ├── output_json/ ← 所有 .json
│ ├── chunks.csv ← 所有 chunks
│ ├── vectors.csv ← 所有 embeddings
│ ├── identities.csv ← 人物身分
│ ├── schema.sql ← 完整 schema
│ └── RELEASE_INFO.txt
└── latest → {version}_{timestamp}
```
## momentry model vs momentry core
| | momentry model | momentry core |
|---|---|---|
| 類比 | 訓練好的 weights | inference engine |
| 內容 | `.json` + chunks + vectors | Rust binary |
| 生命週期 | 每部影片產出一個 | 一個 binary 服務所有影片 |
| 版本 | `{uuid}_v1`base / `{uuid}_v2` / `{uuid}_v3` | `momentry_playground` / `momentry` |
| 交付對象 | 終端使用者 | 部署工程師 |
---
## Wiki 機制:每個 model 都可被調整
每個 momentry model`{uuid}_v1` / `v2` / `v3`)不只是唯讀的產出,而是可透過 wiki 機制持續改善。
### 與傳統 RAG 的區別
| | 傳統 RAG | momentry wiki |
|---|---|---|
| 知識儲存 | vector DBephemeral | model packagepermanent |
| 修正方式 | query 時 LLM 決定是否採用 | 使用者/Agent 直接編輯 |
| 修正持久性 | ❌ 下次 query 就消失 | ✅ 寫入 model版本化保存 |
| 模型改進 | 無(僅改變 prompt | 下次 version bump 時合併為 ground truth |
| 協作方式 | 單向retrieve → generate | 雙向(編輯 → 合併 → 改進) |
| 離線可用 | ❌ 需 vector DB + LLM | ✅ 離線查閱 wiki 目錄 |
**momentry wiki 不是 RAG 的替代品,而是 model 的生命週期管理機制。**
### 概念
```
momentry model (release package)
├── output_json/ ← 唯讀processor 產出
├── chunks.csv ← 唯讀ingestion 產出
├── vectors.csv ← 唯讀embedding 產出
└── wiki/ ← 可編輯,使用者貢獻知識
├── identities.json ← "trace 5 = Audrey Hepburn"
├── objects.json ← "object 42 = 郵票 #1"
├── corrections.json ← "ASR 'Hello' → 'Halo'"
└── changelog.json ← 編輯歷史
```
### 資料流向
```
使用者/Agent 編輯 wiki
DB wiki_entries + wiki_revisions 寫入
下次 release 打包時 merge 進 model
TKG label 更新 (tkg_nodes.label)
新版 model version bump
```
### 與 TKG 的關係
wiki 的 identity 和 object 標註會回寫到 TKG node label
```
(face_trace:5) label="Audrey Hepburn" ← wiki 編輯
(object_instance:42) label="郵票 #1" ← wiki 編輯
```
這些編輯累積後,可做為下一版 model training 的 ground truth。
### 實作方向
**DB 層** — 新 table `wiki_entries` + `wiki_revisions`
```sql
wiki_entries (target_type, target_id, title, body, summary, status, version, file_uuid)
wiki_revisions (entry_id, version, title, body, summary, change_summary, edited_by)
```
**API 層** — CRUD + 版本歷史:
```
GET /api/v1/wiki/{target_type}/{target_id}
PUT /api/v1/wiki/{target_type}/{target_id}
GET /api/v1/wiki/{target_type}/{target_id}/revisions
POST /api/v1/wiki/search
```
**打包層**`release_pack.py` 加入 wiki 匯出,與 model 共存
---
## Phase 3Object Identityv3 model
### 目標
從影片中提取關鍵物體(郵票、手槍、信封、放大鏡...),對同類物體做 instance-level 的跨畫面追蹤與辨識,達到類似 face trace 的效果 — 不只是 detect class還能區分「這一張郵票」vs「那一張郵票」。
### 現狀問題
1. **COCO 80 類不包含關鍵物體** — 郵票、手槍、信封、放大鏡等不在 COCO 資料集中
2. **YOLOv5nano 偵測率低** — 即使是 COCO 類別knife, cell phone在 nano 模型上 recall 不足
3. **無 object instance matching** — 目前只有 frame-level detection沒有跨 frame 的物體追蹤
### 技術方向
```
YOLOv8m/OWL-ViT → 改善 detection coverage
Object Tracker (IoU + embedding類似 face tracker)
object_trace → TKG CO_OCCURS_WITH edges
object identity → 同物體跨場景辨識
```
| 方向 | 方法 | 效果 |
|------|------|------|
| Model upgrade | `yolov5nu``yolov8s.pt` / `yolov8m.pt` | COCO recall 提升 |
| Custom fine-tune | 收集 stamps/guns 資料 fine-tune YOLO | 可偵測非 COCO 物件 |
| Zero-shot | OWL-ViT / Grounding DINO by text prompt | 不用 training但速度慢 |
| Object trace | IoU + embedding 跨 frame 匹配 | instance-level 追蹤 |
| Object identity | clustering 跨場景辨識同一物體 | 可在全片搜尋「這把槍」 |
### 與 TKG 整合
```
face_trace -[:CO_OCCURS_WITH]-> object_instance:5 (這把槍)
face_trace -[:CO_OCCURS_WITH]-> object_instance:42 (這張郵票)
查詢: "Audrey Hepburn 拿這把槍的畫面"
→ face_trace:5 -[:SPEAKS_AS]-> SPEAKER_0
→ face_trace:5 -[:CO_OCCURS_WITH]-> object_instance:5
```
### 交付順序
1. YOLO model upgrade低難度立即見效
2. Object tracker中難度參考 face tracker 實作)
3. Custom fine-tune / zero-shot高難度需資料或新模型

View File

@@ -0,0 +1,361 @@
---
document_type: "design"
service: "MOMENTRY_CORE"
title: "TMDb 整合 — Identity 檔案系統設計"
date: "2026-05-16"
version: "V1.0"
status: "completed"
owner: "M5"
created_by: "OpenCode"
tags:
- "tmdb"
- "identity"
- "cache"
- "file-system"
- "resource"
- "design"
ai_query_hints:
- "查詢 TMDb Identity 檔案系統設計的內容"
- "TMDb 整合的三個階段是什麼"
- "如何從 cache 建立 TMDb identities"
- "identity 檔案化目錄結構"
- "TMDb resource API endpoint 列表"
- "TMDb face matching 整合位置"
related_documents:
- "REFERENCE/Face_Pipeline.md"
- "REFERENCE/Trace_Structure.md"
- "REFERENCE/Demo_EndToEnd.md"
- "REFERENCE/Services_Inventory.md"
---
# TMDb 整合 — Identity 檔案系統設計 V1.0
| 項目 | 內容 |
|------|------|
| 建立者 | OpenCode |
| 建立時間 | 2026-05-16 |
| 文件版本 | V1.0 |
| 狀態 | Completed |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-05-16 | 三階段 TMDb 整合設計Identity 檔案化、Agent Cache、Resource 納管 | OpenCode | DeepSeek V4 Flash |
---
## Overview
三個計劃循序實作,建立 Identity 的 filesystem 副本與 TMDb 外部資源整合:
1. **Plan 1: Identity 檔案化** — 每個 identity 在 `{OUTPUT}/identities/{uuid}/identity.json` 有完整備份
2. **Plan 2: TMDb Agent + Cache** — 唯一外連點fetch TMDb API → cache 到 `{uuid}.tmdb.json`
3. **Plan 3: TMDb 納管** — resource endpoint + health 整合
### 設計原則
- **全本地為預設**TMDb 是唯一需要外連的服務,視為 optional plugin
- **Cache-first**TMDb API 只 call 一次,之後全從 local cache 讀
- **Dual-write**DB + filesystem 保持一致
- **filesystem 為 canonical snapshot**DB 是 primary storefilesystem 是可攜離線副本
---
## Plan 1: Identity 檔案化
### 目的
為每個 identity 建立 filesystem snapshot使 identity 資料:
- **可搬移**`cp -r identities/` 到另一台機器即可
- **可檢查**`cat {uuid}/identity.json` 直接看完整 identity 資料
- **可備份**tar identities/ 即為 identity 完整備份
- **可離線**:不需要 DB 也能取得 identity 基本資訊
### 目錄結構
```
{OUTPUT_DIR}/
├── identities/
│ ├── _index.json ← { uuid: name } 索引
│ ├── a9a901056d6b46ff92da0c3c1a57dff4/
│ │ └── identity.json ← V1: 完整 identity 資訊
│ └── b0b101167e8c4a53a0.../
│ └── identity.json
└── {file_uuid}.tmdb.json ← V2: TMDb raw cache
```
### identity.json 格式
```json
{
"version": 1,
"identity_uuid": "a9a901056d6b46ff92da0c3c1a57dff4",
"name": "Cary Grant",
"identity_type": "people",
"source": "tmdb",
"status": "confirmed",
"tmdb_id": 112,
"tmdb_profile": "https://image.tmdb.org/t/p/w185/abc.jpg",
"metadata": {
"tmdb_character": "Peter Joshua",
"tmdb_cast_order": 0,
"tmdb_movie_id": 4808
},
"file_bindings": [
{
"file_uuid": "3a6c1865...",
"trace_ids": [10, 23],
"face_count": 12
}
],
"created_at": "2026-05-16T12:00:00Z",
"updated_at": "2026-05-16T12:30:00Z"
}
```
### _index.json 格式
```json
{
"version": 1,
"updated_at": "2026-05-16T12:00:00Z",
"entries": {
"a9a901056d6b46ff92da0c3c1a57dff4": "Cary Grant",
"b0b101167e8c4a53a09d6c2a68e0abf1": "Audrey Hepburn"
}
}
```
### 寫入策略Dual-write
任何 identity 變更 → DB write → `save_identity_file()` → filesystem write
```
identity 變更發生處:
├── TMDb probe (probe.rs) → create_identities_from_data() → save_identity_file() per identity
├── Face matching API (identity_agent_api.rs) → match_faces_iterative() → save_identity_file() per matched identity
├── Face matching Worker P2.5 (job_worker.rs) → match_faces_against_tmdb() → save_identity_file() per affected identity
├── Manual bind/unbind (identity_binding.rs) → bind/unbind handler → save_identity_file() per identity
└── One-time migration (migrate_identity_files.py) → 全部 identities 檔案化
```
### API: `storage.rs`
```rust
// structs
IdentityFile { version, identity_uuid, name, identity_type, source, status,
tmdb_id, tmdb_profile, metadata, file_bindings, created_at, updated_at }
FileBinding { file_uuid, trace_ids, face_count }
// core functions
identity_dir(uuid: &str) -> PathBuf
read_identity_file(uuid: &str) -> Result<IdentityFile>
write_identity_file(file: &IdentityFile) -> Result<()>
list_identity_uuids() -> Result<Vec<String>>
count_identity_files() -> usize
// index
read_index() -> Result<HashMap<String, String>>
update_index(uuid: &str, name: &str) -> Result<()>
// dual-write hook
async fn save_identity_file(db: &PostgresDb, uuid: &str) -> Result<()>
// 1. 查 DB 取得 identity full data
// 2. 查 DB 取得 file_bindings
// 3. 寫 identity.json
// 4. 更新 _index.json
```
### 改動清單
| # | 檔案 | 屬性 | 內容 |
|---|------|------|------|
| 1.1 | `src/core/identity/storage.rs` | NEW | IdentityFile struct + CRUD + index + save_identity_file() |
| 1.2 | `src/core/identity/mod.rs` | NEW | module declaration |
| 1.3 | `src/core/mod.rs` | EDIT | `pub mod identity;` |
| 1.4 | `src/core/db/postgres_db.rs` | EDIT | `get_identity_file_bindings(uuid)` helper |
| 1.5 | `src/core/tmdb/probe.rs` | EDIT | hook: save_identity_file() |
| 1.6 | `src/api/identity_binding.rs` | EDIT | hook: bind/unbind |
| 1.7 | `src/api/identity_agent_api.rs` | EDIT | hook: match_faces_iterative |
| 1.8 | `src/worker/job_worker.rs` | EDIT | hook: P2.5 matching |
| 1.9 | `src/api/server.rs` | EDIT | health/detailed: identities section |
| 1.10 | `scripts/migrate_identity_files.py` | NEW | one-time migration DB→filesystem |
---
## Plan 2: TMDb Agent + Cache
### 目的
將 TMDb 設定為「唯一外連點 + local cache」實作全離線 identity enrichment。
### 目錄結構
```
{OUTPUT_DIR}/
├── {file_uuid}.tmdb.json ← TMDb raw cache (file-level)
├── identities/{uuid}/
│ └── identity.json ← Processed identity (identity-level)
```
### Cache 格式 (`{uuid}.tmdb.json`)
```json
{
"file_uuid": "3a6c1865...",
"fetched_at": "2026-05-16T12:00:00Z",
"source": "agent",
"movie": {
"tmdb_id": 4808,
"title": "Charade",
"release_date": "1963-12-05",
"overview": "After Regina Lampert...",
"poster_path": "/8wvQp...jpg"
},
"cast": [
{
"name": "Cary Grant",
"character": "Peter Joshua",
"profile_path": "/abc123.jpg",
"order": 0
}
],
"cast_count": 20,
"identities_created": 0
}
```
### 流程
```
Step 1: POST /agents/tmdb/prefetch
→ tmdb_agent.py (唯一外連) → TMDB API search → credits
→ 寫入 {uuid}.tmdb.json (source: agent)
Step 2: POST /file/:uuid/tmdb-probe
→ probe_from_cache() 讀 {uuid}.tmdb.json
→ INSERT identities (source='tmdb')
→ spawn tmdb_embed_extractor.py (背景)
→ save_identity_file() for each identity (Plan 1 hook)
Step 3: POST /agents/identity/analyze (既存 endpoint)
→ match_faces_iterative() 自動包含 TMDb identities
```
### probe.rs 重構
```rust
// 新增 (讀 cache)
pub async fn probe_from_cache(db, file_uuid) -> Result<TmdbProbeResult> {
let cache = cache::read_tmdb_cache(file_uuid)?;
create_identities_from_data(db, file_uuid, &cache.movie, &cache.cast).await
}
// 共用內部函數 (從 probe_movie 抽離)
async fn create_identities_from_data(db, file_uuid, movie, cast) -> Result<TmdbProbeResult> {
// 原本 probe_movie 的 INSERT + embed spawn + store logic
// 尾端呼叫 save_identity_file() per identity
}
// 保留 (direct API call, 後備)
pub async fn probe_movie(db, filename, file_uuid) -> Result<...> {
let movie_name = extract_movie_name(filename)?;
// search TMDB API → credits
// 可選擇性寫入 cache 供下次使用
create_identities_from_data(db, file_uuid, &movie, &cast).await
}
```
### 改動清單
| # | 檔案 | 屬性 | 內容 |
|---|------|------|------|
| 2.1 | `src/core/tmdb/cache.rs` | NEW | TmdbCache struct + read/write |
| 2.2 | `src/core/tmdb/mod.rs` | EDIT | `pub mod cache;` `pub mod status;` |
| 2.3 | `src/core/tmdb/probe.rs` | EDIT | refactor: probe_from_cache() + create_identities_from_data() |
| 2.4 | `scripts/tmdb_agent.py` | NEW | fetch TMDB API → cache tmdb.json |
| 2.5 | `src/api/tmdb_api.rs` | NEW | 5 routes + 5 handlers |
| 2.6 | `src/api/server.rs` | EDIT | `.merge(tmdb_routes())` |
---
## Plan 3: TMDb 納管
### 目的
將 TMDb 以 managed resource 形式納入系統監控與管理。
### health/detailed 擴充
```json
{
"integrations": {
"tmdb": {
"api_key_configured": true,
"enabled": true,
"api_reachable": true,
"api_latency_ms": 120,
"api_error": null,
"last_check_at": "2026-05-16T12:00:00Z"
}
},
"identities": {
"directory_exists": true,
"files_count": 3481,
"index_ok": true,
"db_count": 3481,
"synced": true
}
}
```
### API
| Method | Path | 說明 |
|--------|------|------|
| `GET` | `/api/v1/resource/tmdb` | TMDb 完整狀態 + stats + cache count |
| `POST` | `/api/v1/resource/tmdb/check` | ping TMDb API → 更新健康狀態 |
### 改動清單
| # | 檔案 | 屬性 | 內容 |
|---|------|------|------|
| 3.1 | `src/core/tmdb/status.rs` | NEW | check_tmdb_api(), count_tmdb_identities(), count_cache_files() |
| 3.2 | `src/api/tmdb_api.rs` | EDIT | GET/POST resource endpoints |
| 3.3 | `src/api/server.rs` | EDIT | integrations in health/detailed |
---
## 完整 API 表 (Plan 2 + 3)
| Method | Path | Handler | Plan | Description |
|--------|------|---------|------|-------------|
| `POST` | `/api/v1/agents/tmdb/prefetch` | `prefetch_tmdb` | 2 | agent fetch TMDB → cache |
| `POST` | `/api/v1/file/:file_uuid/tmdb-probe` | `tmdb_probe` | 2 | cache → identities |
| `GET` | `/api/v1/file/:file_uuid/tmdb-cache` | `tmdb_cache_view` | 2 | view raw cache |
| `GET` | `/api/v1/resource/tmdb` | `tmdb_resource_status` | 3 | full TMDb status |
| `POST` | `/api/v1/resource/tmdb/check` | `tmdb_resource_check` | 3 | ping health check |
## Migration
一次性腳本:`scripts/migrate_identity_files.py`
```bash
python3 scripts/migrate_identity_files.py
# → 讀 DB identities table → 寫 identity files → 建 index
```
---
## 執行順序
```
Plan 1 (identity 檔案化) → Plan 2 (TMDb agent) → Plan 3 (TMDb 納管)
1.1 → 1.2 → 1.3 → 2.1 → 2.2 → 2.3 → 3.1 → 3.2 → 3.3
1.4 → 1.5 → 1.6 → 2.4 → 2.5 → 2.6
1.7 → 1.8 → 1.9 →
1.10
```

View File

@@ -0,0 +1,101 @@
# Trace Search API 設計
## 概念
trace 是一種 chunk。
現有的 chunk_type: `cut`, `sentence`, `visual`, `story`
新增 chunk_type: `trace`
每個 trace人物跨 frame 追蹤軌跡)就是一個時間區間 + 區間內的 ASR text。
跟其他 chunk 完全一樣,只是切分維度不同:
- cut chunk = 鏡頭切換
- sentence chunk = 語句邊界
- visual chunk = 畫面物體組合
- **trace chunk = 人物出現區間 + 當下 spoken text**
這樣 trace 可以直接放進現有的 `chunks` 表,共用 embedding、搜尋、Qdrant sync 整套機制,不需要任何新 table。
## chunks 表現有結構
```sql
chunks (
id, file_uuid, chunk_type, -- 'trace' 新增
start_frame, end_frame, start_time, end_time,
text_content, -- trace 區間的 ASR text
embedding, -- text_content 的 pgvector
metadata JSONB, -- { trace_id, face_count, identity_id, identity_name }
...
)
```
## 資料產生流程worker 擴充)
在 face processing + `store_traced_faces.py` 完成後:
1. 查詢 `face_detections` 聚合每個 trace 的 `MIN(frame)`, `MAX(frame)`, `COUNT(*)`
2. 對每個 trace查詢 `pre_chunks WHERE processor_type='asr'` 中與 trace time range 重疊的 text
3. 彙整 text → EmbeddingGemma 產生 `embedding`
4. 寫入 `chunks``chunk_type='trace'`metadata 含 `trace_id`, `face_count`, `identity_id`
5. embedding 自動進 Qdrant與既有 chunk 同一 collection
## Search API 擴充
Universal Search 的 `types` 原本就支援 `"chunk"`
在 chunk 搜尋中過濾 `chunk_type = 'trace'` 即可。
**Request**
```json
{
"query": "open the door",
"types": ["chunk"],
"filters": { "chunk_type": "trace" },
"uuid": "aeed71342a899fe4b4c57b7d41bcb692",
"page": 1,
"page_size": 20
}
```
**Response**(與既有 Chunk result 相同):
```json
{
"type": "chunk",
"chunk_id": "chunk_42",
"chunk_type": "trace",
"start_frame": 45200, "end_frame": 45900,
"start_time": 1808.0, "end_time": 1836.0,
"score": 0.87,
"text": "Open the door. Come on, hurry up.",
"metadata": {
"trace_id": 5,
"face_count": 42,
"identity_name": "Audrey Hepburn"
}
}
```
完全沿用既有的 `SearchResult::Chunk` variant不用新增 enum variant。
### 搜尋語法
```sql
SELECT c.*
FROM dev.chunks c
WHERE c.file_uuid = $1
AND c.chunk_type = 'trace'
AND c.embedding IS NOT NULL
ORDER BY c.embedding <=> $2
LIMIT $3;
```
## 總結
| 項目 | 作法 |
|------|------|
| 新 table | ❌ 不需要 |
| 新 enum variant | ❌ 不需要 |
| SearchResult 改動 | ❌ 不需要 |
| chunk_type 新增 | ✅ `'trace'` |
| worker 擴充 | ✅ 產生 trace chunk (face done 後) |
| SearchFilters 擴充 | ✅ 加 `chunk_type` filter |
| Qdrant | ✅ 自動(既有 chunk collection |

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,264 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Video Registration"
date: "2026-03-25"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "video"
- "registration"
ai_query_hints:
- "查詢 Video Registration 的內容"
- "Video Registration 的主要目的是什麼?"
- "如何操作或實施 Video Registration"
---
# Video Registration
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-25 |
| 文件版本 | V1.1 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-25 | 創建文件 | Warren | OpenCode |
| V1.1 | 2026-03-26 | 修正 curl 範例,新增 API Key 驗證標頭 | OpenCode | deepseek-reasoner |
---
## 概述
影片註冊 API (`POST /api/v1/register`) 用於將影片加入 Momentry Core 系統進行處理。
## 路徑格式
### 支援的路徑格式
| 格式 | 範例 | 說明 |
|------|------|------|
| 相對路徑 | `./demo/video.mp4` | 推薦格式 |
| 相對路徑(無 ./ | `demo/video.mp4` | 自動加上 `./` |
| 絕對路徑 | `/Users/.../sftpgo/data/demo/video.mp4` | 支援但不推薦 |
### 路徑結構
```
./username/filepath
│ │ │
│ │ └── 檔案路徑(可以是多層目錄)
│ └── 使用者名稱SFTPgo 用戶目錄名稱)
└── 相對路徑前綴
```
**範例**
- `./demo/video.mp4` → username=`demo`, filepath=`video.mp4`
- `./demo/movies/2024/video.mp4` → username=`demo`, filepath=`movies/2024/video.mp4`
- `./warren/project1/interview.mp4` → username=`warren`, filepath=`project1/interview.mp4`
## UUID 計算
### 計算規則
```
UUID = SHA256(username/filepath)[0:16]
```
**範例**
```rust
// 路徑: ./demo/video.mp4
// username: "demo"
// filepath: "video.mp4"
// key: "demo/video.mp4"
// UUID: SHA256("demo/video.mp4")[0:16]
```
### 特性
| 特性 | 說明 |
|------|------|
| 用戶隔離 | 不同用戶的相同檔名會產生不同 UUID |
| 一致性 | 相同相對路徑一定產生相同 UUID |
| 遷移安全 | SFTPgo 資料路徑變更後 UUID 保持一致 |
### 範例
```rust
// 用戶 demo 的影片
compute_uuid_from_relative_path("./demo/video.mp4")
// → "9760d0820f0cf9a7"
// 用戶 warren 的相同檔名影片
compute_uuid_from_relative_path("./warren/video.mp4")
// → "a1b2c3d4e5f6g7h8" (不同的 UUID)
```
## 重複註冊檢查
### 行為
1. 系統檢查 UUID 是否已存在於資料庫
2. 如果存在,返回 `already_exists: true` 和現有影片資訊
3. 如果不存在,創建新的影片記錄
### API 回應
**新註冊**
```json
{
"uuid": "9760d0820f0cf9a7",
"video_id": 18,
"job_id": 2,
"file_name": "video.mp4",
"duration": 159.637188,
"width": 640,
"height": 360,
"already_exists": false
}
```
**重複註冊**
```json
{
"uuid": "9760d0820f0cf9a7",
"video_id": 18,
"job_id": 2,
"file_name": "video.mp4",
"duration": 159.637188,
"width": 640,
"height": 360,
"already_exists": true
}
```
## SFTPgo 整合
### 目錄結構
SFTPgo 的用戶目錄結構:
```
/Users/accusys/momentry/var/sftpgo/data/
├── demo/ ← 用戶目錄
│ ├── video.mp4
│ └── movies/
│ └── movie1.mp4
├── warren/ ← 用戶目錄
│ └── project1/
│ └── interview.mp4
└── momentry/ ← 用戶目錄
└── presentation.mp4
```
### 註冊流程
1. SFTPgo 用戶上傳檔案到各自的目錄
2. n8n 或其他服務調用註冊 API
3. 使用相對路徑格式:`./username/filepath`
4. 系統計算 UUID 並檢查重複
5. 創建處理任務
## 程式碼範例
### 註冊影片
```bash
# 使用相對路徑註冊
curl -X POST http://localhost:3002/api/v1/register \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"path": "./demo/video.mp4"}'
# 或使用多層目錄
curl -X POST http://localhost:3002/api/v1/register \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"path": "./demo/movies/2024/video.mp4"}'
```
### UUID 計算函數
```rust
// 使用相對路徑計算 UUID
pub fn compute_uuid_from_relative_path(relative_path: &str) -> String {
let (username, filepath) = extract_user_from_relative_path(relative_path);
compute_uuid(&username, &filepath)
}
// 從相對路徑提取用戶名和檔案路徑
pub fn extract_user_from_relative_path(relative_path: &str) -> (String, String) {
let path = relative_path.strip_prefix("./").unwrap_or(relative_path);
let path_buf = PathBuf::from(path);
let mut components = path_buf.components();
let username = components
.next()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.unwrap_or_default();
let filepath: String = components
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>()
.join("/");
(username, filepath)
}
```
## 相關 API
### Probe API僅探測不註冊
如果只需要取得影片資訊而不註冊,可以使用 Probe API
```bash
curl -X POST http://localhost:3002/api/v1/probe \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"path": "./demo/video.mp4"}'
```
**回應範例**
```json
{
"uuid": "a1b10138a6bbb0cd",
"file_name": "video.mp4",
"duration": 120.5,
"width": 1920,
"height": 1080,
"fps": 30.0,
"cached": false,
"format": {...},
"streams": [...]
}
```
**與 Register API 的差異**
| 功能 | Probe API | Register API |
|------|-----------|---------------|
| 計算 UUID | ✓ | ✓ |
| 執行 ffprobe | ✓ | ✓ |
| 儲存 probe.json | ✓ | ✓ |
| 寫入 videos 表 | ✗ | ✓ |
| 建立 monitor_job | ✗ | ✓ |
| 返回 job_id | ✗ | ✓ |
| 適用場景 | 預覽影片資訊 | 註冊並處理影片 |
## 相關檔案
| 檔案 | 說明 |
|------|------|
| `src/core/storage/uuid.rs` | UUID 計算邏輯 |
| `src/api/server.rs` | 註冊與 Probe API 實現 |
| `src/core/probe/ffprobe.rs` | ffprobe 整合 |
| `docs_v1.0/IMPLEMENTATION/SFTPGO_DEMO_USER.md` | SFTPgo 用戶設置 |
| `docs_v1.0/REFERENCE/API_ENDPOINTS.md` | API 端點總覽 |

View File

@@ -0,0 +1,201 @@
# Momentry Eye API Reference
**Vision Agent** — Multi-model zero-shot object detection service.
Port: `5052` | Resource IDs: `eye-gdino`, `eye-paligemma`
---
## Models
| Model | ID | Params | Size | Confidence | Speed | License |
|-------|-----|--------|------|------------|-------|---------|
| Grounding DINO | `grounding-dino` | 232M | 891MB | ✅ 0-1 score | ~340ms | Apache 2.0 |
| PaliGemma 3B | `paligemma` | 2,923M | ~3GB | ❌ no score | ~80ms | Gemma license |
## Endpoints
### `GET /health`
System status and loaded models.
```bash
curl localhost:5052/health
```
Response:
```json
{
"status": "ok",
"models_loaded": ["grounding-dino"],
"models_available": ["grounding-dino", "paligemma"],
"device": "mps",
"port": 5052
}
```
### `GET /models`
List available models with specs.
```bash
curl localhost:5052/models
```
### `POST /detect`
Detect objects in a single video frame.
```bash
curl localhost:5052/detect \
-H "Content-Type: application/json" \
-d '{"time":5461, "prompt":"gun", "model":"grounding-dino"}'
```
**Parameters:**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `uuid` | string | `aeed71342a...` | Video file UUID |
| `time` | float | `0` | Timestamp in seconds |
| `prompt` | string | `"gun"` | Object to detect |
| `model` | string | `"grounding-dino"` | Model: `grounding-dino`, `paligemma`, or `fusion` |
| `threshold` | float | `0.1` | Minimum confidence (GDINO only) |
| `weights` | object | — | Fusion weights, e.g. `{"grounding-dino":0.6,"paligemma":0.4}` |
**Fusion mode** runs both models and combines results with weighted scoring. Default weights: GDINO 0.6, PaliGemma 0.4.
```bash
# Fusion: run both models, combine results
curl localhost:5052/detect \
-d '{"time":206, "prompt":"water gun", "model":"fusion"}'
# Custom fusion weights
curl localhost:5052/detect \
-d '{"time":206, "prompt":"gun", "model":"fusion",
"weights":{"grounding-dino":0.5,"paligemma":0.5}}'
```
**Response:**
```json
{
"model": "grounding-dino",
"detections": [
{"bbox": [726.2, 567.4, 969.0, 694.6], "score": 0.476, "label": "gun"},
{"bbox": [686.7, 567.0, 969.6, 918.3], "score": 0.262, "label": "gun"}
],
"time_ms": 345.2,
"n_detections": 2,
"shot_url": "/shots/aeed7134_5461s_gun_grounding-dino.jpg"
}
```
**Fusion response** also includes `per_model` (detections per model) and `fusion` (deduplicated combined list with `fused_score`).
### `POST /search`
Search across a time range.
```bash
# Natural language query
curl localhost:5052/search \
-d '{"query":"find the gun", "range":"5400-5600", "interval":10}'
```
**Parameters:**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `query` | string | `"find the gun"` | Natural language query (parsed to extract object) |
| `target` | string | — | `file_uuid:chunk_id` or `file_uuid:trace_id` — resolves to time range |
| `range` | string | `"0-6780"` | Manual time range |
| `interval` | int | `30` | Scan interval in seconds |
| `model` | string | `"grounding-dino"` | Detection model |
| `threshold` | float | `0.15` | Minimum confidence |
**Target resolution:**
| Format | Example | Resolves to |
|--------|---------|-------------|
| `file_uuid:chunk_id` | `uuid:uuid_story_90` | Chunk's time range |
| `file_uuid:trace_id` | `uuid:trace_5` | Trace's time range |
| `file_uuid:chunk_index` | `uuid:500` | Chunk index 500's range |
```bash
# Using target
curl localhost:5052/search \
-d '{"target":"aeed71342...:aeed71342..._story_90", "query":"gun"}'
# Using trace
curl localhost:5052/search \
-d '{"target":"aeed71342...:trace_5", "query":"person"}'
```
### `POST /multimodal`
Multi-modal search across sentence chunks — combines ASR text match + visual confirmation.
```bash
# Search for Jean-Louis: ASR match + GDINO child detection
curl localhost:5052/multimodal \
-d '{"keyword":"Jean-Louis", "prompt":"child"}'
# Search trace chunks visually (no ASR)
curl localhost:5052/multimodal \
-d '{"keyword":"", "prompt":"person", "chunk_type":"trace", "range":"3500-4000"}'
```
**Parameters:**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `keyword` | string | — | ASR keyword to search in sentence text |
| `prompt` | string | same as keyword | Visual prompt for GDINO |
| `chunk_type` | string | `"sentence"` | `sentence`, `trace`, `story`, `cut` |
| `target` | string | — | Specific chunk target |
| `range` | string | `"0-6780"` | Time range (for non-sentence chunks) |
| `threshold` | float | `0.15` | Visual detection threshold |
### `GET /shots/<filename>`
Retrieve annotated detection images.
```bash
curl -o result.jpg localhost:5052/shots/aeed7134_5461s_gun_grounding-dino.jpg
```
## Object Detection Performance Summary
| Object type | Size in frame | GDINO | PaliGemma | Best prompt |
|-------------|--------------|-------|-----------|-------------|
| Gun (realistic) | 15-30% | ✅ 0.36-0.67 | ✅ | `pistol` / `handgun` |
| Water gun (toy) | 15-31% | ❌ 0 | ✅ | `water gun` (PaliGemma) |
| Child (Jean-Louis) | 30-60% | ⚠️ 0.3-0.9 | ❌ | `child` (high FP on adults) |
| Stamp | <5% | ❌ FP | ❌ | — |
| Passport | <10% | ❌ FP | ❌ | — |
| Magnifying glass | <5% | ❌ FP | ❌ | — |
| Cup / Bottle | 5-15% | ✅ 0.3-0.5 | — | `cup` / `bottle` |
| Cell phone | 5-10% | ✅ 0.3-0.5 | — | `cell phone` |
## Resource Registration
On startup, the agent auto-registers as resources in `dev.resources`:
| Resource ID | Type | Status |
|-------------|------|--------|
| `eye-gdino` | `vision_model` | `online` |
| `eye-paligemma` | `vision_model` | `online` |
Heartbeat updates every 60 seconds. Discover via:
```sql
SELECT * FROM dev.resources WHERE resource_type = 'vision_model';
```
## Files
| File | Description |
|------|-------------|
| `scripts/vision_agent.py` | Vision Agent server (port 5052) |
| `output_dev/vision_shots/` | Annotated detection screenshots |
| `docs/ZERO_SHOT_DETECTION_RESEARCH.md` | Full model research report |

View File

@@ -0,0 +1,105 @@
# 視覺呈現工具選型 v1.0.0
Momentry 前端視覺化工具選擇記錄。
## SVG內建
| 項目 | 內容 |
|------|------|
| 用途 | Trace 時間軸、泳道圖、長條圖、矩陣 |
| 授權 | 瀏覽器內建,無授權問題 |
| 適用 | V1 TraceThumbnailTimeline、V2 IdentitySwimlane、V3 DurationHistogram、V4 SimilarityMatrix |
| 優點 | 零依賴、向量清晰、可互動 |
| 缺點 | 大規模節點時效能下降 |
## Three.js
| 項目 | 內容 |
|------|------|
| 用途 | 3D 臉部網格、3D 時空立方體 |
| 授權 | **MIT** — 可商用,需保留版權聲明 |
| 適用 | Face3DViewerMediaPipe 468 landmarks、V5 3D Space-Time Cube |
| npm | `three` + `@types/three` |
| 檔案 | `node_modules/three/LICENSE`MIT |
| Bundle | 約 120KB gzip |
| 優點 | WebGL 封裝完整、OrbitControls、社群龐大 |
| 缺點 | 需手動管理 Dispose 避免記憶體洩漏 |
## MediaPipe Face Mesh
| 項目 | 內容 |
|------|------|
| 用途 | 人臉 468 個 3D landmark 偵測 |
| 授權 | **Apache 2.0** — 可商用 |
| 適用 | Face3DViewer |
| 部署 | `scripts/face_landmarks_server.py`port 11437 |
| 輸入 | 臉部裁切 JPEG |
| 輸出 | 478 個 (x, y, z) 3D 座標 |
| 優點 | 輕量即時、跨平台 |
| 缺點 | 僅正面臉部、無紋理 |
## Three.js Face3DViewer 記憶體管理
```typescript
// 正確的 Dispose 模式
function disposeScene() {
cancelAnimationFrame(animId)
for (const obj of objects) {
scene?.remove(obj)
if (obj instanceof THREE.Mesh) {
obj.geometry?.dispose()
if (Array.isArray(obj.material)) obj.material.forEach(m => m.dispose())
else obj.material?.dispose()
}
if (obj instanceof THREE.Points) {
obj.geometry?.dispose()
if (obj.material) obj.material.dispose()
}
}
objects = []
controls?.dispose()
controls = null
if (renderer) { renderer.dispose(); renderer = null }
scene = null; camera = null
}
```
## 技術選型對照
| 視覺化 | 工具 | 授權 | Bundle | 狀態 |
|--------|------|:----:|:-----:|:----:|
| V0 Trace Grid | Vue + Tailwind | — | 0 KB | ✅ |
| V1 Thumbnail Timeline | SVG | — | 0 KB | ✅ |
| V2 Identity Swimlane | SVG | — | 0 KB | ✅ |
| V3 Duration Histogram | SVG | — | 0 KB | ✅ |
| V4 Similarity Matrix | SVG | — | 0 KB | ✅ |
| 3D Face Mesh | Three.js | MIT | ~120 KB | ✅ |
| V5 3D Space-Time Cube | Three.js | MIT | ~120 KB | 🔜 |
| Heatmap (Canvas) | Canvas 2D | — | 0 KB | 🔜 |
| Trace Video | ffmpeg | GPL | 獨立行程 | ✅ |
| **文件渲染** | | | | |
| API 文件 | **Markdown** | — | 0 KB | ✅ |
| API 圖解 | **Mermaid** (flowchart, sequence, ER, mindmap) | MIT | ~50 KB (VS Code 插件) | ✅ |
| CLI 閱讀 | **glow** (terminal MD renderer) | MIT | 獨立 binary | ✅ |
## Markdown
| 項目 | 內容 |
|------|------|
| 用途 | 所有 API 文件、設計規格、測試報告 |
| 授權 | 純文字格式,無授權問題 |
| 工具 | VS Code 內建預覽、`glow` CLI |
| 優點 | 版本控制友善diff 可讀)、純文字、跨平台 |
| 缺點 | 無動態互動能力 |
## Mermaid
| 項目 | 內容 |
|------|------|
| 用途 | API 流程圖sequence、架構圖flowchart、資料模型ER、端點總覽mindmap |
| 授權 | **MIT** — 可商用 |
| VS Code 插件 | `Markdown Preview Mermaid Support` |
| 支援圖表 | flowchart, sequence, class, state, ER, mindmap, pie, gantt |
| 檔案 | `API_USAGE_GUIDE_V1.0.0.md`(含 6 張 Mermaid 圖表) |
| 優點 | Markdown 內嵌、版本控制友善、免截圖 |
| 缺點 | VS Code/GitHub 以外需插件支援 |

View File

@@ -0,0 +1,114 @@
# 語音互動技術選型 v1.0.0
Momentry Demo Runner 語音技術選擇記錄。
## 語音輸出TTS
### macOS `say`(已採用)
| 項目 | 內容 |
|------|------|
| 用途 | 朗讀展示解說文字 |
| 授權 | macOS 內建,無授權問題 |
| 語言 | 支援 40+ 語言含中文Meijia、英文Samantha、日文Kyoko等 |
| 方式 | `subprocess.Popen(["say", "-v", "Meijia", "文字"])` |
| 優點 | 零安裝、零依賴、低延遲、多語系 |
| 缺點 | 僅 macOS、無法控制語速微調 |
**結論**:最適合 Momentry 的 TTS 方案 — macOS 內建、免費、多語系支援完整。
---
## 語音輸入Speech-to-Command
### 方案比較
| 方案 | 本地/雲端 | 語言 | 模型大小 | 延遲 | 精準度 | 授權 |
|------|:---------:|:----:|:--------:|:----:|:------:|:----:|
| **Vosk**(已整合) | ✅ **本地** | 中+英 | 42MB | 即時 | 中高 | Apache 2.0 |
| macOS NSSpeechRecognizer | ✅ 本地 | 多語 | 系統內建 | 即時 | 中 | macOS 內建 |
| Google Speech Recognition | ☁️ 雲端 | 120+ 語言 | — | ~1s | 高 | 免費(有限額) |
| Whisper (tiny) | ✅ 本地 | 100+ 語言 | ~150MB | ~2s | 高 | MIT |
| Porcupine | ✅ 本地 | 關鍵字 | ~2MB | 即時 | 高(限關鍵字) | Apache 2.0 |
### Vosk已採用為本地方案
| 項目 | 內容 |
|------|------|
| 模型 | `vosk-model-small-cn-0.22`42MB中文 |
| 語言 | 中文、英文(需下載對應模型) |
| 方式 | Python `vosk` 套件直接呼叫 |
| 優點 | 純本地、即時、中英皆可、模型小 |
| 缺點 | 需下載模型(一次性)、嘈雜環境精準度下降 |
| 語音 | 僅偵測指令關鍵字next/stop/repeat/goto 等 |
### Google Speech Recognition備援方案
| 項目 | 內容 |
|------|------|
| 用途 | 當 Vosk 模型未安裝時自動降級使用 |
| 方式 | Python `SpeechRecognition` + Google API |
| 優點 | 免下載模型、精準度高、多語系 |
| 缺點 | **需網路**、每次請求 ~1s 延遲、有使用配額限制 |
### 整合策略
```
啟動 --voice-control
├── Vosk 模型存在? → 使用 Vosk本地離線
└── Vosk 不存在? → 使用 Google需網路
└── 也失敗? → 顯示「語音不可用」
```
---
## Demo Runner 整合
### 指令集(中英雙語)
| 指令 | English | 功能 |
|:----:|:-------:|------|
| 下一個 / 繼續 | next / continue | 前進到下一步 |
| 停止 | stop / quit | 結束當前展示 |
| 重複 | repeat / again | 重複朗讀當前解說 |
| 跳到第 N 步 | go to N / step N | 跳到指定步驟 |
### 程式碼結構
```python
# 背景執行緒監聽語音
def voice_command_listener(lang):
# 1. 嘗試 Vosk本地
# 2. 降級 Google Speech Recognition雲端
# 3. 將辨識結果放入佇列
# 主迴圈輪詢佇列
def main():
while demo_running:
cmd = check_voice_command()
if cmd == "next": # 前進
if cmd == "stop": # 停止
if cmd == "goto N": # 跳到第 N 步
```
### 啟動方式
```bash
# 本地語音辨識Vosk不需網路
python3 scripts/demo_runner.py --voice zh_TW --voice-control
# 備援:若 Vosk 模型未安裝,自動使用 Google需網路
```
---
## 相關檔案
| 檔案 | 說明 |
|------|------|
| `scripts/demo_runner.py` | 語音輸出 + 輸入整合 |
| `~/.cache/vosk/vosk-model-small-cn-0.22/` | Vosk 中文模型42MB |
| `docs_v1.0/REFERENCE/DEMO_RUNNER_V1.0.0.md` | Demo Runner 使用文件 |

View File

@@ -0,0 +1,36 @@
# 語音辨識測試記錄 v1.0.0
## 環境
- **機器**: Mac Mini M4
- **輸入裝置**: Display Audio (HDMI loopback)
- **模型**: Vosk small-en-us (40MB)
## 測試結果
| 測試 | 設定 | Max Level | Mean Level | Vosk 辨識 |
|------|------|:---------:|:----------:|:----------:|
| 原始音訊 48kHz | pyaudio direct | 3510 | 654 | ❌ 空 |
| 降噪後 16kHz | highpass200+lowpass4000+afftdn | 1224 | 110 | ❌ 空 |
| 增益 3x | numpy boost | ~10K | ~1800 | ❌ 空 |
| ffmpeg recording | avfoundation :0 | 3698 | 636 | ❌ 空 |
## 發現
1. **Display Audio 確實有收到音訊**mean ~600, max ~3500
2. **背景噪聲偏高**mean 600 遠高於正常麥克風的 10-50
3. 降噪後 noise floor 降至 mean 110但仍無法辨識
4. Vosk small model 對噪聲容忍度不足
## 推測原因
Display Audio 是 **HDMI 音訊回傳通道**,收到的可能是:
- 顯示器內建喇叭的背景噪聲
- 或顯示器本身產生的電氣噪聲
- 不確定顯示器的麥克風是否確實透過 HDMI 回傳
## 待嘗試
- [ ] Whisper (本地,噪聲容忍度高)
- [ ] USB 麥克風直接測試
- [ ] macOS 內建 NSSpeechRecognizer透過 PyObjC

View File

@@ -0,0 +1,190 @@
# Zero-Shot Object Detection Model Research Report
**Date:** 2026-05-10
**Goal:** Evaluate models for detecting arbitrary objects in Charade (1963)
**System:** M5 MacBook Pro (Apple Silicon MPS, 48GB)
---
## Tested Models
| Model | Params | Size | Resolution | Type | License |
|-------|--------|------|------------|------|---------|
| YOLOv8n fine-tune (gun) | 3.2M | 6MB | 640px | Closed-set (4 classes) | AGPL-3.0 |
| OWL-ViT base | 109M | 586MB | 384px | Zero-shot | Apache 2.0 |
| **Grounding DINO Base** | **232M** | **891MB** | **384px** | **Zero-shot** | **Apache 2.0** |
| Grounding DINO Large | 232M | 895MB | 384px | Zero-shot | Apache 2.0 |
| Florence-2 Base | 231M | ~3GB | 384px | Zero-shot (generative) | MIT |
| Florence-2 Large | 776M | ~6GB | 384px | Zero-shot (generative) | MIT |
| PaliGemma 3B mix-224 | 2,923M | ~3GB | 224px | Zero-shot (generative) | Gemma license |
| PaliGemma 3B mix-448 | 2,923M | ~6GB | 448px | Zero-shot (generative) | Gemma license |
## Detection Performance on Charade
### Large Objects (gun)
| Model | 8 timepoints | Best confidence | Runtime |
|-------|-------------|----------------|---------|
| YOLOv8n fine-tune | ❌ 0/5 (all FP) | 0.45 (stamp→pistol) | 0.03s |
| OWL-ViT | ❌ 2/8 | 0.054 | 3.4s |
| **Grounding DINO Base** | **✅ 8/8** | **0.499** | **0.33s** |
| PaliGemma 3B mix-224 | ✅ 3/8 (gun), 3/8 overall | 0.499 | 0.5-3s |
### Small Objects (stamp, passport, magnifying glass)
| Model | Stamp | Passport | Magnifying glass |
|-------|-------|----------|-----------------|
| Grounding DINO Base | ❌ FP (~0.3) | ❌ FP (~0.4) | ❌ FP (~0.3-0.5) |
| PaliGemma 3B mix-224 | ❌ no det | ❌ no det | not tested |
| PaliGemma 3B mix-448 | ❌ (not tested) | ❌ (not tested) | ❌ (not tested) |
**All models fail on objects smaller than ~50px at native 1920x1080 resolution.**
### Other Objects
| Object | YOLO COCO | Grounding DINO | Notes |
|--------|-----------|----------------|-------|
| knife | ✅ 368 frames | ✅ 84 hits | Small but detectable |
| cup | ✅ | ✅ 13 hits | Moderate size |
| bottle | ✅ | ✅ 12 hits | Moderate size |
| cell phone | ✅ | ✅ 5 hits | Hand-held |
| book | ✅ | ✅ 3 hits | Hand-held |
| car | ✅ | ✅ 9 hits | Large object |
| tie | ✅ | ✅ 139 hits | On-person (worn, not held) |
## Detailed Model Analysis
### Grounding DINO Base (Recommended)
**Scores:** Detection confidence 0.1-0.5 (typical for zero-shot)
**Timing per frame (MPS):**
| Component | Time | % of total |
|-----------|------|------------|
| Processor (text+image) | 17ms | 5% |
| Model inference | 310ms | 93% |
| Post-processing | 5ms | 2% |
| **Total** | **331ms** | **100%** |
**Multi-prompt batching:** 8 prompts in 335ms (42ms/prompt vs 309ms single)
**Memory:** ~1GB (MPS)
**License:** Apache 2.0 — fully commercial, no restrictions
### Grounding DINO Large
**Result:** Identical weights to Base. The GitHub "7-dataset" checkpoint is the same 3-dataset version as HuggingFace. The actual 7-dataset version (56.7 AP) was never released.
**Verdict: Do not use.** Base is identical and simpler.
### OWL-ViT
**Result:** Almost useless for this task. Max confidence 0.054. Detect only 2/8 timepoints.
**Verdict: Do not use.**
### Florence-2
**Issue:** `prepare_inputs_for_generation` bug in current transformers version. Cannot run inference without patching model code.
**Task format:** Uses task tokens (`<OD>`) instead of arbitrary text prompts. Cannot do "detect gun" directly — uses generic object detection.
**Verdict: Cannot use in current environment.**
### PaliGemma
**Result:** Works for gun detection (3/8) but misses small objects entirely.
**Key limitation:** No confidence score output (generative model). Either outputs bbox or nothing.
**Issues:**
- 224px variant: Too low resolution for small objects
- 448px variant: 6GB download, suspected better for detail but untested
- Gemma license may restrict commercial use vs Apache 2.0
**Verdict: Inferior to Grounding DINO for this use case.**
### YOLOv8n Fine-tune (Gun Detector)
| Dataset | 905 images (Roboflow CC BY 4.0) |
| Classes | grenade, knife, pistol, rifle |
| Validation mAP50 | 0.813 |
| Charade FP rate | **100%** (all false positives) |
**Root cause:** Training images are close-up gun photos; Charade has distant/partial guns. Distribution mismatch makes this model unusable.
**Verdict: Requires completely new training dataset.**
## Root Cause Analysis: Small Object Failure
### Grounding DINO's Resolution Limit
Grounding DINO processes images at **384×384px**. At this resolution:
```
1920px frame → 384px input (5:1 reduction)
A 50×50px object → 10×10px at 384px → only ~1 patch token
```
For comparison:
- **Gun** at 200×200px (close-up) → 40×40px → still detectable
- **Stamp** at 30×30px → 6×6px → lost in downsampling
- **Passport** at 80×120px → 16×24px → barely visible
- **Magnifying glass** at 40×40px → 8×8px → lost
### Potential Solutions
| Solution | Pros | Cons | Feasibility |
|----------|------|------|-------------|
| **Crop + zoom** on person region | Leverages existing YOLO person detections | Requires two-stage pipeline | ✅ High |
| **PaliGemma 448px** | 448px native (36% more detail) | 6GB, requires download | ⚠️ Medium |
| **YOLO fine-tune on stamps** | Fast inference (6MB) | Need 200+ training images | ⚠️ Medium |
| **Grounding DINO + tiling** | Split image into tiles, run per tile | 4-9x slower | ⚠️ Medium |
| **Florence-2 448px** | Higher resolution | Bug in transformers | ❌ Low |
## Hand-Held Object Detection Feasibility
### Available Data Sources
| Source | Type | Coverage | Usefulness |
|--------|------|----------|------------|
| YOLO `pre_chunks` | Object detections | 169,625 frames | ✅ Every frame |
| Pose `pre_chunks` | Body keypoints (left_wrist, right_wrist) | 4,269 frames | ✅ Hand location |
| Grounding DINO | Zero-shot classification | On-demand | ✅ Object ID |
| ASR dialogue | Text mentions | 4,188 chunks | ✅ "holding a gun" |
### Approach: YOLO + Pose + Grounding DINO
```
Frame
→ YOLO: Find person + objects
→ Pose: Find wrist keypoints
→ Check: Object bbox overlaps with hand region (wrist ±100px)
→ Grounding DINO: Verify object class
```
### Known Limitations
1. **Pose frame alignment:** Pose data (4,269 frames) doesn't always overlap with YOLO data at the same frame
2. **Object proximity ≠ holding:** YOLO objects near hands may be background, not held
3. **Small object blind spot:** Stamps, magnifying glasses at hand positions are too small to detect
## Recommendations
| Priority | Action | Rationale |
|----------|--------|-----------|
| 1 | Use Grounding DINO Base (Apache 2.0) | Best zero-shot detector, proven on guns, clean license |
| 2 | Two-stage pipeline for small objects | YOLO person box → crop → upscale → Grounding DINO |
| 3 | Pose wrist alignment for hand-held confirmation | Reduce false positives by requiring hand proximity |
| 4 | Replace Grounding DINO "Large" ref with Base | Large is identical weights, no benefit |
## Appendix: License Summary
| Model | License | Commercial Use | Requires |
|-------|---------|---------------|----------|
| Grounding DINO | **Apache 2.0** | ✅ Yes | NOTICE file |
| OWL-ViT | Apache 2.0 | ✅ Yes | NOTICE file |
| PaliGemma | Gemma license | ⚠️ Needs review | Google ToS |
| Florence-2 | MIT | ✅ Yes | Copyright notice |
| YOLOv8 | AGPL-3.0 | ⚠️ Needs license | Open source or paid |

View File

@@ -0,0 +1,49 @@
# Zero-Shot Gun Detection Test Plan
**Date:** 2026-05-10
**Goal:** Compare OWL-ViT vs Grounding DINO for detecting guns in Charade (1963)
## Models
| Model | Source | Type |
|-------|--------|------|
| `google/owlvit-base-patch32` | HuggingFace | Zero-shot object detection |
| `IDEA-Research/grounding-dino-base` | HuggingFace | Zero-shot object detection |
## Test Timepoints (8)
| Time | Label | Source |
|------|-------|--------|
| 2646s (44:06) | 2646s | ASR: "He has a gun" |
| 3188s (53:08) | 3188s | Original detection |
| 3697s (61:37) | 3697s | ASR: "Where's your gun" |
| 5341s (89:01) | 5341s | ASR: "He already killed 3 men" |
| 5461s (91:01) | 5461s | Original detection |
| 6309s (1:45:09) | 6309s | Original detection |
| 6377s (1:46:17) | 6377s | Original detection |
| 6479s (1:47:59) | 6479s | Original detection |
## Prompts
`"gun"`, `"pistol"`, `"rifle"`, `"weapon"`
## Matrix
8 timepoints × 2 models × 4 prompts = 64 inferences
## Output
| File | Description |
|------|-------------|
| `output_dev/zero_shot_test/*.jpg` | Annotated screenshots |
| `output_dev/zero_shot_test/zero_shot_results.json` | Detection results |
| `scripts/zero_shot_gun_test.py` | Test script |
## Success Criteria
| Level | Criteria |
|-------|----------|
| Excellent | Finds real gun with confidence > 0.5 |
| Good | Finds real gun with confidence < 0.5 |
| Limited | Finds guns but many false positives |
| Failed | All false positives |

View File

@@ -0,0 +1,67 @@
# Zero-Shot Gun Detection Test Report
**Date:** 2026-05-10
**Goal:** Compare OWL-ViT vs Grounding DINO for detecting guns in Charade (1963)
## Test Setup
| Model | Prompts | Timepoints | Total inferences |
|-------|---------|------------|-----------------|
| `google/owlvit-base-patch32` | gun, pistol, rifle, weapon | 8 | 32 |
| `IDEA-Research/grounding-dino-base` | gun, pistol, rifle, weapon | 8 | 32 |
## Results
| Model | Timepoints with detections | Total detections | Best confidence | Runtime |
|-------|---------------------------|-----------------|-----------------|---------|
| OWL-ViT | 2/8 | 2 | 0.054 | 1.5s |
| **Grounding DINO** | **8/8** | **109** | **0.186** | 11.5s |
## Grounding DINO — Per Timepoint
| Time | Source | Best prompt | Best confidence | Found? |
|------|--------|-------------|-----------------|--------|
| 2646s (44:06) | ASR: "He has a gun" | gun | 0.082 | ✅ |
| **3188s (53:08)** | **Original pistol** | **gun** | **0.149** | **✅** |
| 3697s (61:37) | ASR: "Where's your gun" | gun | 0.159 | ✅ |
| 5341s (89:01) | ASR: "He already killed 3 men" | gun | 0.074 | ✅ |
| **5461s (91:01)** | **Original pistol** | **gun** | **0.186** | **✅** |
| **6309s (1:45:09)** | **Original pistol** | **gun** | **0.077** | **✅** |
| **6377s (1:46:17)** | **Original gun** | **weapon** | **0.118** | **✅** |
| **6479s (1:47:59)** | **Original pistol** | **gun** | **0.060** | **✅** |
### Original 5 Pistol Frames
| Frame | OWL-ViT | Grounding DINO | Verdict |
|-------|---------|----------------|---------|
| 3188s | Not found | ✅ Found (0.149) | ✅ |
| 5461s | Not found | ✅ Found (0.186) | ✅ |
| 6309s | Not found | ✅ Found (0.077) | ✅ |
| 6377s | Not found | ✅ Found (0.118) | ✅ |
| 6479s | Not found | ✅ Found (0.060) | ✅ |
## Analysis
### OWL-ViT
- Almost completely failed: only 2 detections at 0.05 confidence
- Not suitable for this task
### Grounding DINO
- **Found all 8 timepoints**, including all 5 original pistol frames
- Best prompt is consistently `"gun"` (6/8 timepoints)
- Confidence range: 0.060 - 0.186 (typical for zero-shot detection)
- Higher confidence correlates with user-confirmed detections
### Key Finding
The 5 original pistol frames were produced by **Grounding DINO** (not YOLOv8n). The model was downloaded from HuggingFace at 15:43-15:44 on May 9, and the screenshots were generated at 15:49 — confirming OWL-ViT was tested first (failed) and then Grounding DINO was tested (succeeded).
## Integration
Grounding DINO has been integrated into `object_search_agent.py` as `--source zero_shot`:
```
python3 scripts/object_search_agent.py --keyword gun --source zero_shot
```
## Screenshots
All 64 annotated screenshots saved to `output_dev/zero_shot_test/*.jpg`

View File

@@ -0,0 +1,115 @@
# Zero-Shot vs Fine-Tune 物件偵測模型選型報告
**Date:** 2026-05-10
**Goal:** 在 Charade (1963) 中搜尋非 COCO 物件(槍枝、郵票、信封等)
**System:** M5 MacBook Pro (Apple Silicon MPS)
## 動機
YOLOv8 COCO 只有 80 類,不包含 gun、stamp、envelope 等 Charade 核心物件。需要找到能在電影中搜尋任意物件的方法。
## 候選方案
| 方案 | 方法 | 訓練資料 | 開發成本 |
|------|------|---------|---------|
| A. YOLOv8n fine-tune | Fine-tune on gun dataset | 需收集 500+ 張標註圖片 | 高 |
| B. OWL-ViT zero-shot | Vision-language pretraining | 無須訓練 | 低 |
| C. Grounding DINO zero-shot | Vision-language pretraining | 無須訓練 | 低 |
## 模型大小與效能
| Model | 磁碟 | 參數 | 推論時間 (MPS) | 單幀能耗 | 模型類別 |
|-------|------|------|---------------|---------|---------|
| YOLOv8n | **6MB** | **3.2M** | **0.03s** | **~0.5J** | 封閉集80 類) |
| OWL-ViT | 586MB | 109M | 3.4s | ~50J | 開放集zero-shot |
| **Grounding DINO** | **891MB** | **172M** | **4.3s** | **~65J** | **開放集zero-shot** |
## Charade 實測結果
| Model | 8 時間點命中 | 5 個原始 pistol | 最佳 confidence | 推論時間 | 模型大小 |
|-------|-------------|-----------------|----------------|---------|---------|
| YOLOv8n COCO | ❌ N/A無 gun class | — | — | 0.03s | 6MB |
| YOLOv8n fine-tune | 7/7 FP | ❌ 全部 FP | 0.45(郵票誤判) | 0.03s | 6MB |
| OWL-ViT | 2/8 | ❌ 0/5 | 0.054 | 3.4s | 586MB |
| **Grounding DINO Base** | **31/32** | **✅ 5/5** | **0.672** | **11.6s** | **891MB** |
| **Grounding DINO Large** | **32/32** | **✅ 5/5** | **1.000** | **50.1s** | **895MB** |
### Base vs Large 比較
| 指標 | Base (3 datasets) | Large (7 datasets) |
|------|------------------|-------------------|
| 平均最佳 confidence | 0.384 | **1.000** |
| 總偵測數 | 333 | **28,800** |
| COCO zero-shot AP | 48.4 | **56.7** |
| 推論時間 (MPS) | 11.6s | 50.1s |
| Edge 部署 | 較可行 | 較困難 |
### 結論
**效能優先選擇Grounding DINO Large** — 所有 8 個時間點 confidence 1.000,零漏檢。犧牲推論速度但 detection 品質大幅超越 Base 版。
**Edge 部署選擇Grounding DINO Base** — 體積相近但推論快 4.3x,適合資源受限裝置。
### 關鍵結論
1. **YOLOv8n fine-tune 完全失敗** — 905 張 Roboflow 近距離特寫與 Charade 中遠景畫面分布 mismatch訓練無法泛化
2. **OWL-ViT 幾乎無效** — 對電影中的小物體辨識能力不足
3. **Grounding DINO 成功** — 5/5 找回 pistol frames所有 ASR gun mention 時間點也命中
## Grounding DINO 優缺點
### 優點
- **零樣本搜尋**:任何 COCO 以外的物件直接用文字 prompt 搜尋
- **延伸性**:同一模型可搜尋 gun、stamp、envelope、knife、hat 等任意物件
- **無須訓練**:不需要收集標註資料或 fine-tune
- **Apache 2.0 License**:可商用
### 缺點
- **體積大**891MBvs YOLOv8n 的 6MB
- **推論慢**4.3s/framevs YOLOv8n 的 0.03s
- **不適合 real-time**edge device 上無法做即時偵測,只適合離線掃描
## Edge AI 部署考量
| 項目標題 | YOLOv8n | Grounding DINO |
|---------|---------|---------------|
| 模型大小 | 6MB ✅ | 891MB ⚠️ |
| RAM 需求 | ~100MB | ~2.5GB |
| 推論時間 | 30ms | 4.3s |
| 單幀能耗 | ~0.5J | ~65J |
| 搜尋類別數 | 80固定 | 無限(文字 prompt |
| 電池影響1000 幀) | ~500J | ~65,000J |
### 建議策略
```
離線掃描Server/Gateway
用 Grounding DINO 對全片建立物件索引
→ 耗時但可接受113 min 電影約 2-3 小時)
即時查詢Edge Device
查詢時只跑 Grounding DINO 在該 timepoint → 4s/次
→ 查詢體驗還可接受
```
## 整合狀態
- ✅ Grounding DINO 測試通過
- ✅ 整合進 `scripts/object_search_agent.py``--source zero_shot`
- ✅ 測試計畫:`docs/ZERO_SHOT_GUN_TEST_PLAN.md`
- ✅ 測試報告:`docs/ZERO_SHOT_GUN_TEST_REPORT.md`
## License 聲明
Grounding DINO 採用 Apache 2.0 License可商用。
產品若 bundle 此模型,需附 `NOTICE` 檔案:
```
Momentry
Copyright 2026 Accusys
This product includes software developed by IDEA Research:
- Grounding DINO (https://github.com/IDEA-Research/GroundingDINO)
Copyright 2023 IDEA Research
Licensed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0)
```

View File

@@ -0,0 +1,230 @@
# Momentry Core API 存取指南
| 項目 | 內容 |
|------|------|
| 版本 | V1.3 |
| 日期 | 2026-03-25 |
| 用途 | API 存取方式、端點與整合指南 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.3 | 2026-03-25 | 更新: n8n 搜尋回傳 `file_path` 取代 `media_url`,新增 API Key 驗證說明 | OpenCode | deepseek-reasoner |
| V1.2 | 2026-03-24 | 更新網址與服務列表 | Warren | OpenCode / MiniMax M2.5 |
| V1.1 | 2026-03-23 | 初始版本 | Warren | OpenCode / MiniMax M2.5 |
---
## 基本網址
| 環境 | URL | 說明 |
|------|-----|------|
| **本地開發** | `http://localhost:3002` | 直接訪問 API繞過反向代理 |
| **外部訪問** | `https://api.momentry.ddns.net` | 通過 Caddy 反向代理訪問,需網路可達 |
### 何時使用哪個 URL
**使用 `localhost:3002`**
- 開發/測試環境
- 直接在伺服器上操作
- 當反向代理有問題時
**使用 `api.momentry.ddns.net`**
- n8n workflow 中呼叫 API
- 外部系統整合
- 生產環境
## 認證
所有 `/api/v1/*` 端點(除了健康檢查 `/health``/health/detailed`)都需要 API Key 認證。
請在請求標頭中加入:
```
X-API-Key: YOUR_API_KEY
```
**目前示範使用的 API Key**: `demo_api_key_12345`
> **注意**: 正式環境請使用安全的 API Key 管理機制,避免在客戶端暴露 API Key。
---
## 影片搜尋 API
### 語意搜尋
**端點:** `POST /api/v1/search`
**請求:**
```json
{
"query": "charade",
"limit": 5,
"uuid": "a1b10138a6bbb0cd"
}
```
| 欄位 | 類型 | 必填 | 說明 |
|------|------|------|------|
| `query` | 字串 | 是 | 搜尋文字 |
| `limit` | 整數 | 否 | 最大回傳結果數(預設 10 |
| `uuid` | 字串 | 否 | 依影片 UUID 過濾 |
**回應:**
```json
{
"results": [
{
"uuid": "a1b10138a6bbb0cd",
"chunk_id": "sentence_0006",
"chunk_type": "sentence",
"start_time": 48.8,
"end_time": 55.44,
"text": "fun plot twists, Woody Dialog and charming performances...",
"score": 0.526
}
],
"query": "charade"
}
```
---
### n8n 整合搜尋
**端點:** `POST /api/v1/n8n/search`
**請求:**
```json
{
"query": "charade",
"limit": 5
}
```
**回應:**
```json
{
"query": "charade",
"count": 5,
"hits": [
{
"id": "sentence_0006",
"vid": "a1b10138a6bbb0cd",
"start": 48.8,
"end": 55.44,
"title": "Chunk sentence_0006",
"text": "fun plot twists...",
"score": 0.526,
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
}
]
}
```
> **注意**: API 現在返回 `file_path`(檔案系統路徑)而非 `media_url`(網頁 URL。如需在網頁中播放影片請將檔案路徑轉換為可訪問的 URL例如透過 SFTPGo 分享連結)。
---
## 影片管理 API
### 列出所有影片
**端點:** `GET /api/v1/videos`
### 查詢影片資訊
**端點:** `GET /api/v1/lookup?uuid={uuid}``GET /api/v1/lookup?path={path}`
### 取得處理進度
**端點:** `GET /api/v1/progress/{uuid}`
---
## 區塊資料結構
每個搜尋結果包含影片播放的時間資訊:
| 欄位 | 說明 |
|------|------|
| `uuid` | 影片識別碼 |
| `chunk_id` | 區塊唯一識別碼 |
| `chunk_type` | 類型:`sentence``cut``time_based` |
| `start_time` | 開始時間(秒) |
| `end_time` | 結束時間(秒) |
| `text` | 語音轉文字內容 |
| `score` | 相關性分數0-1 |
---
## 整合範例
### JavaScript/fetch
```javascript
const response = await fetch('http://localhost:3002/api/v1/search', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-API-Key': 'YOUR_API_KEY' // 替換為實際的 API Key
},
body: JSON.stringify({ query: 'charade', limit: 5 })
});
const data = await response.json();
console.log(data.results);
```
### PHP/cURL
```php
$ch = curl_init('http://localhost:3002/api/v1/search');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
'query' => 'charade',
'limit' => 5
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-API-Key: YOUR_API_KEY' // 替換為實際的 API Key
]);
$response = curl_exec($ch);
$data = json_decode($response, true);
```
---
## 影片嵌入網址
> **重要**: API 現在返回 `file_path`(檔案系統路徑),而非直接可訪問的網址。您需要將檔案路徑轉換為 SFTPGo 分享連結才能嵌入影片。
**檔案路徑轉換為網址:**
- API 返回的 `file_path` 範例:`/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4`
- 對應的 SFTPGo 分享連結:`https://wp.momentry.ddns.net/demo/video.mp4`
- 轉換方式:移除 `/Users/accusys/momentry/var/sftpgo/data/` 前綴,將剩餘路徑附加到 `https://wp.momentry.ddns.net/`
**手動建立分享連結:**
1. 開啟 SFTPGo Web UI`http://localhost:8080`
2. 使用帳號 `demo` / 密碼 `demopassword123` 登入
3. 導航至 `Files` → 選擇影片檔案
4. 點擊 `Share``Create Link`
5. 複製產生的分享連結
使用搜尋結果中的 `start_time``end_time` 來嵌入影片片段。
---
## 服務列表
| 服務 | 網址 | 用途 |
|------|------|------|
| Momentry API | `http://localhost:3002` | 核心 API |
| SFTPGo | `http://localhost:8080` | 檔案儲存 |
| Qdrant | `http://localhost:6333` | 向量搜尋 |
| PostgreSQL | `localhost:5432` | 資料庫 |
---
## 示範影片
- **檔案:** `Old_Time_Movie_Show_-_Charade_1963.HD.mov`
- **UUID** `a1b10138a6bbb0cd`
- **長度:** 約 6879 秒(約 1.9 小時)
- **區塊數:** 3886 個(句子 + 場景 + 時間)

View File

@@ -0,0 +1,321 @@
# Momentry Core API 端點總覽
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-18 |
| 文件版本 | V1.3 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 |
|------|------|------|--------|
| V1.0 | 2026-03-18 | 創建文件 | OpenCode |
| V1.1 | 2026-03-23 | 更新端點與實際一致 | OpenCode |
| V1.2 | 2026-03-25 | 新增快取/刪除 API | OpenCode |
| V1.3 | 2026-03-26 | 更新API回應格式 (media_url→file_path) | OpenCode |
---
## Base URL
| 環境 | URL |
|------|-----|
| 本地 | `http://localhost:3002` |
| 外部 | `https://api.momentry.ddns.net` |
---
## 認證
除健康檢查端點外,所有 API 端點都需要 API Key。
### Header 方式
```bash
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/videos
```
### 響應
- `401 Unauthorized` - 缺少或無效的 API Key
- `200 OK` - 認證成功
### 取得 API Key
使用 CLI 建立:
```bash
./target/release/momentry api-key create "My API Key" --key-type user
```
---
## 端點列表
### 健康檢查(公開)
| 方法 | 端點 | 說明 |
|------|------|------|
| GET | `/health` | 基本健康檢查 |
| GET | `/health/detailed` | 詳細健康檢查(含服務狀態) |
**範例**:
```bash
curl http://localhost:3002/health
# {"status":"ok","version":"0.1.0","uptime_ms":123456}
```
---
### 影片搜尋
| 方法 | 端點 | 說明 |
|------|------|------|
| POST | `/api/v1/search` | 語意搜尋(標準格式) |
| POST | `/api/v1/n8n/search` | 語意搜尋n8n 專用格式) |
| POST | `/api/v1/search/hybrid` | 混合搜尋 |
**標準搜尋** (`/api/v1/search`):
```bash
curl -X POST http://localhost:3002/api/v1/search \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"query": "test", "limit": 10}'
```
**n8n 格式搜尋** (`/api/v1/n8n/search`):
```bash
curl -X POST http://localhost:3002/api/v1/n8n/search \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"query": "test", "limit": 10}'
```
---
### 影片管理
| 方法 | 端點 | 說明 |
|------|------|------|
| POST | `/api/v1/register` | 註冊影片 |
| POST | `/api/v1/probe` | 探測影片資訊(不註冊) |
| GET | `/api/v1/videos` | 列出所有影片 |
| GET | `/api/v1/lookup` | 查詢影片資訊 |
| GET | `/api/v1/progress/:uuid` | 取得處理進度 |
**註冊影片**:
```bash
curl -X POST http://localhost:3002/api/v1/register \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"path": "/path/to/video.mp4"}'
```
**註冊回應範例**:
```json
{
"uuid": "a1b10138a6bbb0cd",
"video_id": 1,
"job_id": 10,
"file_name": "video.mp4",
"duration": 120.5,
"width": 1920,
"height": 1080,
"already_exists": false
}
```
**探測影片** (不註冊,只取得影片資訊):
```bash
curl -X POST http://localhost:3002/api/v1/probe \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"path": "./demo/video.mp4"}'
```
**Probe 回應範例**:
```json
{
"uuid": "a1b10138a6bbb0cd",
"file_name": "video.mp4",
"duration": 120.5,
"width": 1920,
"height": 1080,
"fps": 30.0,
"cached": false,
"format": {
"filename": "/path/to/video.mp4",
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"duration": "120.5",
"size": "12345678",
"bit_rate": "819200"
},
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_type": "video",
"width": 1920,
"height": 1080,
"r_frame_rate": "30/1",
"duration": "120.5"
}
]
}
```
**列出影片**:
```bash
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/videos
```
**查詢影片**:
```bash
curl -H "X-API-Key: your-api-key" "http://localhost:3002/api/v1/lookup?uuid=5dea6618a606e7c7"
```
**處理進度**:
```bash
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/progress/5dea6618a606e7c7
```
---
### 工作管理
| 方法 | 端點 | 說明 |
|------|------|------|
| GET | `/api/v1/jobs` | 列出所有工作 |
| GET | `/api/v1/jobs/:uuid` | 取得指定工作的詳細資訊 |
**列出工作**:
```bash
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/jobs
```
**取得工作詳細資訊**:
```bash
curl -H "X-API-Key: your-api-key" http://localhost:3002/api/v1/jobs/a03485a40b2df2d3
```
---
### 系統管理
| 方法 | 端點 | 說明 |
|------|------|------|
| POST | `/api/v1/config/cache` | 切換快取功能(管理員) |
| POST | `/api/v1/unregister` | 刪除影片及其所有資料(管理員) |
**快取設定**:
```bash
curl -X POST http://localhost:3002/api/v1/config/cache \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"enabled": true}'
```
**刪除影片**:
```bash
curl -X POST http://localhost:3002/api/v1/unregister \
-H "Content-Type: application/json" \
-H "X-API-Key: your-api-key" \
-d '{"uuid": "5dea6618a606e7c7"}'
```
---
## 端點對照表
| 功能 | n8n 使用 | WordPress 使用 | curl 測試 |
|------|-----------|----------------|------------|
| 健康檢查 | ✓ | ✓ | ✓ |
| 語意搜尋 | ✓ (n8n格式) | ✓ (標準格式) | ✓ |
| 影片探測 | ✓ | ✓ | ✓ |
| 註冊影片 | ✓ | ✓ | ✓ |
| 列出影片 | ✓ | ✓ | ✓ |
| 查詢影片 | ✓ | ✓ | ✓ |
| 處理進度 | ✓ | ✓ | ✓ |
| 工作管理 | ✓ | ✓ | ✓ |
| 快取設定 | ✓ (管理員) | ✓ (管理員) | ✓ (管理員) |
| 刪除影片 | ✓ (管理員) | ✓ (管理員) | ✓ (管理員) |
---
## 回應格式
### n8n 格式 (`/api/v1/n8n/search`)
```json
{
"query": "charade",
"count": 10,
"hits": [
{
"id": "sentence_0001",
"vid": "a1b10138a6bbb0cd",
"start": 48.8,
"end": 55.44,
"title": "Chunk sentence_0001",
"text": "...",
"score": 0.92,
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
}
]
}
```
### 標準格式 (`/api/v1/search`)
```json
{
"results": [
{
"uuid": "a1b10138a6bbb0cd",
"chunk_id": "sentence_0001",
"chunk_type": "sentence",
"start_time": 48.8,
"end_time": 55.44,
"text": "...",
"score": 0.92
}
],
"query": "charade"
}
```
---
## HTTP 狀態碼
| 狀態 | 說明 |
|------|------|
| 200 | 成功 |
| 400 | 請求格式錯誤 |
| 404 | 端點或資源不存在 |
| 500 | 伺服器內部錯誤 |
| 502 | API 服務未啟動 |
---
## 錯誤處理
### 502 Bad Gateway
**原因**: Momentry API 服務未啟動
**解決**:
```bash
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
```
---
## 相關文件
- [API_INDEX.md](./API_INDEX.md) - 文件總覽(起點)
- [API_EXAMPLES.md](./API_EXAMPLES.md) - **完整範例總覽curl / n8n / WordPress**
- [API_N8N_GUIDE.md](./API_N8N_GUIDE.md) - n8n 詳細指南
- [API_WORDPRESS_GUIDE.md](./API_WORDPRESS_GUIDE.md) - WordPress 詳細指南
- [API_CURL_EXAMPLES.md](./API_CURL_EXAMPLES.md) - curl 範例

View File

@@ -0,0 +1,106 @@
# API Error Codes (API 標準錯誤碼)
| 項目 | 內容 |
|------|------|
| 建立者 | OpenCode |
| 建立時間 | 2026-04-25 |
| 文件版本 | V1.0 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-04-25 | 定義全局標準錯誤碼與 Response 格式 | OpenCode | OpenCode |
---
## 1. 錯誤 Response 格式
所有 API 錯誤回應必須遵循以下 JSON 結構:
```json
{
"success": false,
"error": {
"code": "E001_NOT_FOUND",
"message": "找不到指定的資源",
"details": {
"resource": "file_uuid",
"value": "abc-123"
}
}
}
```
---
## 2. 錯誤碼列表
### 2.1 通用錯誤 (E0xx)
| 錯誤碼 | HTTP 狀態 | 說明 |
|--------|-----------|------|
| `E001_NOT_FOUND` | 404 | 找不到資源 (File, Identity, Chunk...) |
| `E002_DUPLICATE` | 409 | 資源已存在 (例如:重複註冊 File UUID) |
| `E003_VALIDATION` | 400 | 請求參數驗證失敗 (缺欄位、格式錯誤) |
| `E004_UNAUTHORIZED` | 401 | 無效的 API Key 或 Token |
| `E005_INTERNAL` | 500 | 系統內部錯誤 (資料庫連線失敗等) |
### 2.2 處理器相關 (E1xx)
| 錯誤碼 | HTTP 狀態 | 說明 |
|--------|-----------|------|
| `E101_PROCESSOR_FAIL` | 500 | Python 腳本執行失敗 (返回非 0 狀態碼) |
| `E102_TIMEOUT` | 504 | 處理超時 (例如:長影片 ASR 處理過久) |
| `E103_RESUME_FAIL` | 500 | 續傳失敗 (找不到 Checkpoint 檔案) |
| `E104_NO_VIDEO` | 400 | 找不到影片路徑 |
### 2.3 身份與 Face (E2xx)
| 錯誤碼 | HTTP 狀態 | 說明 |
|--------|-----------|------|
| `E201_FACE_NOT_FOUND` | 404 | 找不到指定的 Face Pre-chunk |
| `E202_MERGE_CONFLICT` | 409 | Identity 合併衝突 |
| `E203_CANDIDATE_EMPTY` | 404 | 沒有待確認的 Candidates |
---
## 3. 實作建議 (Rust Axum)
`src/api/server.rs` 中,建議使用自訂錯誤型別來統一處理:
```rust
#[derive(Debug)]
pub enum AppError {
NotFound(String),
Validation(String),
Internal(anyhow::Error),
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (code, message, status) = match self {
AppError::NotFound(msg) => ("E001_NOT_FOUND", msg, StatusCode::NOT_FOUND),
AppError::Validation(msg) => ("E003_VALIDATION", msg, StatusCode::BAD_REQUEST),
AppError::Internal(e) => ("E005_INTERNAL", e.to_string(), StatusCode::INTERNAL_SERVER_ERROR),
};
(status, Json(serde_json::json!({
"success": false,
"error": {
"code": code,
"message": message
}
}))).into_response()
}
}
```
---
## 版本資訊
- 版本: V1.0
- 建立日期: 2026-04-25

View File

@@ -0,0 +1,129 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Momentry Core API 文件總覽"
date: "2026-04-23"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "momentry"
- "文件總覽"
- "core"
ai_query_hints:
- "查詢 Momentry Core API 文件總覽 的內容"
- "Momentry Core API 文件總覽 的主要目的是什麼?"
- "如何操作或實施 Momentry Core API 文件總覽?"
---
# Momentry Core API 文件總覽
> **Version**: 3.0 | **Updated**: 2026-04-23
> **Source**: Generated from actual Rust code (`src/api/`)
---
## 📁 文件結構
```
docs_v1.0/
├── REFERENCE/
│ ├── API_REFERENCE.md ← 主要 API 參考文件71 個端點)
│ ├── API_KEY_DESIGN.md ← API Key 系統設計文件
│ └── API_TRAINING_MARCOM.md ← marcom 團隊教育訓練手冊
├── IMPLEMENTATION/
│ ├── API_EXAMPLES.md ← 完整範例curl / n8n / WordPress
│ ├── API_CURL_EXAMPLES.md ← curl 快速範例
│ ├── API_WORDPRESS_GUIDE.md ← WordPress 整合指南
│ └── API_N8N_GUIDE.md ← n8n 整合指南
└── ARCHITECTURE/
└── API_KEY_ARCHITECTURE.md ← API Key 架構圖
```
---
## 快速選擇指南
| 需求 | 閱讀文件 |
|------|----------|
| **我要查看所有 API 端點** | [API_REFERENCE.md](./API_REFERENCE.md) |
| **我要 curl 範例** | [API_EXAMPLES.md](../IMPLEMENTATION/API_EXAMPLES.md) |
| **我是 marcom 團隊** | [API_TRAINING_MARCOM.md](./API_TRAINING_MARCOM.md) |
| **我要整合 n8n** | [API_N8N_GUIDE.md](../IMPLEMENTATION/API_N8N_GUIDE.md) |
| **我要整合 WordPress** | [API_WORDPRESS_GUIDE.md](../IMPLEMENTATION/API_WORDPRESS_GUIDE.md) |
| **我要了解 API Key 設計** | [API_KEY_DESIGN.md](./API_KEY_DESIGN.md) |
---
## 認證
### 使用方式
```bash
export API_KEY="your_api_key_here"
curl -H "X-API-Key: $API_KEY" http://localhost:3002/api/v1/videos
```
### 環境
| 環境 | URL | 使用時機 |
|------|-----|----------|
| **本地開發** | `http://localhost:3002` | 開發/測試 |
| **Playground** | `http://localhost:3003` | 開發測試dev 模式) |
| **外部訪問** | `https://api.momentry.ddns.net` | n8n、WordPress、遠端 |
---
## API 端點總覽
| 類別 | 端點數 | 說明 |
|------|--------|------|
| Health & Stats | 5 | 健康檢查與統計(公開) |
| Core Asset | 6 | 影片註冊、查詢、進度 |
| Processing | 7 | 探針、處理、任務 |
| Search | 7 | 向量、BM25、混合搜索 |
| Visual Chunk | 5 | 視覺分片搜索 |
| Face Recognition | 7 | 人臉識別 |
| Person Identity | 21 | 人物身份管理 |
| Global Identities | 6 | 全局身份 |
| Identity Binding | 6 | 身份綁定 |
| Configuration | 1 | 緩存配置 |
| **Total** | **71** | **可達端點** |
### ⚠️ 未掛載端點
以下端點已定義但**未在 router 中掛載**
| 端點 | 定義位置 |
|------|----------|
| `/api/v1/search/universal` | `universal_search.rs` |
| `/api/v1/search/frames` | `universal_search.rs` |
| `/api/v1/search/persons` | `universal_search.rs` |
| `/api/v1/who` | `who.rs` |
| `/api/v1/who/candidates` | `who.rs` |
---
## 常見問題
### Q: API 返回 401 錯誤?
API Key 無效或過期。請檢查 `X-API-Key` header。
### Q: API 返回 502 錯誤?
```bash
# 檢查服務狀態
launchctl list | grep momentry.api
# 重啟服務
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
```
---
## 相關文件
- [API_REFERENCE.md](./API_REFERENCE.md) - 完整 API 參考
- [INSTALL_MOMENTRY_API.md](../IMPLEMENTATION/INSTALL_MOMENTRY_API.md) - 安裝指南
- [API_KEY_DESIGN.md](./API_KEY_DESIGN.md) - API Key 設計

View File

@@ -0,0 +1,532 @@
# Momentry Core API 快速查詢表
| 版本 | 日期 | 建立者 |
|------|------|--------|
| V1.0 | 2026-03-26 | OpenCode |
---
## 📋 快速導覽
| 類別 | 端點數量 | 說明 |
|------|----------|------|
| 健康檢查 | 2 | 系統狀態監控 |
| 影片管理 | 5 | 影片註冊、查詢、刪除 |
| 搜尋功能 | 3 | 語意搜尋、混合搜尋 |
| 任務管理 | 2 | 處理任務狀態查詢 |
| 系統管理 | 2 | 快取設定、進度查詢 |
---
## 🔐 認證
所有 `/api/v1/*` 端點需要 `X-API-Key` header
```bash
curl -H "X-API-Key: YOUR_API_KEY" ...
```
**公開端點(無需認證):**
- `GET /health`
- `GET /health/detailed`
---
## 📊 端點總表
### 健康檢查
| 方法 | 端點 | 認證 | 描述 |
|------|------|------|------|
| GET | `/health` | 公開 | 基本健康檢查 |
| GET | `/health/detailed` | 公開 | 詳細健康檢查(包含所有服務狀態) |
### 影片管理
| 方法 | 端點 | 認證 | 描述 |
|------|------|------|------|
| POST | `/api/v1/register` | 需要 | 註冊影片並開始處理 |
| POST | `/api/v1/unregister` | 需要 | 刪除影片及其所有資料 |
| POST | `/api/v1/probe` | 需要 | 探測影片資訊(不註冊) |
| GET | `/api/v1/videos` | 需要 | 列出所有已註冊影片 |
| GET | `/api/v1/lookup` | 需要 | 查詢影片資訊 |
### 搜尋功能
| 方法 | 端點 | 認證 | 描述 |
|------|------|------|------|
| POST | `/api/v1/search` | 需要 | 語意搜尋(標準格式) |
| POST | `/api/v1/n8n/search` | 需要 | 語意搜尋n8n 格式) |
| POST | `/api/v1/search/hybrid` | 需要 | 混合搜尋(向量 + 關鍵字) |
### 任務管理
| 方法 | 端點 | 認證 | 描述 |
|------|------|------|------|
| GET | `/api/v1/jobs` | 需要 | 列出所有處理任務 |
| GET | `/api/v1/jobs/:uuid` | 需要 | 取得特定任務詳情 |
### 系統管理
| 方法 | 端點 | 認證 | 描述 |
|------|------|------|------|
| GET | `/api/v1/progress/:uuid` | 需要 | 取得影片處理進度 |
| POST | `/api/v1/config/cache` | 需要 | 切換快取功能 |
---
## 🔧 詳細端點說明
### 1. 健康檢查
#### GET /health
**基本健康檢查**
```bash
curl http://localhost:3002/health
```
**回應:**
```json
{
"status": "ok",
"version": "0.1.0",
"uptime_ms": 14426558
}
```
#### GET /health/detailed
**詳細健康檢查**
```bash
curl http://localhost:3002/health/detailed
```
**回應:**
```json
{
"status": "ok",
"version": "0.1.0",
"uptime_ms": 14441362,
"services": {
"postgres": {"status": "ok", "latency_ms": 50, "error": null},
"redis": {"status": "ok", "latency_ms": 0, "error": null},
"qdrant": {"status": "ok", "latency_ms": 2, "error": null},
"mongodb": {"status": "ok", "latency_ms": 2, "error": null}
}
}
```
### 2. 影片管理
#### POST /api/v1/register
**註冊影片並開始處理**
```bash
curl -X POST http://localhost:3002/api/v1/register \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"path": "/path/to/video.mp4"}'
```
**請求:**
```json
{
"path": "/path/to/video.mp4"
}
```
**回應:**
```json
{
"uuid": "5dea6618a606e7c7",
"video_id": 10,
"job_id": 1,
"file_name": "video.mp4",
"duration": 596.458333,
"width": 320,
"height": 180,
"already_exists": false
}
```
#### POST /api/v1/unregister
**刪除影片及其所有資料**
```bash
curl -X POST http://localhost:3002/api/v1/unregister \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"uuid": "5dea6618a606e7c7"}'
```
**請求:**
```json
{
"uuid": "5dea6618a606e7c7"
}
```
**回應:**
```json
{
"success": true,
"uuid": "5dea6618a606e7c7",
"message": "Video unregistered successfully"
}
```
#### POST /api/v1/probe
**探測影片資訊(不註冊)**
```bash
curl -X POST http://localhost:3002/api/v1/probe \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"path": "/path/to/video.mp4"}'
```
**請求:**
```json
{
"path": "/path/to/video.mp4"
}
```
**回應:**
```json
{
"uuid": "5dea6618a606e7c7",
"file_name": "video.mp4",
"duration": 596.458333,
"width": 320,
"height": 180,
"fps": 24.0,
"cached": true,
"format": {...},
"streams": [...]
}
```
#### GET /api/v1/videos
**列出所有已註冊影片**
```bash
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
```
**回應:**
```json
{
"videos": [
{
"uuid": "a03485a40b2df2d3",
"file_path": "/path/to/video.mp4",
"file_name": "video.mp4",
"duration": 596.458333,
"width": 320,
"height": 180
}
]
}
```
#### GET /api/v1/lookup
**查詢影片資訊**
```bash
# 依 UUID 查詢
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?uuid=a03485a40b2df2d3"
# 依路徑查詢
curl -H "X-API-Key: YOUR_API_KEY" "http://localhost:3002/api/v1/lookup?path=/path/to/video.mp4"
```
**回應:**
```json
{
"uuid": "a03485a40b2df2d3",
"file_path": "/path/to/video.mp4",
"file_name": "video.mp4",
"duration": 596.458333
}
```
### 3. 搜尋功能
#### POST /api/v1/search
**語意搜尋(標準格式)**
```bash
curl -X POST http://localhost:3002/api/v1/search \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"query": "search term", "limit": 5}'
```
**請求:**
```json
{
"query": "search term",
"limit": 5
}
```
**回應:**
```json
{
"results": [
{
"uuid": "a1b10138a6bbb0cd",
"chunk_id": "sentence_0001",
"chunk_type": "sentence",
"start_time": 10.5,
"end_time": 15.2,
"text": "Found text matching query",
"score": 0.85
}
],
"query": "search term"
}
```
#### POST /api/v1/n8n/search
**語意搜尋n8n 格式)**
```bash
curl -X POST http://localhost:3002/api/v1/n8n/search \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"query": "search term", "limit": 5}'
```
**回應:**
```json
{
"query": "search term",
"count": 1,
"hits": [
{
"id": "sentence_0001",
"vid": "a1b10138a6bbb0cd",
"start_time": 10.5,
"end_time": 15.2,
"title": "Chunk sentence_0001",
"text": "Found text matching query",
"score": 0.85,
"file_path": "/path/to/video.mp4"
}
]
}
```
#### POST /api/v1/search/hybrid
**混合搜尋(向量 + 關鍵字)**
```bash
curl -X POST http://localhost:3002/api/v1/search/hybrid \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"query": "search term", "limit": 5}'
```
**請求:**
```json
{
"query": "search term",
"limit": 5
}
```
**回應:**`/api/v1/search` 相同格式
### 4. 任務管理
#### GET /api/v1/jobs
**列出所有處理任務**
```bash
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs
```
**回應:**
```json
{
"jobs": [
{
"id": 10,
"uuid": "a03485a40b2df2d3",
"status": "running",
"current_processor": null,
"progress_current": 0,
"progress_total": 0,
"created_at": "2026-03-26 13:39:37.830465",
"started_at": null
}
]
}
```
#### GET /api/v1/jobs/:uuid
**取得特定任務詳情**
```bash
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs/a03485a40b2df2d3
```
**回應:**
```json
{
"id": 10,
"uuid": "a03485a40b2df2d3",
"status": "running",
"current_processor": null,
"progress_current": 0,
"progress_total": 0,
"processors": [
{
"processor_type": "asr",
"status": "completed",
"started_at": "2026-03-26 05:39:40.275468",
"completed_at": "2026-03-26 07:19:43.166613",
"duration_secs": 6002.891145,
"error_message": null
},
// ... 其他處理器
],
"created_at": "2026-03-26 13:39:37.830465",
"started_at": null,
"updated_at": "2026-03-26 07:19:16.338406"
}
```
### 5. 系統管理
#### GET /api/v1/progress/:uuid
**取得影片處理進度**
```bash
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/a03485a40b2df2d3
```
**回應:**
```json
{
"uuid": "a03485a40b2df2d3",
"user": null,
"group": null,
"file_name": "video.mp4",
"duration": 596.458333,
"overall_progress": 0,
"cpu_percent": 0.2,
"gpu_percent": null,
"memory_percent": 0.1,
"memory_mb": 16720,
"processors": [
{
"name": "asr",
"status": "pending",
"current": 0,
"total": 0,
"progress": 0,
"message": ""
},
// ... 其他處理器
]
}
```
#### POST /api/v1/config/cache
**切換快取功能**
```bash
curl -X POST http://localhost:3002/api/v1/config/cache \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"enabled": true}'
```
**請求:**
```json
{
"enabled": true
}
```
**回應:**
```json
{
"success": true,
"cache_enabled": true,
"message": "Cache enabled"
}
```
---
## 🚀 快速工作流程
### 1. 註冊並處理影片
```bash
# 1. 註冊影片
curl -X POST http://localhost:3002/api/v1/register \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"path": "/path/to/video.mp4"}'
# 回應包含 UUID: 5dea6618a606e7c7
# 2. 監控進度
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/progress/5dea6618a606e7c7
# 3. 查看任務狀態
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/jobs/5dea6618a606e7c7
```
### 2. 搜尋影片內容
```bash
# 1. 列出所有影片
curl -H "X-API-Key: YOUR_API_KEY" http://localhost:3002/api/v1/videos
# 2. 搜尋內容
curl -X POST http://localhost:3002/api/v1/n8n/search \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"query": "charade scene", "limit": 10}'
```
### 3. 系統管理
```bash
# 1. 檢查系統健康
curl http://localhost:3002/health/detailed
# 2. 管理快取
curl -X POST http://localhost:3002/api/v1/config/cache \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"enabled": false}'
# 3. 刪除影片(需要時)
curl -X POST http://localhost:3002/api/v1/unregister \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
-d '{"uuid": "5dea6618a606e7c7"}'
```
---
## 📝 注意事項
1. **API Key 格式:**
- 使用完整 API Key`muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69`
- 系統存儲的是 SHA256 哈希值
2. **路徑格式:**
- 絕對路徑:`/Users/accusys/test_video/video.mp4`
- 相對路徑:`./demo/video.mp4`(相對於 SFTPGo 資料目錄)
3. **回應時間:**
- 健康檢查:< 100ms
- 搜尋:取決於查詢複雜度,通常 100-500ms
- 影片註冊:立即返回,背景處理可能需要數分鐘到數小時
4. **錯誤處理:**
- 401: 認證失敗
- 404: 資源不存在
- 500: 伺服器內部錯誤
---
## 🔗 相關文件
- [API 參考指南](./API_REFERENCE.md) - 詳細 API 說明
- [API 範例總覽](./API_EXAMPLES.md) - 完整使用範例
- [API 端點列表](./API_ENDPOINTS.md) - 端點簡介
- [Curl 範例指南](./API_CURL_EXAMPLES.md) - curl 命令範例
- [n8n 整合指南](./API_N8N_GUIDE.md) - n8n 工作流程整合

View File

@@ -0,0 +1,310 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Momentry Core API Reference"
date: "2026-04-25"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "reference"
- "momentry"
- "core"
ai_query_hints:
- "查詢 Momentry Core API Reference 的內容"
- "Momentry Core API Reference 的主要目的是什麼?"
- "如何操作或實施 Momentry Core API Reference"
---
# Momentry Core API Reference
> **Version**: 1.0 | **Source**: Generated from actual Rust code (`src/api/`)
> **Server**: Port 3002 (production) | 3003 (playground)
> **Auth**: Bearer token via `X-API-Key` header (required for most endpoints)
---
## 📋 Table of Contents
1. [Health & Stats (Public)](#health--stats)
2. [Core Asset Management](#core-asset-management)
3. [Processing Pipeline](#processing-pipeline)
4. [Search APIs](#search-apis)
5. [Visual Chunk Search](#visual-chunk-search)
6. [Face Recognition](#face-recognition)
7. [Person Identity](#person-identity)
8. [Global Identities](#global-identities)
9. [Identity Binding](#identity-binding)
10. [Configuration](#configuration)
---
## Health & Stats
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| GET | `/health` | No | Basic health check |
| GET | `/health/detailed` | No | Detailed health (all services) |
| GET | `/api/v1/stats/ingest` | No | Ingest statistics |
| GET | `/api/v1/stats/sftpgo` | No | SFTPGo service status |
| GET | `/api/v1/stats/inference` | No | Inference service health |
### Example
```bash
curl http://localhost:3002/health
# Response: {"status": "ok"}
```
---
## Core Asset Management
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/api/v1/register` | Yes | Register a new video |
| POST | `/api/v1/unregister` | Yes | Delete a video and all data |
| GET | `/api/v1/videos` | Yes | List all videos |
| GET | `/api/v1/videos/:uuid/details` | Yes | Get video details with chunks |
| GET | `/api/v1/lookup` | Yes | Lookup video by path or UUID |
| GET | `/api/v1/progress/:uuid` | Yes | Get processing progress |
### Register Video
```bash
curl -X POST http://localhost:3002/api/v1/register \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"path": "./demo/video.mp4"}'
```
### Video Details
```bash
curl http://localhost:3002/api/v1/videos/$UUID/details \
-H "X-API-Key: $API_KEY"
```
---
## Processing Pipeline
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/api/v1/probe` | Yes | Probe video metadata |
| GET | `/api/v1/assets/:uuid/probe` | Yes | Get probe result by UUID |
| POST | `/api/v1/assets/:uuid/process` | Yes | Trigger processing pipeline |
| GET | `/api/v1/assets/:uuid/status` | Yes | Get asset processing status |
| GET | `/api/v1/jobs/:job_id` | Yes | Get job status by ID |
| GET | `/api/v1/jobs` | Yes | List all jobs |
| GET | `/api/v1/rules/:rule/status` | Yes | Get rule processing status |
### Trigger Processing
```bash
curl -X POST http://localhost:3002/api/v1/assets/$UUID/process \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"rules": ["rule1"], "processors": ["asr", "yolo", "face"]}'
```
---
## Search APIs
### Vector Search
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/api/v1/search` | Yes | Vector/semantic search |
| POST | `/api/v1/search/hybrid` | Yes | Hybrid search (vector + BM25) |
| POST | `/api/v1/search/bm25` | Yes | BM25 text search |
### N8N Search (Library Functions)
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/api/v1/n8n/search` | Yes | N8N vector search |
| POST | `/api/v1/n8n/search/bm25` | Yes | N8N BM25 search |
| POST | `/api/v1/n8n/search/hybrid` | Yes | N8N hybrid search |
| POST | `/api/v1/n8n/search/smart` | Yes | N8N smart search |
### Search Request Body
```json
{
"query": "男女主角見面的場景",
"uuid": "optional-video-uuid",
"types": ["chunk", "frame", "person"],
"time_range": [0.0, 60.0],
"filters": {
"min_confidence": 0.8,
"required_object_classes": ["person"],
"speaker_id": "speaker_1"
},
"limit": 20,
"offset": 0
}
```
---
## Visual Chunk Search
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/api/v1/search/visual` | Yes | Visual chunk search |
| POST | `/api/v1/search/visual/class` | Yes | Search by object class |
| POST | `/api/v1/search/visual/density` | Yes | Search by spatial density |
| POST | `/api/v1/search/visual/stats` | Yes | Get visual statistics |
| POST | `/api/v1/search/visual/combination` | Yes | Search by object combination |
### Visual Search Request
```bash
curl -X POST http://localhost:3002/api/v1/search/visual \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"uuid": "abc123",
"criteria": {
"min_avg_confidence": 0.8,
"required_classes": ["person", "car"],
"min_spatial_density": 0.5
}
}'
```
### Search by Class
```bash
curl -X POST http://localhost:3002/api/v1/search/visual/class \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"uuid": "abc123",
"object_class": "person",
"min_count": 5,
"max_count": 20
}'
```
---
## Face Recognition
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/api/v1/face/recognize` | Yes | Recognize faces in video |
| POST | `/api/v1/face/register` | Yes | Register a face |
| POST | `/api/v1/face/search` | Yes | Search similar faces |
| GET | `/api/v1/face/list` | Yes | List all faces |
| GET | `/api/v1/face/:face_id` | Yes | Get face details |
| DELETE | `/api/v1/face/:face_id` | Yes | Delete a face |
| GET | `/api/v1/face/results/:file_uuid` | Yes | Get recognition results |
---
## Person Identity
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/api/v1/person/identify` | Yes | Identify persons in video |
| POST | `/api/v1/person/auto-identify` | Yes | Auto-identify persons |
| POST | `/api/v1/person/suggest` | Yes | Get person suggestions |
| GET | `/api/v1/person/list` | Yes | List all persons |
| GET | `/api/v1/person/:person_id` | Yes | Get person details |
| PATCH | `/api/v1/person/:person_id` | Yes | Update person identity |
| GET | `/api/v1/person/:person_id/timeline` | Yes | Get person timeline |
| GET | `/api/v1/person/:person_id/appearances` | Yes | Get person appearances |
| GET | `/api/v1/person/:person_id/thumbnail` | Yes | Get person thumbnail |
| POST | `/api/v1/person/merge` | Yes | Merge two persons |
| POST | `/api/v1/person/merge/undo` | Yes | Undo merge |
| GET | `/api/v1/person/merge/history` | Yes | Get merge history |
| POST | `/api/v1/person/:person_id/split` | Yes | Split a person |
| GET | `/api/v1/person/:person_id/similar` | Yes | Get similar persons |
| PATCH | `/api/v1/person/:person_id/confirm` | Yes | Confirm suggestion |
| POST | `/api/v1/person/:person_id/unbind-speaker` | Yes | Unbind speaker |
| POST | `/api/v1/person/:person_id/reassign-speaker` | Yes | Reassign speaker |
| POST | `/api/v1/person/:person_id/remove-appearance` | Yes | Remove appearance |
| POST | `/api/v1/person/:person_id/reassign-appearance` | Yes | Reassign appearance |
| POST | `/api/v1/person/:person_id/register` | Yes | Register identity |
| GET | `/api/v1/chunks/:chunk_id/persons` | Yes | Get chunk persons |
---
## Global Identities
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/api/v1/identities/from-person` | Yes | Register from person |
| GET | `/api/v1/identities` | Yes | List all identities |
| GET | `/api/v1/identities/:identity_id/videos` | Yes | Get identity videos |
| GET | `/api/v1/identities/:identity_id/faces` | Yes | Get identity faces |
| POST | `/api/v1/identities/search` | Yes | Search identities |
| GET | `/api/v1/videos/:uuid/faces` | Yes | Get video faces |
---
## Identity Binding
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/api/v1/identities/bind` | Yes | Bind identity |
| POST | `/api/v1/identities/unbind` | Yes | Unbind identity |
| GET | `/api/v1/identity/:binding_type/:binding_value` | Yes | Get identity info |
| GET | `/api/v1/signals/unbound` | Yes | List unbound signals |
| GET | `/api/v1/signals/:uuid/:binding_type/:binding_value/timeline` | Yes | Get signal timeline |
| POST | `/api/v1/identities/suggest-av` | Yes | Suggest AV bindings |
---
## Configuration
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | `/api/v1/config/cache` | Yes | Toggle cache |
### Toggle Cache
```bash
curl -X POST http://localhost:3002/api/v1/config/cache \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": false}'
```
---
## Authentication
All endpoints except `/health` and `/health/detailed` require an API key:
```bash
export API_KEY="muser_xxx"
curl -H "X-API-Key: $API_KEY" http://localhost:3002/api/v1/videos
```
---
## ⚠️ Notable Notes
1. **Universal Search routes** (`/api/v1/search/universal`, `/api/v1/search/frames`, `/api/v1/search/persons`) are defined in `universal_search.rs` but **NOT MOUNTED** in `server.rs`.
2. **Who routes** (`/api/v1/who`, `/api/v1/who/candidates`) are defined in `who.rs` but **NOT MOUNTED** in `server.rs`.
3. **Total endpoints**: 71 reachable + 6 unreachable = 77 defined.
---
## 📁 Related Documents
| Document | Location |
|----------|----------|
| API Examples | `IMPLEMENTATION/API_EXAMPLES.md` |
| cURL Examples | `IMPLEMENTATION/API_CURL_EXAMPLES.md` |
| WordPress Guide | `IMPLEMENTATION/API_WORDPRESS_GUIDE.md` |
| n8n Guide | `IMPLEMENTATION/API_N8N_GUIDE.md` |
| API Key Design | `REFERENCE/API_KEY_DESIGN.md` |
| API Key Architecture | `ARCHITECTURE/API_KEY_ARCHITECTURE.md` |

View File

@@ -0,0 +1,427 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Momentry Core API 教育訓練手冊"
date: "2026-04-27"
version: "V1.5"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "momentry"
- "core"
- "教育訓練手冊"
- "processing_status"
ai_query_hints:
- "查詢 Momentry Core API 教育訓練手冊 的內容"
- "Momentry Core API 教育訓練手冊 的主要目的是什麼?"
- "如何操作或實施 Momentry Core API 教育訓練手冊?"
- "processing_status 字段說明"
---
# Momentry Core API 教育訓練手冊
> **對象**: marcom 團隊
> **版本**: V1.5 | **日期**: 2026-04-27
---
## 1. 快速開始
### 基本資訊
| 項目 | 值 |
|------|-----|
| API 網址 | `https://api.momentry.ddns.net` |
| 認證方式 | Header `X-API-Key` |
| 格式 | JSON |
### Demo 測試帳號
#### API Key用於 API 認證)
```
X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69
```
#### SFTPGo用於影片上傳
| 項目 | 值 |
|------|-----|
| SFTP 主機 | `sftpgo.momentry.ddns.net` |
| SFTP 連接埠 | `2022` |
| 用戶名 | `demo` |
| 密碼 | `demopassword123` |
| Web 管理介面 | `https://sftpgo.momentry.ddns.net` |
**使用方式**:透過 SFTP 上傳影片,系統會自動註冊並處理。
---
## 2. 快速範例
### 查詢所有影片
```bash
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"https://api.momentry.ddns.net/api/v1/videos"
```
### 查詢單一影片
```bash
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"https://api.momentry.ddns.net/api/v1/videos/{uuid}/details"
```
### 查詢處理任務狀態
```bash
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"https://api.momentry.ddns.net/api/v1/jobs/{job_id}"
```
---
## 3. API 端點說明
### 3.1 影片相關
#### GET /api/v1/videos
取得所有影片列表
**回應範例**:
```json
{
"videos": [
{
"uuid": "5dea6618a606e7c7",
"filename": "demo_video.mp4",
"duration": 123.45,
"status": "ready",
"created_at": "2026-03-25T10:00:00Z"
}
]
}
```
#### GET /api/v1/videos/:uuid/details
取得單一影片詳情(包含 chunks、processing status 等)
### 3.2 搜尋與分段查詢
#### POST /api/v1/search
向量搜尋查詢分段Chunk詳情
**請求參數**:
| 參數 | 類型 | 必填 | 說明 |
|------|------|------|------|
| `query` | string | 是 | 搜尋關鍵字 |
| `limit` | number | 否 | 回傳數量(預設 10 |
| `uuid` | string | 否 | 只搜尋指定影片 |
**請求範例**:
```json
{
"query": "天氣",
"limit": 10,
"uuid": "5dea6618a606e7c7"
}
```
**回應範例**:
```json
{
"results": [
{
"uuid": "39567a0eb16f39fd",
"chunk_id": "sentence_1471",
"chunk_type": "sentence",
"start_time": 5309.08,
"end_time": 5311.08,
"text": "influenced by a vital way,",
"score": 0.68
}
],
"query": "天氣"
}
```
**Chunk 欄位說明**:
| 欄位 | 說明 | 範例 |
|------|------|------|
| `uuid` | 影片唯一識別碼 | `39567a0eb16f39fd` |
| `chunk_id` | 分段識別碼 | `sentence_1471` |
| `chunk_type` | 分段類型 | `sentence` / `cut` / `time` / `trace` / `story` |
| `start_time` | 開始時間(秒) | `5309.08` |
| `end_time` | 結束時間(秒) | `5311.08` |
| `text` | 內容文字 | `influenced by a vital way` |
| `score` | 相似度分數0-1 | `0.68` |
**Chunk 類型說明**:
| 類型 | 說明 | 來源 |
|------|------|------|
| `sentence` | 語音轉文字片段 | ASR 處理 |
| `cut` | 場景變化片段 | CUT 處理 |
| `time` | 固定時間分段 | 系統自動切割 |
| `trace` | 軌跡追蹤片段 | YOLO 追蹤 |
| `story` | 故事線片段(父子關係) | Story 分析 |
#### POST /api/v1/n8n/search
n8n 專用搜尋(包含完整影片檔案路徑 file_path
**請求參數**: 與 `/search` 相同
**回應範例**:
```json
{
"query": "天氣",
"count": 2,
"hits": [
{
"id": "sentence_1471",
"vid": "39567a0eb16f39fd",
"start": 5309.08,
"end": 5311.08,
"title": "Chunk sentence_1471",
"text": "influenced by a vital way,",
"score": 0.68,
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/video.mp4"
}
]
}
```
**與 /search 的差異**:
| 欄位 | `/search` | `/n8n/search` |
|------|-----------|----------------|
| 影片 UUID | `uuid` | `vid` |
| Chunk ID | `chunk_id` | `id` |
| 開始時間 | `start_time` | `start` |
| 結束時間 | `end_time` | `end` |
| 相似度分數 | `score` | `score` |
| **檔案路徑** | ❌ | ✅ `file_path` |
> **注意**: `file_path` 是影片的實際路徑,可用於本地播放。
### 3.3 任務相關
### 3.4 任務相關
#### GET /api/v1/jobs/:uuid
查詢處理任務狀態
**回應範例**:
```json
{
"uuid": "9760d0820f0cf9a7",
"file_uuid": "5dea6618a606e7c7",
"status": "completed",
"progress": 100,
"created_at": "2026-03-25T10:00:00Z",
"completed_at": "2026-03-25T10:05:00Z"
}
```
#### GET /api/v1/jobs
查詢所有任務
**查詢參數**:
| 參數 | 說明 | 範例 |
|------|------|------|
| `status` | 篩選狀態 | `pending`, `processing`, `completed`, `failed` |
| `limit` | 回傳數量 | `10` |
**範例**:
```bash
curl -s -H "X-API-Key: ..." \
"https://api.momentry.ddns.net/api/v1/jobs?status=completed&limit=5"
```
### 3.5 系統管理
#### POST /api/v1/config/cache
切換快取功能(管理員專用)
**請求範例**:
```json
{
"enabled": true
}
```
**回應範例**:
```json
{
"cache_enabled": true,
"message": "Cache toggled successfully"
}
```
#### POST /api/v1/unregister
刪除影片及其所有關聯資料(管理員專用)
**請求範例**:
```json
{
"uuid": "5dea6618a606e7c7"
}
```
**回應範例**:
```json
{
"success": true,
"message": "Video unregistered successfully",
"uuid": "5dea6618a606e7c7"
}
```
**注意**: 此操作會刪除影片及其所有分段、處理結果、縮圖等關聯資料,**無法復原**。
### 3.6 健康檢查
#### GET /health
服務健康狀態(**無需認證**
**回應**:
```json
{
"status": "ok",
"version": "0.9.20260325_144654"
}
```
---
## 4. n8n Workflow 範例
### 4.1 基本設定
在 n8n workflow 中使用 HTTP Request 節點:
```
┌─────────────────┐
│ HTTP Request │
├─────────────────┤
│ Method: GET │
│ URL: https://api.momentry.ddns.net/api/v1/videos
│ Headers: │
│ X-API-Key: │
│ [YOUR_KEY] │
└────────┬────────┘
┌─────────────────┐
│ 處理回應資料 │
└─────────────────┘
```
### 4.2 範例:檢查任務狀態
```javascript
// n8n Function Node 範例
const jobUuid = $input.item.json.uuid;
return [{
json: {
method: "GET",
url: `https://api.momentry.ddns.net/api/v1/jobs/${jobUuid}`,
headers: {
"X-API-Key": "YOUR_API_KEY"
}
}
}];
```
---
## 5. 常見問題
### Q: 返回 401 錯誤怎麼辦?
確認 Header 中有正確的 `X-API-Key`
### Q: 如何確認影片處理完成?
```
GET /api/v1/jobs/{uuid}
```
檢查 `status` 是否為 `completed`
### Q: 查不到資料?
確認 UUID 格式正確16碼 hex 字串)
---
## 6. 快速參考卡
```
┌─────────────────────────────────────────────────────────────┐
│ Momentry API 速查 │
├─────────────────────────────────────────────────────────────┤
│ 查詢所有影片 GET /api/v1/videos │
│ 查詢單一影片 GET /api/v1/videos/:uuid │
│ 向量搜尋 POST /api/v1/search │
│ n8n 搜尋 POST /api/v1/n8n/search │
│ 查詢任務狀態 GET /api/v1/jobs/:uuid │
│ 查詢所有任務 GET /api/v1/jobs │
│ 快取設定 POST /api/v1/config/cache (管理員) │
│ 刪除影片 POST /api/v1/unregister (管理員) │
│ 健康檢查 GET /health (免認證) │
├─────────────────────────────────────────────────────────────┤
│ Header: X-API-Key: [YOUR_KEY] │
│ URL: https://api.momentry.ddns.net │
└─────────────────────────────────────────────────────────────┘
```
---
## 附錄:回應狀態說明
### 任務狀態 (status)
| 狀態 | 說明 |
|------|------|
| `pending` | 等待處理 |
| `processing` | 處理中 |
| `completed` | 已完成 |
| `failed` | 處理失敗 |
### 影片狀態 (status)
| 狀態 | 說明 |
|------|------|
| `pending` | 等待處理 |
| `processing` | 處理中 |
| `completed` | 已完成 |
| `failed` | 處理失敗 |
### 影片詳細狀態 (processing_status)
| 狀態 | 說明 | Portal 顯示 |
|------|------|-------------|
| `REGISTERED` | 已註冊 | 藍色「已註冊」 |
| `PENDING` | 等待處理 | 黃色「等待處理」 |
| `PROBING` | 探測中 | 紫色「分析中」 |
| `ASR` | 語音識別中 | 靛藍「語音識別」 |
| `OCR` | 文字識別中 | 靛藍「文字識別」 |
| `YOLO` | 物體檢測中 | 靛藍「物體檢測」 |
| `FACE` | 人臉檢測中 | 靛藍「人臉檢測」 |
| `POSE` | 姿態檢測中 | 靛藍「姿態檢測」 |
| `CUT` | 鏡頭分析中 | 靛藍「鏡頭分析」 |
| `COMPLETED` | 完成 | 綠色「已完成」 |
| `FAILED` | 失敗 | 紅色「處理失敗」 |
**說明**Portal 顯示優先使用 `processing_status`詳細狀態Fallback 使用 `status`(基本狀態)。
---
## 附錄:版本歷史
| 版本 | 日期 | 內容 | 操作人 |
|------|------|------|--------|
| V1.0 | 2026-03-25 | 初版建立 | OpenCode |
| V1.1 | 2026-03-25 | 新增快取/刪除 API、搜尋端點文件 | OpenCode |
| V1.2 | 2026-03-25 | 新增 Chunk 欄位說明、類型、播放方式 | OpenCode |
| V1.3 | 2026-03-25 | 新增 Demo 測試帳號SFTPGo| OpenCode |
| V1.4 | 2026-03-25 | 更新 n8n 搜尋回傳欄位說明 (media_url→file_path) | OpenCode |
| V1.5 | 2026-04-27 | 新增 processing_status 字段說明,移除 'ready' 狀態 | OpenCode |

View File

@@ -0,0 +1,159 @@
# Demo Runner System v1.0.0
## 概述
`scripts/demo_runner.py` — 自動播放展示系統。讀取 JSON 腳本,依序執行各類型步驟,展示 Momentry Core API。
## 安裝
```bash
# 相依性Python 3.11+, macOS `say` 指令(語音)
# md_reader選擇性提供更好的 Markdown 預覽)
cd ~/md_reader && cargo build --release
```
## 執行方式
```bash
cd ~/momentry_core_0.1
# 逐步互動模式
python3.11 scripts/demo_runner.py docs_v1.0/API_V1.0.0/DEMO_SCRIPT_v1.0.0.json
# 自動播放 + 中文語音
python3.11 scripts/demo_runner.py docs_v1.0/API_V1.0.0/DEMO_SCRIPT_v1.0.0.json --auto --voice zh_TW
# 指定起始步驟、快放
python3.11 scripts/demo_runner.py demo.json --step 5 --speed 3
# 英文語音
python3.11 scripts/demo_runner.py demo.json --voice en_US
```
## 步驟類型
| type | 功能 | 必要欄位 |
|------|------|---------|
| `curl` | 執行 API 命令並顯示 JSON 回應 | `cmd` |
| `browser` | 在瀏覽器中開啟 URL | `url` |
| `markdown` | 用 md_reader Preview 渲染 .md 文件(含 Mermaid | `cmd`(檔案路徑) |
| `note` | 純文字解說 | `note` |
| `separator` | 章節分隔線 | `label` |
## JSON 腳本結構
```json
{
"title": "展示名稱",
"language": "zh_TW",
"steps": [
{
"type": "curl",
"label": "步驟標題",
"note": "解說文字(語音會朗讀此段)",
"cmd": "curl -s $BASE/api/v1/health",
"expect": "ok"
},
{
"type": "browser",
"label": "開啟頁面",
"note": "說明文字",
"url": "$BASE/api/v1/file/$FILE/trace/5/video?padding=1"
},
{
"type": "markdown",
"label": "文件展示",
"note": "說明文字",
"cmd": "docs_v1.0/API_V1.0.0/API_USAGE_GUIDE_V1.0.0.md",
"focus": "自動聚焦的章節名稱"
}
]
}
```
## 變數
| 變數 | 預設值 | 說明 |
|------|--------|------|
| `$BASE` | `https://api.momentry.ddns.net` | API 伺服器 |
| `$KEY` | `muser_68600856036340...` | API Key |
| `$FILE` | `3abeee81...` | Charade file UUID |
環境變數覆蓋:`DEMO_KEY`, `DEMO_BASE`, `DEMO_FILE`, `DEMO_VOICE`
## 語音功能
## 語音朗讀
- 支援語言:`zh_TW`Meijia`zh_CN`Ting-Ting`en_US`Samantha`ja_JP`Kyoko`ko_KR`Yuna`fr_FR`Amelie
- macOS 內建 `say` 指令,零外部依賴
- **單軌**:每次朗讀完整結束才播放下一個(`subprocess.Popen` + `wait` 阻塞模式)
- **無重疊**:前一句完整發音後才開始下一句
## 語音指令(--voice-control
啟用麥克風語音控制,可用說的操作展示流程:
```bash
python3 scripts/demo_runner.py demo.json --voice zh_TW --voice-control
```
| 指令(中文) | 指令English | 功能 |
|:-----------:|:---------------:|------|
| "下一個" / "繼續" | "next" / "continue" | 前進到下一步 |
| "停止" | "stop" / "quit" | 結束展示 |
| "重複" | "repeat" / "again" | 重複朗讀當前解說 |
| "跳到第 5 步" | "go to 5" | 跳到指定步驟 |
語音辨識使用 Google Speech Recognition需網路背景執行不影響主流程。
## 展示節奏
- 開場倒數 3-2-1
- 語音解說後暫停 1.5 秒
- curl 回應依長度自動決定閱讀時間1.56 秒)
- Browser/markdown 步驟停留 5 秒
- 章節分隔停留 1.5 秒
## 自動聚焦Markdown 步驟)
`focus` 參數讓 md_reader Preview 視窗自動捲到指定章節:
```json
{
"type": "markdown",
"cmd": "docs/API_USAGE_GUIDE.md",
"focus": "搜尋三模式"
}
```
效果:平滑捲動至該標題 → 金色高亮 3 秒後淡出。
## md_reader Preview 視窗功能
| 功能 | 操作 |
|------|------|
| 平移Pan | 工具列 Pan 按鈕 → 滑鼠拖曳 |
| 縮放 | 工具列 / + / Reset |
| 快捷指令 | 按 `/` 輸入 `/zoom 150` |
| Mermaid 圖表 | 自動渲染,可下載 SVG |
| 列印/PDF | 工具列 Print 按鈕 |
| 指令列表 | `/help` |
## 依賴項目
| 元件 | 用途 | 授權 |
|------|------|:----:|
| Python 3.11 | 執行環境 | PSF |
| macOS `say` | 語音合成 | macOS 內建 |
| `md_reader`(選擇性)| Markdown → HTML 含 Mermaid | MIT |
| curl | API 命令執行 | macOS 內建 |
| webbrowserPython| 開啟瀏覽器 | Python 內建 |
## 檔案
| 檔案 | 說明 |
|------|------|
| `scripts/demo_runner.py` | 執行器主程式 |
| `docs_v1.0/API_V1.0.0/DEMO_SCRIPT_v1.0.0.json` | 21 步驟預設展示腳本 |
| `~/_md_reader/target/release/md_reader` | Markdown 渲染工具 |

View File

@@ -0,0 +1,864 @@
---
document_type: "demo_guide"
service: "MOMENTRY_CORE"
title: "Pipeline Demo End-to-End"
date: "2026-05-15"
version: "V1.0"
status: "active"
owner: "M5"
created_by: "OpenCode"
tags:
- "demo"
- "pipeline"
- "end-to-end"
- "api"
ai_query_hints:
- "如何執行端到端 Pipeline demo"
- "Pipeline 處理流程"
- "註冊影片並觸發處理的完整流程"
related_documents:
- "GUIDES/API_ENDPOINTS.md"
- "GUIDES/Pipeline_API_Demo.md"
---
# Momentry Core — Pipeline Demo End-to-End
| 項目 | 內容 |
|------|------|
| 建立者 | OpenCode |
| 建立時間 | 2026-05-15 |
| 文件版本 | V1.0 |
| 目標讀者 | developer |
| 預備知識 | 需有 API Key、Pipeline 基本概念 |
---
## Table of Contents
### Pipeline Phases
| Phase | Step | What happens |
|-------|------|-------------|
| **Pre** | 14 | System check, scan, register, probe |
| **處理中** | 56 | Submit job → Worker picks up → Each processor runs (pending→running→completed) |
| **處理後** | 79 | All results → Search → Identities → Schema verification |
---
## 1. 檢查系統狀況
```bash
API="http://api.momentry.ddns.net"
KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
# Basic health
curl -sf "$API/health" | jq '{status, version, build_git_hash, uptime_ms}'
# Detailed health
curl -sf "$API/health/detailed" | jq '{
services,
schema: .schema.ok,
scripts: .pipeline.scripts_count,
integrity: .pipeline.scripts_integrity,
procs: [.pipeline.processors | to_entries[] | select(.value == true and .key != "total_py_files") | .key]
}'
```
Output:
```json
{
"status": "ok",
"version": "1.0.0",
"build_git_hash": "c41f7e0c",
"uptime_ms": 2756192
}
{
"services": {"postgres": "ok", "redis": "ok", "qdrant": "ok"},
"schema": false,
"scripts": 291,
"integrity": {"matched": 332, "total": 345, "ok": false},
"procs": ["asr","yolo","face","pose","ocr","cut","caption","scene","story","asrx","probe","visual_chunk"]
}
```
---
## 2. 掃描檔案
掃描伺服器上所有與 `exasan` 相關的檔案(支援規則表達式):
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/files/scan?pattern=exasan" | \
jq '[.files[] | {uuid: .file_uuid, name: .file_name, size: .file_size}]'
```
輸出(節錄):
```json
[
{"uuid": "dd61fda85fee441f...", "name": "ExaSAN PCIe series - Director Ou Yu-Zhi Shares His Experience.mp4", "size": 6827600},
{"uuid": "8e2e98c49355935f...", "name": "ExaSAN Webinar by Blake Jones, Vision2see.mp4", "size": 38635889},
{"uuid": "477d8fa7bc0e1a7...", "name": "Thunderbolt ExaSAN at CCBN.mp4", "size": 13126748}
]
```
**Note**: `files/scan` 也可以掃所有檔案,或用於批次註冊。若不指定 pattern回傳伺服器 `sftpgo/data/demo/` 目錄下所有檔案。
---
## 3. 註冊或確認
若檔案尚未註冊,使用 register API。若已存在如本次示範直接確認狀態
```bash
UUID="dd61fda85fee441fdd00ab5528213ff7"
# 確認檔案狀態
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}" | jq '{uuid: .file_uuid[0:16], name: .file_name, status, duration, fps}'
# 若檔案不存在,使用註冊 API
# curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
# -d '{"file_path": "/path/to/video.mp4"}' \
# "$API/api/v1/files/register" | jq '.'
```
**註冊流程**
```
POST /files/register
├─ SHA256 content_hash (dedup 檢查)
├─ file_name 衝突檢查 (自動 rename)
├─ Pre-process (SHA256 + ffprobe + UUID → .pre.json)
├─ UUID = f(mac, mtime, path, filename)
├─ Unified probe (video→ffprobe, doc→Python)
└─ INSERT INTO videos
```
---
## 4. Probe 確認
The probe endpoint returns ffprobe metadata about the registered file.
```bash
# Substitute the actual file_uuid from step 3
FILE_UUID="e1111111111111111111111111111111"
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"http://api.momentry.ddns.net/api/v1/file/${FILE_UUID}/probe" | python3 -m json.tool
```
Output (abbreviated):
```json
{
"file_uuid": "e1111111111111111111111111111111",
"file_name": "demo_test_video.mp4",
"duration": 5.005,
"width": 640,
"height": 480,
"fps": 24.0,
"total_frames": 120,
"cached": true,
"format": {
"filename": "/tmp/demo_test_video.mp4",
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"duration": "5.005000",
"size": "98304",
"bit_rate": "157184"
},
"streams": [
{"index": 0, "codec_type": "video", "codec_name": "h264", "width": 640, "height": 480, ...},
{"index": 1, "codec_type": "audio", "codec_name": "aac", ...}
]
}
```
**Error handling** (Bug #3 fix):
- Non-existent UUID → `{"error":"Video not found"}` + HTTP 404
- File deleted from disk → `{"error":"File does not exist at registered path"}` + HTTP 404
- ffprobe failure → `{"error":"ffprobe failed: ..."}` + HTTP 500
### ⚡ Intermediate Check — Bug #3: Probe Error Verification
Test both error cases return proper JSON + HTTP code instead of bare 500:
```bash
echo "=== Non-existent UUID → expect 404 ==="
curl -s -w "\nHTTP: %{http_code}\n" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"http://api.momentry.ddns.net/api/v1/file/bad_uuid_12345/probe"
# Expect: {"error":"Video not found","file_uuid":"bad_uuid_12345"} HTTP 404
echo ""
echo "=== Non-existent file path → expect 404 ==="
# Temporarily change file_path to a non-existent location
"$PG_BIN/psql" -U accusys -d momentry -c \
"UPDATE dev.videos SET file_path = '/tmp/NONEXISTENT_FILE' WHERE file_uuid = '${FILE_UUID}'"
curl -s -w "\nHTTP: %{http_code}\n" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"http://api.momentry.ddns.net/api/v1/file/${FILE_UUID}/probe"
# Expect: {"error":"File does not exist at registered path",...} HTTP 404
# Restore path
"$PG_BIN/psql" -U accusys -d momentry -c \
"UPDATE dev.videos SET file_path = '/tmp/demo_test_video.mp4' WHERE file_uuid = '${FILE_UUID}'"
```
Output:
```
=== Non-existent UUID → expect 404 ===
{"error":"Video not found","file_uuid":"bad_uuid_12345"}
HTTP: 404
=== Non-existent file path → expect 404 ===
{"error":"File does not exist at registered path","file_uuid":"e1111111111111111111111111111111","file_path":"/tmp/NONEXISTENT_FILE"}
HTTP: 404
```
---
## 5. Process Video
Trigger pipeline processing for specific processors. The available processors are:
| Processor | Function | Script |
|-----------|----------|--------|
| `asr` | Speech-to-text (faster-whisper) | `asr_processor.py` |
| `cut` | Scene detection (PySceneDetect) | `cut_processor.py` |
| `yolo` | Object detection (YOLOv8) | `yolo_processor.py` |
| `face` | Face detection (InsightFace) | `face_processor.py` |
| `pose` | Pose estimation (MediaPipe) | `pose_processor.py` |
| `ocr` | Text detection (PaddleOCR) | `ocr_processor.py` |
| `asrx` | Speaker diarization | `asrx_processor.py` |
| `visual_chunk` | Visual content analysis | `visual_chunk_processor.py` |
| `scene` | Scene classification | `scene_classifier.py` |
| `story` | Story generation (LLM) | `story_processor.py` |
| `caption` | Caption generation | `caption_processor.py` |
```bash
# Trigger only ASR + CUT for quick test
curl -s -X POST "http://api.momentry.ddns.net/api/v1/file/${FILE_UUID}/process" \
-H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
-H "Content-Type: application/json" \
-d '{"processors": ["asr", "cut"]}' | python3 -m json.tool
```
Output:
```json
{
"job_id": 161,
"file_uuid": "e1111111111111111111111111111111",
"status": "PENDING",
"pids": [],
"message": "Processing triggered for demo_test_video.mp4"
}
```
**Processing flow**:
```
POST /process → trigger_processing()
├─ Validate file exists (DB lookup)
├─ Create monitor_job (status: PENDING)
├─ Create processor_result rows for each requested processor (status: pending)
└─ Response { job_id, status: "PENDING" }
```
**Note**: If no processors are specified, all processors are used:
```json
{"processors": ["asr", "cut", "yolo", "ocr", "face", "pose", "asrx", "visual_chunk"]}
```
### ⚡ Intermediate Check — Verify Job + Processor Results after Trigger
```bash
PG_BIN="/Users/accusys/pgsql/18.3/bin"
# Check monitor_jobs table
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT id, uuid, status, current_processor,
to_char(created_at, 'HH24:MI:SS') AS created
FROM dev.monitor_jobs
WHERE uuid = '${FILE_UUID}'
ORDER BY id DESC LIMIT 1
\gx
"
# Check processor_results table
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT id, processor, status
FROM dev.processor_results
WHERE file_uuid = '${FILE_UUID}'
ORDER BY id
"
```
Output:
```
-[ RECORD 1 ]------+-----------------------------
id | 161
uuid | e1111111111111111111111111111111
status | PENDING
current_processor | (null)
created | 19:00:30
id | processor | status
----+-----------+---------
1 | asr | pending
2 | cut | pending
```
**Checklist after trigger:**
- [ ] `monitor_jobs.status = 'PENDING'` — job created, awaiting worker
- [ ] `processor_results` rows match requested processors (2 rows for `asr`, `cut`)
- [ ] Each `processor.status = 'pending'` — not yet executed
---
## 6. Worker Execution
The worker polls for pending jobs and executes them one by one.
```bash
DATABASE_SCHEMA=dev cargo run --bin momentry_playground -- worker \
--max-concurrent 2 --poll-interval 5
```
Or in background:
```bash
DATABASE_SCHEMA=dev nohup target/debug/momentry_playground worker \
--max-concurrent 2 --poll-interval 5 > /tmp/worker_demo.log 2>&1 &
```
**Worker flow**:
```
Worker loop (every 5 seconds):
├─ Poll: SELECT * FROM monitor_jobs WHERE status = 'PENDING'
├─ Set job status → RUNNING
├─ For each pending processor:
│ ├─ SHA256 integrity check (verify_script_integrity)
│ │ └─ checksums.sha256 manifest lookup
│ ├─ Execute script via PythonExecutor
│ │ └─ Command: {MOMENTRY_PYTHON_PATH} scripts/<processor>.py <args>
│ ├─ Verify output (file exists, content valid)
│ └─ Update processor_result (completed/failed)
├─ Check completion: all processors done?
├─ Yes → Set job + video status → COMPLETED
└─ No → Wait for next poll cycle
```
**Worker log output**:
```
[CHECKSUMS] Loaded 345 entries from checksums.sha256
[INTEGRITY] asr_processor.py checksum OK
[ASR] Starting asr_processor.py
[INTEGRITY] cut_processor.py checksum OK
[CUT] Starting cut_processor.py
[ASR] Completed successfully
[CUT] Completed successfully
check_and_complete_job: results=2/2 → Job COMPLETED
```
### ⚡ Intermediate Check — Poll Progress During Worker Execution
While the worker is running, poll the progress endpoint to watch state transitions:
```bash
# Poll every 5 seconds until completed
FILE_UUID="e1111111111111111111111111111111"
for i in $(seq 1 12); do
sleep 5
STATUS=$(curl -sf -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"http://api.momentry.ddns.net/api/v1/progress/${FILE_UUID}" \
| python3 -c "import json,sys;d=json.load(sys.stdin);print(d.get('status','?'))" 2>/dev/null || echo "pending")
echo "Poll $i: status=$STATUS"
[ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ] && break
done
```
Output (typical):
```
Poll 1: status=registered ← worker hasn't picked it up yet
Poll 2: status=pending ← worker picked up, job status changed
Poll 3: status=processing ← worker running ASR
Poll 4: status=processing ← worker running CUT
Poll 5: status=completed ← all done
```
Check status transitions in DB:
```bash
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT id, processor, status,
to_char(started_at, 'HH24:MI:SS') AS started,
to_char(completed_at, 'HH24:MI:SS') AS completed
FROM dev.processor_results
WHERE file_uuid = '${FILE_UUID}'
ORDER BY id
"
```
Output:
```
id | processor | status | started | completed
----+-----------+------------+-----------+-----------
1 | asr | completed | 19:01:02 | 19:01:25
2 | cut | completed | 19:01:02 | 19:01:08
```
### ⚡ Processing Checklist — Step-by-Step Verification
This checklist covers every stage of the pipeline processing flow:
```bash
# ──────────────────────────────────────────────────────
# Stage A: Before Worker Starts
# ──────────────────────────────────────────────────────
PG_BIN="/Users/accusys/pgsql/18.3/bin"
FILE_UUID="e1111111111111111111111111111111"
KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
echo "=== A1. Job status = PENDING ==="
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT id, status, current_processor, created_at FROM dev.monitor_jobs WHERE uuid = '${FILE_UUID}'
"
echo "=== A2. Processor results = pending ==="
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT id, processor, status FROM dev.processor_results WHERE file_uuid = '${FILE_UUID}' ORDER BY id
"
# ──────────────────────────────────────────────────────
# Stage B: Worker Running
# ──────────────────────────────────────────────────────
echo "=== Start worker ==="
DATABASE_SCHEMA=dev nohup target/debug/momentry_playground worker \
--max-concurrent 1 --poll-interval 3 > /tmp/worker_check.log 2>&1 &
WPID=$!
echo "=== B1. Worker picks up job (within 3-10s) ==="
for i in $(seq 1 10); do
sleep 3
JOB_STATUS=$("$PG_BIN/psql" -U accusys -d momentry -t -A -c \
"SELECT status FROM dev.monitor_jobs WHERE uuid = '${FILE_UUID}'" 2>/dev/null)
VIDEO_STATUS=$("$PG_BIN/psql" -U accusys -d momentry -t -A -c \
"SELECT status FROM dev.videos WHERE file_uuid = '${FILE_UUID}'" 2>/dev/null)
echo " Poll $i: job=$JOB_STATUS video=$VIDEO_STATUS"
echo " $(grep '\[INTEGRITY\]\|\[SCHEMA\]\|Starting:\|Completed\|failed\|Job ' /tmp/worker_check.log 2>/dev/null | tail -3)"
# Check alive
kill -0 $WPID 2>/dev/null || { echo " Worker died unexpectedly"; break; }
if [ "$VIDEO_STATUS" = "completed" ] || [ "$VIDEO_STATUS" = "failed" ]; then break; fi
done
echo "=== B2. Each processor status ==="
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT id, processor, status,
to_char(started_at, 'HH24:MI:SS') AS started,
to_char(completed_at, 'HH24:MI:SS') AS completed,
COALESCE(chunks_produced, 0) AS chunks,
COALESCE(frames_processed, 0) AS frames,
COALESCE(error_message, '') AS error
FROM dev.processor_results
WHERE file_uuid = '${FILE_UUID}'
ORDER BY id
"
kill $WPID 2>/dev/null || true
# ──────────────────────────────────────────────────────
# Stage C: After Completion
# ──────────────────────────────────────────────────────
echo "=== C1. Video final status ==="
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT file_uuid, file_name, status, duration, fps, total_frames FROM dev.videos WHERE file_uuid = '${FILE_UUID}'
"
echo "=== C2. Chunks produced ==="
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT chunk_type, count(*) FROM dev.chunk WHERE file_uuid = '${FILE_UUID}' GROUP BY chunk_type ORDER BY chunk_type
"
echo "=== C3. Job final status ==="
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT id, status, current_processor FROM dev.monitor_jobs WHERE uuid = '${FILE_UUID}'
"
```
Expected output (all green):
```
=== A1. Job status = PENDING ===
id | status | current_processor | created_at
----+---------+-------------------+-------------------
161| PENDING | | 2026-05-15 19:00:30
=== A2. Processor results = pending ===
id | processor | status
----+-----------+---------
1 | asr | pending
2 | cut | pending
=== Start worker ===
=== B1. Worker picks up job (within 3-10s) ===
Poll 1: job=PENDING video=registered
Poll 2: job=RUNNING video=processing
[INTEGRITY] asr_processor.py checksum OK
Poll 3: job=RUNNING video=processing
[ASR] Starting: asr_processor.py
Poll 4: job=RUNNING video=processing
[ASR] Completed successfully
Poll 5: job=RUNNING video=processing
[CUT] Completed successfully
Poll 6: job=COMPLETED video=completed
=== B2. Each processor status ===
id | processor | status | started | completed | chunks | frames | error
----+-----------+-----------+-----------+-----------+--------+--------+-------
1 | asr | completed | 19:01:02 | 19:01:25 | 3 | 120 |
2 | cut | completed | 19:01:02 | 19:01:08 | 1 | 120 |
=== C1. Video final status ===
file_uuid | file_name | status | duration | fps | total_frames
--------------+---------------------+-----------+----------+-----+--------------
e11111111... | demo_test_video.mp4 | completed | 5.005 | 24 | 120
=== C2. Chunks produced ===
chunk_type | count
------------+-------
cut | 1
sentence | 3
=== C3. Job final status ===
id | status | current_processor
----+-----------+-------------------
161| COMPLETED | (null)
```
**Checklist during execution:**
| Stage | # | Check | Expected | Pass |
|-------|---|-------|----------|:----:|
| **A. Pre-worker** | A1 | `monitor_jobs.status` | `PENDING` | ☐ |
| | A2 | `processor_results` rows | = requested processor count | ☐ |
| | A3 | Each `processor_results.status` | `pending` | ☐ |
| **B. Running** | B1 | Job picked up (within poll interval) | status → `RUNNING` | ☐ |
| | B2 | SHA256 integrity check in logs | `[INTEGRITY] *.py checksum OK` | ☐ |
| | B3 | Each processor transitions | `pending → running → completed` | ☐ |
| | B4 | `started_at` populated | NOT NULL per processor | ☐ |
| | B5 | Processors complete without error | `error_message` is NULL | ☐ |
| | B6 | Max concurrent respected | ≤ `--max-concurrent` running at once | ☐ |
| **C. Post-completion** | C1 | `videos.status` | `completed` (not `failed`) | ☐ |
| | C2 | `chunks_produced` > 0 | ASR has sentence chunks | ☐ |
| | C3 | `monitor_jobs.status` | `COMPLETED` | ☐ |
| | C4 | `chunk` table has data | rows with this `file_uuid` | ☐ |
| | C5 | Chunk IDs formatted correctly | `{uuid}_{start}_{end}` | ☐ |
---
## 7. Check Results
Monitor job progress:
```bash
# Check job status
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"http://api.momentry.ddns.net/api/v1/jobs?page=1&page_size=5&status=pending,running,completed,failed" \
| python3 -c "import json,sys;d=json.load(sys.stdin);[print(f'{j[\"uuid\"]}: {j[\"status\"]}') for j in d.get('jobs',[])]"
```
Output:
```
9eca53f422f668dd59a9995d29dc9388: completed
e1111111111111111111111111111111: completed
```
### ⚡ Intermediate Check — Bug #2: Chunk Fallback Verification
Verify that both new and old chunk_id formats resolve correctly:
```bash
# Pick a chunk_id from the DB
CHUNK_INFO=$("$PG_BIN/psql" -U accusys -d momentry -t -A -c "
SELECT chunk_id, id FROM dev.chunk WHERE file_uuid = '${FILE_UUID}' LIMIT 1
")
NEW_ID=$(echo "$CHUNK_INFO" | cut -d'|' -f1)
DB_ID=$(echo "$CHUNK_INFO" | cut -d'|' -f2)
echo "=== New format: $NEW_ID ==="
curl -s -w " HTTP %{http_code}" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"http://api.momentry.ddns.net/api/v1/file/${FILE_UUID}/chunk/${NEW_ID}" \
| python3 -c "import json,sys;d=json.load(sys.stdin);print(f'chunk_id={d.get(\"chunk_id\")}')" 2>/dev/null
echo ""
echo "=== Old integer fallback (id=$DB_ID) ==="
curl -s -w " HTTP %{http_code}" -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"http://api.momentry.ddns.net/api/v1/file/${FILE_UUID}/chunk/${DB_ID}" \
| python3 -c "import json,sys;d=json.load(sys.stdin);print(f'chunk_id={d.get(\"chunk_id\")}')" 2>/dev/null
```
Output:
```
=== New format: e1111111111111111111111111111111_0_5 ===
chunk_id=e1111111111111111111111111111111_0_5 HTTP 200
=== Old integer fallback (id=1075655) ===
chunk_id=e1111111111111111111111111111111_0_5 HTTP 200
```
Both return `chunk_id=e1111111111111111111111111111111_0_5` — the fallback correctly resolves `id=1075655` to the same chunk.
### ⚡ Intermediate Check — Verify Chunks after Processing
```bash
PG_BIN="/Users/accusys/pgsql/18.3/bin"
# Count chunks produced
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT chunk_type, count(*) AS count
FROM dev.chunk
WHERE file_uuid = '${FILE_UUID}'
GROUP BY chunk_type
ORDER BY chunk_type
"
# Sample chunk content
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT chunk_id, chunk_type, start_frame, end_frame,
substring(text_content, 1, 60) AS text_preview
FROM dev.chunk
WHERE file_uuid = '${FILE_UUID}'
ORDER BY start_frame
LIMIT 5
"
```
Output:
```
chunk_type | count
------------+-------
cut | 1
sentence | 3
chunk_id | chunk_type | start_frame | end_frame | text_preview
--------------------------------------------------+------------+-------------+-----------+-----------------------------------------------------
e1111111111111111111111111111111_0_5 | cut | 0 | 120 | demo_test_video_auto_demo.mp4
e1111111111111111111111111111111_0_0 | sentence | 0 | 120 | test pattern test pattern color bars test pattern ...
```
Check per-processor results in DB:
```bash
"$PG_BIN/psql" -U accusys -d momentry -c "
SELECT processor, status, error_message,
to_char(started_at, 'HH24:MI:SS') AS started,
to_char(completed_at, 'HH24:MI:SS') AS completed,
COALESCE(chunks_produced, 0) AS chunks
FROM dev.processor_results
WHERE file_uuid='${FILE_UUID}'
ORDER BY id;
"
```
Output:
```
processor | status | error_message | started | completed | chunks
-----------+-----------+---------------+-----------+-----------+--------
asr | completed | | 19:01:02 | 19:01:25 | 3
cut | completed | | 19:01:02 | 19:01:08 | 1
```
**Checklist after processing:**
- [ ] `video.status = 'completed'` — pipeline finished
- [ ] `processor_results` all show `status = 'completed'`
- [ ] `chunks_produced > 0` — each processor produced output
- [ ] `chunk` table has rows with correct chunk_type (`cut`, `sentence`)
- [ ] `chunk_id` format is `{file_uuid}_{start}_{end}` (Bug #2 fix verified)
---
## 8. Search Chunks
After processing, search the generated chunks:
```bash
# Text search (ASR output)
curl -s -X POST "http://api.momentry.ddns.net/api/v1/search/universal" \
-H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
-H "Content-Type: application/json" \
-d "{\"query\": \"test\", \"uuid\": \"${FILE_UUID}\", \"limit\": 5}" \
| python3 -c "
import json,sys;d=json.load(sys.stdin)
print(f'Total hits: {d[\"total\"]}')
for r in d['results']:
if r.get('chunk_id'):
print(f' {r[\"chunk_id\"]}: \"{r.get(\"text\",\"\")[:60]}\" score={r.get(\"score\",0):.3f}')
"
```
Output:
```
Total hits: 3
e1111111111111111111111111111111_0_5: "test pattern test pattern..." score=0.423
e1111111111111111111111111111111_5_10: "silence" score=0.215
```
Get a specific chunk by ID:
```bash
# Single chunk detail
curl -s -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
"http://api.momentry.ddns.net/api/v1/file/${FILE_UUID}/chunk/${FILE_UUID}_0_5" \
| python3 -c "
import json,sys;d=json.load(sys.stdin)
print(f'Type: {d[\"chunk_type\"]} Rule: {d[\"rule\"]}')
print(f'Frame: {d[\"start_frame\"]}{d[\"end_frame\"]} FPS: {d[\"fps\"]}')
print(f'Text: {d[\"text_content\"][:100]}')
"
```
---
## 9. Health Check
```bash
# Basic health
curl -sf http://api.momentry.ddns.net/health | python3 -m json.tool
# Detailed health (services + pipeline + schema + resources)
curl -sf http://api.momentry.ddns.net/health/detailed | python3 -c "
import json,sys;d=json.load(sys.stdin)
p=d['pipeline'];s=d['schema']
print(f'Status: {d[\"status\"]}')
print(f'Build: {d[\"build_git_hash\"]}')
print(f'Services: postgres={d[\"services\"][\"postgres\"][\"status\"]} redis={d[\"services\"][\"redis\"][\"status\"]}')
print(f'Schema: {s[\"applied\"][-1][\"filename\"] if s[\"applied\"] else \"none\"} ({len(s[\"applied\"])}/{len(s[\"required\"])} applied, ok={s[\"ok\"]})')
print(f'Scripts: {p[\"scripts_count\"]} files, integrity={p[\"scripts_integrity\"][\"matched\"]}/{p[\"scripts_integrity\"][\"total\"]}')
print(f'Procs: ' + ' '.join([k for k,v in p['processors'].items() if v and k != 'total_py_files']))
"
```
Output:
```
Status: ok
Build: 0e73d2a
Services: postgres=ok redis=ok
Schema: migrate_fix_chunk_id_format.sql (8/8 applied, ok=True)
Scripts: 286 files, integrity=345/345
Procs: asr yolo face pose ocr cut caption scene story asrx probe visual_chunk
```
---
## 10. Schema Version
Each binary embeds a list of required migrations. At startup and via `/health/detailed`, the server verifies all migrations are applied.
```bash
# Check schema version via API
curl -sf http://api.momentry.ddns.net/health/detailed | python3 -c "
import json,sys;d=json.load(sys.stdin)['schema']
print(f'Table exists: {d[\"table_exists\"]}')
print(f'All OK: {d[\"ok\"]}')
for m in d['required']:
match = '✓' if any(a['filename']==m['filename'] and a['checksum']==m['checksum'] for a in d['applied']) else '✗'
print(f' {match} {m[\"filename\"]} {m[\"checksum\"][:16]}')
"
```
Output:
```
Table exists: True
All OK: True
✓ migrate_add_content_hash.sql 42b81554248c4bec
✓ migrate_add_registered_status.sql 566fdfcdc624f6fa
✓ migrate_add_schema_version.sql 585b31df6056a937
✓ migrate_cleanup_inactive_identities.sql daa52a0827b24a77
✓ migrate_fix_chunk_id_format.sql a1b2c3d4e5f6a7b8
✓ migrate_public_schema_v4.sql 973908076c614363
✓ migrate_public_schema_v4_tables.sql 1d62dc42e4dec8f4
✓ migrate_public_v4_complete.sql 2a6fda7d2c5660e4
```
If a migration is missing at startup:
```
[SCHEMA] 7/8 migrations applied. Missing: migrate_fix_chunk_id_format.sql
```
---
---
## Summary Checklist
After completing a pipeline run, verify all items:
### Registration
| # | Check | Expected | Pass |
|---|-------|----------|:----:|
| 1 | `videos.status` | `registered` | ☐ |
| 2 | file_uuid consistency | API response uuid = DB uuid | ☐ |
| 3 | Probe returns metadata | `duration > 0`, `fps > 0` | ☐ |
| 4 | Probe error (Bug #3) | Bad UUID → JSON error + 404 | ☐ |
### Processing
| # | Check | Expected | Pass |
|---|-------|----------|:----:|
| 5 | Job created | `monitor_jobs.status = PENDING` | ☐ |
| 6 | Processors queued | `processor_results` rows = requested count | ☐ |
| 7 | Worker picks up job | `monitor_jobs.status → RUNNING` | ☐ |
| 8 | SHA256 integrity (Bug #2) | `[INTEGRITY] *.py checksum OK` | ☐ |
| 9 | Each processor completes | `processor_results.status = completed` | ☐ |
| 10 | No processor errors | `error_message` all NULL | ☐ |
| 11 | Pipeline completes | `videos.status = completed` | ☐ |
### Results
| # | Check | Expected | Pass |
|---|-------|----------|:----:|
| 12 | Chunks produced | `chunk` table has > 0 rows | ☐ |
| 13 | Chunk ID format | `chunk_id = {uuid}_{start}_{end}` | ☐ |
| 14 | Chunk fallback (Bug #2) | Old integer ID → 200 via handler fallback | ☐ |
| 15 | Search works | `POST /search/universal` returns hits | ☐ |
| 16 | Schema version | `schema.ok = true` in `/health/detailed` | ☐ |
---
## Full Automation Script
Save as `demo_full_cycle.sh`:
```bash
#!/bin/bash
set -euo pipefail
API="http://api.momentry.ddns.net"
KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
PG="/Users/accusys/pgsql/18.3/bin"
# Generate test video
ffmpeg -y -f lavfi -i "testsrc=duration=5:size=640x480:rate=24" \
-f lavfi -i "anullsrc=r=44100:cl=mono" \
-c:v libx264 -preset ultrafast -crf 28 -c:a aac -shortest \
/tmp/auto_demo.mp4 2>/dev/null
# Register
UUID=$(curl -sf -X POST "$API/api/v1/files/register" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"file_path": "/tmp/auto_demo.mp4"}' | python3 -c "import json,sys;print(json.load(sys.stdin)['file_uuid'])")
echo "Registered: $UUID"
# Process
curl -sf -X POST "$API/api/v1/file/$UUID/process" \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"processors":["asr","cut"]}' > /dev/null
echo "Processing triggered"
# Run worker
DATABASE_SCHEMA=dev target/debug/momentry_playground worker \
--max-concurrent 1 --poll-interval 3 &
WPID=$!
sleep 30
kill $WPID 2>/dev/null || true
# Results
"$PG/psql" -U accusys -d momentry -c "
SELECT processor, status FROM dev.processor_results WHERE file_uuid='$UUID' ORDER BY id"
echo "Done: $UUID"
```

View File

@@ -0,0 +1,493 @@
---
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"
---
# Momentry Core — M5API Pipeline Demo
| 項目 | 內容 |
|------|------|
| 建立者 | OpenCode |
| 建立時間 | 2026-05-16 |
| 文件版本 | V1.0 |
| 目標讀者 | developer |
| 預備知識 | 需有 API Key、M5 服務已啟動 |
---
## Prerequisites
```bash
API="https://m5api.momentry.ddns.net"
KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
```
---
## Step 1: System Health Check
```bash
curl -sf "$API/health" | jq '{ip, port, status, version, build_git_hash}'
```
Response:
```json
{
"ip": "192.168.110.201",
"port": 3002,
"status": "ok",
"version": "1.0.0",
"build_git_hash": "c41f7e0c"
}
```
All core services verified:
```bash
curl -sf "$API/health/detailed" | jq '{
services, schema: .schema.ok,
scripts: .pipeline.scripts_count,
integrity: .pipeline.scripts_integrity,
procs: [.pipeline.processors | to_entries[] | select(.value==true and .key!="total_py_files") | .key]
}'
```
Response:
```json
{
"services": {
"postgres": {"status": "ok"},
"redis": {"status": "ok"},
"qdrant": {"status": "ok"},
"mongodb": {"status": "ok"}
},
"schema": true,
"scripts": 286,
"integrity": {"matched": 345, "total": 345, "ok": true},
"procs": ["asr","yolo","face","pose","ocr","cut","caption","scene","story","asrx","probe","visual_chunk"]
}
```
---
## Step 2: List Registered Files
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/files?page=1&page_size=5" | \
jq '{total, files: [.data[]? | {name: .file_name[0:50], status}]}'
```
Response:
```json
{
"total": 56,
"files": [
{"name": "Charade (1963) Cary Grant & Audrey Hepburn ...", "status": "completed"},
{"name": "ExaSAN PCIe series - Director Ou Yu-Zhi ...", "status": "completed"},
{"name": "Old_Time_Movie_Show_-_Charade_1963.HD.mov", "status": "completed"},
{"name": "Old Felix the Cat Cartoon.mp4", "status": "unregistered"},
{"name": "short_clip.mov", "status": "completed"}
]
}
```
---
## Step 3: Register a New File
```bash
# POST with file_path (must exist on server filesystem)
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"file_path": "/path/to/video.mp4"}' \
"$API/api/v1/files/register" | jq '{success, file_uuid, file_name, file_type, duration, fps, already_exists}'
```
Response (new registration):
```json
{
"success": true,
"file_uuid": "3abeee81d94597629ed8cb943f182e94",
"file_name": "Charade (1963) Cary Grant & Audrey Hepburn ...mp4",
"file_type": "video",
"duration": 6785.014,
"fps": 23.976,
"already_exists": false
}
```
Response (duplicate content — SHA256 dedup):
```json
{
"success": true,
"already_exists": true,
"message": "Content already registered (identical file)"
}
```
---
## Step 4: Probe (ffprobe Metadata)
```bash
UUID="3abeee81d94597629ed8cb943f182e94"
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}/probe" | \
jq '{name: .file_name, video: "\(.width)x\(.height)", fps, duration, cached, streams: [.streams[] | {type: .codec_type, codec: .codec_name}]}'
```
Response:
```json
{
"name": "Charade (1963) Cary Grant & Audrey Hepburn ...mp4",
"video": "720x304",
"fps": 23.976,
"duration": 6785.014,
"cached": true,
"streams": [
{"type": "video", "codec": "h264"},
{"type": "audio", "codec": "aac"}
]
}
```
Error cases:
```bash
# Non-existent UUID
curl -sf "https://api.momentry.ddns.net/api/v1/file/bad_uuid/probe"
# → {"error":"Video not found","file_uuid":"bad_uuid"} HTTP 404
# File deleted from disk
# → {"error":"File does not exist at registered path","file_uuid":"...","file_path":"..."} HTTP 404
```
---
## Step 5: Submit Processing Job
```bash
# Specific processors
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"processors":["asr","cut","yolo","face","pose","ocr"]}' \
"$API/api/v1/file/${UUID}/process" | jq '{job_id, file_uuid: .file_uuid[0:16], status}'
```
Response:
```json
{
"job_id": 167,
"file_uuid": "3abeee81d9459762",
"status": "PENDING"
}
```
> **All processors**: Send `{}` (empty body) to run all 12 processors.
> Available: `asr`, `cut`, `yolo`, `face`, `pose`, `ocr`, `asrx`, `visual_chunk`, `scene`, `story`, `caption`
---
## Step 6: Monitor Progress
```bash
while true; do
PROGRESS=$(curl -sf -H "X-API-Key: $KEY" "$API/api/v1/progress/${UUID}")
STATUS=$(echo "$PROGRESS" | jq -r '.status // "?"')
PROCS=$(echo "$PROGRESS" | jq -r '[.processors[]? | "\(.name)=\(.status)(\(.frames_processed))"] | join(" ")')
echo "$(date +%H:%M:%S): $PROCS"
echo "$PROCS" | grep -q "completed" && break
sleep 10
done
```
Typical output:
```
12:30:01: asr=pending(0) cut=pending(0) yolo=pending(0) face=pending(0) pose=pending(0) ocr=pending(0)
12:30:11: asr=running(0) cut=running(0) yolo=pending(0) face=pending(0) pose=pending(0) ocr=pending(0)
12:30:21: asr=running(0) cut=completed(8951) yolo=running(0) face=pending(0) pose=pending(0) ocr=pending(0)
12:30:31: asr=running(0) cut=completed(8951) yolo=completed(8951) face=running(0) pose=pending(0)
12:30:41: asr=running(0) cut=completed(8951) yolo=completed(8951) face=completed(8951) pose=running(0)
12:30:51: asr=completed(8951) cut=completed(8951) yolo=completed(8951) face=completed(8951) pose=completed(8951) ocr=running(0)
12:31:01: asr=completed(8951) cut=completed(8951) yolo=completed(8951) face=completed(8951) pose=completed(8951) ocr=completed(8951)
```
**Status transition chain**: `pending → running → completed`
Check job state:
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/jobs?uuid=${UUID}" | \
jq '[.jobs[]? | {id, status}]'
```
---
## Step 7: Verify Results
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/progress/${UUID}" | \
jq '{processors: [.processors[] | {name, status, frames: .frames_processed}]}'
```
Response:
```json
{
"processors": [
{"name": "asr", "status": "completed", "frames": 162568},
{"name": "cut", "status": "completed", "frames": 162568},
{"name": "yolo", "status": "completed", "frames": 162568},
{"name": "face", "status": "completed", "frames": 162568},
{"name": "pose", "status": "completed", "frames": 162568},
{"name": "ocr", "status": "completed", "frames": 162568}
]
}
```
---
## Step 8: Universal Search
```bash
# Search for a person name
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"query\":\"Audrey\",\"uuid\":\"${UUID}\",\"limit\":3}" \
"$API/api/v1/search/universal" | \
jq '{total, hits: [.results[]? | {chunk_id: .chunk_id[0:40], text: .text[0:80], score}]}'
```
Response:
```json
{
"total": 2,
"hits": [
{
"chunk_id": "3abeee81d94597629ed8cb943f182e94_998192",
"text": "Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran",
"score": 0.9
},
{
"chunk_id": "3abeee81d94597629ed8cb943f182e94_998193",
"text": "Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran",
"score": 0.9
}
]
}
```
```bash
# Search Chinese text
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"query\":\"導演\",\"uuid\":\"${UUID}\",\"limit\":3}" \
"$API/api/v1/search/universal" | jq '{total}'
```
**Search modes**: The universal search endpoint supports:
- Text match (ILIKE on `text_content` and `content` columns)
- Time range filtering (`time_range: [start, end]`)
- Speaker/person ID filtering
- Chunk type filtering
- Visual content filtering (objects, density, classes)
---
## Step 9: Get Chunk Detail
```bash
CHUNK_ID="3abeee81d94597629ed8cb943f182e94_998192"
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}/chunk/${CHUNK_ID}" | \
jq '{chunk_id, chunk_type, text: .text_content, fps, start_frame, end_frame}'
```
Response:
```json
{
"chunk_id": "3abeee81d94597629ed8cb943f182e94_998192",
"chunk_type": "sentence",
"text": "Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran",
"fps": 23.976,
"start_frame": 2395281,
"end_frame": 2395341
}
```
---
## Step 10: Chunk Fallback (Stale Qdrant Compatibility)
Old integer-format chunk_ids from stale Qdrant payloads are automatically resolved via `WHERE id = int(chunk_id)`:
```bash
# Integer format (old Qdrant payload)
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}/chunk/998192" | \
jq '{chunk_id, text: .text_content}'
```
Response (same chunk as above):
```json
{
"chunk_id": "3abeee81d94597629ed8cb943f182e94_998192",
"text": "Shorede stars two legends of classical Hollywood, Audrey Hepburn and Carrie Gran"
}
```
**Both formats work:**
- `chunk/{uuid}_{id}` → exact `chunk_id` match
- `chunk/{id}` → fallback by primary key `id`
---
## Step 11: File Detail
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}" | \
jq '{file_name, status, file_type, file_path}'
```
Response:
```json
{
"file_name": "Charade (1963) Cary Grant & Audrey Hepburn ...mp4",
"status": "completed",
"file_type": "video",
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Charade..."
}
```
---
## Step 12: File Identities
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}/identities" | \
jq '{total, identities: [.data[]? | {name, face_count, confidence}]}'
```
Response:
```json
{
"total": 2,
"identities": [
{"name": "Audrey Hepburn", "face_count": 22082, "confidence": 0.93},
{"name": "Cary Grant", "face_count": 15334, "confidence": 0.91}
]
}
```
---
## Step 13: Identity Detail
```bash
# List all global identities
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/identities?page=1&page_size=3" | \
jq '{total, identities: [.data[]? | {name, type: .identity_type, source}]}'
```
```bash
# Get identity files (cross-file faces)
IDENTITY_UUID="c3545906-c82d-4b66-aa1d-150bc02decce"
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/identity/${IDENTITY_UUID}/files" | \
jq '{total, files: [.data[]? | {file_uuid: .file_uuid[0:16], face_count}]}'
```
---
## Step 14: Schema & Integrity Verification
```bash
curl -sf "$API/health/detailed" | jq '{
ip, port,
schema: .schema.ok,
migrations: [.schema.applied[]?.filename],
integrity: .pipeline.scripts_integrity
}'
```
Response:
```json
{
"ip": "192.168.110.201",
"port": 3002,
"schema": true,
"migrations": [
"migrate_add_content_hash.sql",
"migrate_add_registered_status.sql",
"migrate_add_schema_version.sql",
"migrate_cleanup_inactive_identities.sql",
"migrate_public_schema_v4_tables.sql",
"migrate_public_schema_v4.sql",
"migrate_public_v4_complete.sql",
"migrate_fix_chunk_id_format.sql",
"migrate_add_identity_indexes.sql"
],
"integrity": {"matched": 345, "total": 345, "ok": true}
}
```
---
## Full Automation Script
```bash
#!/bin/bash
set -euo pipefail
API="${API:-https://m5api.momentry.ddns.net}"
KEY="${KEY:-muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69}"
# 1. Health
echo "=== Health ==="
curl -sf "$API/health" | jq '{status, version, build_git_hash}'
# 2. Register file (argument: file path)
FILE_PATH="${1:?Usage: $0 <file_path>}"
echo "=== Register ==="
REG=$(curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"file_path\":\"$FILE_PATH\"}" "$API/api/v1/files/register")
echo "$REG" | jq '{success, file_uuid, file_name}'
UUID=$(echo "$REG" | jq -r '.file_uuid')
[ -z "$UUID" ] && { echo "Registration failed"; exit 1; }
# 3. Probe
echo "=== Probe ==="
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}/probe" | \
jq '{name, fps, duration}'
# 4. Submit job
echo "=== Process ==="
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{}' "$API/api/v1/file/${UUID}/process" | jq '{job_id, status}'
# 5. Poll progress
echo "=== Waiting for pipeline... ==="
while true; do
PROGRESS=$(curl -sf -H "X-API-Key: $KEY" "$API/api/v1/progress/${UUID}")
STATUS=$(echo "$PROGRESS" | jq -r '.status // "?"')
echo "$(date +%H:%M:%S): $(echo "$PROGRESS" | jq -r '[.processors[]? | "\(.name)=\(.status)(\(.frames_processed))"] | join(" ")')"
echo "$PROGRESS" | jq -e '[.processors[]? | select(.status == "pending")] | length == 0' >/dev/null && break
sleep 10
done
# 6. Search
echo "=== Search ==="
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"query\":\"test\",\"uuid\":\"${UUID}\",\"limit\":3}" \
"$API/api/v1/search/universal" | jq '{total, hits: [.results[]? | {chunk_id: .chunk_id[0:30], text: .text[0:60]}]}'
echo ""
echo "✅ Done: $UUID"
```

View File

@@ -0,0 +1,412 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Playground Binary Implementation Plan"
date: "2026-03-23"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "binary"
- "plan"
- "implementation"
- "playground"
ai_query_hints:
- "查詢 Playground Binary Implementation Plan 的內容"
- "Playground Binary Implementation Plan 的主要目的是什麼?"
- "如何操作或實施 Playground Binary Implementation Plan"
---
# Playground Binary Implementation Plan
| Item | Content |
|------|---------|
| Author | Warren |
| Created | 2026-03-23 |
| Document Version | V1.0 |
---
## Version History
| Version | Date | Purpose | Operator | Tool/Model |
|---------|------|---------|----------|------------|
| V1.0 | 2026-03-23 | Create implementation plan | Warren | OpenCode |
---
## Overview
Create separate `momentry_playground` binary with distinct configuration from `momentry` (production).
| Aspect | Production (`momentry`) | Development (`momentry_playground`) |
|--------|------------------------|-------------------------------------|
| **Port** | 3002 | 3003 |
| **Redis Prefix** | `momentry:` | `momentry_dev:` |
| **Worker** | Enabled | Disabled |
| **Purpose** | Production deployment | Testing/Development |
---
## Files to Modify
```
Files Changed: 6 files (+1 new)
├── src/core/config.rs ← Add server_port(), redis_key_prefix()
├── src/core/db/redis_client.rs ← Replace hardcoded prefixes
├── src/core/cache/redis_cache.rs ← Use configurable prefix
├── src/main.rs ← Update CLI defaults
├── src/playground.rs ← NEW: Development binary
├── Cargo.toml ← Add new binary
└── .env.development ← NEW: Dev environment config
```
---
## Implementation Steps
### Step 1: Update `src/core/config.rs`
Add after line 51 (after `MEDIA_BASE_URL`):
```rust
pub static SERVER_PORT: Lazy<u16> = Lazy::new(|| {
env::var("MOMENTRY_SERVER_PORT")
.unwrap_or_else(|_| "3002".to_string())
.parse()
.unwrap_or(3002)
});
pub static REDIS_KEY_PREFIX: Lazy<String> = Lazy::new(|| {
env::var("MOMENTRY_REDIS_PREFIX")
.unwrap_or_else(|_| "momentry:".to_string())
});
```
---
### Step 2: Update `src/core/db/redis_client.rs`
Replace all hardcoded `momentry:` prefixes with configurable prefix.
**Import at top:**
```rust
use crate::core::config::REDIS_KEY_PREFIX;
```
**Pattern for each method:**
```rust
let prefix = REDIS_KEY_PREFIX.as_str();
let key = format!("{}job:{}", prefix, uuid);
```
**Affected lines:**
| Line | Key Pattern |
|------|-------------|
| 47 | `job:{uuid}` |
| 81, 109 | `job:{uuid}:processor:{processor}` |
| 136, 146 | `progress:{uuid}` |
| 172 | `jobs:active` |
| 179 | `jobs:active``jobs:completed` |
| 187 | `jobs:active``jobs:failed` |
| 194 | `jobs:active` |
| 201, 208 | `health:momentry_core` |
| 214 | `monitor:job:{uuid}` |
| 242, 300 | `errors:{uuid}` |
| 258, 281 | `anomaly:alerts`, `anomaly:key:{key_id}` |
| 317, 346, 364, 392, 397 | `worker:job:{uuid}...` |
| 406, 410 | `worker:job:*` |
---
### Step 3: Update `src/core/cache/redis_cache.rs`
**Import:**
```rust
use crate::core::config::REDIS_KEY_PREFIX;
```
**Replace line 10:**
```rust
// Remove: const KEY_PREFIX: &str = "momentry:cache:";
```
**Update `prefixed_key` method (line 24):**
```rust
fn prefixed_key(&self, key: &str) -> String {
format!("{}cache:{}", REDIS_KEY_PREFIX.as_str(), key)
}
```
**Update tests (lines 161-162):**
```rust
#[test]
fn test_prefixed_key() {
// Note: This test will use the configured prefix
let cache = RedisCache::new().unwrap();
// With default prefix "momentry:"
assert_eq!(cache.prefixed_key("test"), "momentry:cache:test");
assert_eq!(cache.prefixed_key("video:abc"), "momentry:cache:video:abc");
}
```
---
### Step 4: Update `src/main.rs`
**Change CLI defaults (Lines 691-695):**
```rust
// Before:
#[arg(long, default_value = "3000")]
port: u16,
// After:
#[arg(long)]
port: Option<u16>,
```
**Update Server match arm (around line 2398):**
```rust
Commands::Server { host, port } => {
let port = port.unwrap_or_else(|| *crate::core::config::SERVER_PORT);
momentry_core::api::start_server(&host, port).await?;
Ok(())
}
```
**Update Redis key usage (Line 1098):**
```rust
// Before:
let key = format!("momentry:job:{}:processor:{}", uuid, processor);
// After:
let key = format!(
"{}job:{}:processor:{}",
crate::core::config::REDIS_KEY_PREFIX.as_str(),
uuid,
processor
);
```
---
### Step 5: Create `src/playground.rs`
```rust
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
// ... same imports as main.rs ...
fn main() -> Result<()> {
// Load development environment first
dotenv::from_filename(".env.development").ok();
tracing_subscriber::fmt::init();
tracing::info!("Starting momentry_playground (development binary)");
tracing::info!("Port: {}", *momentry_core::core::config::SERVER_PORT);
tracing::info!("Redis prefix: {}", *momentry_core::core::config::REDIS_KEY_PREFIX);
let cli = Cli::parse();
// ... rest identical to main.rs ...
}
```
---
### Step 6: Update `Cargo.toml`
**Add after line 90:**
```toml
[[bin]]
name = "momentry_playground"
path = "src/playground.rs"
```
**Add dependency (if not present):**
```toml
dotenv = "0.15"
```
---
### Step 7: Create `.env.development`
```bash
# Development Environment Configuration
# Used by: momentry_playground binary
# Server Configuration
MOMENTRY_SERVER_PORT=3003
MOMENTRY_REDIS_PREFIX=momentry_dev:
# Worker Configuration (disabled for development)
MOMENTRY_WORKER_ENABLED=false
MOMENTRY_MAX_CONCURRENT=1
MOMENTRY_POLL_INTERVAL=10
# Database (can use separate dev database)
DATABASE_URL=postgres://accusys@localhost:5432/momentry
MONGODB_URL=mongodb://accusys:Test3200Test3200@localhost:27017/admin
# Redis
REDIS_URL=redis://:accusys@localhost:6379
```
---
### Step 8: Update `.env` (Production)
Add these lines:
```bash
# Production Environment Configuration
# Used by: momentry binary
# Server Configuration
MOMENTRY_SERVER_PORT=3002
MOMENTRY_REDIS_PREFIX=momentry:
# Worker Configuration
MOMENTRY_WORKER_ENABLED=true
MOMENTRY_MAX_CONCURRENT=2
MOMENTRY_POLL_INTERVAL=5
```
---
## Testing Checklist
### 1. Build and Run Production Binary
```bash
cargo build --release --bin momentry
cargo run --bin momentry -- server
# Expected: Listening on http://127.0.0.1:3002
cargo run --bin momentry -- worker
# Expected: Worker started with momentry: prefix
```
### 2. Build and Run Development Binary
```bash
cargo build --bin momentry_playground
cargo run --bin momentry_playground -- server
# Expected: Listening on http://127.0.0.1:3003
```
### 3. Verify Redis Key Isolation
```bash
# Production data
redis-cli KEYS "momentry:*"
# Development data
redis-cli KEYS "momentry_dev:*"
# Should be separate
```
### 4. Run Both Simultaneously
```bash
# Terminal 1: Production
cargo run --bin momentry -- server
# Terminal 2: Development
cargo run --bin momentry_playground -- server
# Both should run without port conflicts
```
### 5. Unit Tests
```bash
cargo test --lib
# All tests should pass
```
---
## Redis Key Structure
### Production (`momentry:`)
```
momentry:job:{uuid} # Job status
momentry:job:{uuid}:processor:{name} # Processor progress
momentry:progress:{uuid} # Progress pub/sub
momentry:jobs:active # Active job set
momentry:jobs:completed # Completed job set
momentry:jobs:failed # Failed job set
momentry:health:momentry_core # Health status
momentry:cache:{key} # Cache entries
momentry:worker:job:{uuid} # Worker job
momentry:worker:job:{uuid}:processor:{name}
```
### Development (`momentry_dev:`)
```
momentry_dev:job:{uuid}
momentry_dev:job:{uuid}:processor:{name}
momentry_dev:progress:{uuid}
momentry_dev:jobs:active
momentry_dev:jobs:completed
momentry_dev:jobs:failed
momentry_dev:health:momentry_core
momentry_dev:cache:{key}
momentry_dev:worker:job:{uuid}
momentry_dev:worker:job:{uuid}:processor:{name}
```
---
## Potential Issues & Solutions
| Issue | Solution |
|-------|----------|
| `dotenv` crate not in dependencies | Add to Cargo.toml |
| Tests use hardcoded prefix | Update tests to use config, or use `#[cfg(test)]` defaults |
| Worker starts in playground | Check `MOMENTRY_WORKER_ENABLED=false` in `.env.development` |
| Port already in use | Graceful error message with suggestion to use `--port` flag |
| Mixed data in Redis | Ensure prefix is loaded before any Redis operations |
---
## Files Summary
| File | Lines Changed | Purpose |
|------|---------------|---------|
| `src/core/config.rs` | +15 | Add SERVER_PORT and REDIS_KEY_PREFIX |
| `src/core/db/redis_client.rs` | ~50 | Replace hardcoded prefixes |
| `src/core/cache/redis_cache.rs` | ~10 | Use configurable prefix |
| `src/main.rs` | ~15 | Update CLI defaults, Redis key usage |
| `src/playground.rs` | NEW (~2800) | Development binary |
| `Cargo.toml` | +4 | Add binary definition |
| `.env.development` | NEW (~20) | Development environment |
**Total**: ~60 lines modified + ~2800 lines new file
---
## Reference Documents
| Document | Purpose |
|----------|---------|
| `docs_v1.0/REFERENCE/SERVICES.md` | Port allocations |
| `docs_v1.0/REFERENCE/MOMENTRY_CORE_REDIS_KEYS.md` | Redis key design |
| `AGENTS.md` | Code style and conventions |
---
## Version History
| Version | Date | Author | Changes |
|---------|------|--------|---------|
| 1.0 | 2025-03-25 | OpenCode | Initial implementation plan |

View File

@@ -0,0 +1,418 @@
---
document_type: "demo_guide"
service: "MOMENTRY_CORE"
title: "Portal API Demo 示範指南"
date: "2026-04-30"
version: "V1.0"
status: "active"
current_state: "approved"
owner: "Warren"
created_by: "OpenCode"
tags:
- "portal"
- "api-demo"
- "wordpress"
- "frontend"
- "query"
- "operation"
- "application"
ai_query_hints:
- "查詢 Portal API Demo 示範指南的內容"
- "Portal API Demo 的主要目的是什麼?"
- "如何使用 Portal API Demo 頁面?"
- "Portal API Demo 頁面分類與功能"
- "如何設定 API Demo 頁面"
- "API Demo 查詢/展示/操作/應用頁面說明"
- "Momentry Playground 啟動方式"
related_documents:
- "GUIDES/API_INDEX.md"
- "GUIDES/API_ENDPOINTS.md"
- "GUIDES/PORTAL_DEVELOPMENT_PLAN.md"
- "FILE_UUID_SPEC.md"
---
# Portal API Demo 示範指南
| 項目 | 內容 |
|------|------|
| 建立者 | OpenCode |
| 建立時間 | 2026-04-30 |
| 文件版本 | V1.0 |
| 目標讀者 | developer, end_user |
| 預備知識 | 需有 API Key |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-04-30 | 創建 Portal API Demo 示範指南 | OpenCode | big-pickle |
---
## 概述
本文檔說明 Momentry Portal 中四個 API Demo 頁面的功能、設定方式與使用流程。
Demo 頁面以 **file-centric** 設計理念為核心,將檔案 (file) 作為主要管理目標,
身份 (identity) 為附隨目標,分類系統用於形容主體。
---
## 關鍵術語定義
| 術語 | 定義 |
|------|------|
| file_uuid | 檔案唯一識別碼,由 MAC、Birthday、Path、Filename 計算得出 |
| identity_uuid | 全域人員身份識別碼,跨檔案關聯 |
| file-centric | 以檔案為中心的設計理念,檔案是主要管理目標 |
| Birth/Migration | 檔案註冊與遷移的身份模型 |
| Portal | WordPress 前端展示與操作介面 |
| Playground | Momentry 開發伺服器 (port 3003) |
---
## 頁面分類總覽
Momentry Portal 提供四個 API Demo 頁面,涵蓋查詢、展示、操作、應用四大類別:
| 頁面 | 檔案名稱 | 類別 | 主要功能 |
|------|----------|------|----------|
| API Demo - 查詢 | `page-api-demo-query.php` | 查詢 | 檔案查詢、身份查詢、處理狀態、遷移歷史、語義搜尋 |
| API Demo - 展示 | `page-api-demo-display.php` | 展示 | 檔案詳情儀表板、身份視覺化、片段展示、分類結果 |
| API Demo - 操作 | `page-api-demo-operation.php` | 操作 | 檔案註冊、身份綁定、處理觸發、身份合併、處理器重試 |
| API Demo - 應用 | `page-api-demo-application.php` | 應用 | 完整工作流程、身份追蹤、遷移示範、批次處理、語義搜尋工作流 |
---
## 檔案位置
| 類型 | 路徑 | 說明 |
|------|------|------|
| 查詢頁面 | `/wp-content/themes/momentry/page-api-demo-query.php` | WordPress 頁面模板 |
| 展示頁面 | `/wp-content/themes/momentry/page-api-demo-display.php` | WordPress 頁面模板 |
| 操作頁面 | `/wp-content/themes/momentry/page-api-demo-operation.php` | WordPress 頁面模板 |
| 應用頁面 | `/wp-content/themes/momentry/page-api-demo-application.php` | WordPress 頁面模板 |
| 共用樣式 | `/wp-content/themes/momentry/style.css` | CSS 樣式表 |
| 設定說明 | `/wp-content/themes/momentry/API_DEMO_README.md` | 技術設定文件 |
---
## 環境需求
| 項目 | 狀態 | 說明 |
|------|------|------|
| WordPress | ✅ 已安裝 | 本地 WordPress 環境 |
| Momentry Theme | ✅ 已安裝 | 自定義 momentry 主題 |
| PostgreSQL | ✅ 已安裝 | Momentry Core 資料庫 |
| Momentry Playground | 🔄 需啟動 | 開發伺服器 (port 3003) |
---
## 設定步驟
### Step 1: 啟動 Momentry Playground
API Demo 頁面需要連線到 Momentry Playground API server
```bash
cd /Users/accusys/momentry_core_0.1
cargo run --bin momentry_playground -- server --host 0.0.0.0 --port 3003
```
驗證伺服器啟動:
```bash
curl http://localhost:3003/api/v1/health
```
### Step 2: 在 WordPress 建立頁面
1. 進入 WordPress 後台:`http://localhost/wp-admin`
2. 點擊 **Pages > Add New**
3. 建立以下四個頁面:
| 頁面標題 | URL Slug | Template |
|----------|----------|----------|
| API Demo - 查詢 | `api-demo-query` | API Demo - 查詢 |
| API Demo - 展示 | `api-demo-display` | API Demo - 展示 |
| API Demo - 操作 | `api-demo-operation` | API Demo - 操作 |
| API Demo - 應用 | `api-demo-application` | API Demo - 應用 |
1. 建立時,在右側 **Page Attributes** 選擇對應的 **Template**
2. 點擊 **Publish**
### Step 3: 訪問示範頁面
| 頁面 | URL |
|------|-----|
| 查詢 | `http://localhost/api-demo-query/` |
| 展示 | `http://localhost/api-demo-display/` |
| 操作 | `http://localhost/api-demo-operation/` |
| 應用 | `http://localhost/api-demo-application/` |
---
## 頁面功能詳解
### 1. 查詢頁面 (Query)
查詢頁面用於示範各類資料查詢 API 的使用方式。
#### 1.1 檔案查詢 (GET /api/v1/files/:uuid)
- **用途**:透過 file_uuid 查詢檔案的完整資訊
- **操作**:輸入 file_uuid點擊「查詢」
- **回應**:檔案元數據、處理狀態、分類標籤等
#### 1.2 身份查詢 (GET /api/v1/identities/:uuid)
- **用途**:查詢跨檔案的全域身份資訊
- **操作**:輸入 identity_uuid點擊「查詢」
- **回應**:身份名稱、關聯檔案、臉部特徵、品質分數
#### 1.3 處理狀態查詢 (GET /api/v1/jobs/:uuid/status)
- **用途**:查詢檔案的處理進度與各處理器狀態
- **操作**:輸入 file_uuid點擊「查詢」
- **回應**:處理進度百分比、已完成/失敗的處理器列表
#### 1.4 檔案遷移歷史 (GET /api/v1/files/:uuid/history)
- **用途**:查詢檔案因移動而產生的身份變更鏈
- **操作**:輸入 file_uuid點擊「查詢」
- **回應**parent_uuid 關聯鏈、遷移時間記錄
#### 1.5 語義搜尋 (POST /api/v1/search)
- **用途**:使用自然語言搜尋相關的影片片段或身份
- **操作**:輸入搜尋查詢,選擇搜尋類型,點擊「搜尋」
- **回應**:搜尋結果列表、相似度分數
---
### 2. 展示頁面 (Display)
展示頁面用於示範如何將 API 資料轉化為視覺化的展示元件。
#### 2.1 檔案詳情儀表板
- **用途**:整合展示檔案的元數據、處理進度、分類標籤等完整資訊
- **操作**:輸入 file_uuid點擊「載入」
- **展示內容**
- 基本資訊:檔案名稱、類型、時長、解析度、幀率
- 處理狀態:狀態徽章、處理進度、已完成處理器
- 分類標籤:分類標籤、語義標籤
- 關聯身份:檢測到身份數量、主要身份
#### 2.2 身份視覺化
- **用途**:展示身份的跨檔案關聯、臉部檢測統計、品質分數
- **操作**:輸入 identity_uuid點擊「視覺化」
- **展示內容**
- 身份名稱與品質分數
- 關聯檔案列表
- 臉部統計 (檢測次數、平均品質)
- 角度覆蓋視覺化
#### 2.3 影片片段展示
- **用途**:展示影片的語義片段、說話者分段、鏡頭切換等分類結果
- **操作**:輸入 file_uuid選擇片段類型點擊「載入片段」
- **片段類型**:語義片段、鏡頭切換、時間片段
#### 2.4 分類結果展示
- **用途**:展示 YOLO 檢測、姿勢估計、動作識別等視覺分類結果
- **操作**:輸入 file_uuid選擇處理器類型點擊「載入結果」
- **處理器類型**YOLO、Pose、Face、OCR
---
### 3. 操作頁面 (Operation)
操作頁面用於示範各類寫入與修改 API 的實際使用。
#### 3.1 檔案註冊 (POST /api/v1/register)
- **用途**:將新影片或音訊檔案註冊到系統
- **操作**:輸入檔案路徑,點擊「註冊」
- **快速測試**:提供預設測試路徑按鈕
#### 3.2 身份綁定 (POST /api/v1/identities/bind)
- **用途**:將臉部檢測綁定到特定身份
- **操作**:輸入 Face ID 和 Identity UUID點擊「綁定」
#### 3.3 處理觸發 (POST /api/v1/files/:uuid/process)
- **用途**:手動觸發檔案的處理流程
- **操作**:輸入 file_uuid選擇要執行的處理器 (ASR、YOLO、Face、OCR、Pose、CUT),點擊「觸發處理」
#### 3.4 身份合併 (POST /api/v1/identities/merge)
- **用途**:將多個身份合併為單一身份
- **操作**:輸入目標 Identity UUID 和來源 Identity UUIDs (逗號分隔),點擊「合併」
#### 3.5 處理器重試 (POST /api/v1/jobs/:uuid/retry)
- **用途**:重試失敗的處理器
- **操作**:輸入 file_uuid選擇要重試的處理器點擊「重試」
---
### 4. 應用頁面 (Application)
應用頁面示範結合多個 API 的實際應用場景與工作流程。
#### 4.1 完整工作流程示範
端到端展示從檔案註冊到處理完成的完整流程:
| 步驟 | 操作 | 說明 |
|------|------|------|
| 1 | 註冊檔案 | 輸入影片路徑,呼叫 `/register` |
| 2 | 查詢處理狀態 | 定期檢查 `/jobs/:uuid/status` 直到完成 |
| 3 | 查詢檢測結果 | 取得身份和片段資訊 |
| 4 | 搜尋身份 | 展示檔案中檢測到的身份列表 |
每步完成後自動解鎖下一步,狀態以顏色標示 (等待中/執行中/完成)。
#### 4.2 跨檔案身份追蹤
- **用途**:追蹤特定身份在所有檔案中的出現情況
- **操作**:輸入 Identity UUID點擊「開始追蹤」
- **展示內容**
- 身份名稱與關聯檔案數量
- 時間軸展示各檔案中的出現記錄
- 統計資訊 (總檢測次數、平均品質、覆蓋角度)
#### 4.3 檔案遷移與身份繼承示範
展示 Birth/Migration 模型的實際運作:
| 步驟 | 操作 | 說明 |
|------|------|------|
| 1 | 原始註冊 | 註冊原始路徑的檔案 |
| 2 | 模擬移動 | 使用新路徑重新註冊,系統產生新的 file_uuid |
| 3 | 查詢歷史 | 透過 `/files/:uuid/history` 查看遷移鏈 |
#### 4.4 批次檔案處理
- **用途**:一次註冊多個檔案,監控批次處理進度
- **操作**:輸入多個檔案路徑 (每行一個),點擊「批次註冊」
- **展示內容**:進度條、每個檔案的註冊結果
#### 4.5 語義搜尋與片段提取工作流
- **用途**:使用語義搜尋找到相關片段,然後提取詳細資訊
- **操作**:輸入自然語言查詢,點擊「搜尋」
- **展示內容**:搜尋結果摘要、詳細片段列表 (含相似度分數)
---
## API 端點參考
### 查詢類 API
| 端點 | 方法 | 說明 |
|------|------|------|
| `/api/v1/files/:uuid` | GET | 查詢檔案詳細資訊 |
| `/api/v1/files` | GET | 查詢檔案列表 |
| `/api/v1/identities/:uuid` | GET | 查詢身份資訊 |
| `/api/v1/jobs/:uuid/status` | GET | 查詢處理狀態 |
| `/api/v1/files/:uuid/history` | GET | 查詢遷移歷史 |
| `/api/v1/search` | POST | 語義搜尋 |
### 操作類 API
| 端點 | 方法 | 說明 |
|------|------|------|
| `/api/v1/register` | POST | 註冊檔案 |
| `/api/v1/identities/bind` | POST | 綁定身份 |
| `/api/v1/files/:uuid/process` | POST | 觸發處理 |
| `/api/v1/identities/merge` | POST | 合併身份 |
| `/api/v1/jobs/:uuid/retry` | POST | 重試處理器 |
---
## 常見問題
### Q1: 頁面無法連線到 API
- 確認 Playground server 已啟動:`cargo run --bin momentry_playground -- server`
- 檢查 API base URL 設定 (各頁面的 `const API_BASE = 'http://localhost:3003/api/v1'`)
- 確認 CORS 設定允許來自 WordPress 的請求
### Q2: 註冊檔案時返回錯誤
- 確認檔案路徑正確且檔案存在
- 確認 PostgreSQL 資料庫連線正常
- 檢查 Playground server 日誌
### Q3: 遷移歷史查詢無結果
- 確認檔案確實有 parent_uuid 記錄
- 使用 `SELECT file_uuid, parent_uuid FROM dev.videos WHERE parent_uuid IS NOT NULL;` 檢查資料庫
---
## 常用指令
```bash
# 啟動 Playground 伺服器
cargo run --bin momentry_playground -- server --host 0.0.0.0 --port 3003
# 檢查 API 健康狀態
curl http://localhost:3003/api/v1/health
# 查詢檔案列表
curl http://localhost:3003/api/v1/files?limit=5
# 註冊檔案
curl -X POST http://localhost:3003/api/v1/register \
-H "Content-Type: application/json" \
-d '{"file_path": "/path/to/video.mp4"}'
# 查詢檔案詳情
curl http://localhost:3003/api/v1/files/<file_uuid>
# 查詢遷移歷史
curl http://localhost:3003/api/v1/files/<file_uuid>/history
```
---
## 設計理念
### File-Centric 架構
Momentry 系統採用 **file-centric** 設計理念:
| 概念 | 說明 |
|------|------|
| **File (檔案)** | 主要管理目標file_uuid 為核心識別 |
| **Identity (身份)** | 附隨目標,跨檔案關聯人員身份 |
| **Classification (分類)** | 形容主體的標籤系統 (YOLO、ASR、Face 等處理器結果) |
### Birth/Migration 模型
| 概念 | 說明 |
|------|------|
| **Birth (註冊)** | 檔案首次註冊,產生初始 file_uuid |
| **Migration (遷移)** | 檔案移動後重新註冊,產生新 file_uuid 並記錄 parent_uuid |
| **Birthday (生日)** | 原始註冊時間,遷移時保留以證明身份連續性 |
### UUID 計算公式
```
file_uuid = SHA256(MAC_Address | Birthday | Canonical_Path | Filename)[0:32]
```
---
## 版本資訊
- 版本: V1.0
- 建立日期: 2026-04-30
- 文件更新: 2026-04-30

View File

@@ -0,0 +1,122 @@
# Portal 開發計畫
> 建立時間: 2026-04-25
> 狀態: 進行中
---
## 一、已完成功能
### 認證系統
- ✅ Login API (`/api/v1/auth/login`) - demo/demo
- ✅ Logout API (`/api/v1/auth/logout`)
- ✅ Session 管理 (localStorage)
- ✅ CORS 配置 (AllowOrigin::any)
### 納管檔案 (/files)
- ✅ 影片列表(分頁)
- ✅ 狀態過濾:未處理 / 已處理 / 全選
- ✅ 檔名搜尋 + 高亮匹配文字
- ✅ 影片詳情頁 (probe_json)
- ✅ 臉部群組顯示
- ✅ Tauri VideosResponse 修復 (count 欄位)
- ✅ API 狀態過濾修復 (pending/ready)
- ✅ 檔名搜尋功能修復
---
## 二、檔案管理增強
### 2.1 批量操作
- [ ] 全選/多選影片
- [ ] 批量納管Register
- [ ] 批量取消納管Unregister
- [ ] 批量觸發處理Process
- [ ] 批量刪除
### 2.2 進階搜尋過濾
- [ ] 日期範圍過濾
- [ ] 解析度過濾
- [ ] 檔案大小過濾
- [ ] 處理狀態過濾
### 2.3 處理控制
- [ ] 處理進度顯示Progress bar
- [ ] 處理日誌查看
- [ ] 錯誤訊息顯示
### 2.4 操作功能
- [ ] 取消納管Unregister
- [ ] 重新處理Reprocess
- [ ] 檔案下載
---
## 三、人物管理
### 3.1 人物列表 (/persons)
- [ ] 搜尋人物名稱
- [ ] 出現次數/時間排序
- [ ] 人物照片縮圖顯示
- [ ] 分頁
### 3.2 人物詳情 (/identity/:id)
- [ ] 所屬影片列表
- [ ] 時間軸顯示
- [ ] 截圖展示
- [ ] 出現片段時間
### 3.3 身份管理 (/identities)
- [ ] 已註冊身份列表
- [ ] 註冊新人物(從圖片)
- [ ] 人物更名
- [ ] 合併人物
- [ ] 刪除人物
### 3.4 臉部群組管理
- [ ] 群組審核/確認
- [ ] 未註冊群組列表
- [ ] 點擊註冊身份
- [ ] 群組截圖預覽
---
## 四、整體架構
```
首頁 (/home)
├── 搜尋 (/search)
├── 人物管理 (/persons)
│ ├── 已註冊人物列表
│ ├── 未註冊群組
│ └── 人物詳情 (/identity/:id)
├── 納管檔案 (/files)
│ ├── 影片列表
│ ├── 狀態過濾
│ ├── 檔名搜尋(高亮)
│ └── 影片詳情 (/videos/:uuid)
├── 設定 (/settings)
└── Console 面板
```
---
## 五、技術債務
### 待修復
- [ ] `hash_password` 函式未使用server.rs:35
- [ ] CORS 改為 AllowOrigin::any安全考量
- [ ] MongoCache 未考慮 status/query 過濾
### API 不一致
- [ ] API 返回 `count`,前端原使用 `total`
- [ ] Tauri 與 HTTP 模式返回格式差異
---
## 六、待確認問題
1. Identities 頁面是否要合併到 Persons
2. 是否需要「影片處理批次」功能?
3. 人物搜尋是否需要中文拼音/相似度?
4. 是否需要匯出報表功能?

View File

@@ -0,0 +1,472 @@
---
document_type: "demo_guide"
service: "MOMENTRY_CORE"
title: "Pipeline API Demo"
date: "2026-05-16"
version: "V1.0"
status: "active"
owner: "M5"
created_by: "OpenCode"
tags:
- "demo"
- "pipeline"
- "api"
- "playground"
ai_query_hints:
- "Pipeline API demo"
- "如何使用 API 執行 Pipeline"
- "3003 playground 操作"
related_documents:
- "GUIDES/Demo_EndToEnd.md"
- "GUIDES/API_ENDPOINTS.md"
---
# Momentry Core — Pipeline API Demo
| 項目 | 內容 |
|------|------|
| 建立者 | OpenCode |
| 建立時間 | 2026-05-16 |
| 文件版本 | V1.0 |
| 目標讀者 | developer |
| 預備知識 | 需有 API Key、3003 playground 已啟動 |
---
## Table of Contents
1. [Health Check](#1-health-check)
2. [Register File](#2-register-file)
3. [Probe File](#3-probe-file)
4. [Submit Processing Job](#4-submit-processing-job)
5. [Monitor Progress (Polling)](#5-monitor-progress-polling)
6. [Run Worker](#6-run-worker)
7. [Verify Progress](#7-verify-progress)
8. [Universal Search](#8-universal-search)
9. [Chunk Detail](#9-chunk-detail)
10. [Chunk Fallback (Stale Qdrant)](#10-chunk-fallback-stale-qdrant)
11. [File Detail](#11-file-detail)
12. [List Identities](#12-list-identities)
13. [Schema & Integrity Verify](#13-schema--integrity-verify)
---
## 0. Env Setup
```bash
API="http://127.0.0.1:3003"
KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
```
---
## 1. Health Check
```bash
curl -sf "$API/health" | jq '{status, version, build_git_hash, uptime_ms}'
```
Response:
```json
{
"status": "ok",
"version": "1.0.0",
"build_git_hash": "c41f7e0c",
"uptime_ms": 1234567
}
```
```bash
curl -sf "$API/health/detailed" | jq '{
ip, port, services,
schema: .schema.ok,
scripts: .pipeline.scripts_count,
integrity: .pipeline.scripts_integrity,
procs: [.pipeline.processors | to_entries[] | select(.value==true and .key!="total_py_files") | .key]
}'
```
Response:
```json
{
"ip": "192.168.110.201",
"port": 3003,
"services": {
"postgres": {"status": "ok", "latency_ms": 6},
"redis": {"status": "ok", "latency_ms": 0},
"qdrant": {"status": "ok", "latency_ms": 1},
"mongodb": {"status": "ok", "latency_ms": 0}
},
"schema": true,
"scripts": 286,
"integrity": {"matched": 345, "total": 345, "ok": true},
"procs": ["asr","yolo","face","pose","ocr","cut","caption","scene","story","asrx","probe","visual_chunk"]
}
```
---
## 2. Register File
```bash
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"file_path": "/path/to/video.mp4"}' \
"$API/api/v1/files/register" | jq '{success, file_uuid, file_name, file_type, duration, fps, already_exists}'
```
Response:
```json
{
"success": true,
"file_uuid": "078975658e04529ee06f8d11cd7ba226",
"file_name": "Gamma 8-Director Chih-Lin Yang Shares His Experience:楊智麟導演經驗分享.mp4",
"file_type": "video",
"duration": 298.665042,
"fps": 29.97002997002997,
"already_exists": false
}
```
> **Note**: If the file was already registered (same `content_hash`), the response returns `already_exists: true`.
---
## 3. Probe File
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}/probe" | \
jq '{name: .file_name, video: "\(.width)x\(.height)", fps, duration, cached, streams: [.streams[] | {type: .codec_type, codec: .codec_name}]}'
```
Response:
```json
{
"name": "Gamma 8-Director Chih-Lin Yang Shares His Experience:楊智麟導演經驗分享.mp4",
"video": "1280x720",
"fps": 29.97,
"duration": 298.665,
"cached": true,
"streams": [
{"type": "video", "codec": "h264"},
{"type": "audio", "codec": "aac"}
]
}
```
> **Error cases**:
> - Non-existent UUID → `{"error":"Video not found"}` + HTTP 404
> - File deleted from disk → `{"error":"File does not exist at registered path"}` + HTTP 404
---
## 4. Submit Processing Job
```bash
# Submit with specific processors
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"processors":["asr","cut","yolo","face","pose","ocr"]}' \
"$API/api/v1/file/${UUID}/process" | jq '{job_id, file_uuid: .file_uuid[0:16], status}'
```
Response:
```json
{
"job_id": 167,
"file_uuid": "078975658e04529e",
"status": "PENDING"
}
```
> **Submit all processors**: Send empty `{}` to run all processors.
> Available processors: `asr`, `cut`, `yolo`, `face`, `pose`, `ocr`, `asrx`, `visual_chunk`, `scene`, `story`, `caption`
---
## 5. Monitor Progress (Polling)
```bash
while true; do
PROGRESS=$(curl -sf -H "X-API-Key: $KEY" "$API/api/v1/progress/${UUID}")
STATUS=$(echo "$PROGRESS" | jq -r '.status // "?"')
PROCS=$(echo "$PROGRESS" | jq -r '[.processors[]? | "\(.name)=\(.status)(\(.frames_processed))"] | join(" ")')
echo "$(date +%H:%M:%S): $PROCS"
echo "$PROCS" | grep -q "completed" && break
sleep 10
done
```
Output:
```
12:30:01: asr=pending(0) cut=pending(0) yolo=pending(0) face=pending(0) pose=pending(0) ocr=pending(0)
12:30:11: asr=running(0) cut=running(0) yolo=pending(0) face=pending(0) pose=pending(0) ocr=pending(0)
12:30:21: asr=running(0) cut=completed(8951) yolo=running(0) face=pending(0) pose=pending(0) ocr=pending(0)
12:30:31: asr=running(0) cut=completed(8951) yolo=completed(8951) face=running(0) pose=pending(0) ocr=pending(0)
12:30:41: asr=running(0) cut=completed(8951) yolo=completed(8951) face=completed(8951) pose=running(0) ocr=pending(0)
12:30:51: asr=completed(8951) cut=completed(8951) yolo=completed(8951) face=completed(8951) pose=completed(8951) ocr=running(0)
12:31:01: asr=completed(8951) cut=completed(8951) yolo=completed(8951) face=completed(8951) pose=completed(8951) ocr=completed(8951)
```
**Status transitions**: `pending → running → completed`
Also monitor job state:
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/jobs?uuid=${UUID}" | \
jq '[.jobs[]? | {id, status}]'
```
---
## 6. Run Worker
```bash
DATABASE_SCHEMA=dev target/debug/momentry_playground worker \
--max-concurrent 2 --poll-interval 5
```
Worker output:
```
Starting worker with max_concurrent=2, poll_interval=5s
[CHECKSUMS] Loaded 345 entries from checksums.sha256
[INTEGRITY] asr_processor.py checksum OK
[ASR] Starting asr_processor.py
[ASR] Completed successfully
[INTEGRITY] cut_processor.py checksum OK
[CUT] Starting cut_processor.py
[CUT] Completed successfully
...
check_and_complete_job: results=6/6 → Job COMPLETED
```
---
## 7. Verify Progress
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/progress/${UUID}" | \
jq '{processors: [.processors[] | {name, status, frames: .frames_processed}]}'
```
Response:
```json
{
"processors": [
{"name": "asr", "status": "completed", "frames": 8951},
{"name": "cut", "status": "completed", "frames": 8951},
{"name": "yolo", "status": "completed", "frames": 8951},
{"name": "face", "status": "completed", "frames": 8951},
{"name": "pose", "status": "completed", "frames": 8951},
{"name": "ocr", "status": "completed", "frames": 8951}
]
}
```
---
## 8. Universal Search
```bash
# Search by text content
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"query\":\"導演\",\"uuid\":\"${UUID}\",\"limit\":5}" \
"$API/api/v1/search/universal" | \
jq '{total, hits: [.results[]? | {chunk_id: .chunk_id[0:40], text: .text[0:80], score}]}'
```
Response:
```json
{
"total": 5,
"hits": [
{"chunk_id": "078975658e04529ee06f8d11cd7ba226_39202", "text": "我 是 一 個 導 演", "score": 0.892},
{"chunk_id": "078975658e04529ee06f8d11cd7ba226_39203", "text": "我 是 一 個 導 演", "score": 0.890},
{"chunk_id": "078975658e04529ee06f8d11cd7ba226_39204", "text": "之前 在 拍 紀 錄 片", "score": 0.754}
]
}
```
Search by English text:
```bash
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"query\":\"camera\",\"uuid\":\"${UUID}\",\"limit\":3}" \
"$API/api/v1/search/universal" | jq '{total}'
```
---
## 9. Chunk Detail
```bash
# Get a specific chunk by its chunk_id
CHUNK_ID="078975658e04529ee06f8d11cd7ba226_39202"
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}/chunk/${CHUNK_ID}" | \
jq '{chunk_id, chunk_type, text: .text_content, fps, start_frame, end_frame, rule}'
```
Response:
```json
{
"chunk_id": "078975658e04529ee06f8d11cd7ba226_39202",
"chunk_type": "sentence",
"text": "我 是 一 個 導 演",
"fps": 29.97,
"start_frame": 60,
"end_frame": 120,
"rule": "rule1"
}
```
---
## 10. Chunk Fallback (Stale Qdrant)
If a chunk_id is an old integer format (e.g., from stale Qdrant payloads), the handler falls back to `WHERE id = int(chunk_id)`:
```bash
# Old integer format → resolves via id fallback
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}/chunk/39202" | \
jq '{chunk_id, text: .text_content}'
```
Response:
```json
{
"chunk_id": "078975658e04529ee06f8d11cd7ba226_39202",
"text": "我 是 一 個 導 演"
}
```
Both formats return the same chunk:
- `chunk/078975658e...226_39202` → exact `chunk_id` match
- `chunk/39202` → fallback by `id`
---
## 11. File Detail
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}" | \
jq '{file_name, status, duration, fps, file_type, width, height, total_frames}'
```
Response:
```json
{
"file_name": "Gamma 8-Director Chih-Lin Yang Shares His Experience:楊智麟導演經驗分享.mp4",
"status": "completed",
"duration": 298.665,
"fps": 29.97,
"file_type": "video",
"width": 1280,
"height": 720,
"total_frames": 8951
}
```
---
## 12. List Identities
```bash
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}/identities" | \
jq '{total, identities: [.data[]? | {name, face_count, confidence}]}'
```
Response:
```json
{
"total": 2,
"identities": [
{"name": "Chih-Lin Yang", "face_count": 847, "confidence": 0.93},
{"name": "Interviewer", "face_count": 312, "confidence": 0.87}
]
}
```
---
## 13. Schema & Integrity Verify
```bash
curl -sf "$API/health/detailed" | jq '{
ip, port, schema: .schema.ok,
migrations: [.schema.applied[]?.filename],
integrity: .pipeline.scripts_integrity
}'
```
Response:
```json
{
"ip": "192.168.110.201",
"port": 3003,
"schema": true,
"migrations": [
"migrate_add_content_hash.sql",
"migrate_add_registered_status.sql",
"migrate_add_schema_version.sql",
"migrate_cleanup_inactive_identities.sql",
"migrate_public_schema_v4_tables.sql",
"migrate_public_schema_v4.sql",
"migrate_public_v4_complete.sql",
"migrate_fix_chunk_id_format.sql",
"migrate_add_identity_indexes.sql"
],
"integrity": {
"matched": 345,
"total": 345,
"ok": true
}
}
```
---
## Full Automation Script
```bash
#!/bin/bash
set -euo pipefail
API="${API:-http://127.0.0.1:3003}"
KEY="${KEY:-muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69}"
# Health
curl -sf "$API/health" | jq '{status, version, build_git_hash}'
# Register
REG=$(curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"file_path":"'"$1"'"}' "$API/api/v1/files/register")
echo "$REG" | jq '{success, file_uuid, file_name}'
UUID=$(echo "$REG" | jq -r '.file_uuid')
# Probe
curl -sf -H "X-API-Key: $KEY" "$API/api/v1/file/${UUID}/probe" | \
jq '{name: .file_name, fps, duration}'
# Process
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{}' "$API/api/v1/file/${UUID}/process" | jq '{job_id, status}'
# Worker
DATABASE_SCHEMA=dev target/debug/momentry_playground worker \
--max-concurrent 2 --poll-interval 5 &
WPID=$!
# Wait
while true; do
PROGRESS=$(curl -sf -H "X-API-Key: $KEY" "$API/api/v1/progress/${UUID}")
STATUS=$(echo "$PROGRESS" | jq -r '.status')
echo "$(date +%H:%M:%S): $(echo "$PROGRESS" | jq -r '[.processors[]? | "\(.name)=\(.status)(\(.frames_processed))"] | join(" ")')"
echo "$PROGRESS" | jq -e '[.processors[]? | select(.status == "pending")] | length == 0' >/dev/null && break
sleep 10
done
kill $WPID 2>/dev/null || true
# Search
curl -sf -X POST -H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d "{\"query\":\"test\",\"uuid\":\"${UUID}\",\"limit\":3}" \
"$API/api/v1/search/universal" | jq '{total, hits: [.results[]? | {chunk_id: .chunk_id[0:30], text: .text[0:60]}]}'
echo "Done: $UUID"
```

View File

@@ -0,0 +1,499 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Momentry 使用手冊"
date: "2026-03-21"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "momentry"
- "使用手冊"
ai_query_hints:
- "查詢 Momentry 使用手冊 的內容"
- "Momentry 使用手冊 的主要目的是什麼?"
- "如何操作或實施 Momentry 使用手冊?"
---
# Momentry 使用手冊
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-21 |
| 文件版本 | V1.0 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-21 | 創建使用手冊 | Warren | OpenCode |
---
**目標讀者**: 系統管理員、開發者
---
## 目錄
1. [快速開始](#1-快速開始)
2. [安裝與設定](#2-安裝與設定)
3. [CLI 命令參考](#3-cli-命令參考)
4. [影片管理](#4-影片管理)
5. [API Key 管理](#5-api-key-管理)
6. [第三方整合](#6-第三方整合)
7. [監控與維護](#7-監控與維護)
8. [疑難排解](#8-疑難排解)
---
## 1. 快速開始
### 1.1 最小啟動流程
```bash
# 1. 啟動服務
sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
# 2. 設定環境變數
source .env
# 3. 啟動 API 伺服器
momentry server --host 127.0.0.1 --port 3000
# 4. 建立 API Key
momentry api-key create my-first-key --key-type user --ttl 90
```
### 1.2 驗證安裝
```bash
# 檢查系統狀態
curl http://localhost:3002/health
# 檢查版本
momentry --help
```
---
## 2. 安裝與設定
### 2.1 環境需求
| 項目 | 需求 |
|------|------|
| 作業系統 | macOS (Apple Silicon) |
| Rust | 1.70+ |
| PostgreSQL | 15+ |
| Redis | 7+ |
| Python | 3.11+ (用於 AI 處理) |
### 2.2 安裝步驟
```bash
# 1. 複製專案
git clone <repository-url>
cd momentry_core_0.1
# 2. 編譯
cargo build --release
# 3. 安裝到系統
cp target/release/momentry /usr/local/bin/
```
### 2.3 環境變數設定
建立 `.env` 檔案:
```bash
# Database
DATABASE_URL=postgres://accusys@localhost:5432/momentry
# Redis
REDIS_URL=redis://:accusys@localhost:6379
# Gitea (選用)
GITEA_URL=http://localhost:3000
# n8n (選用)
N8N_URL=https://n8n.momentry.ddns.net
# API Server
API_HOST=127.0.0.1
API_PORT=3000
# 監控目錄
WATCH_DIRECTORIES=~/Videos
```
---
## 3. CLI 命令參考
### 3.1 一般命令
| 命令 | 說明 |
|------|------|
| `momentry --help` | 顯示幫助 |
| `momentry server` | 啟動 API 伺服器 |
| `momentry register <path>` | 註冊影片 |
| `momentry process <uuid>` | 處理影片 |
| `momentry query <text>` | RAG 查詢 |
### 3.2 影片管理
```bash
# 註冊影片
momentry register /path/to/video.mp4
# 處理影片
momentry process <uuid>
# 生成縮圖
momentry thumbnails <uuid> --count 6
# 查看狀態
momentry status <uuid>
```
### 3.3 API Key 管理
```bash
# 建立 Key
momentry api-key create <name> --key-type <type> --ttl <days>
# 列出 Keys
momentry api-key list
# 驗證 Key
momentry api-key validate --key <key>
# 撤銷 Key
momentry api-key revoke --key <key>
# 請求輪換
momentry api-key rotate --key <key>
# 統計資訊
momentry api-key stats
```
### 3.4 Gitea 整合
```bash
# 建立 Token
momentry gitea create \
--username <user> \
--password <pwd> \
--token-name <name> \
--scopes "read:repository,write:repository"
# 列出 Tokens
momentry gitea list --username <user> --password <pwd>
# 刪除 Token
momentry gitea delete \
--username <user> \
--password <pwd> \
--token-name <name>
```
### 3.5 n8n 整合
```bash
# 建立 API Key
momentry n8n create \
--api-key <existing-key> \
--label <name> \
--expires-in-days 90
# 列出 Keys
momentry n8n list --api-key <key>
# 刪除 Key
momentry n8n delete --api-key <key> --label <name>
```
### 3.6 備份管理
```bash
# 列出備份
momentry backup list
# 清理舊備份
momentry backup cleanup --days 30
```
---
## 4. 影片管理
### 4.1 影片生命週期
```
上傳 → 註冊 → 處理 → 儲存 → 查詢
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
檔案 資料庫 AI分析 向量庫 RAG
```
### 4.2 註冊影片
```bash
# 自動偵測格式
momentry register ~/Videos/my-video.mp4
# 輸出:
# UUID: a1b2c3d4e5f6g7h8
# Duration: 120.5s
# Resolution: 1920x1080
```
### 4.3 處理流程
處理包含以下階段:
| 階段 | 說明 | 時間 (約) |
|------|------|-----------|
| Probe | 影片資訊分析 | 5s |
| ASR | 語音辨識 | 視長度 |
| OCR | 文字辨識 | 視長度 |
| YOLO | 物件偵測 | 視長度 |
| Cut | 場景切割 | 30s |
| Chunk | 內容分段 | 10s |
| Vector | 向量化 | 20s |
### 4.4 查詢影片
```bash
# RAG 查詢
momentry query "影片中有什麼內容?"
# 取得特定影片
momentry resolve <uuid>
```
---
## 5. API Key 管理
### 5.1 Key 類型
| 類型 | 前綴 | 用途 | 預設 TTL |
|------|------|------|----------|
| System | `msys_` | 系統內部 | 365 天 |
| User | `muser_` | 個人用戶 | 90 天 |
| Service | `msvc_` | 服務間通訊 | 180 天 |
| Integration | `mint_` | 第三方整合 | 30 天 |
| Emergency | `memg_` | 緊急存取 | 1 天 |
### 5.2 建立 Key
```bash
# 一般 Key
momentry api-key create my-service --key-type service --ttl 90
# 緊急 Key (24小時有效)
momentry api-key create emergency-access --key-type emergency
# 輸出:
# ✅ API Key created successfully!
# Key ID: msvc_xxxxxxxx
# API Key: msvc_xxxxxxxx_xxxxx_xxxxx
# Expires: 2026-06-19
```
### 5.3 Key 生命週期
```
建立 → 使用 → 過期/撤銷 → 清理
│ │ │ │
▼ ▼ ▼ ▼
資料庫 驗證 停用 定期刪除
```
### 5.4 安全建議
| 建議 | 說明 |
|------|------|
| 定期輪換 | 每 90 天更新 Key |
| 最小權限 | 只授予必要權限 |
| 監控使用 | 定期檢查使用統計 |
| 及時撤銷 | 異常時立即撤銷 |
---
## 6. 第三方整合
### 6.1 Gitea
```bash
# 建立 CI/CD 用 Token
momentry gitea create \
--username admin \
--password "your-password" \
--token-name "ci-pipeline" \
--scopes "read:repository,write:repository"
# 在 CI 中使用
export GITEA_TOKEN="token-sha1-value"
curl -H "Authorization: token $GITEA_TOKEN" \
http://localhost:3000/api/v1/user
```
### 6.2 n8n
```bash
# 建立工作流用 Key
momentry n8n create \
--api-key "existing-n8n-key" \
--label "workflow-key" \
--expires-in-days 90
# 在 n8n 中使用
# HTTP Request Header: X-N8N-API-Key: <key>
```
### 6.3 Webhook 通知
```bash
# 設定 Webhook
export WEBHOOK_URL="https://n8n.example.com/webhook/alerts"
export WEBHOOK_EVENTS="anomaly_detected,key_expired"
```
---
## 7. 監控與維護
### 7.1 系統監控
```bash
# 檢查服務狀態
ps aux | grep momentry
ps aux | grep postgres
redis-cli -a accusys ping
# 檢查日誌
tail -f /Users/accusys/momentry/log/momentry.log
tail -f /Users/accusys/momentry/log/redis.log
```
### 7.2 資料庫維護
```bash
# 檢查資料庫大小
psql -U accusys -d momentry -c "SELECT pg_size_pretty(pg_database_size('momentry'));"
# 清理過期記錄
momentry api-key stats # 檢查統計
# 定期清理由系統自動執行
```
### 7.3 備份
```bash
# 手動備份 PostgreSQL
pg_dump -U accusys momentry > backup_$(date +%Y%m%d).sql
# 恢復備份
psql -U accusys momentry < backup_20260321.sql
```
---
## 8. 疑難排解
### 8.1 常見問題
#### Q: 無法連接資料庫
```bash
# 檢查 PostgreSQL 狀態
pg_isready -h localhost -p 5432
# 檢查連線
psql -U accusys -d momentry -c "SELECT 1;"
```
#### Q: Redis 連線失敗
```bash
# 檢查 Redis 狀態
redis-cli -a accusys ping
# 檢查認證
redis-cli -a accusys INFO server | grep redis_version
```
#### Q: API Key 驗證失敗
```bash
# 檢查 Key 狀態
momentry api-key validate --key "your-key"
# 檢查是否過期
momentry api-key list
```
### 8.2 錯誤碼對照
| 錯誤碼 | 說明 | 解決方式 |
|--------|------|----------|
| `E001` | 資料庫連線失敗 | 檢查 PostgreSQL |
| `E002` | Redis 連線失敗 | 檢查 Redis |
| `E003` | API Key 無效 | 重新建立 Key |
| `E004` | 影片不存在 | 檢查 UUID |
| `E005` | 處理失敗 | 檢查日誌 |
### 8.3 日誌位置
| 日誌 | 路徑 |
|------|------|
| Momentry | `/Users/accusys/momentry/log/momentry.log` |
| PostgreSQL | `/Users/accusys/momentry/log/postgresql.log` |
| Redis | `/Users/accusys/momentry/log/redis.log` |
| Gitea | `/Users/accusys/momentry/log/gitea.log` |
---
## 附錄
### A. 完整命令列表
```bash
momentry --help
momentry register --help
momentry process --help
momentry api-key --help
momentry gitea --help
momentry n8n --help
momentry backup --help
```
### B. 環境變數總覽
| 變數 | 預設值 | 說明 |
|------|--------|------|
| `DATABASE_URL` | `postgres://accusys@localhost:5432/momentry` | PostgreSQL |
| `REDIS_URL` | `redis://:accusys@localhost:6379` | Redis |
| `GITEA_URL` | `http://localhost:3000` | Gitea |
| `N8N_URL` | `https://n8n.momentry.ddns.net` | n8n |
| `API_HOST` | `127.0.0.1` | API 主機 |
| `API_PORT` | `3000` | API 埠號 |
### C. 相關文件
| 文件 | 說明 |
|------|------|
| `docs_v1.0/IMPLEMENTATION/API_CURL_EXAMPLES.md` | API curl 範例 |
| `docs_v1.0/IMPLEMENTATION/N8N_INTEGRATION_GUIDE.md` | n8n 整合指南 |
| `docs_v1.0/REFERENCE/API_KEY_MANAGEMENT.md` | API Key 設計 |
| `CHANGELOG.md` | 版本記錄 |

View File

@@ -0,0 +1,127 @@
{
"info": {
"name": "Momentry Core API",
"description": "Video RAG API for Momentry Core - Video search and management",
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
},
"variable": [
{
"key": "baseUrl",
"value": "http://localhost:3002/api/v1"
}
],
"item": [
{
"name": "Search",
"item": [
{
"name": "Semantic Search",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"query\": \"charade\",\n \"limit\": 5\n}"
},
"url": {
"raw": "{{baseUrl}}/search",
"host": ["{{baseUrl}}"],
"path": ["search"]
},
"description": "Semantic search across video chunks using vector embeddings"
}
},
{
"name": "Search with UUID Filter",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"query\": \"charade\",\n \"limit\": 5,\n \"uuid\": \"a1b10138a6bbb0cd\"\n}"
},
"url": {
"raw": "{{baseUrl}}/search",
"host": ["{{baseUrl}}"],
"path": ["search"]
},
"description": "Search within a specific video by UUID"
}
},
{
"name": "n8n Integration Search",
"request": {
"method": "POST",
"header": [
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"query\": \"charade\",\n \"limit\": 5\n}"
},
"url": {
"raw": "{{baseUrl}}/n8n/search",
"host": ["{{baseUrl}}"],
"path": ["n8n", "search"]
},
"description": "Search formatted for n8n workflow integration"
}
}
]
},
{
"name": "Videos",
"item": [
{
"name": "List All Videos",
"request": {
"method": "GET",
"url": {
"raw": "{{baseUrl}}/videos",
"host": ["{{baseUrl}}"],
"path": ["videos"]
},
"description": "Get list of all registered videos"
}
},
{
"name": "Get Video by UUID",
"request": {
"method": "GET",
"url": {
"raw": "{{baseUrl}}/videos/a1b10138a6bbb0cd",
"host": ["{{baseUrl}}"],
"path": ["videos", "a1b10138a6bbb0cd"]
},
"description": "Get details for a specific video"
}
},
{
"name": "Get Video Chunks",
"request": {
"method": "GET",
"url": {
"raw": "{{baseUrl}}/videos/a1b10138a6bbb0cd/chunks",
"host": ["{{baseUrl}}"],
"path": ["videos", "a1b10138a6bbb0cd", "chunks"]
},
"description": "Get all chunks for a video"
}
}
]
}
]
}

View File

@@ -0,0 +1,106 @@
# n8n REST API Fix Summary
## Issue
n8n REST API was returning 404 errors for all endpoints (`/api/v1/workflows`, `/rest/workflows`, etc.)
## Root Cause
Port 5678 was occupied by the **n8n worker** process, preventing the main n8n instance from starting properly.
## Solution
### 1. Identified Port Conflict
- Worker process was listening on port 5678 (same as main instance)
- Main n8n couldn't start because port was in use
### 2. Fixed Worker Configuration
Updated `/Library/LaunchDaemons/com.momentry.n8n.worker.plist`:
- Added `N8N_PORT=5680` to worker environment variables
- Workers shouldn't need HTTP ports, but this prevents port conflict
### 3. Restarted Services
```bash
# Kill all n8n processes
sudo pkill -9 -f n8n
# Start main n8n (now successfully binds to port 5678)
sudo launchctl enable system/com.momentry.n8n.main
sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.n8n.main.plist
```
## Current Status
### n8n Instance
- **URL**: http://localhost:5678
- **Version**: 2.3.5
- **Status**: Running ✅
- **API Enabled**: Yes ✅
### API Key
```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlNjdiY2UzOS1iY2RkLTRjMjEtYmMwYy0yODNhYmI3ZjVjMjMiLCJpc3MiOiJuOG4iLCJhdWQiOiJwdWJsaWMtYXBpIiwiaWF0IjoxNzc0MTk4NzgwfQ.zke_Qc-saILl_tcwXm2K3J4slCmaXnzCfxVbdVPPvCE
```
### MCP Configuration
File: `~/.config/opencode/opencode.json`
```json
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"gitea": {
"type": "local",
"enabled": true,
"command": [
"/opt/homebrew/bin/gitea-mcp-server",
"-token", "<GITEA_TOKEN>",
"-host", "http://localhost:3000"
]
},
"n8n": {
"type": "local",
"enabled": true,
"command": ["/opt/homebrew/bin/mcp-n8n"],
"environment": {
"N8N_BASE_URL": "http://localhost:5678",
"N8N_API_KEY": "<N8N_API_KEY>"
}
}
}
}
```
## Verified Endpoints
### List Workflows
```bash
curl -H "X-N8N-API-KEY: <API_KEY>" http://localhost:5678/api/v1/workflows
```
**Result**: ✅ 30 workflows returned
### List Executions
```bash
curl -H "X-N8N-API-KEY: <API_KEY>" http://localhost:5678/api/v1/executions
```
**Result**: ✅ 100 executions returned
## Next Steps
1. **Start n8n Worker** (optional for MCP):
Workers handle job processing but aren't required for API access.
2. **Test MCP Integration**:
Restart OpenCode to load the MCP configuration and test n8n integration.
3. **Verify Workflow Management**:
- Create workflow via API
- Execute workflow
- Monitor execution status
## Files Modified
- `/Library/LaunchDaemons/com.momentry.n8n.worker.plist` - Added N8N_PORT=5680
## API Documentation
- Base URL: `http://localhost:5678/api/v1`
- Authentication: Header `X-N8N-API-KEY: <token>`
- Available endpoints: workflows, executions, credentials, users, etc.
See full API reference: https://docs.n8n.io/api/

View File

@@ -0,0 +1,321 @@
---
document_type: "reference_doc"
service: "N8N"
title: "n8n Video Search 工作流程 - 成功設定指南"
date: "2026-03-22"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "成功設定指南"
- "video"
- "工作流程"
- "search"
ai_query_hints:
- "查詢 n8n Video Search 工作流程 - 成功設定指南 的內容"
- "n8n Video Search 工作流程 - 成功設定指南 的主要目的是什麼?"
- "如何操作或實施 n8n Video Search 工作流程 - 成功設定指南?"
---
# n8n Video Search 工作流程 - 成功設定指南
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-22 |
| 文件版本 | V1.1 |
| 適用版本 | n8n 2.3.5 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-22 | 創建文件 | Warren | OpenCode |
| V1.1 | 2026-03-26 | 更新 API 範例,新增 X-API-Key 驗證標頭 | OpenCode | deepseek-reasoner |
---
## ✅ 成功案例
| 項目 | 內容 |
|------|------|
| **工作流程名稱** | Video Search - Working v3 |
| **ID** | 4vQo8I4SXEaR5E1A |
| **狀態** | ✅ 成功運作 |
| **測試結果** | 成功搜尋 "charade",返回 3 個結果 |
---
## 正確的 HTTP Request Node 設定
### 成功的設定方式
```json
{
"url": "https://api.momentry.ddns.net/api/v1/n8n/search",
"method": "POST",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"query\":\"charade\",\"limit\":3}",
"options": {
"headers": {
"X-API-Key": "muser_68600856036340bcafc01930eb4bd839"
}
}
}
```
### 關鍵設定說明
| 設定項目 | 正確值 | 錯誤值 | 說明 |
|---------|--------|--------|------|
| **specifyBody** | `"json"` | `"body"` | 必須選擇 `"json"` |
| **jsonBody** | 字串 `"{...}"` | 物件 `{}` | 必須是 JSON 字串格式 |
| **轉義** | `\"query\"` | `"query"` | 引號需要轉義 |
---
## 工作流程架構
```
┌─────────────────────────┐
│ Manual Trigger │ ← 點擊 "Execute Workflow"
└───────────┬─────────────┘
┌─────────────────────────┐
│ HTTP Request Node │ ← 呼叫 Momentry API
│ - specifyBody: "json" │
│ - jsonBody: "{...}" │
└───────────┬─────────────┘
┌─────────────────────────┐
│ Code Node │ ← 格式化輸出
│ - console.log() │
│ - return json │
└─────────────────────────┘
```
---
## 完整的 Workflow JSON
```json
{
"name": "Video Search - Working v3",
"nodes": [
{
"parameters": {},
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [250, 300]
},
{
"parameters": {
"url": "https://api.momentry.ddns.net/api/v1/n8n/search",
"method": "POST",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "{\"query\":\"charade\",\"limit\":3}",
"options": {
"headers": {
"X-API-Key": "muser_68600856036340bcafc01930eb4bd839"
}
}
},
"name": "Search API",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [450, 300]
},
{
"parameters": {
"jsCode": "const data = $input.first().json;\nconsole.log('Response:', JSON.stringify(data, null, 2));\nreturn [{ json: data }];"
},
"name": "Show Result",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [650, 300]
}
],
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [[{"node": "Search API", "type": "main", "index": 0}]]
},
"Search API": {
"main": [[{"node": "Show Result", "type": "main", "index": 0}]]
}
},
"settings": {"executionOrder": "v1"},
"staticData": null
}
```
---
## 使用步驟
### 步驟 1: 導入工作流程
1. 開啟 n8n UI: `https://n8n.momentry.ddns.net`
2. 點擊 **Add Workflow** (+)
3. 點擊 **Import from File**
4. 選擇上面的 JSON 檔案
### 步驟 2: 執行測試
1. 點擊 **"Execute Workflow"** 按鈕 (▶️)
2. 等待執行完成
3. 點擊 **"Show Result"** 節點
4. 查看右側 **JSON** 面板
### 步驟 3: 修改搜尋關鍵字
1. 點擊 **"Search API"** 節點
2. 修改 `jsonBody`:
```json
"{\"query\":\"您的關鍵字\",\"limit\":5}"
```
3. 點擊 **Save** (Ctrl+S)
4. 重新執行
---
## 成功的回應範例
```json
{
"query": "charade",
"count": 3,
"hits": [
{
"id": "sentence_0006",
"vid": "a1b10138a6bbb0cd",
"start": 48.8,
"end": 55.44,
"title": "Chunk sentence_0006",
"text": "fun plot twists, Woody Dialog and charming performances...",
"score": 0.526,
"file_path": "/Users/accusys/momentry/var/sftpgo/data/demo/Old_Time_Movie_Show_-_Charade_1963.HD.mov"
}
]
}
```
---
## 常見錯誤與解決
### ❌ 錯誤 1: "Your request is invalid"
**原因**: `specifyBody` 設為 `"body"` 而不是 `"json"`
**解決**:
```json
✅ "specifyBody": "json"
❌ "specifyBody": "body"
```
### ❌ 錯誤 2: "$httpRequest is not defined"
**原因**: Code Node 中使用 `$httpRequest`,但您的 n8n 版本不支援
**解決**: 使用 **HTTP Request Node** 代替 Code Node
### ❌ 錯誤 3: Body 格式錯誤
**原因**: `body` 使用物件格式 `{query: "..."}`
**解決**: 使用 `jsonBody` 字串格式 `{"query":"..."}`
### ❌ 錯誤 4: JSON 引號未轉義
**原因**: `"{query: "charade"}"` - 引號衝突
**解決**: `\"` 轉義 `"{\"query\":\"charade\"}"`
---
## 測試指令
### 直接測試 API
```bash
curl -X POST https://api.momentry.ddns.net/api/v1/n8n/search \
-H "Content-Type: application/json" \
-H "X-API-Key: muser_68600856036340bcafc01930eb4bd839" \
-d '{"query":"charade","limit":3}'
```
### 驗證服務狀態
```bash
# 檢查 Momentry Core
curl -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839" https://api.momentry.ddns.net/api/v1/videos
# 檢查 n8n
curl http://localhost:5678/api/v1/workflows \
-H "X-N8N-API-KEY: <your_api_key>"
```
---
## 服務資訊
| 服務 | URL | 說明 |
|------|-----|------|
| **n8n UI** | https://n8n.momentry.ddns.net | 工作流程管理 |
| **Momentry API** | https://api.momentry.ddns.net | 影片搜尋 API |
| **工作流程** | https://n8n.momentry.ddns.net/workflow/4vQo8I4SXEaR5E1A | 成功案例 |
---
## 進階使用
### 添加 Webhook 觸發器
如果你想從外部呼叫這個工作流程:
1. 在第一個節點前添加 **Webhook** Node
2. 設定:
```
Method: POST
Path: video-search
Response Mode: Last Node
```
3. 將 Webhook 連接到 Search API
4. 儲存並執行
5. 使用生成的 Webhook URL 呼叫:
```bash
curl -X POST <webhook_url> \
-d '{"query":"charade","limit":3}'
```
### 使用動態變數
修改 jsonBody 使用表達式:
```json
"{\"query\":\"={{ $json.query }}\",\"limit\":{{ $json.limit }}}"
```
然後在前面添加 Set Node 設定變數。
---
## 相關文件
- `docs_v1.0/IMPLEMENTATION/N8N_SETUP_COMPLETE.md` - 完整設定總結
- `docs_v1.0/IMPLEMENTATION/N8N_HTTP_REQUEST_GUIDE.md` - HTTP Request 詳細指南
- `docs/API_URL_EXAMPLES.md` - API URL 範例
---
## 完成!🎉
您現在擁有一個可以成功搜尋影片的 n8n 工作流程!
**關鍵成功要素**:
1. ✅ 使用 `specifyBody: "json"`
2. ✅ 使用 `jsonBody` 字串格式
3. ✅ 正確轉義 JSON 引號
4. ✅ 使用外部 API URL (`https://api.momentry.ddns.net`)

View File

@@ -0,0 +1,818 @@
{
"id": "o9MZ3XaJ5Vyf4kJ9",
"name": "Momentry Search API - Core v1.2",
"active": true,
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "search",
"responseMode": "responseNode",
"options": {}
},
"id": "b5f92603-1071-42e0-85b1-6c1655f9c992",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-384,
1408
],
"webhookId": "79d6584e-c37c-49c4-9e83-c72896cef416"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"name": "query",
"value": "={{$json.body.query || $json.query}}",
"type": "string",
"id": "f9a4b19c-b478-4fd3-9969-cd78298d2138"
},
{
"name": "mode",
"value": "={{ $json.body?.mode || $json.mode || \"mock\" }}",
"type": "string",
"id": "59066dec-1e8e-4339-ab5e-ce538c0dc216"
},
{
"name": "request_source",
"value": "portal",
"type": "string",
"id": "1ff615c2-23b2-45af-a580-9d27a6a8d4bc"
},
{
"name": "limit",
"value": 10,
"type": "number",
"id": "16adb464-079b-4a91-bbb9-bd9b44db83ed"
},
{
"id": "d298ad4d-4fab-41f0-a11a-7d089cf78ae3",
"name": "query_normalized",
"value": "={{ ($json.body.query || $json.query || \"\").toLowerCase().trim() }}",
"type": "string"
}
]
},
"options": {}
},
"id": "fadff1c5-038b-4aa7-ad18-957440d0942c",
"name": "Build Search Request",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-160,
1408
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{$json.query_normalized}}",
"rightValue": "sun",
"operator": {
"type": "string",
"operation": "contains"
},
"id": "54556e33-0dff-4e3d-ada0-81ea89f422c2"
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{$json.query_normalized}}",
"rightValue": "morning",
"operator": {
"type": "string",
"operation": "contains"
},
"id": "cca4c190-d840-486c-937f-221d62fcf05f"
}
],
"combinator": "and"
}
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "88239dfd-773c-4337-9f2e-521e6368305b",
"leftValue": "={{$json.query_normalized}}",
"rightValue": "error",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
}
}
]
},
"options": {
"fallbackOutput": "extra"
}
},
"id": "613caa5d-d3f7-4099-a5f3-5c642264d32c",
"name": "Search Adapter (Mock Router)",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
512,
1664
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"name": "query",
"value": "={{$json.query}}",
"type": "string",
"id": "7af70495-2dbb-4e7b-aed3-79cd7e970183"
},
{
"name": "search_results",
"value": "={{[\n {\n \"moment_id\": \"m001\",\n \"video_id\": \"v001\",\n \"t_start\": 3,\n \"t_end\": 8,\n \"title\": \"Sunset moment\",\n \"snippet\": \"The sun slowly sets over the sea\",\n \"score\": 0.92,\n \"video_url\": \"https://wp.momentry.ddns.net/wp-content/uploads/2026/03/H_moment3.mp4\"\n },\n {\n \"moment_id\": \"m002\",\n \"video_id\": \"v002\",\n \"t_start\": 3,\n \"t_end\": 7,\n \"title\": \"Morning sunlight\",\n \"snippet\": \"Sunlight enters the room\",\n \"score\": 0.88,\n \"video_url\": \"https://wp.momentry.ddns.net/wp-content/uploads/2026/03/O_moment3.mp4\"\n }\n]}}",
"type": "array",
"id": "e504493f-0dec-4a80-b434-7586059159f0"
}
]
},
"options": {}
},
"id": "38b6c814-3e60-45e1-a31f-b93fcf9b2fd5",
"name": "Mock Sun Results",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
736,
1456
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"name": "query",
"value": "={{$json.query}}",
"type": "string",
"id": "137b4cd7-5ef8-41f2-94bc-921c9040e0e5"
},
{
"name": "search_results",
"value": "={{[\n {\n \"moment_id\": \"m002\",\n \"video_id\": \"v002\",\n \"t_start\": 3,\n \"t_end\": 7,\n \"title\": \"Morning sunlight\",\n \"snippet\": \"Sunlight enters the room\",\n \"score\": 0.88,\n \"video_url\": \"https://wp.momentry.ddns.net/wp-content/uploads/2026/03/H_moment3.mp4\"\n }\n]}}",
"type": "array",
"id": "181df2c1-9469-4d3d-9f23-2ca839091583"
}
]
},
"options": {}
},
"id": "e0bc4873-b4a7-4cdc-a4e8-01aef82a79b8",
"name": "Mock Morning Results",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
736,
1648
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"name": "query",
"value": "={{$json.query}}",
"type": "string",
"id": "be01ea1f-f92f-41c7-8dab-f998e7c07bec"
},
{
"name": "search_results",
"value": "={{ [] }}",
"type": "array",
"id": "21e46d9d-0a35-4641-b936-806713ff021c"
}
]
},
"options": {}
},
"id": "e7c3a7b0-6cee-4832-b6ad-9cdc30f59131",
"name": "Mock Empty Results",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
736,
2032
]
},
{
"parameters": {
"assignments": {
"assignments": [
{
"name": "query",
"value": "={{$json.query}}",
"type": "string"
},
{
"name": "total",
"value": "={{$json.search_results.length}}",
"type": "number"
},
{
"name": "results",
"value": "={{$json.search_results}}",
"type": "array"
}
]
},
"options": {}
},
"id": "e746705c-ddfc-4c6b-904b-a5a8ed5d7420",
"name": "Format Search Response",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
960,
1648
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { \"query\": $json.query, \"total\": $json.total, \"results\": $json.results } }}",
"options": {}
},
"id": "d7f9806c-ad9f-4e56-bbdc-a943f1d416fd",
"name": "Respond Search",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
1184,
1168
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "99d7d111-05ae-42aa-8b86-a5de7a7fdb52",
"leftValue": "={{ $json.mode }}",
"rightValue": "mock",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "mock"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "d24d518d-bd6b-4b99-a4c9-cc98c49027ca",
"leftValue": "={{ $json.mode }}",
"rightValue": "core_stub",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "core_stub"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
64,
1408
],
"id": "550273ba-8d33-4653-a26b-2b83218ce993",
"name": "Mode Router"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c2c253a2-689c-4d8f-92d1-017dacbf1eca",
"name": "error",
"value": true,
"type": "boolean"
},
{
"id": "523b04a9-9843-4542-846d-d027f697ceff",
"name": "message",
"value": "mock backend error",
"type": "string"
},
{
"id": "ef30be3a-cdf7-4461-90b9-6d3ee6a88ba3",
"name": "query",
"value": "={{$json.query}}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
736,
1840
],
"id": "be069c6a-da6b-4a1b-a930-b535818b9ae2",
"name": "Mock Error Response"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { \"error\": true, \"message\": $json.message, \"query\": $json.query } }}",
"options": {
"responseCode": 500
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
960,
1888
],
"id": "6777eded-6c8e-426d-82b2-550580f74591",
"name": "Respond Search Error"
},
{
"parameters": {
"jsCode": "const query = $json.query ?? \"\";\nconst hits = Array.isArray($json.hits) ? $json.hits : [];\n\nreturn [\n {\n json: {\n query,\n total: typeof $json.count === \"number\" ? $json.count : hits.length,\n results: hits.map(hit => ({\n moment_id: hit.id ?? \"\",\n video_id: hit.vid ?? \"\",\n t_start: Number(hit.start ?? 0),\n t_end: Number(hit.end ?? 0),\n title: hit.title ?? \"\",\n snippet: hit.text ?? \"\",\n score: Number(hit.score ?? 0),\n video_url: hit.media_url ?? \"\"\n }))\n }\n }\n];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
960,
1120
],
"id": "34662909-6e7a-4cf4-967b-1f474748777a",
"name": "Normalize Core Response"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "c51bf73f-e1e0-4811-985c-4de5ec70b9d8",
"leftValue": "={{ $json.error ? \"yes\" : \"no\" }}",
"rightValue": "=yes",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
736,
1168
],
"id": "cb6a5372-05a5-4f1b-ae5e-3b33bf009d4c",
"name": "Check Core Error"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "fad95504-6182-4d4c-bc95-8927c25c7f31",
"name": "ok",
"value": false,
"type": "boolean"
},
{
"id": "d267371c-90d8-401e-8180-a753e807441b",
"name": "error_code",
"value": "={{ $json.error?.code === \"ECONNABORTED\" ? \"SEARCH_TIMEOUT\" : \"SEARCH_BACKEND_ERROR\" }}",
"type": "string"
},
{
"id": "4992c4d9-e61d-453a-a826-8895374f56b8",
"name": "message",
"value": "={{ $json.error?.code === \"ECONNABORTED\" ? \"Search service timeout\" : \"Search service unavailable\" }}",
"type": "string"
},
{
"id": "56b9fbfa-4c98-4265-b992-498e3edf733b",
"name": "query",
"value": "={{ $node[\"Build Search Request\"].json.query || \"unknown\" }}",
"type": "string"
},
{
"id": "c50010c1-a0c8-47b9-a763-e7212a8db93c",
"name": "results",
"value": "={{ [] }}",
"type": "array"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
960,
1312
],
"id": "1c811ef9-91da-465c-9121-6247549d16fc",
"name": "Map Search Error"
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ {\n \"ok\": $json.ok,\n \"error_code\": $json.error_code,\n \"message\": $json.message,\n \"query\": $json.query,\n \"results\": $json.results\n} }}",
"options": {
"responseCode": 500
}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1184,
1360
],
"id": "797824a7-85ac-40d9-bd2b-42ea11dfd1a1",
"name": "Respond Search Error1"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "c08206fb-5d74-40f0-a682-f2116c290af1",
"name": "query",
"value": "={{ $json.query }}",
"type": "string"
},
{
"id": "85e83fbc-b4f0-4b22-8d37-516609387918",
"name": "limit",
"value": "={{ Number($json.limit || 10) }}",
"type": "number"
},
{
"id": "823706fe-288a-4a71-a498-0ad3daad5081",
"name": "mode",
"value": "={{ $json.mode }}",
"type": "string"
}
]
},
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
288,
1168
],
"id": "89366f65-c36a-4b19-9970-706a5d729594",
"name": "Prepare Core Request Context"
},
{
"parameters": {
"method": "POST",
"url": "https://api.momentry.ddns.net/api/v1/n8n/search",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ {\n \"query\": $json.query,\n \"limit\": $json.limit,\n \"request_query\": $json.query\n} }}",
"options": {
"timeout": 3000
}
},
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.3,
"position": [
512,
1168
],
"id": "d5ea8f1b-5bd7-4c88-9c38-70e5b14ca1d8",
"name": "Call Momentry Core API",
"onError": "continueRegularOutput"
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "Build Search Request",
"type": "main",
"index": 0
}
]
]
},
"Build Search Request": {
"main": [
[
{
"node": "Mode Router",
"type": "main",
"index": 0
}
]
]
},
"Search Adapter (Mock Router)": {
"main": [
[
{
"node": "Mock Sun Results",
"type": "main",
"index": 0
}
],
[
{
"node": "Mock Morning Results",
"type": "main",
"index": 0
}
],
[
{
"node": "Mock Error Response",
"type": "main",
"index": 0
}
],
[
{
"node": "Mock Empty Results",
"type": "main",
"index": 0
}
]
]
},
"Mock Sun Results": {
"main": [
[
{
"node": "Format Search Response",
"type": "main",
"index": 0
}
]
]
},
"Mock Morning Results": {
"main": [
[
{
"node": "Format Search Response",
"type": "main",
"index": 0
}
]
]
},
"Mock Empty Results": {
"main": [
[
{
"node": "Format Search Response",
"type": "main",
"index": 0
}
]
]
},
"Format Search Response": {
"main": [
[
{
"node": "Respond Search",
"type": "main",
"index": 0
}
]
]
},
"Mode Router": {
"main": [
[
{
"node": "Search Adapter (Mock Router)",
"type": "main",
"index": 0
}
],
[
{
"node": "Prepare Core Request Context",
"type": "main",
"index": 0
}
]
]
},
"Mock Error Response": {
"main": [
[
{
"node": "Respond Search Error",
"type": "main",
"index": 0
}
]
]
},
"Normalize Core Response": {
"main": [
[
{
"node": "Respond Search",
"type": "main",
"index": 0
}
]
]
},
"Check Core Error": {
"main": [
[
{
"node": "Map Search Error",
"type": "main",
"index": 0
}
],
[
{
"node": "Normalize Core Response",
"type": "main",
"index": 0
}
]
]
},
"Map Search Error": {
"main": [
[
{
"node": "Respond Search Error1",
"type": "main",
"index": 0
}
]
]
},
"Prepare Core Request Context": {
"main": [
[
{
"node": "Call Momentry Core API",
"type": "main",
"index": 0
}
]
]
},
"Call Momentry Core API": {
"main": [
[
{
"node": "Check Core Error",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"availableInMCP": false,
"binaryMode": "separate"
},
"staticData": null,
"pinData": {
"Webhook": [
{
"json": {
"headers": {
"host": "n8n.momentry.ddns.net",
"user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36 Edg/145.0.0.0",
"content-length": "15",
"accept": "*/*",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "zh-TW,zh;q=0.9,en-US;q=0.8,en;q=0.7,zh-CN;q=0.6,zh-HK;q=0.5",
"content-type": "application/json",
"origin": "https://wp.momentry.ddns.net",
"priority": "u=1, i",
"referer": "https://wp.momentry.ddns.net/",
"sec-ch-ua": "\"Not:A-Brand\";v=\"99\", \"Microsoft Edge\";v=\"145\", \"Chromium\";v=\"145\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"macOS\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"via": "3.0 Caddy",
"x-forwarded-for": "111.243.8.96",
"x-forwarded-host": "n8n.momentry.ddns.net",
"x-forwarded-proto": "https"
},
"params": {},
"query": {},
"body": {
"query": "sun"
},
"webhookUrl": "https://n8n.momentry.ddns.net/webhook/search",
"executionMode": "production"
},
"pairedItem": {
"item": 0
}
}
]
},
"triggerCount": 1,
"createdAt": "2026-03-23T19:06:43.592+08:00",
"updatedAt": "2026-03-23T20:06:51.588+08:00"
}

View File

@@ -0,0 +1,123 @@
{
"name": "Momentry Video Search - Simple",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "video-search-simple",
"responseMode": "lastNode",
"options": {}
},
"id": "webhook-simple",
"name": "Webhook (Simple)",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
250,
300
],
"webhookId": "video-search-simple"
},
{
"parameters": {
"url": "http://localhost:3002/api/v1/n8n/search",
"method": "POST",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $json.body }}"
},
{
"name": "limit",
"value": 5
}
]
},
"options": {
"headers": {
"Content-Type": "application/json",
"X-API-Key": "demo_api_key_12345"
}
}
},
"id": "http-search",
"name": "搜尋 Momentry",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
500,
300
]
},
{
"parameters": {
"jsCode": "// 處理 Momentry 搜尋結果\nconst data = $input.first().json;\nconst hits = data.hits;\n\nif (!hits || hits.length === 0) {\n return {\n json: {\n success: false,\n message: '找不到相關結果',\n query: data.query\n }\n };\n}\n\n// 格式化結果\nconst formattedResults = hits.map((hit, idx) => {\n return {\n index: idx + 1,\n id: hit.id,\n title: hit.title,\n text: hit.text,\n startTime: hit.start,\n endTime: hit.end,\n relevance: Math.round(hit.score * 100) + '%',\n file_path: hit.file_path\n };\n});\n\nreturn {\n json: {\n success: true,\n query: data.query,\n totalFound: data.count,\n results: formattedResults\n }\n};"
},
"id": "code-process-simple",
"name": "處理結果",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [
750,
300
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ $json }}"
},
"id": "respond-webhook",
"name": "回傳結果",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
1000,
300
]
}
],
"connections": {
"Webhook (Simple)": {
"main": [
[
{
"node": "搜尋 Momentry",
"type": "main",
"index": 0
}
]
]
},
"搜尋 Momentry": {
"main": [
[
{
"node": "處理結果",
"type": "main",
"index": 0
}
]
]
},
"處理結果": {
"main": [
[
{
"node": "回傳結果",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"id": "momentry-video-search-simple",
"versionId": "1",
"createdAt": "2026-03-23T00:00:00.000Z",
"updatedAt": "2026-03-23T00:00:00.000Z"
}

View File

@@ -0,0 +1,89 @@
{
"name": "Momentry Video Search Test",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "video-search-test",
"responseMode": "lastNode",
"options": {}
},
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [250, 300]
},
{
"parameters": {
"url": "http://localhost:3002/api/v1/n8n/search",
"method": "POST",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ query: $json.body.query || \"test\", limit: $json.body.limit || 5 }) }}",
"options": {}
},
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [500, 300]
},
{
"parameters": {
"jsCode": "return $input.first().json;"
},
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [750, 300]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}"
},
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [1000, 300]
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "HTTP Request",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
},
"Code": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null
}

View File

@@ -0,0 +1,109 @@
{
"name": "Momentry Video RAG MCP",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "video-rag-mcp",
"responseMode": "lastNode",
"options": {}
},
"name": "Webhook Trigger",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
250,
300
]
},
{
"parameters": {
"url": "http://localhost:3002/api/v1/n8n/search",
"method": "POST",
"sendBody": true,
"contentType": "json",
"body": "={{ JSON.stringify({query: $json.body.query || $json.body, limit: $json.body.limit || 5, uuid: $json.body.uuid}) }}",
"options": {
"timeout": 30000,
"headers": {
"X-API-Key": "demo_api_key_12345"
}
}
},
"name": "Search Momentry Core",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.1,
"position": [
500,
300
]
},
{
"parameters": {
"jsCode": "// Process Momentry Core search results\nconst data = $input.first().json;\nconst hits = data.hits || [];\n\nif (hits.length === 0) {\n return {\n json: {\n success: false,\n message: 'No relevant results found',\n query: data.query,\n results: []\n }\n };\n}\n\n// Format results for RAG\nconst formattedResults = hits.map((hit, idx) => {\n return {\n index: idx + 1,\n id: hit.id || hit.chunk_id,\n title: hit.title || 'Unknown Video',\n text: hit.text || hit.content || '',\n startTime: hit.start_time || hit.start || 0,\n endTime: hit.end_time || hit.end || 0,\n relevance: Math.round((hit.score || 0) * 100) + '%',\n videoUuid: hit.video_uuid || hit.uuid,\n file_path: hit.file_path || ''\n };\n});\n\n// Build context for RAG\nconst context = formattedResults\n .map(r => \\`[\\${r.index}] \\${r.text} (Video: \\${r.title}, Time: \\${r.startTime}s-\\${r.endTime}s)\\`)\n .join('\\n\\n');\n\nreturn {\n json: {\n success: true,\n query: data.query,\n totalFound: data.count || hits.length,\n context: context,\n results: formattedResults\n }\n};"
},
"name": "Process RAG Results",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
750,
300
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}",
"options": {
"statusCode": 200
}
},
"name": "Respond to Webhook",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1000,
300
]
}
],
"connections": {
"Webhook Trigger": {
"main": [
[
{
"node": "Search Momentry Core",
"type": "main",
"index": 0
}
]
]
},
"Search Momentry Core": {
"main": [
[
{
"node": "Process RAG Results",
"type": "main",
"index": 0
}
]
]
},
"Process RAG Results": {
"main": [
[
{
"node": "Respond to Webhook",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"staticData": null
}

View File

@@ -0,0 +1,138 @@
{
"name": "Momentry Video Search",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "video-search",
"responseMode": "lastNode",
"options": {}
},
"id": "webhook-trigger",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"typeVersion": 1,
"position": [
250,
300
],
"webhookId": "video-search"
},
{
"parameters": {
"url": "http://localhost:3002/api/v1/n8n/search",
"method": "POST",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "query",
"value": "={{ $json.body }}"
},
{
"name": "limit",
"value": 5
}
]
},
"options": {
"headers": {
"Content-Type": "application/json",
"X-API-Key": "demo_api_key_12345"
}
}
},
"id": "http-request-search",
"name": "搜尋 Momentry",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
500,
300
]
},
{
"parameters": {
"jsCode": "const hits = $input.first().json.hits;\n\nif (!hits || hits.length === 0) {\n return {\n json: {\n message: '找不到相關結果',\n query: $input.first().json.query\n }\n };\n}\n\nconst results = hits.map((hit, index) => {\n return {\n number: index + 1,\n text: hit.text,\n start: hit.start,\n end: hit.end,\n score: Math.round(hit.score * 100) + '%',\n video_title: hit.title,\n file_path: hit.file_path\n };\n});\n\nreturn {\n json: {\n query: $input.first().json.query,\n count: $input.first().json.count,\n results: results\n }\n};"
},
"id": "code-process",
"name": "處理結果",
"type": "n8n-nodes-base.code",
"typeVersion": 1,
"position": [
750,
300
]
},
{
"parameters": {
"method": "POST",
"url": "https://api.telegram.org/bot{{ $env.TELEGRAM_BOT_TOKEN }}/sendMessage",
"sendBody": true,
"bodyParameters": {
"parameters": [
{
"name": "chat_id",
"value": "={{ $json.chat_id }}"
},
{
"name": "text",
"value": "=🎬 搜尋結果: \"{{ $json.query }}\"\n\n{{ $json.results.map(r => r.number + '️⃣ \"' + r.text.substring(0, 50) + '...\"\n⏱ ' + r.start + 's - ' + r.end + 's\n📊 相關度: ' + r.score).join('\n\n') }}"
}
]
},
"options": {}
},
"id": "telegram-send",
"name": "Telegram 通知",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 3,
"position": [
1000,
300
],
"continueOnFail": true
}
],
"connections": {
"Webhook": {
"main": [
[
{
"node": "搜尋 Momentry",
"type": "main",
"index": 0
}
]
]
},
"搜尋 Momentry": {
"main": [
[
{
"node": "處理結果",
"type": "main",
"index": 0
}
]
]
},
"處理結果": {
"main": [
[
{
"node": "Telegram 通知",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"id": "momentry-video-search",
"versionId": "1",
"createdAt": "2026-03-23T00:00:00.000Z",
"updatedAt": "2026-03-23T00:00:00.000Z"
}

View File

@@ -0,0 +1,100 @@
#!/bin/bash
echo "=========================================="
echo "Momentry Core API 測試腳本"
echo "=========================================="
echo ""
# 顏色定義
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 測試 1: Momentry Core 搜尋
echo -e "${YELLOW}測試 1: Momentry Core 搜尋 API${NC}"
echo "URL: http://localhost:3002/api/v1/n8n/search"
echo ""
RESPONSE=$(curl -s -X POST http://localhost:3002/api/v1/n8n/search \
-H "Content-Type: application/json" \
-d '{"query":"charade","limit":2}')
if echo "$RESPONSE" | grep -q '"hits"'; then
echo -e "${GREEN}✅ 成功!${NC}"
echo "$RESPONSE" | python3 -m json.tool | grep -E '"query"|"count"' | head -3
echo ""
echo "前 2 個結果:"
echo "$RESPONSE" | python3 -c "
import sys, json
data = json.load(sys.stdin)
for i, hit in enumerate(data.get('hits', [])[:2]):
print(f\" [{i+1}] {hit.get('text', '')[:50]}...\")
print(f\" 時間: {hit.get('start')}s - {hit.get('end')}s\")
print(f\" 影片: {hit.get('title')}\")
"
else
echo -e "${RED}❌ 失敗${NC}"
echo "$RESPONSE"
fi
echo ""
# 測試 2: 列出影片
echo -e "${YELLOW}測試 2: 列出所有影片${NC}"
echo "URL: http://localhost:3002/api/v1/videos"
echo ""
RESPONSE=$(curl -s http://localhost:3002/api/v1/videos)
if echo "$RESPONSE" | grep -q '"videos"'; then
echo -e "${GREEN}✅ 成功!${NC}"
echo "$RESPONSE" | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(f\"找到 {len(data.get('videos', []))} 個影片:\")
for v in data.get('videos', []):
print(f\" - {v.get('file_name')} (UUID: {v.get('uuid')[:8]}...)\")
"
else
echo -e "${RED}❌ 失敗${NC}"
fi
echo ""
# 測試 3: n8n Webhook (Test Mode)
echo -e "${YELLOW}測試 3: n8n Webhook (Test Mode)${NC}"
echo "URL: http://localhost:5678/webhook-test/video-rag-mcp"
echo ""
echo "⚠️ 注意: 請先在 n8n UI 中點擊 'Execute workflow' 按鈕"
echo ""
RESPONSE=$(curl -s -X POST http://localhost:5678/webhook-test/video-rag-mcp \
-H "Content-Type: application/json" \
-d '{"query":"charade","limit":2}')
if echo "$RESPONSE" | grep -q '"success": true'; then
echo -e "${GREEN}✅ Webhook 測試成功!${NC}"
echo "$RESPONSE" | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(f\"查詢: {data.get('query')}\")
print(f\"找到: {data.get('totalFound')} 個結果\")
print(f\"Context 長度: {len(data.get('context', ''))} 字元\")
"
elif echo "$RESPONSE" | grep -q '404'; then
echo -e "${RED}❌ Webhook 未找到${NC}"
echo "請在 n8n UI 中:"
echo " 1. 開啟 'Momentry Video RAG MCP' 工作流程"
echo " 2. 點擊 'Execute workflow' 按鈕"
echo " 3. 30 秒內再次執行此腳本"
else
echo -e "${RED}❌ 錯誤${NC}"
echo "$RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE"
fi
echo ""
echo "=========================================="
echo "測試完成!"
echo "=========================================="
echo ""
echo "快速參考:"
echo " Momentry API: http://localhost:3002/api/v1"
echo " n8n UI: https://n8n.momentry.ddns.net"
echo " Webhook Test: http://localhost:5678/webhook-test/video-rag-mcp"

View File

@@ -0,0 +1,33 @@
#!/bin/bash
# Test Momentry Core API directly
# This bypasses n8n and tests the API directly
echo "=========================================="
echo "Testing Momentry Core API Directly"
echo "=========================================="
echo ""
# Test 1: Health check
echo "Test 1: Health Check"
curl -s http://localhost:3002/api/v1/health 2>&1 | head -5
echo ""
# Test 2: Search API
echo "Test 2: Search API"
echo "Query: 'charade'"
curl -s -X POST http://localhost:3002/api/v1/n8n/search \
-H "Content-Type: application/json" \
-d '{"query":"charade","limit":3}' | python3 -m json.tool
echo ""
# Test 3: List videos
echo "Test 3: List Videos"
curl -s http://localhost:3002/api/v1/videos | python3 -m json.tool 2>/dev/null || echo "No videos or endpoint error"
echo ""
echo "=========================================="
echo "If all tests show JSON responses, API is working!"
echo ""
echo "Next step: Use the API in n8n workflows"
echo "=========================================="

View File

@@ -0,0 +1,104 @@
#!/bin/bash
# Test Momentry Video RAG MCP Workflow
# Usage: ./test_workflow.sh [query] [limit] [uuid]
N8N_URL="http://localhost:5678"
WEBHOOK_PATH="webhook-test/video-rag-mcp"
echo "⚠️ 注意:使用 Test Webhook URL"
echo "(生產環境 Webhook 需要工作流程手動激活後才能使用)"
echo ""
# Default values
QUERY="${1:-charade}"
LIMIT="${2:-3}"
UUID="${3:-}"
echo "=========================================="
echo "Testing Video RAG Workflow"
echo "=========================================="
echo "Query: $QUERY"
echo "Limit: $LIMIT"
if [ -n "$UUID" ]; then
echo "UUID: $UUID"
fi
echo ""
# Build JSON payload
if [ -n "$UUID" ]; then
PAYLOAD=$(cat <<JSON
{
"query": "$QUERY",
"limit": $LIMIT,
"uuid": "$UUID"
}
JSON
)
else
PAYLOAD=$(cat <<JSON
{
"query": "$QUERY",
"limit": $LIMIT
}
JSON
)
fi
echo "Request:"
echo "$PAYLOAD" | python3 -m json.tool 2>/dev/null || echo "$PAYLOAD"
echo ""
echo "=========================================="
echo "Response:"
echo "=========================================="
echo "URL: ${N8N_URL}/${WEBHOOK_PATH}"
echo ""
# Make the request
RESPONSE=$(curl -s -X POST "${N8N_URL}/${WEBHOOK_PATH}" \
-H "Content-Type: application/json" \
-d "$PAYLOAD")
# Format and display response
echo "$RESPONSE" | python3 -m json.tool 2>/dev/null || echo "$RESPONSE"
echo ""
echo "=========================================="
# Check if successful
if echo "$RESPONSE" | grep -q '"success": true'; then
echo "✅ Test PASSED"
# Extract and display summary
echo ""
echo "Summary:"
echo "$RESPONSE" | python3 -c "
import sys, json
data = json.load(sys.stdin)
if data.get('success'):
print(f\" Query: {data.get('query', 'N/A')}\")
print(f\" Total Found: {data.get('totalFound', 0)}\")
results = data.get('results', [])
print(f\" Results Count: {len(results)}\")
print()
print(' Top Results:')
for r in results[:3]:
print(f\" [{r.get('index')}] {r.get('title', 'Unknown')}\")
text = r.get('text', '')[:50]
print(f\" Text: {text}...\")
print(f\" Time: {r.get('startTime')}s - {r.get('endTime')}s\")
print(f\" Relevance: {r.get('relevance')}\")
print()
" 2>/dev/null || true
elif echo "$RESPONSE" | grep -q '"code": 404'; then
echo "❌ Webhook not found"
echo ""
echo "可能的解決方案:"
echo " 1. 在 n8n UI 中開啟工作流程: https://n8n.momentry.ddns.net"
echo " 2. 點擊右上角的 'Active' 開關"
echo " 3. 或者使用 test webhook: webhook-test/video-rag-mcp"
else
echo "❌ Test FAILED or no results found"
fi
echo "=========================================="

View File

@@ -0,0 +1,69 @@
# file_uuid 生成規則
## 公式
```
file_uuid = SHA256(mac_address | birthday | physical_path_at_birth | filename)[0:32]
```
- 使用 SHA-256 雜湊
- 四個元件以 `|` 串接
- 取 64 字元 hex 的前 32 字元作為 file_uuid
- 結果為 32 字元小寫 hex0-9, a-f**不含連字號**
## 輸入元件
| 元件 | 格式 | 範例 | 說明 |
|------|------|------|------|
| `mac_address` | `xx:xx:xx:xx:xx:xx` | `a1:b2:c3:d4:e5:f6` | 內建網路介面 MAC優先 en0 > en1 > en2 |
| `birthday` | ISO 8601 + timezone | `2026-04-27T22:00:00+08:00` | **檔案 mtime**fs::metadata().modified()),固定不變 |
| `physical_path_at_birth` | 絕對路徑 | `/Users/demo/raw/video.mp4` | 註冊時的正規化絕對路徑,尾端 `/` 會去除 |
| `filename` | 含副檔名 | `video.mp4` | 純檔名含副檔名 |
## 特性
- **Deterministic確定性**:相同輸入永遠產生相同輸出
- **Location-bound位置綁定**:不同路徑產生不同 file_uuid → 搬移檔案 = 新 identity
- **MAC-bound機器綁定**:不同機器產生的 file_uuid 不同
- **No hyphen無連字號**32 字元純 hex`identity_uuid`(可含連字號)格式不同
- **128 bits entropy**32 hex chars = 128 bits
## MAC 位址擷取規則
1. 執行 `ifconfig -a`
2. 解析 `ether` / `lladdr` 欄位
3. 優先順序en0Wi-Fi > en1 > en2 > 其他 en* > 所有其餘介面
4. 排除 `00:00:00:00:00:00``ff:ff:ff:ff:ff:ff`、USB/Thunderbolt 非內建介面
5. 無法取得時 fallback 為 `00:00:00:00:00:00`
## birthday 說明
`birthday` 實際上採用**檔案 mtime**`fs::metadata().modified()`),而非 birthtimebtime。原因
- `rsync -a` 會保留 mtime但不會保留 btime
- 使用 mtime 確保檔案透過 rsync 在不同機器間傳輸後file_uuid 仍保持一致
- `birthday` 在第一次計算後永久固定,永不修改
## 驗證規則
```rust
pub fn is_birth_uuid(uuid: &str) -> bool {
uuid.len() == 32 && !uuid.contains('_')
}
```
## 與 identity_uuid 的差異
| | file_uuid | identity_uuid |
|---|---|---|
| 長度 | 32 chars | 32 chars |
| 連字號 | 無 | 可有可無API 皆可接受) |
| 生成方式 | SHA256(mac\|birthday\|path\|filename) | gen_random_uuid() |
| 確定性 | ✅ 確定性(相同輸入→相同輸出) | ❌ 每次不同 |
| 用途 | 檔案識別、位置綁定 | 人物身分識別 |
## 原始碼位置
- **實作**`src/core/storage/uuid.rs``compute_birth_uuid()`, `get_mac_address()`, `is_birth_uuid()`
- **規格文件**`docs_v1.0/REFERENCE/file_uuid_spec.md` V2.0
- **設計文件**`docs_v1.0/DESIGN/FILE_LIFECYCLE_V1.0.md``docs_v1.0/DESIGN/MARKBASE_DESIGN_V2.0.md`

View File

@@ -0,0 +1,57 @@
# 3002 vs 3003 API 比較報告
**日期**: 2026-05-13 | **目的**: 正式版與開發版 API 差異Release 前)
---
## 1. 版本差異
| 項目 | 3002 (正式版) | 3003 (開發版) |
|------|:---:|:---:|
| **Build 日期** | 2026-05-13 21:07 | 2026-05-13 (持續開發) |
| **Version** | v1.0.0 | v1.0.0 |
| **Build Git Hash** | `d34bcae+` | `d34bcae+` |
| **程式碼狀態** | ✅ 同步(從 dev release binary | ✅ 最新 |
| **Schema** | `public`(需執行 `chunks→chunk` RENAME | `dev` |
---
## 2. 共同端點JSON 結構一致)
| 端點 | 3002 | 3003 | 備註 |
|------|:---:|:---:|------|
| `GET /health` | ✅ | ✅ | 含 `version` + `build_git_hash` |
| `GET /health/detailed` | ✅ | ✅ | 同上 |
| `GET /api/v1/files` | ✅ | ✅ | `total` 從 DB 讀取(不再寫死 0 |
| `GET /api/v1/files/scan` | ✅ | ✅ | 含 `.jpg/.png` 掃描(不再限 mp4 |
| `GET /api/v1/file/:uuid/process` | ✅ | ✅ | |
| `GET /api/v1/file/:uuid/chunk/:id` | ✅ | ✅ | |
| `GET /api/v1/identities` | ✅ | ✅ | 含分頁 |
| `GET /api/v1/identities/:id` | ✅ | ✅ | |
| `GET /api/v1/identity_bindings` | ✅ | ✅ | |
| `POST /api/v1/search/universal` | ✅ | ✅ | |
| `GET /api/v1/resources` | ✅ | ✅ | |
| `GET /api/v1/traces/:tid/faces` | ✅ | ✅ | |
| `GET /api/v1/traces/:tid/video` | ✅ | ✅ | |
---
## 3. 差異
| 項目 | 3002 | 3003 |
|------|------|------|
| PostgreSQL Schema | `public`(需 rename `chunks→chunk` | `dev`(已為 `chunk` |
| MongoDB Database | `momentry` | `momentry_dev` |
| Redis Prefix | `momentry:` | `momentry_dev:` |
| Qdrant Collection Prefix | `momentry_` | `momentry_dev_` |
| Output Dir | `/Users/accusys/momentry/output` | `/Users/accusys/momentry/output_dev` |
| `.env` | `.env``DATABASE_SCHEMA=public` | `.env.development``DATABASE_SCHEMA=dev` |
---
## 4. Release 必要步驟
1. **Binary**:使用 M5 交付的 `momentry_v1.0.0` 取代 port 3002 binary
2. **Schema**`ALTER TABLE public.chunks RENAME TO chunk;`
3. **Deploy**`bash deploy.sh`9 步驟,含 vec0.dylib
4. **Identity**:保留 15 TMDB + merge dev data`file_uuid` 欄位輔助)

View File

@@ -0,0 +1,139 @@
# Brew → Source 遷移報告
**Date**: 2026-05-15
**Status**: Planning
**Next action**: 逐項驗證 SHA256 + 下載 Source → Build
---
## 總覽
Momentry Core 目前有 18 個核心服務透過 Homebrew 管理。目標是將這些服務全部遷移到 source build原始碼編譯實現 source code 可追蹤、可驗證、可重複建置。
---
## Momentry Core Brew 套件一覽
| # | Formula | Version | Binary Path | SHA256 | Status |
|---|---------|---------|-------------|--------|:------:|
| 1 | **php** | 8.5.5 | `/opt/homebrew/bin/php` | `173fd1ca36f3dd4952f5442572e06a14b7c005751ae15e7e42161606e931645c` | 🔴 brew |
| 2 | **mariadb** | 12.2.2 | `/opt/homebrew/bin/mariadbd` | `38cb48f0be673d4136c43a89c1aca5b314d30042dd09537d93b7995f52f90206` | 🔴 brew |
| 3 | **redis** | 8.6.3 | `/opt/homebrew/bin/redis-server` | ? | 🟡 brew+src |
| 4 | **mongodb-community** | 8.2.7 | `/opt/homebrew/bin/mongod` | ? | 🔴 brew |
| 5 | **ffmpeg** | 8.1.1 | `/opt/homebrew/bin/ffmpeg` | ? | 🟡 brew+src |
| 6 | **ffmpeg-full** | 8.1.1 | `/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg` | ? | 🟡 brew+src |
| 7 | **node** | 25.9.0 | `/opt/homebrew/bin/node` | `fba87e4402c55ea4fc7ca9b9838790c32534e3e77c9c7834c37073752d070678` | 🔴 brew |
| 8 | **go** | 1.26.2 | `/opt/homebrew/bin/go` | ? | 🟡 brew |
| 9 | **python@3.11** | 3.11.15 | `/opt/homebrew/bin/python3.11` | ? | ✅ pyenv src |
| 10 | **ollama** | 0.23.1 | `/opt/homebrew/bin/ollama` | ? | 🔴 brew |
| 11 | **yt-dlp** | 2026.3.17 | `/opt/homebrew/bin/yt-dlp` | ? | 🔴 brew |
| 12 | **whisper-cpp** | 1.8.4 | `/opt/homebrew/bin/whisper-cpp` | ? | 🔴 brew |
| 13 | **tesseract** | 5.5.2 | `/opt/homebrew/bin/tesseract` | ? | 🔴 brew |
| 14 | **sdl2** | 2.32.10 | `/opt/homebrew/lib/libsdl2.dylib` | ? | 🟡 lib only |
| 15 | **cmake** | 4.3.2 | `/opt/homebrew/bin/cmake` | ? | ✅ src in `services/src/` |
| 16 | **mongosh** | 2.8.3 | `/opt/homebrew/bin/mongosh` | ? | 🔴 brew |
| 17 | **pgvector** | 0.8.2 | PostgreSQL extension | ? | 🟡 extension |
| 18 | **protobuf** | - | `/opt/homebrew/bin/protoc` | ? | 🟡 build dep |
---
## 遷移優先級
### Phase 1 — 直接影響 Momentry 運行的服務
| Service | Source | 遷移原因 |
|---------|--------|---------|
| PHP 8.5.5 | `https://www.php.net/distributions/php-8.5.5.tar.gz` | WordPress hosting, FPM 不可中斷 |
| MariaDB 12.2.2 | `https://github.com/MariaDB/server/archive/mariadb-12.2.2.tar.gz` | WordPress + Momentry DB, data 需 migrate |
| Node.js 25.9.0 | `https://nodejs.org/dist/v25.9.0/node-v25.9.0.tar.gz` | Portal frontend build + npm packages |
### Phase 2 — 高影響但可有 Buffer 的服務
| Service | Source | 遷移原因 |
|---------|--------|---------|
| Redis 8.6.3 | `https://redis.io/download/` | 已有 source in `services/src/redis-7.4.3.tar.gz` |
| MongoDB 8.2.7 | `https://github.com/mongodb/mongo` | Momentry cache, data 需 migrate |
| ffmpeg 8.1.1 | `https://ffmpeg.org/releases/ffmpeg-8.1.1.tar.xz` | 已有 source in `services/src/ffmpeg-7.1.1.tar.xz` |
### Phase 3 — 輔助工具
| Service | Source |
|---------|--------|
| ollama 0.23.1 | `https://github.com/ollama/ollama` |
| yt-dlp | `https://github.com/yt-dlp/yt-dlp` |
| tesseract 5.5.2 | `https://github.com/tesseract-ocr/tesseract` |
| whisper-cpp 1.8.4 | `https://github.com/ggerganov/whisper.cpp` |
| protobuf | `https://github.com/protocolbuffers/protobuf` |
---
## Source 歸檔對照
| Source Archive | Status | Path |
|---------------|--------|------|
| `redis-7.4.3.tar.gz` | ✅ | `release/system/v1.0/services/src/` |
| `ffmpeg-7.1.1.tar.xz` | ✅ | `release/system/v1.0/services/src/` |
| `cmake-4.2.0-macos-universal.tar.gz` | ✅ | `release/system/v1.0/services/src/` |
| `sftpgo-main.tar.gz` | ✅ | `release/system/v1.0/services/src/` |
| `postgresql-18.3.tar.gz` | ✅ | `release/system/v1.0/services/src/` |
| `llama.cpp/` | ✅ | `release/system/v1.0/services/src/` |
| `go/` | ✅ | `release/system/v1.0/services/src/` |
| `pyenv/` | ✅ | `release/system/v1.0/services/src/` |
| `php-*.tar.gz` | ❌ 需下載 | `release/system/v1.0/services/src/` |
| `mariadb-*.tar.gz` | ❌ 需下載 | `release/system/v1.0/services/src/` |
| `node-*.tar.gz` | ❌ 需下載 | `release/system/v1.0/services/src/` |
| `mongodb-*.tar.gz` | ❌ 需下載 | `release/system/v1.0/services/src/` |
| `ollama-*.tar.gz` | ❌ 需下載 | `release/system/v1.0/services/src/` |
---
## SHA256 Checksum 填空
已知的 SHA256待補其餘
```yaml
php: 173fd1ca36f3dd4952f5442572e06a14b7c005751ae15e7e42161606e931645c
mariadb (mariadbd): 38cb48f0be673d4136c43a89c1aca5b314d30042dd09537d93b7995f52f90206
node: fba87e4402c55ea4fc7ca9b9838790c32534e3e77c9c7834c37073752d070678
sftpgo (source): 6607334148917dd80a687706a3ae63ea8c532d10c6717c87491da23939c96d4a
sftpgo (binary): 9991d2a1c877d5bcae17cb4e026de939862e4b880924589cf4ed15ac7291ec7e
```
---
## Brew Leavesuser-installed only
```
cmake, e2fsprogs, ffmpeg, ffmpeg-full, go,
mariadb, mongodb/brew/mongodb-community, ollama,
ossp-uuid, pgvector, php, pkgconf, protobuf,
python@3.11, redis, yt-dlp, zlib
```
---
## 執行步驟(待有時間時)
```bash
# 1. 下載 source
cd /tmp
curl -O https://www.php.net/distributions/php-8.5.5.tar.gz
curl -LO https://github.com/MariaDB/server/archive/mariadb-12.2.2.tar.gz
# 2. Archive + SHA256
tar czf release/system/v1.0/services/src/php-8.5.5.tar.gz php-8.5.5/
shasum -a 256 release/system/v1.0/services/src/php-8.5.5.tar.gz
# 3. Build PHP
tar xzf php-8.5.5.tar.gz
cd php-8.5.5
./configure --prefix=/Users/accusys/bin/php --with-fpm-user=accusys --with-fpm-group=staff
make -j$(sysctl -n hw.ncpu)
make install
# 4. Update plist
sed -i '' 's|/opt/homebrew/bin/php|/Users/accusys/bin/php/bin/php|g' momentry_runtime/plist/com.momentry.php.plist
# 5. Record in dev.resources
# INSERT INTO dev.resources ...
```

View File

@@ -0,0 +1,371 @@
# 交付程序M4_workspace → M5 → Public Release
**Date**: 2026-05-13
**Version**: 1.1
---
## 流程總覽
```
M4 回報問題 → M5 修復 → M5 自驗 → Release package → Deploy → M4 驗證 → Public Release
↑ ↓
└── 失敗回退 ──────────────────────────┘
```
---
## Phase 1M4 回報M4_workspace/
---
## Phase 1M4 回報M4_workspace/
M4 將問題寫入 `docs_v1.0/M4_workspace/YYYY-MM-DD_<topic>.md`,格式:
```markdown
# {問題標題}
**Date**: YYYY-MM-DD
**From**: M4
**To**: M5
---
## Bug / 建議
(問題描述 + 影響分析)
## Fix 建議
(選擇性:程式碼範例或解決方向)
```
---
## Phase 2M5 處理
| 步驟 | 動作 | 檢查點 |
|------|------|--------|
| 2.1 | 讀取 M4 報告,理解問題 | 必要時回覆確認 |
| 2.2 | 實作修復 | `cargo check` / `cargo test` 通過 |
| 2.3 | 更新 Registry如影響座標、detector | 相關 `.md` 同步更新 |
| 2.4 | `git commit` | commit message 含 M4 issue 參考 |
| 2.5 | 回覆 `M4_workspace/YYYY-MM-DD_<topic>_response.md` | 說明修復方式 + commit hash |
### 修復分類與回應形式
| 修復類型 | 回應文件 | 同步項目 |
|----------|---------|---------|
| **Bug fix**座標、script 錯誤) | `*_response.md` + `git commit` | 無需額外 |
| **模型替換**YOLO v5→v8 等) | `*_response.md` + Registry 更新 | `DETECTOR_REGISTRY.md` |
| **架構變更**(新 module、pipeline 重排) | `*_response.md` + Registry 更新 | `SPATIAL_COORDINATE_REGISTRY.md` |
| **新功能**heuristic_scene 等) | `*_response.md` + 新文件 | 新 `REFERENCE/*.md` |
| **新測試包** | `*_test_report.md` | 上傳至 `release/files/` |
### 回應文件規範
```markdown
# {主題} — 回覆
**Date**: YYYY-MM-DD
**From**: M5
**To**: M4
**Ref**: `YYYY-MM-DD_source_file.md`
---
## 修正
| # | 項目 | 狀態 | Commit |
|---|------|:--:|--------|
| 1 | {問題 A 修復} | ✅ | `abc1234` |
| 2 | {功能 B 新增} | ✅ | `abc1234` |
## 檔案變更
| 檔案 | 說明 |
|------|------|
| `path/to/file` | 改動摘要 |
```
---
## Phase 2.5M5 內部自驗
Release 前先自驗,降低 M4 測試失敗的機率。
### 自驗清單
| # | 項目 | 方法 |
|---|------|------|
| 2.5.1 | Rust build | `cargo build` / `cargo test` |
| 2.5.2 | API 測試 | `bash api_test.sh` → 全 passed |
| 2.5.3 | 場景驗證 | 抽 1 個 CUT segment用 MaskFormer 或 PaliGemma 確認 scene type |
| 2.5.4 | SQLite 驗證 | `python3 export_sqlite.py {uuid}` → vec0 tables 正確 |
| 2.5.5 | Identity 驗證 | TMDB 演員 + auto PERSON count 合理 |
| 2.5.6 | TKG 驗證 | edges > 0, nodes > 0 |
| 2.5.7 | file_info.json | `momentry_version` + `momentry_build` 正確 |
### 自驗不通過
```
❌ cargo build → 修復編譯錯誤 → 重新 commit
❌ api_test → 修復 → 重新 commit
❌ 場景驗證 → 確認是 bug 還是 flicker → 必要時開 issue
```
### 回應文件規範
```markdown
# {主題} — 回覆
**Date**: YYYY-MM-DD
**From**: M5
**To**: M4
**Ref**: `YYYY-MM-DD_source_file.md`
---
## 修正
| # | 項目 | 狀態 | Commit |
|---|------|:--:|--------|
| 1 | {問題 A 修復} | ✅ | `abc1234` |
| 2 | {功能 B 新增} | ✅ | `abc1234` |
## 檔案變更
| 檔案 | 說明 |
|------|------|
| `path/to/file` | 改動摘要 |
```
---
## Phase 3Release Package
### 時機
| 條件 | 動作 |
|------|------|
| 重大修復完成 | 產生新 package |
| M4 要求測試 | 產生 package`*_test_report.md` |
| 版本里程碑 | 正式 release含 version bump |
### 產生流程
```bash
# 1. 確認所有變更已 commit
git log --oneline -5
# 2. Build release binary
cargo build --bin release
# 3. 產生 package含 sql/ 目錄、vec0.dylib、deploy.sh、verify.sh
cargo run --bin release -- package {file_uuid}
# 4. 檢查 output
ls -la release/files/{uuid}_v{timestamp}.tar.gz
# 5. 驗證 package 內容
tar tzf release/files/{uuid}_v{timestamp}.tar.gz
```
### Package 內容規範
```
{file_uuid}/
├── file_info.json (含 momentry_version + momentry_build)
├── data.sql (→ 指引 sql/)
├── deploy.sh (→ 9 步驟)
├── verify.sh
├── vec0.dylib (SQLite vector extension)
├── sql/
│ ├── dev_videos.sql
│ ├── dev_chunk.sql
│ ├── dev_chunk_vectors.sql (768D)
│ ├── dev_face_detections.sql (512D)
│ ├── dev_identities.sql
│ ├── dev_identity_bindings.sql
│ ├── dev_tkg_nodes.sql
│ └── dev_tkg_edges.sql
├── {uuid}.face.json (landmark 已修復)
├── {uuid}.yolo.json
├── {uuid}.asr.json
├── {uuid}.asrx.json
├── {uuid}.cut.json
├── {uuid}.sqlite (含 vec0 向量表)
└── *.mp4 / *.mov
```
---
## Phase 4Deploy
### 前置檢查
```bash
# 1. 確認伺服器正常
curl -s http://localhost:3003/health
# 預期: {"status":"ok","version":"1.0.0","build_git_hash":"d34bcae",...}
# 2. 確認資料庫可連線
/Users/accusys/pgsql/18.3/bin/psql -U accusys -d momentry -c "SELECT version()"
# 3. 解包
tar xzf release/files/{uuid}_v{timestamp}.tar.gz -C /tmp/deploy/
cd /tmp/deploy/{file_uuid}
```
### 執行 deploy
```bash
bash deploy.sh
```
### 預期輸出
```
[0/9] Checking system version and build... ✅ Server v1.0.0 matches
[1/9] Verifying package... ✅
[2/9] Pre-cleaning existing identities... ✅
[3/9] Importing DB data...
Importing dev_videos.sql... COPY 1
Importing dev_chunk.sql... COPY 2407
Importing dev_chunk_vectors.sql... COPY 2407
Importing dev_face_detections.sql... COPY 70691
Importing dev_identities.sql... COPY 441
Importing dev_identity_bindings.sql... COPY 18635
Importing dev_tkg_nodes.sql... COPY 6457
Importing dev_tkg_edges.sql... COPY 21028
[4/9] Copy video... ✅
[5/9] Setting status=completed ✅
[6/9] Copying output files... ✅ N files
[7/9] Installing vec0.dylib... ✅ /tmp/vec0.dylib
[8/9] Verify deployment...
Chunks: 2407
Faces: 70691
Identities: 417
TKG nodes: 6457
TKG edges: 21028 ✅
```
### Deploy 失敗處理
| 失敗點 | 原因 | 處理方式 |
|--------|------|---------|
| Step 0: 版本檢查 | server version ≠ package version | 取得 matching upgrade package |
| Step 3: 任 table 匯入失敗 | FK constraint、duplicate key | 檢查該 table 的 sql/*.sql 內容,修復後再跑 |
| Step 4: 影片複製 | 磁碟空間不足 | `df -h` 確認,清理後重跑 |
| Step 8: 驗證 row count 不符 | import 不完整 | 逐 table 比對 COPY count vs actual count |
> 每個 table 獨立 import各含 auto-commit單一 table 失敗不會 rollback 其他 table。
> 重新執行 `bash deploy.sh` 會先清掉該 file 的 identity 和資料後重新 import。
### 驗證後確認
```bash
# 確認所有 table row count 正確
for tbl in videos chunk chunk_vectors face_detections identities identity_bindings tkg_nodes tkg_edges; do
echo "$tbl: $(psql -U accusys -d momentry -t -A -c "SELECT COUNT(*) FROM dev.$tbl WHERE file_uuid='$UUID'" 2>/dev/null)"
done
```
### Production (port 3002)
Production deploy 步驟與 dev 相同,但需注意:
| 項目 | Dev (3003) | Production (3002) |
|------|:----------:|:-----------------:|
| Schema | `dev.*` | `public.*`(或 `dev.*` 若已 migration |
| Port | 3003 | 3002 |
| Deploy URL | `localhost:3003/health` | `localhost:3002/health` |
| 版本檢查 | 可接受 `unknown` build | 強制匹配 version |
```bash
# Production deploy 需明確設定 Server URL
SERVER_URL=http://localhost:3002 bash deploy.sh
```
---
## Phase 5Public Release
### 版本策略
| Bump | 幅度 | 適用時機 | 範例 |
|------|:----:|---------|------|
| **patch** | 0.0.x | Bug fix only無 API 變更 | 1.0.0 → 1.0.1 |
| **minor** | 0.x.0 | 新功能、模型替換、API 向後相容 | 1.0.0 → 1.1.0 |
| **major** | x.0.0 | Breaking change、schema 遷移 | 1.0.0 → 2.0.0 |
> 目前版本 `1.0.0`所有已完成的變更YOLO 替換、座標修復、pipeline 重排)尚未 bump。
### 條件
- [ ] M4 驗證通過deploy test 全部綠色)
- [ ] 所有 open issues 已回應或關閉
- [ ] API 文件與實際行為一致(或已開 doc issue 追蹤 M4 更新)
- [ ] 版本號已 bump`Cargo.toml``build.rs``BUILD_VERSION`
- [ ] package 已上傳到 `release/files/`
- [ ] 變更已 `git commit` + `git push`
### M4 驗證失敗的回退
```
M4 deploy test ❌
M5 分析失敗原因
├── Bug → 回到 Phase 2 修復 → 重新 commit不 bump 版本)
├── Script 錯誤 → 修復 deploy.sh → 重新 commit
└── Package 內容缺漏 → 重新 `cargo run --bin release -- package`
M5 重新通知 M4新 *response.md + git commit
M4 重新測試
```
### Release Info 產出
### Release Info 產出
```markdown
# Release v{version} — {YYYY-MM-DD}
## Changes
- {change 1}
- {change 2}
## Files
- release/files/{uuid}_v{timestamp}.tar.gz
## Verification
- Deploy test: ✅ (M4)
- API test: 18/18 ✅
- Chunks: {n}
- Faces: {n}
- Identities: {n}
- TKG nodes/edges: {n}/{n}
```
### 保存
```bash
# 備份 release 資訊
echo "Release: v{VERSION}" > $RELEASE_DIR/RELEASE_INFO.txt
echo "Date: $(date)" >> $RELEASE_DIR/RELEASE_INFO.txt
echo "Git: $(git rev-parse HEAD)" >> $RELEASE_DIR/RELEASE_INFO.txt
echo "Package: {uuid}_v{timestamp}.tar.gz" >> $RELEASE_DIR/RELEASE_INFO.txt
```
---
## 文件對照
| 文件 | 用途 |
|------|------|
| `M4_workspace/*.md` | M4 問題回報 |
| `M4_workspace/*_response.md` | M5 修復回覆 |
| `M4_workspace/*_test_report.md` | 測試包報表 |
| `REFERENCE/DETECTOR_REGISTRY.md` | Detector 規格 |
| `REFERENCE/DETECTOR_SELECTION_SOP.md` | 選型標準 |
| `REFERENCE/SPATIAL_COORDINATE_REGISTRY.md` | 座標系統 |
| `REFERENCE/IDENTITY_LIFECYCLE.md` | Identity 生命週期 |
| `M4_HANDOVER/` | M4 交付目錄 |

View File

@@ -0,0 +1,559 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Momentry Core 開發日誌"
date: "2026-03-18"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "開發日誌"
- "momentry"
- "core"
ai_query_hints:
- "查詢 Momentry Core 開發日誌 的內容"
- "Momentry Core 開發日誌 的主要目的是什麼?"
- "如何操作或實施 Momentry Core 開發日誌?"
---
# Momentry Core 開發日誌
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-18 |
| 文件版本 | V1.0 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
---
> **文檔維護開始**2026-03-18
> **⚠️ 補充說明**事後補記2026-03-18 以前),僅供參考。未來紀錄將即時記錄,參考價值較高。
---
## 開發工具
### Coding LLM 模型
| 階段 | 工具 | 模型 | ID | 說明 |
|------|------|------|-----|------|
| **初期** | Claude CLI | - | - | 初始專案架構建立 |
| **中期** | OpenCode | big-pickle | opencode/big-pickle | 主要開發協作者 |
**切換記錄**
- 初期使用 Claude CLI 建立專案基本架構
- 中期切換至 OpenCode (big-pickle) 進行主要功能開發
---
## 2026-03-17
### ML 模型選用
| Processor | 模型 | 版本/大小 | 說明 |
|----------|------|-----------|------|
| **ASR** | WhisperX (faster-whisper) | base, int8 | 語音識別 + 對話分段 |
| **CUT** | PySceneDetect | 0.6.7.1 | ContentDetector 場景檢測 |
| **YOLO** | YOLOv8n | yolov8n.pt (6.2MB) | 物體檢測nano 版本最快) |
| **OCR** | EasyOCR | 1.7.2 | 文字識別 |
| **Face** | OpenCV Haar Cascade | built-in | 人臉檢測(無需額外下載) |
| **Pose** | YOLOv8n-Pose | yolov8n-pose.pt (6.5MB) | 姿態估計nano 版本) |
**模型下載**
- YOLOv8n: `yolov8n.pt` (6.2MB)
- YOLOv8n-Pose: `yolov8n-pose.pt` (6.5MB)
**Python 依賴**
```
torch==2.8.0
whisperx==3.8.2
ultralytics==8.4.23
scenedetect==0.6.7.1
easyocr==1.7.2
opencv-python==4.13.0.92
```
---
### ASR 實作完成
- 完成 Python ML processor scripts使用本地模型
- `asrx_processor.py` - whisperx for speaker diarization
- `cut_processor.py` - PySceneDetect for scene detection
- `yolo_processor.py` - YOLOv8 for object detection
- `ocr_processor.py` - EasyOCR for text recognition
- `face_processor.py` - OpenCV Haar Cascade for face detection
- `pose_processor.py` - YOLOv8 Pose for pose estimation
- 更新 `requirements.txt` with all dependencies
- 安裝完成torch 2.8.0, whisperx 3.8.2, ultralytics 8.4.23, scenedetect 0.6.7.1, easyocr 1.7.2, opencv-python 4.13.0.92
- 下載模型YOLOv8n.pt (6.2MB), YOLOv8n-Pose.pt (6.5MB)
### Async Streaming 實作
- 更新 Rust processor modules 使用 async streaming 進行 real-time progress
- `src/core/processor/asr.rs`
- `src/core/processor/cut.rs`
- `src/core/processor/yolo.rs`
- `src/core/processor/ocr.rs`
- `src/core/processor/face.rs`
- `src/core/processor/pose.rs`
### 測試結果
- 測試影片BigBuckBunny_320x180.mp4
- ASR: 4 segments
- CUT: 134 scenes
- YOLO: 14315 frames每幀處理耗時
- OCR: 40 frames with text
- Face: 44 frames with faces
- Pose: Timeout
---
### Warning 清理
修復 clippy warnings
- 移除未使用的 imports (HashMap in mongodb_db.rs, postgres_db.rs)
- 新增 `#[allow(dead_code)]` 標註未使用變數
- 新增 `Default` implementation for MongoDb, QdrantDb
-`probe` module 重新命名為 `ffprobe`
- 新增 `player` feature in Cargo.toml
- 修復 `format_in_format_args` 警告
---
### TUI Progress Window 實作
建立新的 UI module
- 建立 `src/ui/mod.rs`
- 建立 `src/ui/progress/mod.rs`
實作功能:
- ProcessorProgress 結構(追蹤每個 processor 狀態)
- ProgressState 結構(管理所有 processors
- ProgressUi 結構ratatui TUI 渲染)
- 整合到 `src/main.rs` 的 process 命令
TUI 顯示:
```
┌ Processing: BigBuckBunny_320x180.mp4 ────────────────────────────────────────┐
│ ASR [████████████] 100% (4 segs) │
│ CUT [████████████] 100% (134 scenes) │
│ ASRX [████████████] 100% (0 segs) │
│ YOLO [██░░░░░░░░░░░] 30% (4200/14315) ETA 2:30 │
│ OCR [---------] 0% │
│ Face [---------] 0% │
│ Pose [---------] 0% │
└──────────────────────────────────────────────────────────────────────────────┘
```
---
### 輸出位置討論
討論 stdout vs stderr vs TUI 的輸出配置:
- 最終結果 → stdout
- Python progress → 需改用 Redis Pub/Sub
- TUI Progress → stderr (ratatui)
---
## 2026-03-18
### Redis Message Bus 設計
討論使用 Redis 作為消息總線,分離 Python 輸出與 Rust TUI 顯示。
設計重點:
1. 頻道命名:`momentry:progress:{uuid}`
2. 本地 Redis`localhost:6379`
3. 失敗策略:完全失效(因 stdout 問題未解決)
### UUID 使用時機分析
分析 Redis Key 上使用 UUID 的時機:
**全局 Keys無 UUID**
- health, stats, jobs 管理
**Per-Video KeysUUID 必要)**
- job:{uuid}, progress:{uuid}, metrics:{uuid}
**Per-Processor KeysUUID + Processor 必要)**
- job:{uuid}:processor:{name}
### 備份系統整合
參考 `docs_v1.0/IMPLEMENTATION/SERVICE_ADDITION_GUIDE.md` 設計規範,規劃 OutputDir 模組:
1. **環境變數**
- `MOMENTRY_OUTPUT_DIR` - JSON 輸出目錄
- `MOMENTRY_BACKUP_DIR` - 備份目錄(預設:`/Users/accusys/momentry/backup/momentry`
- `MOMENTRY_BACKUP_ENABLED` - 啟用備份
2. **命名格式**
- 備份格式:`momentry_data_{YYYYMMDD}_{HHMMSS}_{uuid}.{ext}`
- 校驗和:`{filename}.sha256`
3. **CLI 命令**
- `cargo run -- backup list` - 列出備份
- `cargo run -- backup cleanup` - 清理舊備份
- `cargo run -- backup verify` - 驗證備份
---
### 監控系統整合
討論將 momentry_core 納入監控系統:
1. **Layer 2: Service 監控**
- 新增 momentry_core CLI 檢查
2. **Layer 7: Backup 監控**
- 新增 momentry 備份配置
3. **Redis 監控**
- 健康檢查
- Job 狀態監控
- 即時進度監控
---
## 實作完成項目
### 程式碼變更
| 日期 | 檔案 | 變更 |
|------|------|------|
| 2026-03-17 | `src/core/processor/*.rs` | Async streaming 更新 |
| 2026-03-17 | `src/ui/mod.rs` | 新增 UI module |
| 2026-03-17 | `src/ui/progress/mod.rs` | 新增 Progress TUI |
| 2026-03-17 | `src/main.rs` | 整合 Progress UI |
| 2026-03-18 | `src/core/storage/output_dir.rs` | 新增 OutputDir 模組 |
| 2026-03-18 | `src/core/storage/mod.rs` | 新增 output_dir export |
| 2026-03-18 | `src/core/db/redis_client.rs` | 新增 Redis 客戶端Hash + Pub/Sub |
| 2026-03-18 | `src/core/db/mod.rs` | 新增 redis_client export |
### 新增檔案
| 日期 | 檔案 | 說明 |
|------|------|------|
| 2026-03-18 | `docs_v1.0/REFERENCE/MOMENTRY_CORE_REDIS_KEYS.md` | Redis Key 設計規範 |
| 2026-03-18 | `docs_v1.0/OPERATIONS/MOMENTRY_CORE_MONITORING.md` | 監控規範(暫定) |
| 2026-03-18 | `scripts/redis_publisher.py` | Redis 訊息發布模組 |
### 更新檔案
| 日期 | 檔案 | 說明 |
|------|------|------|
| 2026-03-17 | `Cargo.toml` | 新增 player feature |
| 2026-03-17 | `src/lib.rs` | 新增 ui module exports |
| 2026-03-18 | `docs_v1.0/REFERENCE/PENDING_ISSUES.md` | 新增問題 #2, #3 |
| 2026-03-18 | `src/core/storage/output_dir.rs` | 預設改為 `./output` |
| 2026-03-18 | `scripts/yolo_processor.py` | 新增 --uuid 參數 + Redis |
| 2026-03-18 | `scripts/cut_processor.py` | 新增 Redis |
| 2026-03-18 | `scripts/ocr_processor.py` | 新增 Redis |
| 2026-03-18 | `scripts/face_processor.py` | 新增 --uuid 參數 + Redis |
| 2026-03-18 | `scripts/pose_processor.py` | 新增 --uuid 參數 + Redis |
| 2026-03-18 | `scripts/asr_processor.py` | 新增 --uuid 參數 + Redis |
| 2026-03-18 | `scripts/asrx_processor.py` | 新增 --uuid 參數 + Redis |
| 2026-03-18 | `requirements.txt` | 新增 redis>=5.0.0 |
| 2026-03-18 | `src/core/processor/yolo.rs` | 新增 uuid 參數 |
| 2026-03-18 | `src/core/processor/cut.rs` | 新增 uuid 參數 |
| 2026-03-18 | `src/core/processor/ocr.rs` | 新增 uuid 參數 |
| 2026-03-18 | `src/core/processor/face.rs` | 新增 uuid 參數 |
| 2026-03-18 | `src/core/processor/pose.rs` | 新增 uuid 參數 |
| 2026-03-18 | `src/core/processor/asr.rs` | 新增 uuid 參數 |
| 2026-03-18 | `src/core/processor/asrx.rs` | 新增 uuid 參數 |
| 2026-03-18 | `src/main.rs` | 更新所有 processor 調用傳入 uuid |
| 2026-03-18 | `Cargo.toml` | 新增 futures-util 依賴 |
| 2026-03-18 | `src/core/db/redis_client.rs` | 新增 subscribe_and_callback 方法,密碼認證 |
| 2026-03-18 | `src/ui/progress/mod.rs` | 新增 update_from_redis 方法 |
| 2026-03-18 | `scripts/redis_publisher.py` | 新增密碼認證支援 |
| 2026-03-18 | 測試 | Redis Pub/Sub 成功運作 |
---
## 待解決問題
### 問題 #1: sqlx async INSERT 不會實際寫入數據庫
- 狀態:待解決
- 影響:`store_vector` 函數PVector 存儲
### 問題 #2: TUI 與 stdout 輸出混合
- 狀態:已解決
- 解決方案:使用 Redis Message Bus
- 進度:
- ✅ Redis 客戶端 (`src/core/db/redis_client.rs`)
- ✅ Python redis_publisher.py
- ✅ 所有 Python processors 更新完成
- ✅ 所有 Rust processor 函數更新完成
- ✅ main.rs 調用更新完成
- ✅ Rust TUI Redis 訂閱已完成
### 問題 #3: Redis Message Bus 尚未實作
- 狀態:已解決
- 詳細設計:參考 `docs_v1.0/REFERENCE/MOMENTRY_CORE_REDIS_KEYS.md`
- 進度Python 端 + Rust 端均已完成
---
## 環境變數
```bash
# 輸出目錄
MOMENTRY_OUTPUT_DIR=./output # 預設
# 備份
MOMENTRY_BACKUP_ENABLED=false # 預設
MOMENTRY_BACKUP_DIR=/Users/accusys/momentry/backup/momentry
# Redis未來實作
REDIS_URL=redis://localhost:6379
REDIS_PASSWORD=accusys
```
---
## 數據庫
- PostgreSQL: `postgres://accusys@localhost:5432/momentry`
- Redis: `localhost:6379`(待實作)
- Qdrant: `localhost:6333`
---
## 指令範例
```bash
# 註冊視頻
cargo run -- register /path/to/video.mp4
# 處理視頻
cargo run -- process <uuid>
# 列出備份
cargo run -- backup list
# 清理備份
cargo run -- backup cleanup
# 驗證備份
cargo run -- backup verify
# 查看狀態
cargo run -- status
# API Server
cargo run -- server --host 0.0.0.0 --port 3000
```
---
## 2026-03-18 (進行中)
### Redis Message Bus 實作
**問題**TUI 與 Python stdout 輸出混合,導致 TUI 顯示混亂
**解決方案**:使用 Redis Pub/Sub 作為訊息匯流排
**實作內容**
| 元件 | 檔案 | 狀態 |
|------|------|------|
| Redis 客戶端 | `src/core/db/redis_client.rs` | ✅ |
| Progress 訂閱 | `src/main.rs` | ✅ |
| UI 更新 | `src/ui/progress/mod.rs` | ✅ |
| Python Publisher | `scripts/redis_publisher.py` | ✅ |
| Python Processors | 7 個 `scripts/*_processor.py` | ✅ |
| Rust 函數 | `src/core/processor/*.rs` | ✅ |
**流程**
```
Python Processor ──(Redis Pub)──> Redis ──(Subscribe)──> Rust TUI
```
**測試結果**
- Redis 連線 ✅
- 密碼認證 ✅
- 即時進度發布 ✅
- TUI 即時更新 ✅
**新增依賴**
- `futures-util = "0.3"` (Cargo.toml)
- `redis >= 5.0.0` (requirements.txt)
---
## 2026-03-18 (HTTP API)
### HTTP API 實作
**問題**TUI 運作正常但使用者偏好 HTTP API 來查詢進度
**解決方案**:建立 HTTP 端點 + Redis Hash 儲存
**實作內容**
| 元件 | 檔案 | 變更 |
|------|------|------|
| HTTP 端點 | `src/api/server.rs` | 新增 `/api/v1/progress/:uuid` |
| Redis Hash 查詢 | `src/core/db/redis_client.rs` | 新增 `get_processor_status` 方法 |
| Progress 儲存 | `src/main.rs` | 新增 Redis HSET 儲存進度 |
**API 端點**
```
GET /api/v1/progress/:uuid
Response:
{
"uuid": "5dea6618a606e7c7",
"processors": [
{"name": "asr", "status": "complete", "current": 0, "total": 0, "message": "7 segments"},
{"name": "cut", "status": "complete", "current": 134, "total": 134, "message": "134 scenes"},
{"name": "yolo", "status": "complete", "current": 14300, "total": 14315, "message": "..."},
...
]
}
```
**流程**
```
Python Processor ──(Redis Pub)──> Redis ──(Subscribe)──> Rust TUI
└──(HSET)──> Redis Hash
HTTP Client ──(GET /progress/:uuid)──> Rust API ─(HGETALL)──> Redis Hash
```
**測試結果**
- ✅ 編譯成功
- ✅ API 伺服器啟動 (port 3002)
- ✅ 即時進度查詢
- ✅ 完整流程測試 (BigBuckBunny_320x180.mp4)
**除錯記錄**
1. 語法錯誤main.rs 有重複程式碼區塊 (lines 297-322),已移除
2. DB 連線池:從 5 增加到 10 個連線
3. PostgreSQL 狀態:處理 shutdown 狀態,殺掉 stale 連線
**新增變更**
- `src/api/server.rs` - 新增進度端點
- `src/core/db/redis_client.rs` - 新增 `get_processor_status` 方法
- `src/core/db/postgres_db.rs` - 連線池 5→10
- `src/main.rs` - Redis Hash 儲存 + 語法修復
**使用方式**
```bash
# 啟動 API 伺服器
cargo run --bin momentry -- server --host 127.0.0.1 --port 3002
# 註冊影片
cargo run --bin momentry -- register ~/test_video/BigBuckBunny_320x180.mp4
# 處理影片
cargo run --bin momentry -- process <uuid>
# 查詢進度
curl http://127.0.0.1:3002/api/v1/progress/<uuid>
```
---
## 2026-03-18 (Dashboard)
### Web Dashboard 實作
**目標**:建立 Web 介面監控 momentry_core 處理進度
**技術選擇**Static HTML + JavaScript (非 WASM)
**實作內容**
| 元件 | 檔案 | 說明 |
|------|------|------|
| Dashboard | `momentry_dashboard/dist/index.html` | 靜態 HTML 頁面 |
| API 代理 | Caddyfile port 3200 | 反向代理到 API server |
**功能**
- 影片列表顯示
- 即時進度條 (每 5 秒自動刷新)
- 搜尋功能
- 處理器狀態 (ASR/CUT/YOLO/OCR/Face/Pose)
**訪問**
- Dashboard: http://localhost:3200
- API: http://localhost:3200/api/v1/*
---
## 發生問題記錄
### HTTP API 問題
1. **語法錯誤** (main.rs)
- 位置lines 297-322
- 原因:重複的程式碼區塊
- 解決:移除重複區塊
2. **DB 連線池耗盡**
- 原因:預設 5 個連線不足
- 解決:增加到 10 個連線
3. **PostgreSQL shutdown 狀態**
- 原因:共享記憶體未釋放
- 解決:殺掉 stale 連線
### WASM Dashboard 問題
1. **Yew 版本問題**
- 嘗試yew 0.21 → 0.23
- 問題feature 名稱變更 (`web-sys``web_sys``csr`)
- 解決:放棄 WASM改用靜態 HTML
2. **編譯錯誤**
- `wasm32-unknown-unknown` target 未安裝
- 解決:`rustup target add wasm32-unknown-unknown`
3. **Yew 0.23 API 變更**
- Properties 需要 PartialEq derive
- 多處 API 語法變更
- 放棄 WASM 方案
### Gitea Push 問題
1. **Remote URL 錯誤**
- 原因:使用 localhost:3000 而非 gitea.momentry.ddns.net
- 解決:建立新 repo `momentry_core_0_1`
2. **認證問題**
- SSH key 未授權
- 密碼認證成功推送
### Caddy 設定問題
1. **API 代理順序**
- 問題try_files 在 reverse_proxy 之前導致 API 回傳 HTML
- 解決:使用 `handle` 區塊明確定義順序
```caddyfile
:3200 {
handle /api/* {
reverse_proxy localhost:3002
}
handle {
root * /Users/accusys/momentry_dashboard/dist
try_files {path} /index.html
file_server
}
}
```
---
## 未來工作
- [ ] 修復 WASM Dashboard (Yew 0.23 相容性)
- [ ] 新增影片播放器整合
- [ ] WebSocket 實時推送
- [ ] 移動端響應式設計

View File

@@ -0,0 +1,538 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Momentry JSON 輸出檔案規範"
date: "2026-03-16"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "momentry"
- "json"
- "輸出檔案規範"
ai_query_hints:
- "查詢 Momentry JSON 輸出檔案規範 的內容"
- "Momentry JSON 輸出檔案規範 的主要目的是什麼?"
- "如何操作或實施 Momentry JSON 輸出檔案規範?"
---
# Momentry JSON 輸出檔案規範
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-16 |
| 文件版本 | V1.0 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
---
本文檔定義 Momentry Core 系統中所有 JSON 輸出檔案的結構、命名規範與儲存位置。
---
## 1. 輸出檔案總覽
### 1.1 檔案類型
| 類型 | 前綴 | 說明 | 狀態 |
|------|------|------|------|
| **Probe** | `{uuid}.probe.json` | 影片元數據 | ✅ 已實作 |
| **ASR** | `{uuid}.asr.json` | 語音識別結果 | ✅ 已實作 |
| **ASRx** | `{uuid}.asrx.json` | 說話者分離 | 🔜 規劃中 |
| **OCR** | `{uuid}.ocr.json` | 文字辨識結果 | 🔜 規劃中 |
| **YOLO** | `{uuid}.yolo.json` | 物件偵測結果 | 🔜 規劃中 |
| **Face** | `{uuid}.face.json` | 人臉偵測結果 | 🔜 規劃中 |
| **Pose** | `{uuid}.pose.json` | 姿態估計結果 | 🔜 規劃中 |
| **Thumbnail** | `{uuid}/thumb_XXX.jpg` | 縮圖檔案 | ✅ 已實作 |
### 1.2 命名規範
```
{UUID}.{類型}.json
範例:
1636719dc31f78ac.probe.json - 影片探測結果
1636719dc31f78ac.asr.json - 語音識別結果
1636719dc31f78ac.ocr.json - 文字辨識結果
```
- **UUID**: 16 字元,基於檔案路徑計算
- **類型**: 小寫 snake_case
- **副檔名**: `.json`
---
## 2. 輸出目錄結構
### 2.1 預設輸出位置
```
momentry_core_0.1/
├── {uuid}.probe.json # 影片探測
├── {uuid}.asr.json # 語音識別
├── {uuid}.asrx.json # 說話者分離
├── {uuid}.ocr.json # 文字辨識
├── {uuid}.yolo.json # 物件偵測
├── {uuid}.face.json # 人臉偵測
├── {uuid}.pose.json # 姿態估計
└── thumbnails/
└── {uuid}/
├── thumb_000.jpg
├── thumb_001.jpg
└── ...
```
### 2.2 儲存策略
| 資料類型 | 儲存位置 | 說明 |
|----------|----------|------|
| JSON 檔案 | 專案根目錄 | 方便快速存取 |
| 縮圖 | thumbnails/{uuid}/ | 分離儲存 |
| 資料庫 | PostgreSQL | 長期儲存 |
---
## 3. JSON 結構定義
### 3.1 Probe (影片探測)
**檔案**: `{uuid}.probe.json`
```json
{
"streams": [
{
"index": 0,
"codec_name": "h264",
"codec_type": "video",
"width": 1920,
"height": 1080,
"r_frame_rate": "60000/1001",
"duration": "6879.329524",
"sample_rate": null,
"channels": null
},
{
"index": 1,
"codec_name": "aac",
"codec_type": "audio",
"width": null,
"height": null,
"r_frame_rate": "0/0",
"duration": "6879.245333",
"sample_rate": "48000",
"channels": 2
}
],
"format": {
"filename": "/path/to/video.mov",
"format_name": "mov,mp4,m4a,3gp,3g2,mj2",
"duration": "6879.329524",
"size": "2361629896",
"bit_rate": "2748000"
}
}
```
**欄位說明**:
| 欄位 | 類型 | 說明 |
|------|------|------|
| `streams` | Array | 媒體串流陣列 |
| `streams[].index` | Integer | 串流索引 |
| `streams[].codec_name` | String | 編碼名稱 |
| `streams[].codec_type` | String | 串流類型 (video/audio) |
| `streams[].width` | Integer | 寬度 (video) |
| `streams[].height` | Integer | 高度 (video) |
| `streams[].r_frame_rate` | String | 幀率 |
| `streams[].duration` | String | 持續時間 (秒) |
| `streams[].sample_rate` | String | 採樣率 (audio) |
| `streams[].channels` | Integer | 聲道數 (audio) |
| `format` | Object | 檔案格式資訊 |
| `format.filename` | String | 原始檔案路徑 |
| `format.format_name` | String | 格式名稱 |
| `format.duration` | String | 總時長 (秒) |
| `format.size` | String | 檔案大小 (bytes) |
| `format.bit_rate` | String | 位元率 |
---
### 3.2 ASR (語音識別)
**檔案**: `{uuid}.asr.json`
```json
{
"language": "en",
"language_probability": 0.9945855736732483,
"segments": [
{
"start": 0.0,
"end": 19.04,
"text": "Hello and welcome to the old-time movie show."
},
{
"start": 19.04,
"end": 25.44,
"text": "Today we are featuring the 1963 comedy mystery film Charade."
}
]
}
```
**欄位說明**:
| 欄位 | 類型 | 說明 |
|------|------|------|
| `language` | String | 偵測語言代碼 (ISO 639-1) |
| `language_probability` | Float | 語言偵測機率 (0-1) |
| `segments` | Array | 語音分段陣列 |
| `segments[].start` | Float | 開始時間 (秒) |
| `segments[].end` | Float | 結束時間 (秒) |
| `segments[].text` | String | 識別文字 |
---
### 3.3 ASRx (說話者分離)
**檔案**: `{uuid}.asrx.json`
```json
{
"language": "en",
"language_probability": 0.95,
"segments": [
{
"start": 0.0,
"end": 19.04,
"text": "Hello and welcome to the old-time movie show.",
"speaker_id": "SPEAKER_00",
"speaker_embedding": [0.123, -0.456, ...]
},
{
"start": 19.04,
"end": 25.44,
"text": "Today we are featuring the 1963 comedy mystery film Charade.",
"speaker_id": "SPEAKER_01",
"speaker_embedding": [0.789, -0.123, ...]
}
]
}
```
**欄位說明**:
| 欄位 | 類型 | 說明 |
|------|------|------|
| `language` | String | 偵測語言代碼 |
| `language_probability` | Float | 語言偵測機率 |
| `segments` | Array | 語音分段陣列 |
| `segments[].start` | Float | 開始時間 (秒) |
| `segments[].end` | Float | 結束時間 (秒) |
| `segments[].text` | String | 識別文字 |
| `segments[].speaker_id` | String | 說話者 ID |
| `segments[].speaker_embedding` | Array | 說話者嵌入向量 (可選) |
---
### 3.4 OCR (文字辨識)
**檔案**: `{uuid}.ocr.json`
```json
{
"segments": [
{
"start": 10.5,
"end": 12.3,
"text": "EXAMPLE TEXT",
"boxes": [
{
"x1": 100,
"y1": 50,
"x2": 400,
"y2": 100
}
],
"confidence": 0.95
}
]
}
```
**欄位說明**:
| 欄位 | 類型 | 說明 |
|------|------|------|
| `segments` | Array | OCR 分段陣列 |
| `segments[].start` | Float | 開始時間 (秒) |
| `segments[].end` | Float | 結束時間 (秒) |
| `segments[].text` | String | 辨識文字 |
| `segments[].boxes` | Array | 文字邊界框陣列 |
| `segments[].boxes[].x1` | Integer | 左上 X 座標 |
| `segments[].boxes[].y1` | Integer | 左上 Y 座標 |
| `segments[].boxes[].x2` | Integer | 右下 X 座標 |
| `segments[].boxes[].y2` | Integer | 右下 Y 座標 |
| `segments[].confidence` | Float | 辨識信心度 |
---
### 3.5 YOLO (物件偵測)
**檔案**: `{uuid}.yolo.json`
```json
{
"segments": [
{
"start": 0.0,
"end": 1.0,
"objects": [
{
"class": "person",
"confidence": 0.92,
"box": {
"x1": 150,
"y1": 200,
"x2": 400,
"y2": 800
}
},
{
"class": "car",
"confidence": 0.87,
"box": {
"x1": 800,
"y1": 400,
"x2": 1200,
"y2": 700
}
}
]
}
]
}
```
**欄位說明**:
| 欄位 | 類型 | 說明 |
|------|------|------|
| `segments` | Array | 時間分段陣列 |
| `segments[].start` | Float | 開始時間 (秒) |
| `segments[].end` | Float | 結束時間 (秒) |
| `segments[].objects` | Array | 偵測物件陣列 |
| `segments[].objects[].class` | String | 物件類別 |
| `segments[].objects[].confidence` | Float | 偵測信心度 |
| `segments[].objects[].box` | Object | 邊界框 |
---
### 3.6 Face (人臉偵測)
**檔案**: `{uuid}.face.json`
```json
{
"segments": [
{
"start": 0.0,
"end": 1.0,
"faces": [
{
"face_id": "face_001",
"box": {
"x1": 100,
"y1": 50,
"x2": 300,
"y2": 350
},
"embedding": [0.123, -0.456, ...],
"emotion": "happy",
"age": 35,
"gender": "female"
}
]
}
]
}
```
**欄位說明**:
| 欄位 | 類型 | 說明 |
|------|------|------|
| `segments` | Array | 時間分段陣列 |
| `segments[].start` | Float | 開始時間 (秒) |
| `segments[].end` | Float | 結束時間 (秒) |
| `segments[].faces` | Array | 人臉陣列 |
| `segments[].faces[].face_id` | String | 人臉 ID |
| `segments[].faces[].box` | Object | 邊界框 |
| `segments[].faces[].embedding` | Array | 人臉嵌入向量 |
| `segments[].faces[].emotion` | String | 情緒分類 (可選) |
| `segments[].faces[].age` | Integer | 年齡估計 (可選) |
| `segments[].faces[].gender` | String | 性別估計 (可選) |
---
### 3.7 Pose (姿態估計)
**檔案**: `{uuid}.pose.json`
```json
{
"segments": [
{
"start": 0.0,
"end": 1.0,
"poses": [
{
"person_id": "person_001",
"keypoints": {
"nose": {"x": 320, "y": 120, "confidence": 0.98},
"left_eye": {"x": 335, "y": 110, "confidence": 0.95},
"right_eye": {"x": 305, "y": 110, "confidence": 0.93},
"left_shoulder": {"x": 280, "y": 180, "confidence": 0.91},
"right_shoulder": {"x": 360, "y": 180, "confidence": 0.89}
},
"confidence": 0.92
}
]
}
]
}
```
**欄位說明**:
| 欄位 | 類型 | 說明 |
|------|------|------|
| `segments` | Array | 時間分段陣列 |
| `segments[].start` | Float | 開始時間 (秒) |
| `segments[].end` | Float | 結束時間 (秒) |
| `segments[].poses` | Array | 姿態陣列 |
| `segments[].poses[].person_id` | String | 人員 ID |
| `segments[].poses[].keypoints` | Object | 關鍵點 |
| `segments[].poses[].keypoints.{name}` | Object | 各關鍵點 |
| `segments[].poses[].keypoints.{name}.x` | Integer | X 座標 |
| `segments[].poses[].keypoints.{name}.y` | Integer | Y 座標 |
| `segments[].poses[].keypoints.{name}.confidence` | Float | 信心度 |
| `segments[].poses[].confidence` | Float | 整體信心度 |
---
## 4. 處理流程
### 4.1 處理管線
```
影片檔案
┌─────────────────┐
│ 1. Register │ 建立 UUID註冊影片
└────────┬────────┘
┌────▼────┐
│ 2. Probe │ ffprobe 擷取元數據
└────┬────┘
│ {uuid}.probe.json
┌────▼─────┐
│ 3. ASR │ faster-whisper 語音識別
└────┬─────┘
│ {uuid}.asr.json
┌────▼──────┐
│ 4. ASRx │ 說話者分離 (pyannote)
└────┬──────┘
│ {uuid}.asrx.json
┌────▼────┐
│ 5. OCR │ Tesseract 文字辨識
└────┬────┘
│ {uuid}.ocr.json
┌────▼────┐
│ 6. YOLO │ 物件偵測
└────┬────┘
│ {uuid}.yolo.json
┌────▼────┐
│ 7. Face │ 人臉偵測
└────┬────┘
│ {uuid}.face.json
┌────▼────┐
│ 8. Pose │ 姿態估計
└────┬────┘
│ {uuid}.pose.json
┌────▼──────┐
│ 9. Chunk │ 轉換為資料庫 chunks
└───────────┘
```
### 4.2 失敗處理
| 階段 | 失敗時 | 處理 |
|------|--------|------|
| Probe | 無法讀取影片 | 終止流程,輸出錯誤 |
| ASR | 無音軌 | 產生空 segments繼續流程 |
| OCR/YOLO/Face/Pose | 處理失敗 | 跳過該階段,記錄日誌 |
---
## 5. 資料庫儲存
### 5.1 Chunk 結構
```sql
CREATE TABLE chunks (
id BIGSERIAL PRIMARY KEY,
uuid VARCHAR(16) NOT NULL,
chunk_id VARCHAR(64) NOT NULL,
chunk_index INTEGER NOT NULL,
chunk_type VARCHAR(32) NOT NULL,
start_time DOUBLE PRECISION NOT NULL,
end_time DOUBLE PRECISION NOT NULL,
content JSONB NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(uuid, chunk_id)
);
```
### 5.2 轉換範例
```rust
// ASR → Chunk (Sentence)
for (i, seg) in asr_result.segments.iter().enumerate() {
let chunk = Chunk::new(
uuid.clone(),
i as u32,
ChunkType::Sentence,
seg.start,
seg.end,
serde_json::json!({"text": seg.text}),
);
db.store_chunk(&chunk).await?;
}
```
---
## 6. 版本歷史
| 版本 | 日期 | 變更 |
|------|------|------|
| 1.0.0 | 2026-03-16 | 初始版本 |
---
## 7. 相關文件
- [RUST_DEVELOPMENT.md](./RUST_DEVELOPMENT.md) - Rust 開發規範
- [AGENTS.md](../AGENTS.md) - 開發規範
- [monitor_config.yaml](../monitor/config/monitor_config.yaml) - 監控配置

View File

@@ -0,0 +1,50 @@
# M4 / M5 協作協議
## 核心原則:檔案是 source of truth
所有 processor 的產出是 `{uuid}.{processor}.json` 檔案。
**檔案存在 = 處理完成**,優先於 DB 或 Redis 的任何狀態記錄。
## 絕對禁止
### 1. 不可刪除已存在的輸出檔
- 任何 `{uuid}.{processor}.*` 檔案,無論是 `.json``.json.tmp``.json.partial``.json.err`
- 一律不允許 `rm``unlink``delete`
- 唯一例外:明確的人工指令 `rm` / `Delete this file`
### 2. 不可覆蓋已存在的輸出檔
- 重新執行 processor 前,必須先 **copy非 rename** 加上時間戳備份
- 備份命名:`{uuid}.{processor}.{timestamp}.{original_extension}`
- 若備份名已存在,跳過(不覆蓋不 counter
- 原檔保留不動
### 3. 不可跨域操作
- M4 只能在 M4 機器Mac Mini上操作
- M5 只能在 M5 機器MacBook Pro上操作
- 禁止任何跨機器的檔案操作或 cleanup
## 重跑 processor 的正確流程
1. Worker 檢查 `{uuid}.{processor}.json` 是否存在
2. **存在 → 跳過**(無論 DB/Redis 狀態)
3. 不存在 → copy 備份既有 `{uuid}.{processor}.*` → 執行 processor
4. Processor 輸出寫入 `.tmp` → 完成後 rename 為 `.json`
## 例外處理
| 狀態 | 行為 |
|------|------|
| `.json` 存在 | 跳過,視為完成 |
| `.json.tmp` 存在(無 `.json` | 視為未完成,備份後重跑 |
| `.json.partial` 存在(無 `.json` | 視為未完成,備份後重跑 |
| `.json.err` 存在(無 `.json` | 視為未完成,備份後重跑 |
| Process 被 killSIGKILL | partial 存為 `.json.partial`(非 `.json` |
## 違規後果
2026-05-09 事故M4 release 打包未含 .json → 跨域操作 → M5 cleanup 誤刪 asr.json
→ 導致 ASR 需重跑(完整電影約 1.5hr
→ YOLO 需重跑
→ 損失已完成的 pipeline 進度
此類違規不可再發生。

View File

@@ -0,0 +1,31 @@
# M4 Release Incident — 2026-05-09
## Summary
M4 在進行 release 打包作業時,未依照計畫包含 output `.json` 檔案,僅在 database 中保留 records。此外 M4 違反操作邊界進入 M5 管轄範圍M5 執行 cleanup 時將已完成的 `asr.json` 一併刪除。
## Impact
| 檔案 | 狀態 | 說明 |
|------|------|------|
| `{uuid}.asr.json` | ❌ 遺失 | 已完成的 ASR 輸出被 M5 cleanup 誤刪 |
| `{uuid}.yolo.json` | ❌ 損毀 | JSON parse error需重跑 |
| DB records | ⚠️ 不一致 | processor_results 狀態與實際檔案不符 |
## Root Cause
1. **M4 release 打包遺漏**: Release 流程未將 `.json` 輸出檔納入打包範圍,只保留了 DB。
2. **M4 越界操作**: M4 在 M5 的目錄/範圍內執行操作,違反開發隔離原則。
3. **M5 cleanup 誤刪**: M5 的 cleanup 機制未預期 M4 的產出,將 `asr.json` 視為無用檔案清除。
## 處理
- ASR: 重跑中asr_processor.py完整電影約 6780s
- YOLO: 重跑中yolo_processor.py
- 已修改 worker 邏輯:開機後以 `.json` 檔案存在為 source of truth不再僅依賴 DB/Redis 狀態
## 預防措施
- Release 流程需明確定義 deliverables 包含 `.json` 檔案
- M4/M5 操作邊界需嚴格遵守,禁止跨域操作
- Cleanup 機制應先確認檔案是否為有效 processor output

View File

@@ -0,0 +1,77 @@
# M4 vs M5 Max Comparison
## Hardware
| Spec | M4 (Mac Mini) | M5 (MacBook Pro) |
|------|--------------|-------------------|
| **Model** | Mac Mini (M4) | MacBook Pro (M5 Max) |
| **Hostname** | `accusys-Mac-mini-M4-2.local` | `Accusyss-MacBook-Pro.local` |
| **macOS** | 26.4.1 (Sequoia) | 26.4.1 (Sequoia) |
| **RAM** | 16 GB | **48 GB** |
| **CPU Cores** | 10 | **18** |
| **Disk** | 2TB (est.) | **1.8TB (12GB used, 97% free)** |
| **Network** | 192.168.110.210, 192.168.110.200 | 192.168.110.201, 192.168.31.182 |
## Installed Services
| Service | M4 | M5 |
|---------|-----|------|
| **PostgreSQL** | 18.1 (Homebrew) | **18.3 (Source build)** |
| **pgvector** | Homebrew | **0.8.2 (Source build)** |
| **Redis** | 8.4.0 (Homebrew) | **7.4.3 (Source build)** |
| **Qdrant** | Homebrew/pre-built | **1.17.1 (Source build, `cargo`)** |
| **MongoDB** | Homebrew | 8.2.7 (Homebrew) |
| **MariaDB** | ✗ via brew | **12.2.2 (Homebrew, for WordPress)** |
| **PHP** | ✗ via brew | **8.5.5 (Homebrew, WordPress ext. ✅)** |
| **SFTPGo** | Pre-built binary | **2.7.1 (Source build, patched dep)** |
| **FFmpeg** | 8.1 (Homebrew) | **8.1.1 (Homebrew)** |
| **OpenCode** | 1.14.39 | **1.14.39** |
| **Gemma4 LLM** | ✗ (not enough RAM) | **31B Q5_K_M @ 8081** |
## Build Approach
| Aspect | M4 | M5 |
|--------|-----|-----|
| **PostgreSQL** | `brew install postgresql@18` | `./configure && make && make install` |
| **Redis** | `brew install redis` | `make && cp src/redis-server ~/redis/bin/` |
| **Qdrant** | `brew install qdrant` | `cargo build --release --bin qdrant` (from GitHub) |
| **SFTPGo** | `brew install sftpgo` | `git clone && go build` (patched `go-m1cpu`) |
| **Philosophy** | Mixed (Homebrew + binary) | **Source-first** (GitHub source, checksums recorded) |
## Data Migration (M4 → M5)
| Data | Size | Status |
|------|------|--------|
| **Database (dev schema)** | 837MB dump | ✅ Restored (16 tables) |
| **Video file** | 2.2GB | ✅ Transferred |
| **output_dev JSON** | 2.9GB (462 files) | ✅ Transferred |
| **output JSON** | 65MB (2523 files) | ✅ Transferred |
| **Configs** | small | ✅ Transferred |
## Database Row Counts (M5)
| Table | Rows |
|-------|------|
| `pre_chunks` | 494,339 |
| `face_detections` | 6,211 |
| `tkg_nodes` | 2,414 |
| `identity_bindings` | 2,347 |
| `tkg_edges` | 1,320 |
## Key Differences
### 1. RAM (16GB vs 48GB)
- **M4 (16GB)**: Cannot run Gemma4 31B LLM locally. Memory pressure during concurrent pipeline processing.
- **M5 (48GB)**: Can run Gemma4 31B (Q5_K_M, ~20GB) + databases + playground simultaneously.
### 2. Build Philosophy
- **M4**: Quick setup via Homebrew bottles (pre-compiled).
- **M5**: **Source-first** — every service built from GitHub/official source. `SHA256` checksums recorded. Dependencies patched as needed (SFTPGo `go-m1cpu`).
### 3. Unique M5 Services
- **MariaDB + PHP**: Installed for WordPress/marcom portal development.
- **Gemma4 LLM**: Running on port 8081, accessible for RAG/identity clustering.
- **OpenCode**: Configured with Gemma4 provider for AI-assisted development.
### 4. Data Freshness
- M5 is a **snapshot** of M4's state at 2026-05-06 (commit `bac6c2d`). Changes made on M4 after sync date must be re-synced.

View File

@@ -0,0 +1,259 @@
# M5 Dev Environment Setup Log
**Machine**: M5 MacBook Pro (MacOS 26.4.1, Apple M5 Max, 48GB)
**User**: accusys (admin group, sudo with password)
**Date**: 2026-05-06
**Setup by**: OpenCode
---
## 1. Source Code
| Item | Detail |
|------|--------|
| Repo | `https://gitea.momentry.ddns.net/warren/momentry_core.git` |
| Branch | `main` |
| Commit | `bac6c2d` (feat: identity clustering V3.0) |
| Sync method | rsync from M4 (192.168.110.210) |
| Path | `~/momentry_core_0.1/` |
---
## 2. Installed Services
### 2.1 PostgreSQL 18.3
| Field | Value |
|-------|-------|
| **Source** | [https://ftp.postgresql.org/pub/source/v18.3/postgresql-18.3.tar.gz](https://ftp.postgresql.org/pub/source/v18.3/postgresql-18.3.tar.gz) |
| **GitHub** | [https://github.com/postgresql/postgresql](https://github.com/postgresql/postgresql) |
| **Build method** | Manual `./configure && make && make install` |
| **Prefix** | `~/pgsql/18.3/` |
| **Data dir** | `~/pgsql/data/` |
| **Port** | 5432 |
| **Version** | PostgreSQL 18.3 |
| **SHA256** | `ab04939aafdb9e8487c2f13dda91e6a4a7f4c83368f5bedd23ee4ad1fda64afb` |
| **Start command** | `pg_ctl -D ~/pgsql/data -l ~/pgsql/pg.log start` |
| **Configure flags** | `--prefix=$HOME/pgsql/18.3 --with-uuid=e2fs --with-icu --with-openssl` |
| **Build date** | 2026-05-06 |
| **Notes** | `--with-uuid=e2fs` used (requires Homebrew `e2fsprogs`). macOS built-in UUID not detected by configure. |
### 2.2 pgvector 0.8.2
| Field | Value |
|-------|-------|
| **Source** | [https://github.com/pgvector/pgvector](https://github.com/pgvector/pgvector) |
| **Version** | v0.8.2 |
| **Build method** | `git clone && make && make install` |
| **SHA256** | `65dec31ec078d60ee9d8e1dac59be8a41edf8c79bf380cd0093691b0afd257a8` |
| **Build date** | 2026-05-06 |
| **Notes** | Built against PostgreSQL 18.3 source installation |
### 2.3 Redis 7.4.3
| Field | Value |
|-------|-------|
| **Source** | [https://github.com/redis/redis/archive/refs/tags/7.4.3.tar.gz](https://github.com/redis/redis/archive/refs/tags/7.4.3.tar.gz) |
| **GitHub** | [https://github.com/redis/redis](https://github.com/redis/redis) |
| **Version** | 7.4.3 |
| **Build method** | `make -j$(sysctl -n hw.ncpu)` |
| **Binary path** | `~/redis/bin/redis-server` |
| **Port** | 6379 |
| **SHA256** | `87b6a9ea145c56c1ace724acbb9906b7be4abddd44041545adf44ce9f4d0a615` |
| **Start command** | `redis-server --daemonize yes --port 6379` |
| **Build date** | 2026-05-06 |
### 2.4 Qdrant 1.17.1
| Field | Value |
|-------|-------|
| **Source** | [https://github.com/qdrant/qdrant.git](https://github.com/qdrant/qdrant.git) |
| **Version** | v1.17.1 |
| **Build method** | `cargo build --release --bin qdrant` |
| **Binary path** | `~/momentry_core_0.1/services/qdrant/target/release/qdrant` |
| **Storage dir** | `~/qdrant_storage` |
| **Port** | 6333 (HTTP), 6334 (gRPC) |
| **SHA256** | `8f8aa63840a0f948b43f9b95f784ace69595892de5dc581bb66bd62fd86d6c66` |
| **Build date** | 2026-05-06 |
| **Config** | `~/qdrant_config.yaml` |
| **Start command** | `qdrant --config-path ~/qdrant_config.yaml &` |
| **Build deps** | protoc (Homebrew protobuf), cmake |
### 2.5 MongoDB 8.2.7
| Field | Value |
|-------|-------|
| **Source** | Homebrew `mongodb/brew/mongodb-community` |
| **Version** | 8.2.7 |
| **Port** | 27017 |
| **Start command** | `brew services start mongodb/brew/mongodb-community` |
| **Install date** | 2026-05-06 |
### 2.6 MariaDB 12.2.2
| Field | Value |
|-------|-------|
| **Source** | Homebrew `mariadb` |
| **Version** | 12.2.2-MariaDB |
| **Port** | 3306 |
| **Start command** | `brew services start mariadb` |
| **Install date** | 2026-05-06 |
### 2.7 PHP 8.5.5
| Field | Value |
|-------|-------|
| **Source** | Homebrew `php` |
| **Version** | 8.5.5 |
| **WordPress extensions** | mysqli, pdo_mysql, gd, xml, mbstring, curl, zip, json, intl, bcmath, gmp, openssl |
| **Start command** | `brew services start php` |
| **Install date** | 2026-05-06 |
### 2.8 FFmpeg / FFprobe 8.1.1
| Field | Value |
|-------|-------|
| **Source** | Homebrew `ffmpeg` |
| **Version** | 8.1.1 |
| **SHA256** | `00d01197255300c02122c783dd0126a9e7f47d6c6a19faafae2e6610efd071d3` |
| **Install date** | 2026-05-06 |
### 2.9 SFTPGo 2.7.1
| Field | Value |
|-------|-------|
| **Source** | [https://github.com/drakkan/sftpgo.git](https://github.com/drakkan/sftpgo.git) |
| **Version** | v2.7.1 |
| **Build method** | `git clone && go build -o sftpgo_bin ./` |
| **Binary path** | `~/momentry_core_0.1/services/sftpgo_bin` |
| **SHA256** | `550b6653f8f2cd7c58620e128e85be571a6702c79cf374824ad9b420ca039db1` |
| **Build date** | 2026-05-06 |
| **Patch** | Upgraded `go-m1cpu` from v0.2.0 → v0.2.1 to fix SIGTRAP crash on macOS 26.4.1 |
| **Notes** | Pre-built binary from GitHub releases crashed with `go-m1cpu` cgo compatibility issue. Source build with patched dependency resolved. |
### 2.10 OpenCode 1.14.39
| Field | Value |
|-------|-------|
| **Source** | [https://opencode.ai/install](https://opencode.ai/install) |
| **Version** | 1.14.39 |
| **Binary path** | `~/.opencode/bin/opencode` |
| **SHA256** | `def4a786c257bd6a965e46a2b069802496681b9eea20261d7d1b55629af3d1da` |
| **Install date** | 2026-05-06 |
### 2.11 Python 3.11 + Packages
| Field | Value |
|-------|-------|
| **Source** | Homebrew `python@3.11` |
| **Version** | 3.11.15 |
| **Path** | `/opt/homebrew/bin/python3.11` |
| **Key packages** | coremltools, opencv-python, numpy, psycopg2, torch, transformers, whisperx, etc. |
| **Requirements** | `~/momentry_core_0.1/requirements.txt` |
| **Install date** | 2026-05-06 |
| **FaceNet model** | `models/facenet512.mlpackage` (512D CoreML, loads OK) |
### 2.12 Build Tools
| Tool | Version | Source |
|------|---------|--------|
| Rust | 1.95.0 | rustup (pre-installed) |
| Go | 1.26.2 | Homebrew `go` |
| cmake | 4.3.2 | Homebrew `cmake` |
| pkg-config | - | Homebrew `pkg-config` |
---
## 3. Momentry Configuration
### 3.1 Environment Files
| File | Purpose |
|------|---------|
| `.env` | Production config (port 3002) |
| `.env.development` | Development config (port 3003) |
Key settings:
- `DATABASE_URL=postgres://accusys@localhost:5432/momentry`
- `REDIS_URL=redis://:accusys@localhost:6379`
- `DATABASE_SCHEMA=dev`
- `MOMENTRY_SERVER_PORT=3003` (dev) / `3002` (prod)
- `MOMENTRY_API_KEY=muser_test_apikey`
- `MOMENTRY_PYTHON_PATH=/opt/homebrew/bin/python3.11`
- `MOMENTRY_SCRIPTS_DIR=/Users/accusys/momentry_core_0.1/scripts`
### 3.2 Database Tables Created
| Table | Created by |
|-------|-----------|
| `dev.videos` | Manual SQL |
| `dev.chunks` | Manual SQL |
| `dev.monitor_jobs` | Manual SQL |
| `dev.processor_results` | Manual SQL |
| `dev.talents` | Manual SQL |
| `dev.identity_bindings` | Manual SQL |
| `dev.api_keys` | Manual SQL |
### 3.3 API Key
- Key: `muser_test_apikey`
- Hash (SHA256): `3f2fa16e44ff74267786fdf979b9c33dac0cad515282e4937a0776756a61e821`
- Status: active
---
## 4. Running Services (Verified)
| Service | Port | Status |
|---------|------|--------|
| PostgreSQL | 5432 | ✅ |
| Redis | 6379 | ✅ |
| Qdrant | 6333 | ✅ |
| MongoDB | 27017 | ✅ |
| MariaDB | 3306 | ✅ |
| Momentry Playground | 3003 | ✅ |
| Gemma4 LLM | 8081 | ✅ (pre-installed) |
---
## 5. PATH Configuration
`.zshrc`:
```zsh
export PATH="/opt/homebrew/bin:/opt/homebrew/opt/postgresql@18/bin:$HOME/.opencode/bin:$PATH"
```
Also available:
- `$HOME/pgsql/18.3/bin` — source-built PostgreSQL tools
- `$HOME/redis/bin` — source-built Redis
- `$HOME/.cargo/bin` — Rust/Cargo tools
---
## 6. M5 End-to-End Test Results (Charade Full Movie)
Run date: 2026-05-06 20:38-20:57
| Stage | Time | Result |
|-------|------|--------|
| **Swift_face** (Vision ANE detection) | 867s (14.5 min) | 3999 frames (interval=30) |
| **CoreML FaceNet** (512D embedding) | 271s (4.5 min) | 6186 face embeddings |
| **Face tracker** (scene-cut aware) | ~30s | 1538 traces |
| **DB store** | ~5s | 6186 detections in `dev.face_detections` |
| **Total** | ~19 min | 1 long video (412k frames, 2.2GB) |
**Scene-cut effect**: 1538 traces (vs 379 without scene-cut reset in M4 data). Scene boundaries correctly split traces.
**Models used**:
- Face detection: Apple Vision (ANE) via `swift_face`
- Face embedding: CoreML FaceNet 512D via `facenet512.mlpackage`
- Text embedding: `mxbai-embed-large` (1024D) via Ollama
---
## 7. Known Issues
1. **Momentry API status `degraded`**: Expected on fresh setup. Some cache/processing dependencies not fully initialized.
2. **SFTPGo startup requires config**: Binary built from source, needs config file for production use.
3. **Migration scripts not all run**: Base tables created manually. Some migration files (017+) reference tables/columns that need verification.
4. **OpenCode config**: `~/.config/opencode/config.json` not yet configured for M5 Gemma4 provider.

View File

@@ -0,0 +1,303 @@
---
document_type: "reference_doc"
service: "REDIS"
title: "Momentry Core Redis Key 設計規範"
date: "2026-03-17"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "momentry"
- "core"
- "redis"
- "設計規範"
ai_query_hints:
- "查詢 Momentry Core Redis Key 設計規範 的內容"
- "Momentry Core Redis Key 設計規範 的主要目的是什麼?"
- "如何操作或實施 Momentry Core Redis Key 設計規範?"
---
# Momentry Core Redis Key 設計規範
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-17 |
| 文件版本 | V1.0 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-17 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
| V1.1 | 2026-03-25 | 新增可配置 Redis Key Prefix | Warren | OpenCode / GLM-5 |
---
## 1. 概述
本文檔說明 momentry_core 如何使用 Redis 作為監控和狀態管理系統。
## 2. 可配置 Redis Key Prefix
### 2.1 環境變數
從 V1.1 開始,所有 Redis Keys 都支援自定義前綴:
```bash
MOMENTRY_REDIS_PREFIX=momentry:
```
此設定允許多個 momentry 實例共用同一個 Redis 伺服器,例如:
- **生產環境**: `MOMENTRY_REDIS_PREFIX=momentry:`
- **開發環境**: `MOMENTRY_REDIS_PREFIX=momentry_dev:`
### 2.2 Key 格式
所有 Key 都遵循以下格式:
```
{prefix}{key_type}:{uuid}
```
範例 (生產環境):
```
momentry:job:5dea6618a606e7c7
momentry:jobs:active
momentry:health:current
```
範例 (開發環境):
```
momentry_dev:job:5dea6618a606e7c7
momentry_dev:jobs:active
momentry_dev:health:current
```
### 2.3 預設值
| Binary | 預設 Port | 預設 Redis Prefix |
|--------|-----------|-------------------|
| `momentry` (生產) | 3002 | `momentry:` |
| `momentry_playground` (開發) | 3003 | `momentry_dev:` |
## 3. UUID 使用時機
### 3.1 全局 Keys無 UUID
- 單一實例全局狀態
- 聚合統計數據
### 3.2 Per-Video KeysUUID 必要)
- 每個視頻獨立處理狀態
- 即時進度追蹤
### 3.3 Per-Processor KeysUUID + Processor 必要)
- 每個 processor 獨立狀態
## 4. Key 命名空間
```
momentry
├── health: # 健康檢查
│ ├── current # 當前狀態 (TTL: 60s)
│ └── services # 依賴服務狀態
├── config: # 配置
├── stats: # 聚合統計
│ ├── total_jobs # 總 Jobs 數
│ ├── processed_today # 今日處理數
│ ├── cpu:current # 當前 CPU 使用
│ └── memory:current # 當前 Memory 使用
├── jobs: # Jobs 管理
│ ├── active # Set: 運行中 UUIDs
│ ├── completed # Set: 完成 UUIDs
│ └── failed # Set: 失敗 UUIDs
├── job:{uuid} # Per-Video Job 狀態 (TTL: 24h)
│ ├── status # 狀態 String
│ ├── video_path # 視頻路徑
│ ├── current_processor # 當前 processor
│ ├── progress_total # 總進度
│ ├── progress_current # 當前進度
│ ├── started_at # 開始時間
│ ├── updated_at # 最後更新
│ └── processor:{name} # Per-Processor 狀態
│ ├── status
│ ├── progress
│ ├── current
│ ├── total
│ └── started_at
├── progress:{uuid} # Pub/Sub 頻道 (即時進度)
├── result:{uuid} # 處理結果 Hash
├── output:{uuid} # 輸出路徑
├── metrics:{uuid} # Per-Video 指標
│ ├── cpu # CPU 歷史 List (100條, TTL: 1h)
│ ├── memory # Memory 歷史 List (100條, TTL: 1h)
│ └── duration # 處理時長
└── log:{uuid} # 處理日誌 String
```
## 5. Key 詳細說明
### 全局 Keys
| Key | Type | TTL | 說明 |
|-----|------|-----|------|
| `momentry:health:current` | String | 60s | 當前健康狀態 |
| `momentry:health:services` | Hash | 60s | 依賴服務健康狀態 |
| `momentry:stats:total_jobs` | String | - | 總 Jobs 數 |
| `momentry:stats:processed_today` | String | 86400s | 今日處理數 |
| `momentry:stats:cpu:current` | String | 10s | 當前 CPU 使用 |
| `momentry:stats:memory:current` | String | 10s | 當前 Memory 使用 |
| `momentry:jobs:active` | Set | - | 運行中 Job UUIDs |
| `momentry:jobs:completed` | Set | - | 完成 Job UUIDs |
| `momentry:jobs:failed` | Set | - | 失敗 Job UUIDs |
### Per-Video Keys
| Key | Type | TTL | 說明 |
|-----|------|-----|------|
| `momentry:job:{uuid}` | Hash | 24h | Job 完整狀態 |
| `momentry:job:{uuid}:status` | String | 24h | Job 狀態 |
| `momentry:progress:{uuid}` | Pub/Sub | - | 即時進度頻道 |
| `momentry:result:{uuid}` | Hash | 24h | 處理結果 |
| `momentry:output:{uuid}` | String | 24h | 輸出路徑 |
| `momentry:metrics:{uuid}:cpu` | List | 1h | CPU 歷史 (100條) |
| `momentry:metrics:{uuid}:memory` | List | 1h | Memory 歷史 (100條) |
| `momentry:metrics:{uuid}:duration` | String | 24h | 處理時長 |
| `momentry:log:{uuid}` | String | 24h | 處理日誌 |
### Per-Processor Keys
| Key | Type | TTL | 說明 |
|-----|------|-----|------|
| `momentry:job:{uuid}:processor:{name}` | Hash | 24h | Processor 狀態 |
| `momentry:job:{uuid}:processor:{name}:status` | String | 24h | 狀態 |
| `momentry:job:{uuid}:processor:{name}:progress` | String | 24h | 進度百分比 |
| `momentry:job:{uuid}:processor:{name}:current` | String | 24h | 當前項目 |
| `momentry:job:{uuid}:processor:{name}:total` | String | 24h | 總項目數 |
| `momentry:job:{uuid}:processor:{name}:started_at` | String | 24h | 開始時間 |
## 6. TTL 策略
| Key 類型 | TTL | 原因 |
|----------|-----|------|
| Health | 60s | 需要定期更新 |
| Job | 24h | 處理完成後保留一天 |
| Processor | 24h | 處理完成後保留一天 |
| Metrics | 1h | 只保留近期歷史 |
| Progress Pub/Sub | - | 不持久,僅即時訊息 |
| Stats | 無 | 持久統計 |
## 7. 訊息格式
### Pub/Sub 訊息 (progress:{uuid})
```json
{
"type": "info | progress | complete | error",
"processor": "yolo | ocr | face | pose | cut | asr | asrx",
"timestamp": 1700000000,
"data": {
"message": "Processing frame 5000",
"current": 5000,
"total": 14315
}
}
```
### Job 狀態 Hash
```json
{
"uuid": "5dea6618a606e7c7",
"video_path": "/path/to/video.mp4",
"status": "running",
"current_processor": "yolo",
"progress_total": 70,
"progress_current": 50,
"started_at": 1700000000,
"updated_at": 1700000100,
"error_count": 0,
"last_error": ""
}
```
### Processor 狀態 Hash
```json
{
"name": "yolo",
"status": "running",
"progress": 70,
"current_frame": 10000,
"total_frames": 14315,
"started_at": 1700000000,
"updated_at": 1700000100
}
```
## 8. 實作函數 (Rust)
所有 Redis Key 生成函數使用 `REDIS_KEY_PREFIX` 靜態變數:
```rust
use crate::core::config::REDIS_KEY_PREFIX;
fn global_key(key: &str) -> String {
format!("{}{}", REDIS_KEY_PREFIX, key)
}
fn job_key(uuid: &str) -> String {
format!("{}job:{}", REDIS_KEY_PREFIX, uuid)
}
fn processor_key(uuid: &str, processor: &str) -> String {
format!("{}job:{}:processor:{}", REDIS_KEY_PREFIX, uuid, processor)
}
fn progress_channel(uuid: &str) -> String {
format!("{}progress:{}", REDIS_KEY_PREFIX, uuid)
}
fn metrics_key(uuid: &str, metric: &str) -> String {
format!("{}metrics:{}:{}", REDIS_KEY_PREFIX, uuid, metric)
}
fn jobs_set_key(status: &str) -> String {
format!("{}jobs:{}", REDIS_KEY_PREFIX, status)
}
```
**注意**: `REDIS_KEY_PREFIX` 定義於 `src/core/config.rs`,由環境變數 `MOMENTRY_REDIS_PREFIX` 控制。
## 9. 環境變數
```bash
# Redis 連接
REDIS_URL=redis://localhost:6379
REDIS_PASSWORD=accusys
REDIS_DB=0
# Redis Key Prefix (可選,預設: momentry:)
MOMENTRY_REDIS_PREFIX=momentry:
# 生產環境範例 (.env)
MOMENTRY_SERVER_PORT=3002
MOMENTRY_REDIS_PREFIX=momentry:
# 開發環境範例 (.env.development)
MOMENTRY_SERVER_PORT=3003
MOMENTRY_REDIS_PREFIX=momentry_dev:
```
## 11. 監控腳本
使用 Redis 進行監控的腳本應參考:
- `monitor/service/momentry_redis_monitor.sh` - Redis 健康檢查
- `monitor/service/momentry_job_monitor.sh` - Job 狀態監控

View File

@@ -0,0 +1,830 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "待解決問題追蹤"
date: "2026-03-17"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "待解決問題追蹤"
ai_query_hints:
- "查詢 待解決問題追蹤 的內容"
- "待解決問題追蹤 的主要目的是什麼?"
- "如何操作或實施 待解決問題追蹤?"
---
# 待解決問題追蹤
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-17 |
| 文件版本 | V1.0 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-17 | 創建文件 | Warren | OpenCode / MiniMax M2.5 |
| V1.1 | 2026-03-21 | 更新問題狀態 | OpenCode | - |
| V1.2 | 2026-03-21 | 添加備份機制優化待辦 | OpenCode | - |
| V1.3 | 2026-03-21 | 完成清理硬編碼密碼 | OpenCode | - |
| V1.4 | 2026-03-21 | 完成 OpenCode n8n MCP 整合 | OpenCode | - |
| V1.5 | 2026-03-21 | 完成 API Key Management 核心模組 | OpenCode | - |
| V1.6 | 2026-03-23 | 添加 Momentry Core API launchd 待辦 | OpenCode | - |
| V1.7 | 2026-03-23 | 完成 Momentry Core API launchd 設定 | OpenCode | - |
| V1.8 | 2026-03-24 | 完成服務統一遷移,所有服務使用自定義 plist | OpenCode | big-pickle |
| V1.9 | 2026-03-24 | 建立統一會員系統實作計畫 | OpenCode | big-pickle |
| V2.0 | 2026-03-24 | 建立 Job Worker 實作計畫 | OpenCode | big-pickle |
---
## 問題 #1: sqlx async INSERT 不會實際寫入數據庫
### 問題描述
使用 sqlx async 執行 INSERT 時報告成功rows_affected=1但數據沒有實際寫入數據庫。
### 嘗試過的解決方案
| # | 嘗試方法 | 結果 |
|---|---------|------|
| 1 | 使用 `execute()` | 報告成功但未寫入 |
| 2 | 使用 `fetch()` | 同樣問題 |
| 3 | 使用交易 | 同樣問題 |
| 4 | 使用連接池 `acquire()` | 同樣問題 |
| 5 | 每個操作創建新連接池 | 同樣問題 |
| 6 | 使用 `std::process::Command` 同步調用 psql | 同樣問題 |
| 7 | 使用 `tokio::task::spawn_blocking` | 同樣問題 |
### 觀察到的現象
- 直接用 psql 命令行可以成功寫入
- 用另一個 PostgreSQL client 可以成功寫入
- sqlx 查詢 COUNT(*) 可以正確讀取數據
- 但 sqlx INSERT 報告成功卻不寫入
### 懷疑方向
- sqlx 連接池與 PostgreSQL 的某種交互問題
- Rust async runtime 與 PostgreSQL client 的問題
- postgresql.conf 配置問題
### 臨時解決方案
- Qdrant 向量存儲正常工作(不受影響)
- 存儲狀態追蹤功能正常運作
### 負責人
-
### 建立日期
2026-03-17
### 備註
影響 `store_vector` 函數PVector 存儲無法正常工作,但 QVector 正常運作
### 2026-03-21 調查結果
#### 測試結果
- 直接 psql INSERT: ✅ 成功
- 資料寫入驗證: ✅ 成功
#### 發現的問題
`store_vector` 函數 (`postgres_db.rs:819-860`) 存在以下問題:
```rust
// 問題 1: 錯誤被靜默忽略
match join_result {
Ok((cid, Ok(output))) => {
if !output.status.success() {
let err = String::from_utf8_lossy(&output.stderr);
tracing::error!("psql error for {}: {}", cid, err);
// 沒有返回錯誤!只是記錄日誌
}
}
// ...
}
Ok(()) // 即使失敗也返回 Ok
```
#### 建議修復
1.`store_vector` 返回實際結果
2. 在失敗時返回 `Err`
3. 添加單元測試驗證
#### 下一步
- [x] 修復 `store_vector` 錯誤處理
- [x] 添加單元測試
- [ ] 重現並確認問題根因
### 2026-03-21 修復完成
已修復 `store_vector` 函數的錯誤處理:
```rust
// 修復前:錯誤被靜默忽略
match join_result {
Ok((cid, Ok(output))) => {
if !output.status.success() {
tracing::error!("..."); // 只記錄,不返回錯誤
}
}
}
Ok(()) // 即使失敗也返回 Ok
// 修復後:正確傳播錯誤
let (cid, output) = result;
let output = output.map_err(|e| anyhow::anyhow!(...))?;
if !output.status.success() {
anyhow::bail!("psql INSERT failed...");
}
```
---
## 問題 #2: TUI 與 stdout 輸出混合
### 問題描述
Python processors 的 progress 輸出蓋過 TUI導致使用者無法清楚看到處理進度。
### 解決方案
~~使用 TUI 渲染到 stderr~~ → 使用 Redis Pub/Sub 作為消息總線
### 當前狀態
| 子項目 | 狀態 |
|--------|------|
| RedisPublisher Python 端 | ✅ 已實作 |
| Rust Redis 客戶端 | ✅ 已實作 (`redis_client.rs`) |
| Rust 訂閱更新 TUI | ✅ 已實作 (main.rs) |
| 中斷後繼續/重做 | 🔜 待實作 |
### 負責人
-
### 建立日期
2026-03-17
### 更新日期
2026-03-21
### 參考文檔
- `docs_v1.0/REFERENCE/MOMENTRY_CORE_REDIS_KEYS.md`
- `scripts/redis_publisher.py`
- `src/core/db/redis_client.rs`
---
## 問題 #3: Redis Message Bus 尚未實作
### 問題描述
根據設計規範,需要使用 Redis 作為監控和狀態管理系統。
### 當前狀態
| 實作項目 | 狀態 | 說明 |
|---------|------|------|
| Redis 客戶端 (Hash) | ✅ | `redis_client.rs` |
| Redis 客戶端 (Pub/Sub) | ✅ | `redis_client.rs::subscribe_progress()` |
| Python RedisPublisher | ✅ | `scripts/redis_publisher.py` |
| Rust 訂閱頻道 | ✅ | `main.rs` 中的 redis 訂閱邏輯 |
| 監控腳本 | ✅ | `monitor/service/health_check.sh` |
### 負責人
-
### 建立日期
2026-03-17
### 更新日期
2026-03-21
### 優先級
~~高~~ → 中 - 核心功能已完成
### 參考文檔
- `docs_v1.0/REFERENCE/MOMENTRY_CORE_REDIS_KEYS.md`
- `docs_v1.0/OPERATIONS/MOMENTRY_CORE_MONITORING.md`
---
## 架構優化待評估
詳細內容請參考 [ARCHITECTURE_EVALUATION.md](./ARCHITECTURE_EVALUATION.md)
| 項目 | 複雜度 | 優先級 |
|------|--------|--------|
| PostgreSQL → Redis 故障轉移 | 中 | 待評估 |
| 連接池監控 | 低 | 待評估 |
| Processor 重試機制 | 中 | 待評估 |
| PyO3 整合 | 高 | 低 |
| HTTP 健康端點 | 低 | ✅ 已完成 |
| Commit Message Lint | 低 | ✅ 已完成 |
---
## 備份機制優化 (2026-03-21)
### 問題分析
| 問題 | 嚴重性 | 說明 |
|------|--------|------|
| 溫冷分層未啟用 | 中 | weekly/monthly/archive 目錄為空 |
| 清理策略未執行 | 高 | 每日備份保留過多,空間浪費 |
| 無異地備份 | 高 | 無遠程備份(雲端/另一設備) |
| 備份驗證未自動化 | 中 | 只檢查文件存在,沒驗證可恢復性 |
| Gitea 大文件 | 中 | 每次備份 1GB頻率過高 |
| 密碼硬編碼 | 高 | 腳本中有多處明文密碼 |
### 待辦事項
| # | 任務 | 優先級 | 狀態 |
|---|------|--------|------|
| 1 | 啟用溫冷分層 (`backup_monitor.sh tier`) | 高 | 待辦 |
| 2 | 啟用清理策略 (`backup_monitor.sh cleanup`) | 高 | 待辦 |
| 3 | 添加備份驗證腳本 | 高 | 待辦 |
| 4 | 優化 Gitea 備份頻率 (改為週備份) | 中 | 待辦 |
| 5 | 創建外部備份腳本 (rsync/雲端) | 高 | 待辦 |
| 6 | 清理腳本中的硬編碼密碼 | 高 | ✅ 已完成 |
### 推薦備份策略
| 層級 | 保留時間 | 頻率 | 目標 |
|------|----------|------|------|
| Hot | 7 天 | 每日 | 本地 SSD |
| Warm | 30 天 | 每週 | 本地 HDD |
| Cold | 90 天 | 每月 | 外部存儲 |
| Archive | 1 年 | 每季 | 離線/雲端 |
### 建議的 Crontab
```bash
# 每日備份 (排除 Gitea)
0 3 * * 1-6 /Users/accusys/momentry/scripts/backup_all.sh all_except_gitea
# 每週完整備份 (含 Gitea)
0 3 * * 0 /Users/accusys/momentry/scripts/backup_all.sh all
# 每週溫冷分層
0 4 * * 0 /Users/accusys/momentry_core_0.1/monitor/storage/backup_monitor.sh tier
# 每週清理
0 5 * * 0 /Users/accusys/momentry_core_0.1/monitor/storage/backup_monitor.sh cleanup
# 每月驗證
0 6 1 * * /Users/accusys/momentry/scripts/verify_backup.sh
```
### 負責人
-
### 參考文件
- `/Users/accusys/momentry/scripts/backup_all.sh`
- `/Users/accusys/momentry_core_0.1/monitor/storage/backup_monitor.sh`
- `/Users/accusys/momentry/backup/`
---
## OpenCode n8n MCP 整合 (2026-03-21)
### 完成狀態
| 項目 | 狀態 | 說明 |
|------|------|------|
| n8n REST API 啟用 | ✅ | `N8N_PUBLIC_API_ENABLED=true` |
| API Key 生成 | ✅ | Settings → API |
| MCP Server 安裝 | ✅ | `@nextoolsolutions/mcp-n8n` |
| OpenCode 設定 | ✅ | `~/.config/opencode/opencode.json` |
| 43 工具可用 | ✅ | workflows, executions, datatables, tags 等 |
### 設定檔案
`~/.config/opencode/opencode.json`:
```json
{
"mcp": {
"gitea": {
"type": "local",
"enabled": true,
"command": [
"/opt/homebrew/bin/gitea-mcp-server",
"-token", "<GITEA_TOKEN>",
"-host", "http://localhost:3000"
]
},
"n8n": {
"type": "local",
"enabled": true,
"command": ["/opt/homebrew/bin/mcp-n8n"],
"environment": {
"N8N_BASE_URL": "http://localhost:5678",
"N8N_API_KEY": "<N8N_API_KEY>"
}
}
}
}
```
### 重要提醒
1. **API Key 安全**: 避免提交到 Git
2. **n8n 需通過反向代理**: localhost:5678 無法直接訪問 API需通過 Caddy
3. **重啟生效**: 修改 `opencode.json` 後需重啟 OpenCode
### 參考文件
- `docs_v1.0/IMPLEMENTATION/OPENCODE_GUIDE.md` - MCP 設定章節
- `docs_v1.0/IMPLEMENTATION/INSTALL_N8N.md` - n8n 安裝指南
---
## n8n API 備份腳本 (2026-03-21)
### 腳本位置
| 腳本 | 說明 | 依賴 |
|------|------|------|
| `monitor/workflow/backup_n8n_api.py` | REST API 備份 | requests |
| `monitor/workflow/backup_n8n_mcp.py` | MCP 備份(開發中) | mcp SDK |
### 使用方式
```bash
# 備份所有 workflows
N8N_API_KEY="..." python3.11 backup_n8n_api.py
# 只顯示變更(不備份)
N8N_API_KEY="..." python3.11 backup_n8n_api.py --diff
# 差異備份(只備份變更的 workflows
N8N_API_KEY="..." python3.11 backup_n8n_api.py --incremental
# 列出可用備份
python3.11 backup_n8n_api.py --list
# 驗證最新備份
python3.11 backup_n8n_api.py --verify
# 顯示備份統計
python3.11 backup_n8n_api.py --stats
# 只備份啟用的 workflows
N8N_API_KEY="..." python3.11 backup_n8n_api.py --active-only
# 備份失敗的執行記錄
N8N_API_KEY="..." python3.11 backup_n8n_api.py --failed-only
```
### 功能
- [x] REST API 備份
- [x] 變更偵測
- [x] SHA256 校驗
- [x] 備份版本化
- [x] 差異備份(`--incremental`
- [x] 備份驗證(`--verify`
- [x] 備份統計(`--stats`
- [x] 失敗執行記錄備份(`--failed-only`
- [ ] 選擇性備份(按 Tags
### 備份位置
```
/Users/accusys/momentry/backup/n8n_workflows/api/
├── 20260321_122059/
│ ├── workflows.json # 21 workflows
│ ├── workflows.json.sha256 # SHA256 校驗
│ ├── tags.json # Tags若有
│ └── manifest.json # 元數據
└── ...
```
### Crontab 建議
```bash
# 每日備份(下午 3 點)
0 15 * * * N8N_API_KEY="..." /opt/homebrew/bin/python3.11 /Users/accusys/momentry_core_0.1/monitor/workflow/backup_n8n_api.py >> /Users/accusys/momentry/log/monitor/workflow_backup_api.log 2>&1
```
---
## API Key Management System (2026-03-21)
### 設計目標
為 Momentry Core 實現完整的 API Key 管理系統,包括:
- API Key 生成(安全隨機)
- Key 哈希SHA256
- 異常檢測
- 強制輪換機制
- 審計日誌
### 模組架構
```
src/core/api_key/
├── mod.rs # 模組導出
├── models.rs # 數據模型和類型
├── service.rs # 核心服務邏輯
├── anomaly.rs # 異常檢測
└── rotation.rs # 輪換管理
```
### Key 類型
| 類型 | 前綴 | 默認 TTL | 寬限期 |
|------|------|----------|--------|
| System | `msys_` | 365 天 | 72 小時 |
| User | `muser_` | 90 天 | 24 小時 |
| Service | `msvc_` | 180 天 | 48 小時 |
| Integration | `mint_` | 30 天 | 24 小時 |
| Emergency | `memg_` | 1 天 | 0 小時 |
### Key 格式
```
{prefix}{uuid}_{timestamp}_{random}
例如: muser_a1b2c3d4e5f6_1710998400_abc12345
```
### 異常檢測閾值
| 指標 | 閾值 |
|------|------|
| 每分鐘請求數 | 1000 |
| 每小時請求數 | 10000 |
| 錯誤率 | 50% |
| 每小時唯一 IP 數 | 5 |
| 鎖定閾值 | 3 次觸發 |
### 實現狀態
| 組件 | 狀態 | 說明 |
|------|------|------|
| 數據模型 | ✅ 完成 | `models.rs` |
| Key 生成/哈希 | ✅ 完成 | `service.rs` |
| 異常檢測 | ✅ 完成 | `anomaly.rs` |
| 輪換機制 | ✅ 完成 | `rotation.rs` |
| CLI 命令 | ✅ 完成 | `main.rs` |
| 數據庫集成 | ✅ 完成 | `postgres_db.rs` |
| Redis 告警 | ✅ 完成 | `redis_client.rs` |
| 數據庫遷移 | ✅ 完成 | `migrations/001_api_key_management.sql` |
| 單元測試 | ✅ 完成 | 55 個測試通過 |
### CLI 命令
```bash
# 創建 API Key
momentry api-key create <name> --key-type service --ttl 90
# 列出所有 Keys
momentry api-key list
# 驗證 Key
momentry api-key validate --key <key>
# 撤銷 Key
momentry api-key revoke --key <key>
# 請求輪換
momentry api-key rotate --key <key>
# 顯示統計
momentry api-key stats
```
### 參考文件
- `src/core/api_key/` - API Key 模組
- `docs_v1.0/REFERENCE/API_KEY_MANAGEMENT.md` - 設計文檔
- `migrations/001_api_key_management.sql` - 數據庫遷移
---
## 問題 #5: Redis 用戶名問題 (2026-03-21)
### 問題描述
Redis 僅有 `default` 用戶,無 `accusys` 用戶。`.env` 檔案使用 `redis://accusys:accusys@localhost:6379` 格式會導致認證失敗。
### 測試結果
| URL 格式 | 結果 |
|----------|------|
| `redis://accusys:accusys@localhost:6379` | ❌ AUTH failed |
| `redis://:accusys@localhost:6379` | ✅ PONG |
### Redis ACL 狀態
```
user default on sanitize-payload #1bd51c... ~* &* +@all
requirepass: accusys
```
### 根本原因
1. Redis 啟動時僅設定 `--requirepass accusys`
2. 未建立自訂用戶 `accusys`
3. ACL 變更不會持久化(無 config file
### 已執行修復
| 項目 | 修改 |
|------|------|
| `.env` | `redis://accusys:accusys@localhost:6379``redis://:accusys@localhost:6379` |
### 待解決問題
1. **ACL 持久化**Redis 啟動後手動建立的用戶不會保留(重啟後消失)
2. **需配置 ACL 文件**:建議建立 `users.acl` 並在 plist 中指定
### 建議解決方案
#### 方案 A使用默認用戶現行
```bash
# .env
REDIS_URL=redis://:accusys@localhost:6379
```
**優點**:簡單,無需修改 Redis 配置
**缺點**:所有應用共享默認用戶
#### 方案 B建立 ACL 配置文件
```bash
# 1. 創建 ACL 文件
cat > /Users/accusys/momentry/etc/redis/users.acl << 'EOF'
user default on sanitize-payload ~* &* +@all >accusys
user accusys on sanitize-payload ~* &* +@all >accusys
EOF
# 2. 修改 plist 添加 --aclfile 參數
--aclfile /Users/accusys/momentry/etc/redis/users.acl
# 3. 重啟 Redis
sudo launchctl unload /Library/LaunchDaemons/com.momentry.redis.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist
```
**優點**支持多用戶ACL 持久化
**缺點**:需修改 plist 並重啟
### 影響範圍
- `src/core/config.rs` - REDIS_URL 讀取
- `src/core/db/redis_client.rs` - Redis 連線
- `momentry api-key` 命令 - 異常告警
### 狀態
- [x] 已確認問題存在
- [x] 已修改 `.env` 使用默認用戶
- [ ] 待決定是否實施 ACL 方案
---
## 問題 #6: Momentry Core API 未開機自動啟動 (2026-03-23)
### 問題描述
Momentry Core API 服務 (`momentry server --port 3002`) 未設定 launchd導致
1. 系統重啟後 API 服務不會自動啟動
2. `api.momentry.ddns.net` 返回 502 Bad Gateway
3. n8n workflow 呼叫 API 時失敗
### 發現過程
1. n8n workflow 呼叫 `https://api.momentry.ddns.net/api/v1/n8n/search` 返回 502
2. 檢查發現 port 3002 無服務運行
3. Caddy 配置正向確,但後端服務未啟動
4. 手動啟動服務後 API 正常運作
### 配置需求
| 項目 | 值 |
|------|-----|
| 服務名稱 | `com.momentry.api` |
| 二進位檔 | `/Users/accusys/momentry_core_0.1/target/release/momentry` |
| 命令 | `server --port 3002` |
| Port | 3002 |
| 環境變數 | `DATABASE_URL`, `REDIS_URL` 等 |
### 待辦事項
- [x] 建立 `docs_v1.0/IMPLEMENTATION/INSTALL_MOMENTRY_API.md` 安裝文件
- [x] 建立 `/Library/LaunchDaemons/com.momentry.api.plist`
- [x] 設定環境變數 (`DATABASE_URL`, `REDIS_URL` 等)
- [x] 測試 launchctl load/unload
- [x] 驗證開機自動啟動 (launchd 載入成功)
### 完成日期
2026-03-23
### 參考文件
- `/Library/LaunchDaemons/com.momentry.n8n.main.plist` - n8n plist 範例
- `docs_v1.0/IMPLEMENTATION/INSTALL_N8N.md` - plist 配置說明
---
## 服務統一遷移 (2026-03-24)
### 問題描述
Reboot 後發現 n8n workflow 數量從 42 變成 41確認是 PostgreSQL 資料庫問題。經過調查發現:
1. **兩組不同的 PostgreSQL 資料目錄**
- Homebrew plist: `/opt/homebrew/var/postgresql@18` (有最新資料)
- Custom plist: `/Users/accusys/momentry/var/postgresql` (可能是舊資料)
2. **Reboot 時 custom plist 搶先啟動**,使用了錯誤的資料目錄
### 解決方案
1. **統一使用 custom plist**
- 刪除 homebrew plist (`~/Library/LaunchAgents/homebrew.mxcl.postgresql@18.plist`)
- Custom plist 使用 `/Users/accusys/momentry/var/postgresql` 作為資料目錄
- 將所有 14 個服務的 plist 註冊到 launchd
2. **所有已遷移的服務**
| 服務 | Plist | 資料目錄 |
|------|-------|----------|
| PostgreSQL | ✅ | `/Users/accusys/momentry/var/postgresql` |
| MariaDB | ✅ | `/Users/accusys/momentry/var/mariadb` |
| MongoDB | ✅ | `/opt/homebrew/var/mongodb` |
| Redis | ✅ | - |
| Ollama | ✅ | - |
| Qdrant | ✅ | - |
| n8n Main | ✅ | - |
| n8n Worker | ✅ | - |
| Caddy | ✅ | - |
| SFTPGo | ✅ | - |
| Gitea | ✅ | - |
| Gitea MCP | ✅ | - |
| PHP | ✅ | - |
| Momentry API | ✅ | - |
| RustDesk HBBR | ✅ | - |
| RustDesk HBBS | ✅ | - |
### 還發現的 Homebrew 服務 (未遷移)
| 服務 | 建議 |
|------|------|
| homebrew.mxcl.grafana | ⚠️ 考慮遷移 |
| homebrew.mxcl.prometheus | ⚠️ 考慮遷移 |
| homebrew.mxcl.openwebui | ⚠️ 考慮遷移 |
| homebrew.mxcl.kafka | ⚠️ 考慮遷移 |
| homebrew.mxcl.seaweedfs | ⚠️ 考慮遷移 |
| homebrew.mxcl.netdata | ⚠️ 考慮遷移 |
| homebrew.mxcl.ddclient | ⚠️ 動態 DNS |
| homebrew.mxcl.shadowsocks-rust | ⚠️ VPN |
### 預防措施
1. **確保統一資料目錄**:所有服務只使用一個資料目錄
2. **Reboot 測試**:遷移完成後需進行 Reboot 測試
3. **文件同步**plist 檔案同步到 repo
### 完成日期
2026-03-24
### 參考文件
- `docs_v1.0/REFERENCE/SERVICES.md` - 服務管理文檔
- `docs_v1.0/IMPLEMENTATION/SERVICE_ADDITION_GUIDE.md` - 服務添加規範
- `momentry_runtime/plist/` - plist 檔案存放位置
---
## Job Worker 實作 (2026-03-24)
### 目標
實作輪詢式 Job Worker實現檔案註冊後自動觸發處理
1. **輪詢機制**Worker 定期輪詢 jobs 佇列
2. **並行處理**:最多 2 個 processor 同時執行
3. **失敗容忍**:任一模組獨立,失敗可接續
### 設計決策
| 項目 | 決策 | 理由 |
|------|------|------|
| 觸發方式 | 輪詢Job Worker | 暫無可靠 API 觸發 |
| 並行處理 | 最多 2 個 | 可根據 CPU/GPU 調整 |
| 失敗處理 | 獨立模組,部分完成可接續 | 任何模組失敗都產出狀態 |
| Worker 啟動 | 獨立進程 | 隔離、易管理 |
| 並行上限 | 環境變數 + 預設值 | 靈活調整 |
| 狀態同步 | PostgreSQL + Redis | 可靠 + 即時 |
### 環境變數
| 變數 | 預設值 | 說明 |
|------|--------|------|
| `MOMENTRY_MAX_CONCURRENT` | 2 | 最大並行 processor 數 |
| `MOMENTRY_POLL_INTERVAL` | 5 | 輪詢間隔(秒) |
| `MOMENTRY_WORKER_ENABLED` | true | 是否啟用 worker |
### 實作計畫
詳細內容請參考 [JOB_WORKER_IMPLEMENTATION_PLAN.md](./JOB_WORKER_IMPLEMENTATION_PLAN.md)
### Phase 規劃
| Phase | 任務 | 預估工時 |
|-------|------|----------|
| 1 | 資料庫遷移 | 2h |
| 2 | Worker 框架 | 4h |
| 3 | Register API 整合 | 2h |
| 4 | Processor 執行 | 4h |
| 5 | 進度追蹤 | 2h |
| 6 | API 端點 | 3h |
| 7 | CLI 命令 | 2h |
| 8 | 測試 | 4h |
**總預估**: ~23h
### 實作結構
```
src/
├── worker/
│ ├── mod.rs # Worker 模組導出
│ ├── config.rs # Worker 配置
│ ├── worker.rs # Worker 主邏輯
│ ├── processor.rs # Processor 執行器
│ ├── queue.rs # Job 佇列管理
│ └── progress.rs # 進度追蹤
├── api/
│ └── server.rs # 更新 Register API
└── main.rs # 新增 worker 命令
```
### 狀態
- [x] 系統分析完成
- [x] 實作計畫文件建立
- [ ] Phase 1: 資料庫遷移
- [ ] Phase 2: Worker 框架
- [ ] Phase 3: Register API 整合
- [ ] Phase 4: Processor 執行
- [ ] Phase 5-8: 依序實作
### 參考文件
- `docs_v1.0/ARCHITECTURE/JOB_WORKER_IMPLEMENTATION_PLAN.md` - 完整實作計畫
- `docs_v1.0/ARCHITECTURE/PROCESSING_PIPELINE.md` - 處理流程
- `docs_v1.0/REFERENCE/MOMENTRY_CORE_REDIS_KEYS.md` - Redis Key 設計
---
## 統一會員系統 + 影片歸屬追蹤 (2026-03-24)
### 目標
建立統一的會員系統:
1. WordPress 作為唯一登入入口
2. 每個影片關聯到 user_id追蹤歸屬
3. Per-user 配額管理
4. API 端點啟用認證
### 實作計畫
詳細內容請參考 [USER_MANAGEMENT_PLAN.md](./USER_MANAGEMENT_PLAN.md)
### Phase 規劃
| Phase | 任務 | 複雜度 | 優先級 | 預估工時 |
|-------|------|--------|--------|----------|
| 1 | WordPress Application Passwords 測試 | 低 | P0 | 1.5h |
| 2 | 資料庫遷移 (users 表) | 中 | P0 | 3h |
| 3 | API auth middleware | 中 | P0 | 4h |
| 4 | Register API 更新 | 低 | P0 | 2h |
| 5 | Admin users API | 中 | P1 | 4h |
| 6 | n8n workflow | 中 | P1 | 6h |
| 7 | 配額管理 | 中 | P2 | 4h |
| 8 | 測試驗證 | 中 | P2 | 4h |
**總預估**: ~28.5h
### 待確認事項
- [ ] WordPress 用戶建立方式(手動/Elementor表單
- [ ] API Key 格式確認
- [ ] SFTPGo 整合方式
- [ ] 配額管理策略
- [ ] 用戶刪除同步流程
### 狀態
- [x] 系統分析完成
- [x] 實作計畫文件建立
- [ ] Phase 1: WordPress 認證測試
- [ ] Phase 2: 資料庫遷移
- [ ] Phase 3-8: 依序實作
### 參考文件
- `docs_v1.0/ARCHITECTURE/USER_MANAGEMENT_PLAN.md` - 完整實作計畫
- `docs_v1.0/REFERENCE/API_KEY_MANAGEMENT.md` - API Key 管理
- `docs_v1.0/IMPLEMENTATION/SFTPGO_DEMO_USER.md` - SFTPGo 用戶設定

View File

@@ -0,0 +1,150 @@
# Phase 1 Completion Report — v2 (fine-grained ASRX)
**File**: Charade (1963) Cary Grant & Audrey Hepburn
**UUID**: `aeed71342a899fe4b4c57b7d41bcb692`
**Date**: 2026-05-10
**System**: M5 (MacBook Pro, 48GB, Apple Silicon)
---
## 1. Processor Outputs
| File | Size | Description |
|------|------|-------------|
| `asr.json` | 413KB | 3,417 segments, full movie coverage (Whisper small) |
| `asrx.json` | **18MB** | **4,188 segments** (fine-grained, ECAPA-TDNN) |
| `asrx_fine.json` | 45MB | 4,188 fine segments + voice embeddings (intermediate) |
| `cut.json` | 329KB | 2,260 scenes |
| `yolo.json` | 181MB | 169,625 frames with object detections |
| `face.json` | **106MB** | 4,550 frames, 5,910 faces @ 8Hz (CoreML 512D) |
| `face_traced.json` | 110MB | Traced faces with 423 identity traces |
| `lip.json` | 492KB | Lip openness analysis |
| `ocr.json` | 277KB | 606 OCR frames |
| `pose.json` | 26MB | 4,211 pose frames |
| `scene.json` | 403B | Scene classification |
## 2. Pipeline 8-Stage Checklist
| Stage | Status | Detail |
|-------|--------|--------|
| ASR | ✅ | 3,417 segments, last end 6,773s (100%) |
| ASRX | ✅ | **4,188 segments** (fine-grained, 10→3 speakers mapped) |
| Sentence Chunks | ✅ | **4,188 sentence chunks** with yolo_objects + face_ids |
| Vectorization | ✅ | 4,188 Qdrant (768D), all 3 collections updated |
| Face Trace | ✅ | 423 traces, 11,820 detections @ 8Hz |
| TKG Graph | ✅ | 498 nodes, 1,617 edges |
| Trace Chunks | ✅ | 423 trace chunks |
| Phase 1 Release | ✅ | 3.0GB package |
## 3. Speaker Identification
### ASRX Enhancement (3417 → 4188 segments)
The original Whisper ASR merges rapid back-and-forth dialogue into single segments. A sliding-window ECAPA-TDNN approach was developed to detect speaker change points within each ASR segment:
1. **Sliding window**: 1.5s window, 0.75s stride across full audio
2. **ECAPA-TDNN 192D embedding** per window
3. **Classification** against reference centroids (Cary Grant, Audrey Hepburn, Unknown)
4. **Majority-vote smoothing** over 3 adjacent windows
5. **Change point detection** where classified speaker changes
6. **Split** original ASR segment at each change point
**Result**: 3,417 → **4,188 segments** (+771, +22.6%). Validated via gender classification (ECAPA-TDNN → 92.3% agreement with character identity).
### Speaker Mapping (Centroid-based)
| Speaker ID | Name | Segments | Duration | Voice Gender |
|------------|------|----------|----------|-------------|
| SPEAKER_0 | Audrey Hepburn | 1,658 | 2,786s | FEMALE |
| SPEAKER_1 | Cary Grant | 2,033 | 3,962s | MALE |
| SPEAKER_2 | Unknown (minor) | 497 | 806s | MIXED |
Method: Reference centroids built from 3,107 known segments (1,420 Cary + 1,689 Audrey). Each fine segment classified by cosine similarity to nearest centroid. No cross-contamination between speaker clusters.
### Gender Validation
Two small clusters (SPEAKER_5: 10 segs, SPEAKER_9: 10 segs) initially showed MALE voice → Audrey assignment. Video clip verification confirmed these are segments where a male voice speaks while Audrey is on screen (old face-based matching was incorrect). The fine-grained segmentation correctly resolves these.
## 4. Sentence Chunks — Full Migration
All 4,188 fine segments were written to `dev.chunks` with complete data per chunk:
| Chunk Field | Value | Source |
|-------------|-------|--------|
| `start_time`/`end_time` | Fine segment boundaries | `asrx_fine.json` |
| `start_frame`/`end_frame` | time × 25fps | Calculated |
| `content` | `{data: {text, text_normalized}, rule: rule_1}` | ASR text |
| `metadata.yolo_objects` | Dedup class names in frame range | `pre_chunks(yolo)` |
| `metadata.face_ids` | Trace IDs in frame range | `face_detections` |
| `metadata.speaker_name` | Centroid-matched identity | `asrx_fine.json` |
- 4,158/4,188 chunks have YOLO objects (avg 3-5 object classes)
- 398/4,188 chunks have face IDs (face data covers first ~12 min only)
### Parent/Story Chunks
| Metric | Before (v1) | After (v2) |
|--------|-------------|------------|
| Children per parent | 15 (fixed) | 15 (fixed) |
| Total parents | 228 | **280** |
| LLM summaries | 228 (Gemma4) | **280** (Gemma4, regenerated) |
| Qdrant stories | 456 pts | **560 pts** |
## 5. Qdrant Vector Collections
| Collection | Dims | Points | Content | Status |
|-----------|------|--------|---------|--------|
| `momentry_dev_v1` | 768 | **4,188** | Sentence chunk embeddings (EmbeddingGemma) | ✅ |
| `momentry_dev_stories` | 768 | **560** | 280 dialogue + 280 LLM summary | ✅ |
| `momentry_dev_faces` | 512 | 5,910 | Face embeddings (8Hz CoreML) | ✅ |
| `momentry_dev_voice` | 192 | **4,188** | Voice embeddings (ECAPA-TDNN) | ✅ |
| `sentence_story` | 768 | **4,188** | Sentence template with speaker | ✅ |
| `sentence_summary` | 768 | **4,188** | Context-aware LLM sentence summary | ✅ |
## 6. ASR Model Selection
A comprehensive benchmark (5 models × 2 VAD settings × 3 test clips = 30 runs) showed:
| Model | Segments | Chars | Runtime | Verdict |
|-------|----------|-------|---------|---------|
| tiny | 56 avg | 1,730 | **9.2s** | Most segments, best text capture |
| **small** | **55 avg** | **1,704** | **17.6s** | **Best balance (current)** |
| base | 42 avg | 1,751 | 10.1s | Good but fewer segments |
| medium | 52 avg | 1,627 | 339.6s | Slow, loses text |
| large-v3 | 20 avg | 1,249 | 68.8s | **Worst**: merges utterances, loses 26% text |
**Conclusion**: Keep `faster-whisper small (VAD 500ms)`. The missing-text problem is not solvable by model size — even tiny captures more text than large-v3. Root cause is Whisper's lack of speaker turn detection in segment boundary logic, which is solved by the sliding-window ASRX approach above.
## 7. Release Package
| Component | Size |
|-----------|------|
| `output_json/` | 13 processor files |
| `chunks.csv` | 3.2MB |
| `vectors.csv` | 58MB |
| `identities.csv` | 1MB |
| `schema.sql` | 30KB |
| Qdrant snapshots (5 collections) | ~3GB |
| `RELEASE_INFO.txt` | Metadata |
| **Total** | **~3.0GB** |
## 8. Key Technical Decisions
| Decision | Rationale |
|----------|-----------|
| Sliding window 1.5s/0.75s | Optimal balance: captures turn boundaries without over-splitting |
| Centroid-based classification | 0.8+ similarity, no retraining needed, 100% consistent |
| Word-timestamp ASR for text | Re-run with `word_timestamps=True`, 87% coverage; remaining 13% → per-segment ASR fallback |
| Fixed 15 children/parent | Maintains Phase 1 design consistency |
| `yolo_objects` dedup | Only class names stored per chunk (not per-frame) |
| `face_ids` via `trace_id` | `face_id` column is NULL in DB; `trace_id` is the actual identifier |
| Keep ASR small model | Benchmarked 5 models; larger models lose text, not gain it |
| `app.run(threaded=True)` | Dashboard v2: single-threaded Flask was blocking on subprocess calls |
## 9. Phase 2 Preparation
Pending for Phase 2:
- Rule 3 scene chunking (cut-based parent chunks)
- 5W1H Agent (LLM-generated scene summaries)
- Full pipeline + 5W1H release packaging
- Source separation (Demucs/HPSS) for overlapping speech scenarios

View File

@@ -0,0 +1,63 @@
# Phase 1 Release Checklist
**UUID**: `aeed71342a899fe4b4c57b7d41bcb692`
**Model**: v2 (fine-grained ASRX, 4,188 segments)
**Date**: 2026-05-10
## 1. Processor Outputs
- [x] `asr.json` — faster-whisper small, 3,417 segments
- [x] `asrx.json` — ECAPA-TDNN fine-grained, 4,188 segments
- [x] `cut.json` — 2,260 scene cuts
- [x] `yolo.json` — 169,625 frames, object detections
- [x] `face.json` — 4,550 frames, 5,910 faces @ 8Hz
- [x] `face_traced.json` — 423 traced identities
- [x] `lip.json` — Lip openness per ASRX segment
- [x] `ocr.json` — 606 OCR frames
- [x] `pose.json` — 4,211 pose frames
- [x] `scene.json` — Scene classification
## 2. Pipeline Stages
- [x] ASR: 3,417 segments, full movie
- [x] ASRX: 4,188 segments (fine-grained), 3 speakers
- [x] Sentence chunks: 4,188 in `dev.chunks`
- [x] Vectorization: 4,188 in Qdrant `momentry_dev_v1`
- [x] Face trace: 423 traces, 11,820 detections
- [x] TKG: 498 nodes, 1,617 edges
- [x] Trace chunks: 423 in `dev.chunks`
- [x] All 8 stages passing
## 3. Qdrant Collections
- [x] `momentry_dev_v1` — 4,188 pts, 768D (EmbeddingGemma)
- [x] `momentry_dev_stories` — 560 pts, 768D (280 dialogue + 280 summary)
- [x] `momentry_dev_faces` — 5,910 pts, 512D (CoreML FaceNet)
- [x] `momentry_dev_voice` — 4,188 pts, 192D (ECAPA-TDNN)
- [x] `sentence_story` — 4,188 pts, 768D (sentence template)
- [x] `sentence_summary` — 4,188 pts, 768D (context-aware LLM)
## 4. Database (dev.chunks)
- [x] Sentence chunks: 4,188 with speaker_name, speaker_id
- [x] Story chunks: 280 with LLM summaries
- [x] Cut chunks: 1,130
- [x] Trace chunks: 423
- [x] YOLO objects in metadata: 4,158/4,188
- [x] Face IDs in metadata: 398/4,188
- [x] Parent-child relationships set
## 5. Speaker Mapping
- [x] SPEAKER_0 → Audrey Hepburn (1,658 segs, gender FEMALE ✅)
- [x] SPEAKER_1 → Cary Grant (2,033 segs, gender MALE ✅)
- [x] SPEAKER_2 → Unknown (497 segs, minor characters)
- [x] Voice embeddings validated via gender classification
## 6. Release Package
- [x] Phase 1 release packaged at `release/phase1/latest/`
- [x] Qdrant snapshots for all 5 collections
- [x] `chunks.csv`, `vectors.csv`, `identities.csv` exported
- [x] `schema.sql` from PostgreSQL
- [x] Dashboard v2 running at port 5050

View File

@@ -0,0 +1,111 @@
# Release Notes — v1.0.0 (Production 3002)
**Date**: 2026-05-13
**Build**: `301da08`
**Deployed by**: M4
---
## Release Scope
### Binaries
| Binary | Size | Source |
|--------|:----:|--------|
| `momentry` (production) | 21 MB | M5 build |
| `release` CLI | 3.5 MB | M5 build |
### File Packages
| Package | UUID | Content |
|---------|------|---------|
| Charade (HD) | `aeed71342a899fe4b4c57b7d41bcb692` | 1920×1080, 25fps |
| Charade (YouTube) | `23b1c872379d4ec06479e5ed39eef4c5` | 640×360, 23.98fps |
### Data Per Package
| Table | HD | YouTube |
|-------|:--:|:--:|
| chunk | 2,407 | 2,340 |
| chunk_vectors (768D) | 2,407 | 2,340 |
| face_detections | 70,691 | 70,729 |
| identities (TMDB) | 15 actors | 280 clusters |
| identity_bindings | 18,635 | 18,635 |
| tkg_nodes | 6,457 | 5,776 |
| tkg_edges | 21,028 | 18,847 |
### TMDB Matched Actors
| Actor | TMDB ID | Package |
|-------|:------:|:--:|
| Cary Grant | 2638 | HD |
| Audrey Hepburn | 1932 | HD |
| James Coburn | 5563 | HD |
| George Kennedy | 12950 | HD |
| Dominique Minot | 41714 | HD |
| Ned Glass | 18870 | HD |
| Jacques Marin | 26890 | HD |
| Paul Bonifas | 41716 | HD |
---
## Schema Changes
| Change | Schema |
|--------|--------|
| `chunks``chunk` (rename) | public |
| Drop `old_chunk_id`, `chunk_index` | public |
| Add `timestamp_secs` to `face_detections` | public |
| Add `file_uuid` to `identities` | public |
| Drop `chunk_vectors_chunk_id_key` (duplicate unique) | public |
---
## Verification
### API (all 200)
| Category | Endpoints | Result |
|----------|-----------|:--:|
| Health | `/health`, `/health/detailed` | ✅ |
| Auth | login, logout | ✅ |
| Files | list, scan, detail, probe | ✅ |
| Chunk | `/file/{uuid}/chunk/{id}` | ✅ |
| Search | universal, frames, visual | ✅ |
| Identities | list, detail, files, chunks | ✅ |
| Face Trace | sortby, faces, 3D | ✅ |
| Media | video, thumbnail, trace video | ✅ |
| Resources | list | ✅ |
### Database
| Table | Count |
|-------|------:|
| Files registered | 38 |
| TMDB identities | 15 |
| Total chunks (both files) | 4,747 |
| Total faces (both files) | 141,420 |
| Total TKG nodes | 12,233 |
| Total TKG edges | 39,875 |
---
## Deployment Process
1. **Backup**: Full `public` schema dump (1.3 GB)
2. **Schema Migration**: `chunks→chunk`, drop deprecated columns, add new columns
3. **Deploy aeed7134**: All 8 tables imported via `sed dev.→public.` per-table SQL files
4. **Deploy 23b1c87**: Fixed `chunk_vectors_chunk_id_key` constraint conflict, all 8 tables imported
5. **Binary Swap**: Old binary stopped, M5 `momentry_v1.0.0` deployed, restarted on 3002
6. **Verification**: All API endpoints 200, both files queryable
---
## Known Notes
| Item | Note |
|------|------|
| `/files` status | API hardcodes `"ready"`, does not reflect actual DB status (reported to M5) |
| `/files/scan` | Only scans video extensions (mp4/mov/mkv/avi/webm), misses jpg/png |
| deploy.sh schema | Uses `sed dev.→public.` for public deployment (pending M5 native SCHEMA support) |
| chunk_vectors constraint | `chunk_vectors_chunk_id_key` dropped from public (was preventing multi-file import) |

View File

@@ -0,0 +1,280 @@
# Release SOP — Dev → Production (3002)
**Date**: 2026-05-13
**Version**: 2.0
**Status**: Draft — M5 Review Required
---
## 1. Prerequisites
### 必須確認
| # | 檢查項 | 方法 | 門檻 |
|:--:|------|------|:--:|
| 1 | Dev (3003) 39/39 API test | `bash api_test.sh` | ✅ 39/39 |
| 2 | 檔案內容包匯入成功 | 9 tables all > 0 | ✅ 通過 |
| 3 | TMDB identities 匹配 | `SELECT * FROM dev.identities WHERE source='tmdb'` | ✅ 7 actors |
| 4 | TKG 到位 | tkg_nodes + tkg_edges > 0 | ✅ 通過 |
| 5 | Release binary 已編譯 | `cargo build --release --bin momentry` | ✅ 成功 |
| 6 | `.env.development` 切換確認 | `DATABASE_SCHEMA=public` | ✅ |
## 2. Backup
### 2.1 Database
```bash
BACKUP_DIR="/Users/accusys/momentry_core_releases/backup_$(date +%Y%m%d_%H%M)"
mkdir -p "$BACKUP_DIR"
# Full public schema dump
pg_dump -U accusys -d momentry --schema=public \
--no-owner --no-acl \
> "$BACKUP_DIR/public_schema_full.sql"
# Data-only dump (smaller, faster restore)
pg_dump -U accusys -d momentry --schema=public \
--data-only --no-owner --no-acl \
> "$BACKUP_DIR/public_schema_data.sql"
echo "Backup: $BACKUP_DIR"
```
### 2.2 Binary
```bash
cp /path/to/release/momentry "$BACKUP_DIR/momentry_$(date +%Y%m%d)"
```
### 2.3 Release Info
```bash
echo "Release: v1.0.0" > "$BACKUP_DIR/RELEASE_INFO.txt"
echo "Date: $(date)" >> "$BACKUP_DIR/RELEASE_INFO.txt"
echo "Binary: $(ls -la target/release/momentry)" >> "$BACKUP_DIR/RELEASE_INFO.txt"
echo "Schema: public" >> "$BACKUP_DIR/RELEASE_INFO.txt"
```
## 3. Schema Migration
### 3.1 Apply Migration
```sql
-- Rename chunks → chunk (if not already done)
ALTER TABLE IF EXISTS public.chunks RENAME TO chunk;
-- Drop deprecated columns
ALTER TABLE public.chunk DROP COLUMN IF EXISTS old_chunk_id;
ALTER TABLE public.chunk DROP COLUMN IF EXISTS chunk_index;
-- Add new columns (v1.0.3+)
ALTER TABLE public.face_detections ADD COLUMN IF NOT EXISTS timestamp_secs float8;
```
### 3.2 Verify Schema
```sql
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'chunk'
ORDER BY ordinal_position;
```
Expected: 24 columns, no `old_chunk_id`, no `chunk_index`.
## 4. Deploy Package
### 4.1 Deploy via deploy.sh
```bash
cd path/to/package/
PG_BIN="/opt/homebrew/opt/postgresql@18/bin" \
DB_NAME="momentry" DB_USER="accusys" \
DATABASE_SCHEMA=public \
bash deploy.sh
```
### 4.2 Manual Verification (after deploy)
```sql
SELECT 'chunk' as tbl, count(*) FROM public.chunk WHERE file_uuid = '{uuid}';
SELECT 'identities (tmdb)', count(*) FROM public.identities WHERE source = 'tmdb';
SELECT 'tkg_nodes', count(*) FROM public.tkg_nodes WHERE file_uuid = '{uuid}';
SELECT 'tkg_edges', count(*) FROM public.tkg_edges WHERE file_uuid = '{uuid}';
```
Expected: chunk > 0, identities > 0, tkg_nodes > 0, tkg_edges > 0.
### 4.3 Set Status
```sql
UPDATE public.videos SET status = 'completed' WHERE file_uuid IN (
'aeed71342a899fe4b4c57b7d41bcb692',
'23b1c872379d4ec06479e5ed39eef4c5'
);
```
## 5. Binary Swap & Restart
### 5.1 Stop Old Server
```bash
# Graceful stop
pkill -TERM momentry
sleep 5
# Force if needed
pkill -9 momentry 2>/dev/null
```
### 5.2 Deploy New Binary
```bash
cp target/release/momentry /path/to/production/binary
chmod +x /path/to/production/binary
```
### 5.3 Start New Server
```bash
DATABASE_SCHEMA=public /path/to/production/binary server --port 3002
# or with .env
MOMENTRY_SERVER_PORT=3002 DATABASE_SCHEMA=public cargo run --release -- server
```
### 5.4 Health Check
```bash
curl http://localhost:3002/health
# Expected: {"status":"ok","version":"1.0.0","build_git_hash":"..."}
```
## 6. Verification
### 6.1 API Tests
```bash
API_KEY="muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69"
HOST="http://localhost:3002"
# Health
curl "$HOST/health"
curl "$HOST/health/detailed"
# Files
curl -H "X-API-Key: $API_KEY" "$HOST/api/v1/files?page=1&page_size=5"
curl -H "X-API-Key: $API_KEY" "$HOST/api/v1/files/scan"
# Search
curl -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d '{"query":"Audrey Hepburn","uuid":"aeed71342a899fe4b4c57b7d41bcb692","limit":3}' \
"$HOST/api/v1/search/universal"
# Identities
curl -H "X-API-Key: $API_KEY" "$HOST/api/v1/identities?page=1&page_size=5"
# Chunk detail (v1.0.3+)
curl -H "X-API-Key: $API_KEY" "$HOST/api/v1/file/aeed71342a899fe4b4c57b7d41bcb692/chunk/0"
# Face trace
curl -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
-d '{"sort_by":"face_count","limit":3}' \
"$HOST/api/v1/file/aeed71342a899fe4b4c57b7d41bcb692/face_trace/sortby"
# Trace video
curl -o /dev/null -w "%{http_code}" -H "X-API-Key: $API_KEY" \
"$HOST/api/v1/file/aeed71342a899fe4b4c57b7d41bcb692/trace/1934/video?padding=1"
# Expected: 200
# Full test suite
bash api_test.sh
# Expected: 39/39
```
### 6.2 Portal Check
| Page | URL | Expect |
|------|-----|--------|
| FilesView | `http://localhost:1420/files` | Charade = ✅ 已就绪 |
| SearchView | `http://localhost:1420/search` | File dropdown has Charade |
| Trace View | File → Traces | Face list visible |
| Video Playback | Search → Play | Video plays |
### 6.3 Demo
```bash
DEMO_BASE="http://localhost:3002" \
DEMO_FILE="aeed71342a899fe4b4c57b7d41bcb692" \
python3 scripts/demo_runner.py API_V1.0.0/DEMO_SCRIPT_v1.0.0.json --auto --speed 0.03
# Expected: 21/21 steps passed
```
## 7. Rollback
### 7.1 Binary Rollback
```bash
pkill momentry
cp "$BACKUP_DIR/momentry_$(date +%Y%m%d)" /path/to/production/binary
DATABASE_SCHEMA=public /path/to/production/binary server --port 3002
```
### 7.2 Schema Rollback
```sql
-- Rename back if needed
ALTER TABLE public.chunk RENAME TO chunks;
```
### 7.3 Data Rollback
```bash
psql -U accusys -d momentry < "$BACKUP_DIR/public_schema_full.sql"
```
### 7.4 Downtime Estimation
| Step | Est. Time |
|------|:--------:|
| Backup | 2 min |
| Schema migration | 1 min |
| Package deploy | 5-10 min |
| Binary swap | 1 min |
| Verification | 5 min |
| **Total** | **~15-20 min** |
## 8. Decision Log
| # | Decision | Owner | Decision |
|---|----------|:--:|------|
| 1 | Production binary source | M5 | M5 提供 release binary or M4 自編譯 |
| 2 | Schema: rename chunks→chunk? | M5 | M5 決定 public schema 結構 |
| 3 | Identity merge strategy | M5 | Keep prod 15 TMDB + merge with dev data? |
| 4 | Downtime window | M5 | 維護模式 (403 all) or hard stop? |
| 5 | Release scope | M5 | `aeed7134` only or both HD + YouTube? |
| 6 | .env / config for public schema | M5 | Production binary reads `DATABASE_SCHEMA=public` |
---
## Appendix: Release Checklist
- [ ] Dev 3003 passed all tests (39/39 + demo 21/21)
- [ ] Production old binary backed up
- [ ] Production DB full backup
- [ ] Schema migration SQL ready
- [ ] Package deployed to public schema
- [ ] Status set to completed
- [ ] New binary deployed
- [ ] Server restarted on 3002
- [ ] Health check returns ok
- [ ] 39/39 API tests pass on 3002
- [ ] Portal shows files correctly
- [ ] Demo runs 21/21 on 3002

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,504 @@
# SFTPGo Demo 用戶指南
## Web 管理介面
**URL**: https://sftpgo.momentry.ddns.net
### 登入方式
| 角色 | 用戶名 | 密碼 |
|------|--------|------|
| **Demo 用戶** | `demo` | `demopassword123` |
### 可用功能
- 瀏覽個人目錄結構
- 上傳、下載檔案
- 查看上傳記錄
---
## 快速連線資訊
| 項目 | 值 |
|------|-----|
| **主機** | `sftpgo.momentry.ddns.net` |
| **SFTP 連接埠** | `2022` |
| **用戶名** | `demo` |
| **密碼** | `demopassword123` |
| **主目錄** | `/demo` |
---
## 連線方式
### 1. 命令列 SFTP
```bash
# 使用密碼連線
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net
# 使用金鑰連線 (需先設定)
sftp -P 2022 -i ~/.ssh/id_rsa demo@sftpgo.momentry.ddns.net
```
### 2. FileZilla
1. **主機**: `sftp://sftpgo.momentry.ddns.net`
2. **連接埠**: `2022`
3. **協定**: `SFTP`
4. **登入類型**: `一般`
5. **用戶名**: `demo`
6. **密碼**: `demopassword123`
### 3. Cyberduck (macOS)
1. 選擇 **連線 > 新連線**
2. 協定選擇 **SFTP (SSH File Transfer Protocol)**
3. 伺服器: `sftpgo.momentry.ddns.net`
4. 連接埠: `2022`
5. 使用者名稱: `demo`
6. 密碼: `demopassword123`
### 4. curl 上傳
```bash
curl -u demo:demopassword123 \
-T /path/to/video.mp4 \
sftp://sftpgo.momentry.ddns.net:2022/demo/
```
---
## SFTP 基本操作
### 連線後常用指令
```bash
# 進入互動式模式
sftp demo@sftpgo.momentry.ddns.net -P 2022
# 常用指令
sftp> pwd # 顯示目前目錄
sftp> ls # 列出檔案
sftp> ls -la # 詳細列表
sftp> cd uploads # 切換目錄
sftp> mkdir videos # 建立目錄
sftp> put local.mp4 # 上傳檔案
sftp> get remote.mp4 # 下載檔案
sftp> rm old.mp4 # 刪除檔案
sftp> exit # 斷線
```
### 批次上傳
```bash
# 上傳多個檔案
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net <<EOF
cd uploads
put video1.mp4
put video2.mp4
put video3.mp4
bye
EOF
# 使用 glob 上傳
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net <<EOF
mput /path/to/videos/*.mp4
bye
EOF
```
---
## 自動上傳腳本
### Bash 腳本
```bash
#!/bin/bash
# upload.sh - 上傳視頻到 Momentry
HOST="sftpgo.momentry.ddns.net"
PORT="2022"
USER="demo"
PASS="demopassword123"
REMOTE_DIR="/demo/uploads"
# 要上傳的檔案
FILE="$1"
if [ -z "$FILE" ]; then
echo "用法: $0 <檔案路徑>"
exit 1
fi
sshpass -p "$PASS" sftp -P $PORT $USER@$HOST <<EOF
mkdir $REMOTE_DIR
cd $REMOTE_DIR
put "$FILE"
bye
EOF
echo "上傳完成: $FILE"
```
使用方式:
```bash
chmod +x upload.sh
./upload.sh /path/to/video.mp4
```
### Python 腳本
```python
#!/usr/bin/env python3
"""上傳檔案到 Momentry SFTP"""
import paramiko
import sys
import os
def upload_file(local_path, remote_dir="/demo/uploads"):
host = "sftpgo.momentry.ddns.net"
port = 2022
username = "demo"
password = "demopassword123"
transport = paramiko.Transport((host, port))
transport.connect(username=username, password=password)
sftp = paramiko.SFTPClient.from_transport(transport)
filename = os.path.basename(local_path)
remote_path = f"{remote_dir}/{filename}"
sftp.put(local_path, remote_path)
print(f"已上傳: {filename} -> {remote_path}")
sftp.close()
transport.close()
if __name__ == "__main__":
if len(sys.argv) < 2:
print("用法: python upload_sftp.py <檔案路徑>")
sys.exit(1)
upload_file(sys.argv[1])
```
安裝依賴:
```bash
pip install paramiko
```
---
## WebDAV 替代方案
如果 SFTP 連線有問題,可使用 WebDAV:
| 項目 | 值 |
|------|-----|
| **URL** | `https://momentry.ddns.net/webdav/` |
| **用戶名** | `demo` |
| **密碼** | `demopassword123` |
### curl 使用 WebDAV
```bash
# 上傳
curl -u demo:demopassword123 \
-T video.mp4 \
"https://momentry.ddns.net/webdav/demo/uploads/"
# 下載
curl -u demo:demopassword123 \
-o video.mp4 \
"https://momentry.ddns.net/webdav/demo/uploads/video.mp4"
# 列出目錄
curl -u demo:demopassword123 \
-X PROPFIND \
"https://momentry.ddns.net/webdav/demo/" \
-H "Depth: 1"
```
---
## 故障排除
### 連線被拒絕
```bash
# 檢查 SFTPGo 是否運行
curl -s http://localhost:8080/api/v2/status | jq .status
# 檢查連接埠
nc -zv momentry.ddns.net 2022
```
### 認證失敗
確認密碼是否正確:
```bash
# 測試認證
curl -u demo:demopassword123 \
"https://momentry.ddns.net/webdav/" -I
```
### 權限不足
上傳目錄可能需要先建立:
```bash
sshpass -p "demopassword123" sftp -P 2022 demo@sftpgo.momentry.ddns.net <<EOF
mkdir uploads
mkdir videos
bye
EOF
```
---
## 檔案上傳後自動化
上傳後SFTPGo 會自動:
1. 觸發 Hook 腳本
2. 記錄上傳事件到 `/Users/accusys/sftpgo_test/hook.log`
3. 呼叫 Momentry Core API 註冊視頻
查看上傳日誌:
```bash
tail -f /Users/accusys/sftpgo_test/hook.log
```
---
## 管理手冊
### 管理員帳戶
| 角色 | 用戶名 | 密碼 | 說明 |
|------|--------|------|------|
| **WebAdmin** | `admin` | `Test3200Test3200` | SFTPGo 管理介面 |
**WebAdmin URL**: https://sftpgo.momentry.ddns.net
### Admin 創建方式
根據官方文檔SFTPGo 有兩種方式創建管理員:
#### 方式 1: Web UI (首次設定)
1. 訪問 `http://localhost:8080/web/admin`
2. 如果沒有管理員,會顯示設定畫面
3. 輸入用戶名和密碼創建第一個管理員
#### 方式 2: 自動創建 (推薦)
需要同時滿足以下條件:
1. 配置文件中設定 `"create_default_admin": true`
2. 設定環境變數 `SFTPGO_DEFAULT_ADMIN_USERNAME``SFTPGO_DEFAULT_ADMIN_PASSWORD`
### 設定步驟
#### Step 1: 確保配置文件正確
確認 `/Users/accusys/momentry/etc/sftpgo/sftpgo.json` 中有:
```json
{
"data_provider": {
"create_default_admin": true
},
"httpd": {
"setup": {
"installation_code": "Test3200Test3200"
}
}
}
```
#### Step 2: 更新 plist 加入環境變數
編輯 `/Library/LaunchDaemons/com.momentry.sftpgo.plist`,加入:
```xml
<key>EnvironmentVariables</key>
<dict>
<key>SFTPGO_DEFAULT_ADMIN_USERNAME</key>
<string>admin</string>
<key>SFTPGO_DEFAULT_ADMIN_PASSWORD</key>
<string>Test3200Test3200</string>
</dict>
```
#### Step 3: 重啟 SFTPGo
```bash
launchctl unload homebrew.mxcl.sftpgo
launchctl load homebrew.mxcl.sftpgo
```
#### Step 4: 驗證管理員
```bash
curl -s -X GET "http://localhost:8080/api/v2/token" \
-u "admin:Test3200Test3200"
```
成功回應:
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 1200
}
```
### REST API 認證流程
#### 1. 獲取 Token
```bash
curl -s -X GET "http://localhost:8080/api/v2/token" \
-u "admin:Test3200Test3200"
```
#### 2. 使用 Token 訪問 API
```bash
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
-u "admin:Test3200Test3200" | jq -r '.access_token')
# 查看所有用戶
curl -s http://localhost:8080/api/v2/users \
-H "Authorization: Bearer $TOKEN"
# 查看系統狀態
curl -s http://localhost:8080/api/v2/status \
-H "Authorization: Bearer $TOKEN"
```
### 常用管理操作
#### 查看所有用戶
```bash
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
-u "admin:Test3200Test3200" | jq -r '.access_token')
curl -s http://localhost:8080/api/v2/users \
-H "Authorization: Bearer $TOKEN" | jq .
```
#### 建立新用戶
```bash
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
-u "admin:Test3200Test3200" | jq -r '.access_token')
curl -s -X POST http://localhost:8080/api/v2/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "newuser",
"password": "userpassword123",
"email": "user@example.com",
"status": 1,
"home_dir": "/Users/accusys/momentry/var/sftpgo/data/newuser",
"uid": 501,
"gid": 20,
"permissions": {
"/": ["*"]
}
}'
```
#### 建立用戶組
```bash
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
-u "admin:Test3200Test3200" | jq -r '.access_token')
curl -s -X POST http://localhost:8080/api/v2/groups \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "editors",
"description": "Editor group with upload permissions"
}'
```
#### 刪除用戶
```bash
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
-u "admin:Test3200Test3200" | jq -r '.access_token')
curl -s -X DELETE http://localhost:8080/api/v2/users/username \
-H "Authorization: Bearer $TOKEN"
```
#### 修改用戶密碼
```bash
TOKEN=$(curl -s -X GET "http://localhost:8080/api/v2/token" \
-u "admin:Test3200Test3200" | jq -r '.access_token')
curl -s -X PUT http://localhost:8080/api/v2/users/demo \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"password": "newpassword456"
}'
```
### 重要設定
| 設定項目 | 值 | 說明 |
|----------|-----|------|
| **SFTP 連接埠** | `2022` | SSH 檔案傳輸協定 |
| **HTTP/WebDAV 連接埠** | `8080` | 內部 HTTP 服務 |
| **WebAdmin 連接埠** | `8080` | 管理介面 (`/web/admin`) |
| **WebClient 連接埠** | `8080` | 用戶介面 (`/web/client`) |
| **資料庫** | PostgreSQL | 用戶和設定儲存 |
| **Hook 腳本** | `/Users/accusys/sftpgo_test/register_hook.sh` | 上傳後自動化處理 |
| **安裝碼** | `Test3200Test3200` | 首次設定管理員所需 |
| **create_default_admin** | `true` | 自動創建管理員 |
| **Token 有效期** | 1200 秒 (20分鐘) | JWT 過期時間 |
### 用戶目錄結構
所有 SFTPGo 用戶資料統一存放在 `/Users/accusys/momentry/var/sftpgo/data/` 目錄下:
| 用戶 | 資料夾路徑 | 密碼 | 說明 |
|------|------------|------|------|
| **demo** | `/Users/accusys/momentry/var/sftpgo/data/demo` | `demopassword123` | Demo 用戶上傳目錄 |
| **momentry** | `/Users/accusys/momentry/var/sftpgo/data/momentry` | `momentry123` | Momentry 系統用戶 |
| **warren** | `/Users/accusys/momentry/var/sftpgo/data/warren` | `warren123` | 其他用戶 |
### API Token 獲取方式
```bash
# 注意:使用 GET 而非 POST
curl -s -X GET "http://localhost:8080/api/v2/token" \
-u "admin:Test3200Test3200" | jq .access_token
```
### 故障排除
| 問題 | 解決方案 |
|------|----------|
| 無法獲取 Token | 確認環境變數已正確設定並重啟 SFTPGo |
| SFTP 連線被拒絕 | 檢查 SFTPGo 服務: `launchctl list \| grep sftpgo` |
| 無法登入 WebAdmin | 確認 admin 用戶存在: 檢查 plist 中環境變數是否正確 |
| 上傳失敗 | 檢查 Hook 腳本: `tail -f /Users/accusys/momentry/log/sftpgo.error.log` |
| 權限不足 | 檢查用戶權限或更新 `permissions` 設定 |
| API 返回 401 | Token 過期,需重新獲取: `curl -X POST .../token -u "admin:pass"` | |
---
## 安全注意事項
- **密碼保護**: `demopassword123` 為 demo 帳戶密碼
- **限制存取**: Demo 用戶只能訪問 `/demo` 目錄
- **監控**: 所有上傳都有日誌記錄
- **生產環境**: 正式環境應使用更強的密碼和金鑰認證

View File

@@ -0,0 +1,235 @@
# SFTPGo 生命週期管理 (Source → Install → Config → Use)
**Date**: 2026-05-15
**Status**: Active, Verified
---
## 生命週期總覽
```
Source → Archive → Build → Install → Config → Start → Verify → Use
① ② ③ ④ ⑤ ⑥ ⑦ ⑧
```
---
## ① Source Code
| Field | Value |
|-------|-------|
| **Repository** | `https://github.com/drakkan/sftpgo.git` |
| **Branch** | `main` (commit `6e543c6`) |
| **License** | AGPL v3 |
| **Language** | Go 1.26+ |
| **Go files** | 246 |
| **Source size** | 23 MB |
## ② Archive
```bash
# Archive command
cd /tmp
tar czf release/system/v1.0/services/src/sftpgo-main.tar.gz sftpgo/
# Verify
shasum -a 256 release/system/v1.0/services/src/sftpgo-main.tar.gz
# → 6607334148917dd80a687706a3ae63ea8c532d10c6717c87491da23939c96d4a
```
**Archive location**: `release/system/v1.0/services/src/sftpgo-main.tar.gz` (9.2 MB)
## ③ Build
```bash
# Clone source
git clone --depth 1 https://github.com/drakkan/sftpgo.git /tmp/sftpgo
# Build binary
cd /tmp/sftpgo
go build -o /Users/accusys/bin/sftpgo .
# Verify binary
shasum -a 256 /Users/accusys/bin/sftpgo
# → 9991d2a1c877d5bcae17cb4e026de939862e4b880924589cf4ed15ac7291ec7e
ls -lh /Users/accusys/bin/sftpgo
# → 88 MB
```
**Binary**: `/Users/accusys/bin/sftpgo` (88 MB)
## ④ Install
### Database
```bash
# Create dedicated PostgreSQL database + user
psql -U accusys -h /tmp -d postgres -c "CREATE DATABASE sftpgo"
psql -U accusys -h /tmp -d postgres -c "CREATE USER sftpgo WITH PASSWORD 'sftpgo_pass_2026'"
psql -U accusys -h /tmp -d sftpgo -c "GRANT ALL ON SCHEMA public TO sftpgo"
```
### Templates & Static Files
```bash
# Copy from source (required by SFTPGo)
cp -r /tmp/sftpgo/templates/ /Users/accusys/momentry/etc/sftpgo/templates/
cp -r /tmp/sftpgo/static/ /Users/accusys/momentry/etc/sftpgo/static/
cp -r /tmp/sftpgo/openapi/ /Users/accusys/momentry/etc/sftpgo/openapi/
```
## ⑤ Configuration
**Config file**: `/Users/accusys/momentry/etc/sftpgo/sftpgo.json`
### Key Settings
| Section | Key | Value |
|---------|-----|-------|
| `data_provider` | `driver` | `postgresql` |
| `data_provider` | `name` | `sftpgo` |
| `data_provider` | `users_base_dir` | `/Users/accusys/momentry/var/sftpgo/data` |
| `httpd.bindings[0]` | `port` | `8080` |
| `sftpd.bindings[0]` | `port` | `2022` |
| `webdavd.bindings[0]` | `port` | `8090` |
### launchd Plist
**File**: `momentry_runtime/plist/com.momentry.sftpgo.plist`
```xml
<key>ProgramArguments</key>
<array>
<string>/Users/accusys/bin/sftpgo</string>
<string>serve</string>
<string>-c</string>
<string>/Users/accusys/momentry/etc/sftpgo/</string>
</array>
```
## ⑥ Start
### Initialize Provider (first time only)
```bash
SFTPGO_DEFAULT_ADMIN_USERNAME=admin \
SFTPGO_DEFAULT_ADMIN_PASSWORD=Test3200Test3200 \
/Users/accusys/bin/sftpgo initprovider -c /Users/accusys/momentry/etc/sftpgo/
```
### Start Serve
```bash
SFTPGO_DEFAULT_ADMIN_USERNAME=admin \
SFTPGO_DEFAULT_ADMIN_PASSWORD=Test3200Test3200 \
nohup /Users/accusys/bin/sftpgo serve \
-c /Users/accusys/momentry/etc/sftpgo/ \
> /Users/accusys/momentry/log/sftpgo.log 2>&1 &
```
## ⑦ Verify
```bash
# Service check
curl -sI http://localhost:8080/
# → Server: SFTPGo/2.7.99-dev
# HTTPS
curl -sI https://m5sftpgo.momentry.ddns.net/
# → Server: SFTPGo/2.7.99-dev
# → Via: 1.1 Caddy
# Auth
curl -s -u "admin:Test3200Test3200" http://localhost:8080/api/v2/token
# → {"access_token":"eyJ...","expires_at":"..."}
```
## ⑧ Usage
### User Management
**Admin**: `admin` / `Test3200Test3200`
**Demo user**: `demo` / `demopassword123`
```bash
# Get admin token
TOKEN=$(curl -s -u "admin:Test3200Test3200" \
"http://localhost:8080/api/v2/token" | \
python3 -c "import json,sys;print(json.load(sys.stdin).get('access_token',''))")
# Create user
curl -s -X POST "http://localhost:8080/api/v2/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username":"demo","password":"demopassword123",
"home_dir":"/Users/accusys/momentry/var/sftpgo/data/demo",
"permissions": {"/": ["*"]}, "status": 1}'
# Update user password
curl -s -X PUT "http://localhost:8080/api/v2/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$(curl -s http://localhost:8080/api/v2/users -H 'Authorization: Bearer $TOKEN' | python3 -c \"import json,sys;u=[u for u in json.load(sys.stdin) if u['username']=='demo'][0];u['password']='newpass';print(json.dumps(u))\")"
# List users
curl -s "http://localhost:8080/api/v2/users" \
-H "Authorization: Bearer $TOKEN"
```
### External Access
```bash
# Via HTTPS
curl -s "https://m5sftpgo.momentry.ddns.net/api/v2/status"
# SFTP (port 2022)
sftp -P 2022 demo@m5sftpgo.momentry.ddns.net
# WebDAV (port 8090)
# http://m5sftpgo.momentry.ddns.net:8090/
```
---
## 資源管理記錄
### dev.resources
```sql
INSERT INTO dev.resources (resource_id, resource_type, category, capabilities, config)
VALUES (
'sftpgo', 'system_tool', 'file_upload',
'["sftp", "file_transfer", "webdav"]',
'{"binary": "/Users/accusys/bin/sftpgo",
"version": "2.7.99-dev",
"port": 8080,
"source_sha256": "6607334148917dd80a687706a3ae63ea8c532d10c6717c87491da23939c96d4a",
"binary_sha256": "9991d2a1c877d5bcae17cb4e026de939862e4b880924589cf4ed15ac7291ec7e",
"source_archive": "release/system/v1.0/services/src/sftpgo-main.tar.gz",
"plist": "momentry_runtime/plist/com.momentry.sftpgo.plist"}'
)
ON CONFLICT (resource_id) DO UPDATE SET
resource_type = EXCLUDED.resource_type,
category = EXCLUDED.category,
config = EXCLUDED.config;
```
---
## SHA256 Checksums Reference
| Asset | SHA256 |
|-------|--------|
| Source archive (`sftpgo-main.tar.gz`) | `6607334148917dd80a687706a3ae63ea8c532d10c6717c87491da23939c96d4a` |
| Binary (`/Users/accusys/bin/sftpgo`) | `9991d2a1c877d5bcae17cb4e026de939862e4b880924589cf4ed15ac7291ec7e` |
---
## Ports Summary
| Port | Protocol | Service | External URL |
|------|----------|---------|-------------|
| 8080 | HTTP | Web Admin + REST API | `https://m5sftpgo.momentry.ddns.net` |
| 2022 | SFTP | File Transfer | `sftp://m5sftpgo.momentry.ddns.net:2022` |
| 8090 | WebDAV | File Access | `https://m5sftpgo.momentry.ddns.net:8090/` |

View File

@@ -0,0 +1,237 @@
# SFTPGo Installation & Setup
**Date**: 2026-05-15
**Version**: 2.6.7 (source build from main branch)
**Status**: Active
---
## Top Info
| Field | Value |
|-------|-------|
| **Source** | `https://github.com/drakkan/sftpgo.git` (main branch, ~2.7.99-dev) |
| **Source archive** | `release/system/v1.0/services/src/sftpgo-main.tar.gz` (9.2MB) |
| **Source SHA256** | `6607334148917dd80a687706a3ae63ea8c532d10c6717c87491da23939c96d4a` |
| **Build method** | `git clone && go build -o /Users/accusys/bin/sftpgo .` |
| **Binary** | `/Users/accusys/bin/sftpgo` (88MB) |
| **Binary SHA256** | `9991d2a1c877d5bcae17cb4e026de939862e4b880924589cf4ed15ac7291ec7e` |
| **Config** | `/Users/accusys/momentry/etc/sftpgo/sftpgo.json` |
| **Templates** | `/Users/accusys/momentry/etc/sftpgo/templates/` (copied from source) |
| **Database** | PostgreSQL `sftpgo` database, user `sftpgo` |
| **Plist** | `momentry_runtime/plist/com.momentry.sftpgo.plist` |
| **Ports** | 8080 (HTTP/WebAdmin), 2022 (SFTP), 8090 (WebDAV) |
| **Resource ID** | `sftpgo` in `dev.resources` (type: `system_tool`, category: `file_upload`) |
---
## Build from Source
### Prerequisites
- Go 1.26+ (`go version`)
### Build
```bash
# Clone source
git clone --depth 1 https://github.com/drakkan/sftpgo.git /tmp/sftpgo
# Build binary
cd /tmp/sftpgo
go build -o /Users/accusys/bin/sftpgo .
# Verify
/Users/accusys/bin/sftpgo --version
```
### Archive Source
```bash
tar czf release/system/v1.0/services/src/sftpgo-main.tar.gz -C /tmp sftpgo/
shasum -a 256 release/system/v1.0/services/src/sftpgo-main.tar.gz
```
---
## Database Setup
```bash
# Create database and user
psql -U accusys -h /tmp -d postgres -c "CREATE DATABASE sftpgo"
psql -U accusys -h /tmp -d postgres -c "CREATE USER sftpgo WITH PASSWORD 'sftpgo_pass_2026'"
psql -U accusys -h /tmp -d sftpgo -c "GRANT ALL ON SCHEMA public TO sftpgo"
```
---
## Start Server
### Initialize Provider (first time only)
```bash
SFTPGO_DEFAULT_ADMIN_USERNAME=admin \
SFTPGO_DEFAULT_ADMIN_PASSWORD=Test3200Test3200 \
/Users/accusys/bin/sftpgo initprovider \
-c /Users/accusys/momentry/etc/sftpgo/
```
### Start Serve
```bash
SFTPGO_DEFAULT_ADMIN_USERNAME=admin \
SFTPGO_DEFAULT_ADMIN_PASSWORD=Test3200Test3200 \
nohup /Users/accusys/bin/sftpgo serve \
-c /Users/accusys/momentry/etc/sftpgo/ \
> /Users/accusys/momentry/log/sftpgo.log 2>&1 &
```
Note: The `-c` flag must point to the config directory (containing `sftpgo.json`, `templates/`, `static/`, `openapi/`).
### Verify
```bash
# Health check (HTTP 200 = running)
curl -s http://127.0.0.1:8080/api/v2/status
# Should return: {"error":"no token found","message":"Unauthorized"}
# Get admin token
curl -s -u "admin:Test3200Test3200" "http://127.0.0.1:8080/api/v2/token"
```
---
## User Management
### Get Admin Token
The SFTPGo API uses JWT tokens. All user/management API calls require `Authorization: Bearer <token>` header.
```bash
TOKEN=$(curl -s -u "admin:Test3200Test3200" \
"http://127.0.0.1:8080/api/v2/token" | \
python3 -c "import json,sys;print(json.load(sys.stdin).get('access_token',''))")
```
### Create Demo User
```bash
curl -s -X POST "http://127.0.0.1:8080/api/v2/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"username": "demo",
"password": "demopassword123",
"home_dir": "/Users/accusys/momentry/var/sftpgo/data/demo",
"permissions": {"/": ["*"]},
"status": 1,
"quota_size": 0,
"quota_files": 0
}'
```
### Update User Password
```bash
# Get current user data
USER_DATA=$(curl -s "http://127.0.0.1:8080/api/v2/users" \
-H "Authorization: Bearer $TOKEN" | \
python3 -c "
import json,sys
users=json.load(sys.stdin)
u=[u for u in users if u['username']=='demo'][0]
u['password']='newpassword'
print(json.dumps(u))
")
# Update user
curl -s -X PUT "http://127.0.0.1:8080/api/v2/users" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$USER_DATA"
```
### List Users
```bash
curl -s "http://127.0.0.1:8080/api/v2/users" \
-H "Authorization: Bearer $TOKEN"
```
---
## Configuration
Key settings in `/Users/accusys/momentry/etc/sftpgo/sftpgo.json`:
| Section | Key | Value | Note |
|---------|-----|-------|------|
| `data_provider` | `driver` | `postgresql` | User/auth database |
| `data_provider` | `name` | `sftpgo` | Database name |
| `data_provider` | `users_base_dir` | `/Users/accusys/momentry/var/sftpgo/data` | Base directory for user homes |
| `httpd.bindings[0]` | `port` | `8080` | Web admin + REST API |
| `sftpd.bindings[0]` | `port` | `2022` | SFTP server |
| `webdavd.bindings[0]` | `port` | `8090` | WebDAV server |
| `setup` | `installation_code` | `momentry2026` | Web setup wizard code |
---
## launchd Plist
```xml
<!-- /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.sftpgo.plist -->
<key>ProgramArguments</key>
<array>
<string>/Users/accusys/bin/sftpgo</string>
<string>serve</string>
<string>-c</string>
<string>/Users/accusys/momentry/etc/sftpgo/</string>
</array>
```
Load with launchctl:
```bash
launchctl load ~/Library/LaunchAgents/com.momentry.sftpgo.plist
```
---
## Resource Record
```sql
INSERT INTO dev.resources (resource_id, resource_type, category, capabilities, config)
VALUES (
'sftpgo',
'system_tool',
'file_upload',
'["sftp", "file_transfer", "webdav"]',
'{"binary": "/Users/accusys/bin/sftpgo", "version": "2.6.7", "port": 8080,
"source_sha256": "6607334148917dd80a687706a3ae63ea8c532d10c6717c87491da23939c96d4a",
"binary_sha256": "9991d2a1c877d5bcae17cb4e026de939862e4b880924589cf4ed15ac7291ec7e",
"source_archive": "release/system/v1.0/services/src/sftpgo-main.tar.gz",
"plist": "momentry_runtime/plist/com.momentry.sftpgo.plist"}'
)
ON CONFLICT (resource_id) DO UPDATE SET
resource_type = EXCLUDED.resource_type,
category = EXCLUDED.category,
config = EXCLUDED.config;
```
---
## Ports Summary
| Port | Service | Purpose |
|------|---------|---------|
| 8080 | HTTP/HTTPS | Web admin UI + REST API |
| 2022 | SFTP | File transfer over SSH |
| 8090 | WebDAV | File access via WebDAV |
---
## Credentials
| User | Password | Role |
|------|----------|------|
| `admin` | `Test3200Test3200` | Administrator (API + Web Admin) |
| `demo` | `demopassword123` | Demo user (file upload) |

View File

@@ -0,0 +1,84 @@
# SFTPGo Source Code Verification Report
**Date**: 2026-05-15
**Version**: 2.7.99-dev (main branch)
**Status**: ✅ Verified
---
## 1. Source Archive
| Item | Value |
|------|-------|
| **Archive** | `release/system/v1.0/services/src/sftpgo-main.tar.gz` |
| **Size** | 9.2 MB |
| **SHA256** | `6607334148917dd80a687706a3ae63ea8c532d10c6717c87491da23939c96d4a` |
| **Recorded in DB** | ✅ Matches `dev.resources.config->>'source_sha256'` |
| **Git remote** | `https://github.com/drakkan/sftpgo.git` |
| **Git commit** | `6e543c6` |
## 2. Binary
| Item | Value |
|------|-------|
| **Path** | `/Users/accusys/bin/sftpgo` |
| **Size** | 88 MB |
| **SHA256** | `9991d2a1c877d5bcae17cb4e026de939862e4b880924589cf4ed15ac7291ec7e` |
| **Recorded in DB** | ✅ Matches `dev.resources.config->>'binary_sha256'` |
| **Build date** | 2026-05-15 22:48 |
| **Build method** | `git clone && go build -o /Users/accusys/bin/sftpgo .` |
## 3. Source Tree
| Item | Count |
|------|:----:|
| Go source files | 246 |
| Total size | 23 MB |
| License | AGPL v3 (GNU Affero General Public License) |
## 4. Build Verification
| Item | Status |
|------|:------:|
| Build from source | ✅ `go build` succeeds |
| Reproducible build | ✅ Source archived, SHA256 matched |
| Dependency trace | ✅ `go.mod` + `go.sum` included in archive |
## 5. Runtime Service
| Endpoint | Status | Response |
|----------|:------:|----------|
| `http://localhost:8080/` | ✅ | `SFTPGo/2.7.99-dev` |
| `https://m5sftpgo.momentry.ddns.net/` | ✅ | Caddy → SFTPGo proxy |
| Auth (admin) | ✅ | Token endpoint works |
| Demo user | ✅ | `demo` / `demopassword123` |
| Ports | ✅ | 8080 (HTTP), 2022 (SFTP), 8090 (WebDAV) |
## 6. Resource Registration
```sql
INSERT INTO dev.resources (resource_id, resource_type, category, capabilities, config)
VALUES (
'sftpgo', 'system_tool', 'file_upload',
'["sftp", "file_transfer", "webdav"]',
'{"binary": "/Users/accusys/bin/sftpgo", "version": "2.6.7", "port": 8080,
"source_sha256": "6607334148917dd80a687706a3ae63ea8c532d10c6717c87491da23939c96d4a",
"binary_sha256": "9991d2a1c877d5bcae17cb4e026de939862e4b880924589cf4ed15ac7291ec7e",
"source_archive": "release/system/v1.0/services/src/sftpgo-main.tar.gz",
"plist": "momentry_runtime/plist/com.momentry.sftpgo.plist"}'
);
```
## 7. Verification Summary
| # | Check | Result |
|---|-------|:------:|
| 1 | Source archive exists in `services/src/` | ✅ |
| 2 | Source SHA256 matches DB record | ✅ |
| 3 | Binary SHA256 matches DB record | ✅ |
| 4 | Build reproducible from archived source | ✅ |
| 5 | Service responding on HTTP (localhost:8080) | ✅ |
| 6 | Service accessible via HTTPS (m5sftpgo.momentry.ddns.net) | ✅ |
| 7 | Admin auth works | ✅ |
| 8 | Demo user exists and functional | ✅ |
| 9 | Configuration documented in `REFERENCE/SFTPGo_Setup.md` | ✅ |

View File

@@ -0,0 +1,208 @@
# Momentry Core — 完整服務清單
**Date**: 2026-05-15
**Version**: 1.0
**Status**: Active
---
## 1. 來源分類
| 來源 | 說明 | 數量 |
|------|------|:----:|
| 🔵 Source build | 原始碼編譯SHA256 可驗證 | 7 |
| 🟡 Homebrew | 透過 brew 安裝,待遷移 | 18 |
| 🟢 Production only | 僅在 production 運行 | 1 |
| 📦 Third-party | 預編譯 binary / 套件 | 5 |
---
## 2. 完整服務一覽
### 🔵 Source Build (SHA256 已記錄)
| Service | Version | Binary | Source Archive | Resource ID |
|---------|---------|--------|---------------|-------------|
| **PostgreSQL** | 18.3 | `$HOME/pgsql/18.3/bin/postgres` | `release/.../postgresql-18.3.tar.gz` | - |
| **Redis** | 7.4.3 | `/opt/homebrew/bin/redis-server`(brew) | `release/.../redis-7.4.3.tar.gz` | - |
| **FFmpeg** | 7.1.1 | `/opt/homebrew/bin/ffmpeg`(brew) | `release/.../ffmpeg-7.1.1.tar.xz` | - |
| **RSync** | 3.4.2 | `/Users/accusys/bin/rsync` | `release/.../rsync-official-3.4.2.tar.gz` | `rsync` |
| **SFTPGo** | 2.7.99-dev | `/Users/accusys/bin/sftpgo` | `release/.../sftpgo-main.tar.gz` | `sftpgo` |
| **llama.cpp** | - | `$HOME/llama/bin/llama-server` | `release/.../llama.cpp/` | - |
| **Momentry** | 1.0.0 | `target/release/momentry` | (git repo) | - |
### 🟡 Homebrew (待遷移)
| Service | Version | Brew Binary | Dev.Resources |
|---------|---------|-------------|:-------------:|
| **PHP** | 8.5.5 | `/opt/homebrew/bin/php` | ✅ |
| **PHP-FPM** | 8.5.5 | `/opt/homebrew/sbin/php-fpm` | ✅ (同 php) |
| **MariaDB** | 12.2.2 | `/opt/homebrew/bin/mariadbd` | ✅ |
| **Node.js** | 25.9.0 | `/opt/homebrew/bin/node` | ❌ |
| **MongoDB** | 8.2.7 | `/opt/homebrew/bin/mongod` | ❌ |
| **MongoSH** | 2.8.3 | `/opt/homebrew/bin/mongosh` | ❌ |
| **Ollama** | 0.23.1 | `/opt/homebrew/bin/ollama` | ❌ |
| **yt-dlp** | 2026.3.17 | `/opt/homebrew/bin/yt-dlp` | ❌ |
| **whisper-cpp** | 1.8.4 | `/opt/homebrew/bin/whisper-cpp` | ❌ |
| **Tesseract** | 5.5.2 | `/opt/homebrew/bin/tesseract` | ❌ |
| **SDL2** | 2.32.10 | `/opt/homebrew/lib/libsdl2.dylib` | ❌ |
| **Go** | 1.26.2 | `/opt/homebrew/bin/go` | ❌ |
| **CMake** | 4.3.2 | `/opt/homebrew/bin/cmake` | ❌ |
| **Python 3.11** | 3.11.15 | `/opt/homebrew/bin/python3.11` | ❌ |
| **Python 3.14** | 3.14.4 | `/opt/homebrew/bin/python3.14` | ❌ |
| **protobuf** | - | `/opt/homebrew/bin/protoc` | ❌ |
| **pgvector** | 0.8.2 | (PG extension) | ❌ |
| **FFmpeg-full** | 8.1.1 | `/opt/homebrew/opt/ffmpeg-full/bin/ffmpeg` | ❌ |
### 🟢 Production Only (僅 production 有)
| Service | URL | Role | Resource ID |
|---------|-----|------|:-----------:|
| **WordPress** | `https://wp.momentry.ddns.net` | CMS + Portal | ✅ (offline) |
### 📦 Third-Party (預編譯 / 外部依賴)
| Service | Version | Location | Note |
|---------|---------|----------|------|
| **Qdrant** | - | `/Users/accusys/momentry_core_0.1/services/qdrant/target/release/qdrant` | Rust source in `services/` |
| **EmbeddingGemma** | - | Python script `scripts/embeddinggemma_server.py` | 由 server 管理 |
| **GroundingDINO** | - | `release/.../services/src/GroundingDINO/` | Python ML model |
| **PaliGemma** | - | `release/.../services/src/paligemma/` | Python ML model |
| **Odoo** | - | `release/.../services/src/odoo/` | ERP (未啟用) |
| **Gitea** | - | `release/.../services/src/gitea/` | Git hosting (未啟用) |
| **LibreOffice** | 26.2.3 | `release/.../LibreOffice_26.2.3_MacOS_aarch64.dmg` | Document conversion |
| **Swift** | 6.3.1 | `release/.../swift-6.3.1-RELEASE.tar.gz` | Processor scripts |
| **SQLite-vec** | - | `release/.../sqlite-vec/` + `vec0.dylib` | Vector extension |
| **librsvg** | - | `release/.../librsvg/` | SVG conversion |
| **macmon** | 0.7.2 | `$HOME/bin/macmon` | Monitoring |
| **mactop** | latest | `$HOME/bin/mactop` | Monitoring |
| **mermaid-cli** | 11.14.0 | npm global | Diagram rendering |
---
## 3. Plist (launchd) 管理
| Plist | Binary Path | Source Type | Status |
|-------|-------------|:-----------:|:------:|
| `com.momentry.sftpgo.plist` | `/Users/accusys/bin/sftpgo` | ✅ source | ✅ |
| `com.momentry.redis.plist` | `/opt/homebrew/bin/redis-server` | 🟡 brew | ❌ 待更新 |
| `com.momentry.postgresql.plist` | `$HOME/pgsql/18.3/bin/postgres` | ✅ source | ✅ |
| `com.momentry.ollama.plist` | `/opt/homebrew/bin/ollama` | 🟡 brew | ❌ 待更新 |
| `com.momentry.llama.plist` | `$HOME/llama/bin/llama-server` | ✅ source | ✅ |
---
## 4. Port 分配
| Port | Service | Source Type | Running |
|:----:|---------|:-----------:|:-------:|
| 3002 | Momentry API (production) | ✅ build | ✅ |
| 3003 | Momentry Playground (dev) | ✅ build | ✅ |
| 8080 | SFTPGo Web Admin | ✅ build | ✅ |
| 2022 | SFTPGo SFTP | ✅ build | ✅ |
| 8090 | SFTPGo WebDAV | ✅ build | ✅ |
| 5432 | PostgreSQL | ✅ build | ✅ |
| 6379 | Redis | 🟡 brew | ✅ |
| 27017 | MongoDB | 🟡 brew | ✅ |
| 6333 | Qdrant | 📦 service | ✅ |
| 11434 | Ollama API | 🟡 brew | ✅ |
| 11436 | EmbeddingGemma | 📦 script | ✅ |
| 8082 | llama-server (Gemma4) | ✅ build | ✅ |
| 9000 | PHP-FPM | 🟡 brew | ❌ |
| 8081 | LLM (deprecated) | 🟡 brew | ❌ |
---
## 5. Database
| Database | Engine | Port | Schema | Momentry Role |
|----------|--------|:----:|--------|---------------|
| `momentry` | PostgreSQL | 5432 | `dev` | Dev playground (3003) |
| `momentry_3002` | PostgreSQL | 5432 | `public` | M5 production (3002) |
| `sftpgo` | PostgreSQL | 5432 | `public` | SFTPGo users |
| `momentry` | MongoDB | 27017 | `momentry` | Cache |
| Qdrant | Qdrant | 6333 | `momentry_dev_rule1_v2` | Vector search |
| Redis | Redis | 6379 | - | Worker progress, cache |
---
## 6. Source Archives Status (`release/system/v1.0/services/src/`)
### ✅ 已歸檔 (32 items)
```
cmake-4.2.0-macos-universal.tar.gz ffmpeg-7.1.1.tar.xz
freetype-2.13.3.tar.gz postgresql-18.3.tar.gz
redis-7.4.3.tar.gz rsync-official-3.4.2.tar.gz
sftpgo-main.tar.gz swift-6.3.1-RELEASE.tar.gz
llama.cpp/ x264/
go/ pyenv/
macmon-0.7.2.tar.gz mactop-latest.tar.gz
rustc-1.92.0-src.tar.xz rustup-1.28.1.tar.gz
sqlite-amalgamation-3490100.zip sqlite-vec/
vec0.dylib yt-dlp/
mermaid-js-mermaid-cli-11.14.0.tgz LibreOffice_26.2.3_MacOS_aarch64.dmg
libreoffice-26.2.3.2.tar.xz librsvg/
GroundingDINO/ paligemma/
gitea/ erpnext/
frappe/ odoo/
python_probe_deps.txt
```
### ❌ 缺少需補
| Service | Expected Source URL |
|---------|-------------------|
| PHP 8.5.5 | `https://www.php.net/distributions/php-8.5.5.tar.gz` |
| MariaDB 12.2.2 | `https://github.com/MariaDB/server/archive/mariadb-12.2.2.tar.gz` |
| Node.js 25.9.0 | `https://nodejs.org/dist/v25.9.0/node-v25.9.0.tar.gz` |
| MongoDB 8.2.7 | `https://github.com/mongodb/mongo/archive/r8.2.7.tar.gz` |
| Ollama 0.23.1 | `https://github.com/ollama/ollama/archive/v0.23.1.tar.gz` |
---
## 7. Health Endpoint (`/health/detailed`)
| Field | Source | Covers |
|-------|--------|--------|
| `services.postgres` | direct check | PostgreSQL |
| `services.redis` | direct check | Redis |
| `services.qdrant` | direct check | Qdrant |
| `services.mongodb` | direct check | MongoDB |
| `pipeline.ffmpeg` | `which ffmpeg` | FFmpeg |
| `pipeline.llm` | HTTP check port 8082 | llama-server |
| `pipeline.embedding_server` | HTTP check port 11436 | EmbeddingGemma |
| `pipeline.rsync` | file check | RSync |
| `pipeline.scripts_integrity` | SHA256 vs manifest | Processor scripts(345) |
| `schema` | DB query | Schema migrations(9) |
| `processors` | file check | 12 processors |
| `resources` | system query | CPU/Mem/GPU |
### 未涵蓋的服務
- PHP / PHP-FPM
- MariaDB
- Node.js / npm
- Ollama
- SFTPGo (有 `/api/v1/stats/sftpgo` 但不在 health response)
- yt-dlp / whisper-cpp / tesseract
- WordPress
---
## 8. Migration Priority Matrix
```
Priority Service Binary Source Dev.Resources Plist Health
──────────────────────────────────────────────────────────────────────────────
P1:high PHP brew → build ❌ need ✅ recorded ❌ brew ❌ missing
P1:high MariaDB brew → build ❌ need ✅ recorded ❌ brew ❌ missing
P1:high Node.js brew → build ❌ need ❌ not in db ❌ brew ❌ missing
P2:med Redis brew → build ✅ have ❌ not in db ❌ brew ❌ missing
P2:med MongoDB brew → build ❌ need ❌ not in db ❌ brew ❌ missing
P2:med ffmpeg brew → build ✅ have ❌ not in db ❌ brew ✅ basic
P3:low Ollama brew → build ❌ need ❌ not in db ❌ brew ❌ missing
P3:low yt-dlp brew → archive ✅ have ❌ not in db ❌ brew ❌ missing
P3:low whisper brew → build ❌ need ❌ not in db ❌ brew ❌ missing
P3:low tesseract brew → build ❌ need ❌ not in db ❌ brew ❌ missing
```

View File

@@ -0,0 +1,280 @@
---
document_type: "reference_doc"
service: "MOMENTRY_CORE"
title: "Momentry Core 版本管理規範"
date: "2026-03-23"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
- "momentry"
- "core"
- "版本管理規範"
ai_query_hints:
- "查詢 Momentry Core 版本管理規範 的內容"
- "Momentry Core 版本管理規範 的主要目的是什麼?"
- "如何操作或實施 Momentry Core 版本管理規範?"
---
# Momentry Core 版本管理規範
| 項目 | 內容 |
|------|------|
| 建立者 | Warren |
| 建立時間 | 2026-03-23 |
| 文件版本 | V1.0 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-03-23 | 創建版本管理規範 | Warren | OpenCode |
---
## 1. 版本與通訊埠對照表
| 版本 | Binary | Port | Redis Prefix | 用途 |
|------|--------|------|--------------|------|
| **Production** | `momentry` | **3002** | `momentry:` | 正式環境 |
| **Development** | `momentry_playground` | **3003** | `momentry_dev:` | 開發測試 |
### 通訊埠嚴禁事項
- ❌ 開發版嚴禁使用 3002
- ❌ 任何 `cargo run` 直接啟動的 server 嚴禁綁定 3002
- ❌ Debug build 嚴禁部署到 3002
---
## 2. 開發環境隔離原則
### 2.1 開發流程
```bash
# 永遠在 3003 開發測試
cd /Users/accusys/momentry_core_0.1
# 開發版啟動 (3003)
cargo run --bin momentry_playground -- server
# 或
cargo run --bin momentry -- server --port 3003
```
### 2.2 測試完成後
1. 確認所有功能在 3003 正常運作
2. 進行 `cargo clippy --lib` 檢查
3. 進行 `cargo test --lib` 測試
4. 確認後才能進行 release
### 2.3 環境變數隔離
```bash
# Development
export MOMENTRY_SERVER_PORT=3003
export MOMENTRY_REDIS_PREFIX=momentry_dev:
# Production (launchd 管理,勿手動設定)
# MOMENTRY_SERVER_PORT=3002
# MOMENTRY_REDIS_PREFIX=momentry:
```
---
## 3. Release 版本管理
### 3.1 Release 前檢查清單
```
□ 開發版 (3003) 功能測試完成
□ cargo clippy --lib 通過
□ cargo test --lib 通過
□ cargo fmt -- --check 通過
□ 所有修改已 commit 到 Gitea
```
### 3.2 Release 流程
```bash
# 1. 確保目前是乾淨的工作目錄
git status
# 2. 備份當前 production binary
BACKUP_DIR="/Users/accusys/momentry/backup/bin"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
cp /Users/accusys/momentry/bin/momentry "${BACKUP_DIR}/momentry_${TIMESTAMP}"
# 3. 停止 production server
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
# 或
pkill -f "target/release/momentry server"
# 4. 編譯 release 版本
cargo build --release --bin momentry
# 5. 部署到正式位置
cp target/release/momentry /Users/accusys/momentry/bin/momentry
# 6. 啟動 production server
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
# 7. 驗證
curl http://localhost:3002/health
```
### 3.3 Backup 存放位置
```
/Users/accusys/momentry/backup/bin/
├── momentry_20260325_143000 (backup)
├── momentry_20260324_100000
├── momentry_20260323_090000
└── ...
```
---
## 4. Gitea 版本控制
### 4.1 Commit 規範
```
feat: 新功能
fix: 錯誤修復
refactor: 重構
docs: 文件更新
chore: 杂项
test: 测试
```
### 4.2 Release Tag 規範
```bash
# 建立 release tag
git tag -a v0.1.1 -m "Release v0.1.1 - API Key Authentication"
git push origin v0.1.1
```
### 4.3 版本號命名
```
v{major}.{minor}.{patch}
│ │ └── Patch version (bug fix)
│ └───────── Minor version (新功能)
└──────────────── Major version (破壞性變更)
```
### 4.4 Gitea Release 建立
1. 在 Gitea Repo > Releases > New Release
2. 選擇對應的 Tag
3. 填寫 Release Notes
4. 上傳 compiled binary如需要
---
## 5. 服務管理
### 5.1 Production Service (launchd)
```bash
# Plist 位置
/Library/LaunchDaemons/com.momentry.api.plist
# 管理指令
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist # 啟動
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist # 停止
sudo launchctl list | grep momentry # 狀態
```
### 5.2 緊急回滾
```bash
# 1. 停止當前服務
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
# 2. 恢復上一個 backup
BACKUP_FILE=$(ls -t /Users/accusys/momentry/backup/bin/ | head -1)
cp "/Users/accusys/momentry/backup/bin/${BACKUP_FILE}" /Users/accusys/momentry/bin/momentry
# 3. 重啟服務
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
# 4. 驗證
curl http://localhost:3002/health
```
---
## 6. 快速參考卡片
### Development
```bash
# 啟動開發版
cd /Users/accusys/momentry_core_0.1
cargo run --bin momentry_playground -- server
# 或手動指定 port
cargo run --bin momentry -- server --port 3003
# 測試端點
curl -H "X-API-Key: muser_68600856036340bcafc01930eb4bd839_1774418104_97221b69" \
http://localhost:3003/api/v1/jobs
```
### Production
```bash
# 查看狀態
sudo launchctl list | grep momentry
# 重啟服務
sudo launchctl unload /Library/LaunchDaemons/com.momentry.api.plist
sudo launchctl load /Library/LaunchDaemons/com.momentry.api.plist
# 查看日誌
tail -f /Users/accusys/momentry/log/momentry_release.log
```
### 常用指令
```bash
# 檢查 port 使用
lsof -i :3002 # Production
lsof -i :3003 # Development
# 檢查 process
ps aux | grep momentry | grep server
# 停止所有 momentry server
pkill -9 -f "momentry.*server"
```
---
## 7. 禁止事項
| 項目 | 說明 |
|------|------|
| ❌ 禁止在 3002 測試 | 3002 是 Production嚴禁用於測試 |
| ❌ 禁止覆蓋 production binary | 使用 backup + deploy 流程 |
| ❌ 禁止跳過測試直接 release | 必須完成檢查清單 |
| ❌ 禁止在未備份的情況下部署 | 每次部署前必須備份 |
---
## 8. 疑難排解
### Q: 3002 無法綁定怎麼辦?
```bash
# 檢查誰在使用
lsof -i :3002
# 停止舊的 server
pkill -9 -f "momentry.*server"
```
### Q: 如何確認使用的是哪個版本?
```bash
# 檢查 binary 位置和版本
file $(which momentry)
./target/release/momentry --version 2>/dev/null || echo "No version flag"
```
### Q: 如何確認有沒有 API Key 驗證?
```bash
# 沒有 API Key 應該返回 401
curl -s http://localhost:3002/api/v1/jobs
# HTTP/1.1 401 Unauthorized
```

View File

@@ -0,0 +1,321 @@
---
document_type: "rca"
service: "MARKBASE"
title: "RCAHTML Preview 被 MarkBase 歡迎頁面截圖取代"
date: "2026-05-15"
version: "V1.0"
status: "active"
owner: "M4"
created_by: "OpenCode"
severity: "P2"
tags:
- "rca"
- "markbase"
- "html-preview"
- "iframe-polling"
- "sandbox"
- "screenshot-overwrite"
- "display-engine"
- "cross-origin"
ai_query_hints:
- "查詢 MarkBase HTML preview 截圖 bug 的 RCA"
- "為什麼 HTML iframe preview 被歡迎頁面覆蓋"
- "Display.html polling 如何影響 MarkBase preview"
- "sandbox 屬性如何修復 iframe JavaScript 執行"
- "MarkBase version polling guard 三層檢查"
related_documents:
- "STANDARDS/DOCS_STANDARD.md"
- "REFERENCE/MARKBASE_DESIGN_V2.0.md"
- "~/markbase/src/page.html"
- "~/markbase/src/server.rs"
---
# RCAHTML Preview 被 MarkBase 歡迎頁面截圖取代
| 項目 | 內容 |
|------|------|
| 建立者 | M4 / OpenCode |
| 建立時間 | 2026-05-15 |
| 文件版本 | V1.0 |
| 嚴重等級 | P2中度影響不影響系統穩定性但嚴重影響使用者體驗 |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-05-15 | 初始 RCA 報告 | M4 / OpenCode | DeepSeek V4 Pro |
---
## 1. 事件摘要
### 1.1 問題描述
在 MarkBase 檔案樹中點擊 `.html` 檔案(如 `Display.html`)查看 preview 時detail panel 內的 `<iframe>` 會短暫顯示正確的 HTML 渲染內容,但隨即被 MarkBase 歡迎頁面(含 📺 電視機 icon 和底部 control bar的截圖完全取代。使用者看到的是「整個畫面被截圖取代」的視覺效果。
### 1.2 影響範圍
| 項目 | 說明 |
|------|------|
| 受影響功能 | detail panel + quick preview 中的 `.html` 檔案預覽 |
| 受影響檔案 | `Display.html`demo 目錄中的 MarkBase page template |
| 使用者體驗 | 預覽內容被覆蓋,無法正常查看 HTML 檔案渲染效果 |
| 系統穩定性 | 不受影響 |
### 1.3 時間線
| 時間 | 事件 |
|------|------|
| 2026-05-15 09:00 | 使用者回報 `.html` 檔案預覽只顯示文字,無渲染 |
| 2026-05-15 09:18 | 修改 `isTxt` 排除 `html`,將 `.html` 路由到 `isDocPdf``<iframe>` 渲染) |
| 2026-05-15 09:24 | 修改 `stream_file` 加入 `text/html` MIME type |
| 2026-05-15 09:40 | 使用者回報渲染後馬上被「歡迎頁面截圖」蓋掉 |
| 2026-05-15 09:50 | 初次嘗試:加入 polling guard`_tv` + overlay check |
| 2026-05-15 10:10 | 二次嘗試polling guard 三層保護interval 開頭 + `/version` callback + `/body` callback |
| 2026-05-15 10:20 | 三次嘗試:完全停用 polling排除變因 |
| 2026-05-15 10:30 | 使用者提供關鍵線索:「截圖包含底部 control bar」 |
| 2026-05-15 10:35 | **發現根因**`Display.html` 是完整 MarkBase page內含 `setInterval` polling `/version``/body` |
| 2026-05-15 10:40 | 修復方案:`<iframe sandbox>` + 刪除 `Display.html` 內的 polling scripts |
| 2026-05-15 10:45 | 驗證通過 |
---
## 2. 調查過程
### 2.1 初始假設:父頁面 polling
最初懷疑是父頁面 `page.html` 的 version polling每 500ms 抓 `/version` + `/body`)在 detail panel 打開時替換了 `#mb-content`,導致視覺覆蓋。
**調查步驟**
1. 檢查 polling 機制:`setInterval``fetch("/version")``fetch("/body")``#mb-content.innerHTML = h`
2. 確認 `#mb-overlay``#mb-detail` 位於 `#mb-content` **外部**body-level elements
3. 理論上 `innerHTML` 替換不應影響 sibling elements
4. 但仍加入三層 guardinterval 開頭 + version callback + body callback確保 overlay 打開時跳過 polling
**結果**:問題未解決。使用者回報「還是一樣」。
### 2.2 第二次假設race condition
懷疑 polling 檢查與 fetch callback 之間存在時間差detail panel 在檢查後才開啟。
**調查步驟**
1. 在 version callback 和 body callback 內各加一層 overlay 檢查
2. 確保整個非同步流程中任一時間點 overlay 打開時都跳過
**結果**:問題未解決。
### 2.3 第三次假設:完全停用 polling 排除變因
將父頁面 polling `setInterval` 完全註解掉,讓頁面不再自動刷新。
**結果**:問題仍然存在。確認根因**不在父頁面 polling**。
### 2.4 關鍵突破:使用者提供線索
使用者回報:「螢幕截圖是包含下面的 control bar」。
這個線索非常關鍵:**截圖包含了 `#mb-bar`(底部 control bar**。如果只是 `#mb-content` 被替換,截圖不會包含 `#mb-bar`。這意味著:
- 產生截圖的動作會產生**完整頁面**的內容
- 只有 `render::page()`(產生完整 HTML page template才會包含 `#mb-bar`
- `render::page()` 只在 `POST /display` 或初始化時被呼叫
### 2.5 根因發現
檢查 `Display.html` 檔案內容時發現,該檔案是**完整的 MarkBase page template**,包含:
```html
<!-- Display.html 內含的 polling -->
<script>
var _v=-1;
setInterval(function(){
fetch("/version").then(r=>r.json()).then(d=>{
if(d.v!=_v){_v=d.v;if(_v>=0)fetch("/body").then(r=>r.text()).then(h=>{
var e=document.getElementById("mb-content");
if(e){e.innerHTML=h;mermaid.run()}
})
})
},500)
</script>
```
**攻擊鏈**
```
1. 使用者點擊 Display.html → showDetail → 建立 <iframe src="stream">
2. iframe 載入 Display.html同 origin: localhost:11438
3. Display.html 內的 setInterval 啟動(每 500ms
4. fetch("/version") → 取得 server 目前的 page version
5. fetch("/body") → 取得 server 目前的顯示內容MarkBase 歡迎頁)
6. document.getElementById("mb-content").innerHTML = h
→ iframe 內的 #mb-content 被替換為歡迎頁內容
7. 歡迎頁內容含 📺 icon + 全部 CSS + #mb-bar 結構
8. 使用者看到 iframe 內顯示完整歡迎頁 → 視覺效果 = 「截圖取代」
```
**為什麼截圖包含 `#mb-bar`**`/body` endpoint 的 `body_handler` 函數有截斷 bug它尋找 `<div id=mb-content>` 後的第一個 `</div>`,但歡迎頁內容有巢狀 div。這導致回傳的 HTML fragment 包含部分 `#mb-bar` 之前的內容結構。
---
## 3. 根本原因分析
### 3.1 主要原因
| # | 原因 | 說明 |
|---|------|------|
| 1 | **`Display.html` 是完整 MarkBase page template** | 檔案內含 MarkBase 的 version polling JavaScript會從 server 拉取當前顯示內容並替換自身的 `#mb-content` |
| 2 | **iframe 在同 origin 下執行** | `localhost:11438` 的檔案在 iframe 中載入時,與父頁面共享 originiframe 內的 `fetch()` 直接打到同一個 server |
| 3 | **`body_handler` 回傳 server 當前顯示狀態** | `/body` endpoint 回傳 `state.html``#mb-content` 的內容片段,而 server 當前顯示的是歡迎頁 |
### 3.2 次要原因
| # | 原因 | 說明 |
|---|------|------|
| 4 | **`body_handler` 截斷 bug** | `html[s..].find("</div>")` 只找第一個 `</div>`,巢狀結構時回傳不完整 HTML |
| 5 | **quickPreview 的 iframe 缺少 `sandbox`** | 只有 `showDetail` 加了 `sandbox``quickPreview` 漏掉 |
### 3.3 因果鏈
```
Display.html 被當作一般 HTML 檔案預覽
→ 檔案本身是 MarkBase page template含 polling JS
→ iframe 載入後 JS 開始執行(同 origin無 sandbox 限制)
→ JS 定期從 server 抓取當前顯示內容
→ server 的 state.html 內容是歡迎頁
→ 歡迎頁被注入 iframe 的 #mb-content
→ 使用者看到 iframe 變成歡迎頁截圖
```
---
## 4. 解決方案與實施
### 4.1 修復方案
| 層級 | 修復 | 檔案 | 說明 |
|------|------|------|------|
| **根因** | 刪除 `Display.html` 內 polling JS | `demo/Display.html` | 移除 `setInterval` / `fetch("/version")` / `fetch("/body")``<script>` blocks |
| **防禦層 1** | iframe `sandbox` | `page.html` showDetail + quickPreview | `<iframe sandbox='allow-same-origin'>` 封鎖 JS 執行 |
| **防禦層 2** | 父頁面 polling guard | `page.html` | 三層檢查interval 開頭 + version callback + body callback |
### 4.2 實施細節
**修復 1Display.html**
```bash
# 刪除包含 fetch("/version") 和 fetch("/body") 的 <script> blocks
# 保留無害的 scriptmermaid.initialize, volume, labels
```
**修復 2page.html — showDetail**
```javascript
// Before
h+="<iframe src='"+src+"' style='width:100%;height:400px;...'></iframe>";
// After
h+="<iframe sandbox='allow-same-origin' src='"+src+"' style='...'></iframe>";
```
**修復 3page.html — quickPreview**
```javascript
// Before
inner="<iframe src='"+src+"' style='width:90vw;height:85vh;...'></iframe>";
// After
inner="<iframe sandbox='allow-same-origin' src='"+src+"' style='...'></iframe>";
```
**修復 4page.html — polling guard**
```javascript
setInterval(function(){
if(_tv)return; // guard 1
var ov=document.getElementById("mb-overlay");
if(ov&&ov.style.display==="block")return; // guard 1
fetch("/version").then(function(r){return r.json()}).then(function(d){
if(d.v!=_v){_v=d.v;
var ov2=document.getElementById("mb-overlay");
if(ov2&&ov2.style.display==="block")return; // guard 2
if(_v>=0)fetch("/body").then(function(r){return r.text()}).then(function(h){
var ov3=document.getElementById("mb-overlay");
if(ov3&&ov3.style.display==="block")return; // guard 3
var e=document.getElementById("mb-content");
if(e){e.innerHTML=h;mermaid.run()}
})
}})
},500)
```
---
## 5. 預防措施
### 5.1 短期(已完成)
- [x] 上傳檔案時檢查是否為 MarkBase page template檢測 polling `<script>` 特徵)
- [x] 所有 preview iframe 預設加上 `sandbox='allow-same-origin'`
- [x] 父頁面 polling 加入 guardtree panel 或 detail panel 打開時跳過)
### 5.2 中期(建議)
- [ ] 建立上傳檔案安全掃描機制:檢測 `<script>` blocks若發現 polling patterns 則警告或自動移除
- [ ] 修復 `body_handler` 截斷 bug使用真正的 HTML parser 而非 naive `find("</div>")`
- [ ]`sandbox` 屬性抽取為共用常數,避免 showDetail/quickPreview 不同步
### 5.3 長期(建議)
- [ ] MarkBase page template 與 preview 分離:考慮移除 demo 目錄中的 MarkBase page template 檔案,或標記為 system file
- [ ] 建立 Content Security Policy (CSP) header
- [ ] iframe 內容類型判斷:對於 `text/html` 類型的 stream 回應server 端可選擇性 strip `<script>` tags
---
## 6. 影響評估
| 面向 | 評估 |
|------|------|
| **直接影響** | `.html` 檔案預覽功能無法正常使用 |
| **間接影響** | 使用者對 MarkBase preview 功能失去信心 |
| **資料完整性** | 無影響(未修改任何資料) |
| **安全性** | 無安全風險(僅為同一 origin 內的 UI 干擾) |
| **修復成本** | 低3 行程式碼 + 1 行 CSS + Display.html 清理) |
---
## 7. 經驗教訓
### 7.1 學到的教訓
1. **檔案內容可能包含可執行程式碼**:任何 `.html` 檔案在 iframe 中載入時,其 JavaScript 都會執行。對於預覽用途,必須 sandbox 隔離。
2. **同 origin iframe 的危險**`localhost` 下的所有內容共享同一個 origin。iframe 內的 JS 可以無限制地存取同 origin 的 API endpoints。
3. **Demo 資料可能來自實際系統**`Display.html` 是從 MarkBase page template 複製而來,保留了完整的 polling 機制。
### 7.2 最佳實踐
| 原則 | 說明 |
|------|------|
| **Always Sandbox User Content** | 任何使用者上傳的 HTML 檔案在 iframe 預覽時必須 sandbox |
| **Don't Trust File Contents** | 檔案名稱(`.html`)不能保證內容是靜態的 |
| **Guard at Multiple Layers** | 防禦應該在多個層級檔案內容、iframe 屬性、父頁面邏輯)同時生效 |
| **Key Clues Matter** | 使用者提供的「截圖包含底部 control bar」是找到根因的關鍵線索 |
---
## 8. 相關文件
| 文件 | 說明 |
|------|------|
| `~/markbase/src/page.html` | MarkBase 前端(含 polling guard + sandbox 修復) |
| `~/markbase/src/server.rs` | MarkBase 後端(含 `stream_file` MIME 修復) |
| `~/markbase/src/filetree/convert.rs` | 文件轉換模組Phase 1: textutil + unzip |
| `REFERENCE/MARKBASE_DESIGN_V2.0.md` | MarkBase 設計文件 |
---
## 9. 簽核
| 角色 | 姓名 | 日期 | 簽名 |
|------|------|------|------|
| 調查者 | M4 / OpenCode | 2026-05-15 | ✅ |
| 審核者 | - | - | - |
| 批准者 | - | - | - |

View File

@@ -0,0 +1,372 @@
---
document_type: "standard_doc"
service: "MOMENTRY_CORE"
title: "使用者文件創建規範"
date: "2026-05-17"
version: "V1.0"
status: "active"
owner: "M5"
created_by: "OpenCode"
tags:
- "docs-standard"
- "user-docs"
- "api-reference"
- "demo-guide"
- "troubleshooting"
ai_query_hints:
- "查詢 USER_DOCS_STANDARD.md 的內容"
- "如何撰寫使用者文件"
- "端點文檔模板格式"
- "curl 範例規範"
- "使用者文件檢查清單"
related_documents:
- "STANDARDS/DOCS_STANDARD.md"
- "GUIDES/API_ENDPOINTS.md"
- "GUIDES/Demo_EndToEnd.md"
- "REFERENCE/API_ERROR_CODES.md"
---
# 使用者文件創建規範
| 項目 | 內容 |
|------|------|
| 建立者 | OpenCode |
| 建立時間 | 2026-05-17 |
| 文件版本 | V1.0 |
| 狀態 | Active |
---
## 版本歷史
| 版本 | 日期 | 目的 | 操作人 | 工具/模型 |
|------|------|------|--------|-----------|
| V1.0 | 2026-05-17 | 定義使用者文檔的建立標準與模板 | OpenCode | DeepSeek V4 Flash |
---
## 定位
本文檔是 `DOCS_STANDARD.md` 的**補充**,專門規範**使用者導向文件**User-facing documentation的內容標準。
`DOCS_STANDARD.md` 已定義的規則YAML Frontmatter、檔案命名、格式標準、版本歷史不在此重複本規範僅聚焦於使用者文檔特有的要求。
---
## 1. 文件分類
| document_type | 中文說明 | 適用場景 | 存放目錄 |
|---------------|----------|----------|----------|
| `user_manual` | 使用手冊 | 系統操作、完整功能說明 | `GUIDES/` |
| `quick_start` | 快速入門 | 最小啟動流程、幾分鐘即可完成 | `GUIDES/` |
| `demo_guide` | 示範指南 | 端對端功能示範、step-by-step | `GUIDES/` |
| `api_reference` | API 參考 | 端點列表、請求/回應格式 | `GUIDES/` |
| `troubleshooting` | 疑難排解 | 常見問題與解決方案 | `GUIDES/` |
| `glossary` | 術語表 | 專有名詞定義 | `GUIDES/` |
| `tech_eval` | 技術評估 | 模型選型、方案比較 | `DESIGN/` |
| `design` | 設計文件 | 功能設計、架構設計 | `DESIGN/` |
| `reference_doc` | 參考文件 | 資料模型、規格權威來源 | `REFERENCE/` |
| `operations` | 運維文件 | 基礎設施、發佈、監控 | `OPERATIONS/` |
| `integration` | 整合文件 | 外部服務、n8n 工作流 | `INTEGRATIONS/` |
| `standard_doc` | 規範文件 | 標準、規則、流程 | `STANDARDS/` |
---
## 2. 對象標記Audience Labeling
每份使用者文件開頭的頂部資訊表後,必須標註目標讀者與預備知識:
```markdown
| 項目 | 內容 |
|------|------|
| 建立者 | ... |
| 建立時間 | ... |
| 文件版本 | ... |
| 目標讀者 | system_admin / developer / end_user |
| 預備知識 | none / 需了解 Pipeline / 需有 API Key / 需熟悉 REST API |
```
| 讀者類型 | 說明 | 適用文件 |
|----------|------|----------|
| `system_admin` | 系統管理員,負責安裝、配置、維護 | install guide, operations, troubleshooting |
| `developer` | 開發者/整合者,透過 API 存取系統 | api_reference, demo_guide |
| `end_user` | 終端使用者,透過 Portal 操作 | user_manual, quick_start |
---
## 3. 端點文檔模板
每個 API 端點必須使用以下三段式模板:
### `METHOD /path/to/endpoint`
**Auth**: Required / Optional / Public
**Scope**: file-level / identity-level / system-level
#### Request Parameters
| Field | Type | Required | Default | Description |
|-------|------|----------|---------|-------------|
| `param_name` | string | Yes | - | Parameter description |
#### Example
```bash
curl -s -X POST "$API/path" \
-H "X-API-Key: $KEY" \
-H "Content-Type: application/json" \
-d '{"param": "value"}'
```
#### Response (200)
```json
{
"success": true,
"data": {}
}
```
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always true on 200 |
| `data` | object | Result payload |
#### Error Responses
| Code | HTTP | When |
|------|------|------|
| `E001` | 404 | Resource not found |
| `E003` | 400 | Invalid parameters |
> 錯誤碼對照表請參考 `REFERENCE/API_ERROR_CODES.md`
---
## 4. curl 範例規範
### 4.1 變數使用
```bash
# 每份文件開頭設定
API="http://localhost:3003"
KEY="your-api-key-here"
```
### 4.2 基本範例
```bash
curl -sf "$API/health" | jq '{
status, version
}'
```
### 4.3 POST 範例
```bash
curl -s -X POST "$API/api/v1/resource/tmdb/check" \
-H "X-API-Key: $KEY" \
| jq '.status'
```
### 4.4 規則
| 規則 | 說明 |
|------|------|
| 使用 `$API``$KEY` | 讀者可直接複製貼上,只需改變數值 |
| 包含預期輸出 | `curl` 指令後必須有 ````json` 預期輸出 |
| 使用 `jq` 過濾 | 只顯示關鍵欄位,避免輸出過長 |
| 認證 header 固定顯示 | `-H "X-API-Key: $KEY"` 每次都寫出來 |
---
## 5. Response 展示規範
### 5.1 成功回應
```json
{
"success": true,
"total": 42,
"results": [
{ "file_uuid": "abc...", "name": "Test" }
]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `success` | boolean | Always true on 200 |
| `total` | integer | Total count |
| `results` | array | Result array |
### 5.2 錯誤回應
```json
{
"success": false,
"error": "Resource not found"
}
```
> 錯誤碼請對照 `REFERENCE/API_ERROR_CODES.md`
---
## 6. 黃金範例:端點文檔
### `POST /api/v1/resource/tmdb/check`
**Auth**: Required
**Scope**: system-level
**Description**: Ping TMDb API to verify reachability and measure latency.
#### Request
This endpoint requires no body.
#### 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,
"api_error": null,
"last_check_at": "2026-05-17T12:00:00Z"
}
```
| Field | Type | Description |
|-------|------|-------------|
| `api_key_configured` | boolean | TMDB_API_KEY env var is set |
| `api_reachable` | boolean | TMDb API responded successfully |
| `api_latency_ms` | integer | Response time in milliseconds |
#### Error Responses
| HTTP | When |
|------|------|
| `401` | Missing or invalid API key |
---
### `POST /api/v1/file/:file_uuid/tmdb-probe`
**Auth**: Required
**Scope**: file-level
**Description**: Read local TMDb cache and create/update identities. Requires `tmdb-prefetch` to have been run first.
#### Request
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `file_uuid` | string | Yes (path param) | File UUID of a registered video |
#### Example
```bash
curl -s -X POST "$API/api/v1/file/abc/tmdb-probe" \
-H "X-API-Key: $KEY" | jq '{success, identities_created, movie_title}'
```
#### Response (200 — identities created)
```json
{
"success": true,
"identities_created": 15,
"movie_title": "Charade",
"cast_count": 20
}
```
#### Response (200 — no cache found)
```json
{
"success": false,
"message": "No TMDb cache found. Run tmdb-prefetch first."
}
```
---
### `GET /api/v1/resource/tmdb`
**Auth**: Required
**Scope**: system-level
**Description**: View TMDb resource status including configuration, identity counts, and available cache files.
#### Example
```bash
curl -s "$API/api/v1/resource/tmdb" \
-H "X-API-Key: $KEY" | jq '{identities_seeded, cache_files, operations: [.operations[].path]}'
```
#### Response
```json
{
"success": true,
"status": {
"api_key_configured": true,
"enabled": false,
"api_reachable": null
},
"identities_seeded": 15,
"identities_with_embedding": 12,
"cache_files": 2,
"operations": [
{"method": "GET", "path": "/api/v1/resource/tmdb", "description": "TMDb resource status"}
]
}
```
| Field | Type | Description |
|-------|------|-------------|
| `identities_seeded` | integer | Identities with `source='tmdb'` |
| `identities_with_embedding` | integer | Those with `face_embedding` not null |
| `cache_files` | integer | `*.tmdb.json` files in output dir |
---
## 7. 文件階層圖
```mermaid
USER_MANUAL.md ← 所有使用者入口
├── API_QUICK_REFERENCE.md ← 快速查端點
│ └── API_ENDPOINTS.md ← 每端點詳細 curl + response
│ └── API_ERROR_CODES.md ← 錯誤碼對照
├── Demo_EndToEnd.md ← step-by-step 完整 pipeline demo
├── TMDb_User_Guide.md ← TMDb enrichment 專屬
└── PORTAL_API_DEMO_GUIDE.md ← Portal 操作示範
```
---
## 8. 檢查清單
### 必需P0
- [ ] `document_type` 正確設定(見 §1
- [ ] 頂部資訊表含目標讀者與預備知識§2
- [ ] 有 curl 範例使用 `$API` `$KEY` 變數§4
- [ ] curl 範例後有預期輸出§4.4
- [ ] API 端點使用三段式模板§3
### 建議P1
- [ ] 使用 `jq` 過濾輸出,只顯示關鍵欄位
- [ ] 響應欄位有欄位說明表§5
- [ ] 錯誤回應對照 `API_ERROR_CODES.md`
- [ ] 文件結尾有相關文件交叉連結