From eec2eea880a8e7e2aab72631066abee767954e53 Mon Sep 17 00:00:00 2001 From: Accusys Date: Sun, 17 May 2026 02:26:09 +0800 Subject: [PATCH] docs: file_uuid generation rules for M4 --- docs_v1.0/DESIGN/API_KEY_DESIGN.md | 731 +++++++++ .../DESIGN/ASR_MODEL_SELECTION_REPORT.md | 133 ++ .../DESIGN/ASR_SEGMENTATION_ENHANCEMENT.md | 133 ++ docs_v1.0/DESIGN/DETECTOR_REGISTRY.md | 602 +++++++ docs_v1.0/DESIGN/DETECTOR_SELECTION_SOP.md | 238 +++ .../DESIGN/DOCUMENT_EMBEDDING_STRATEGY.md | 187 +++ docs_v1.0/DESIGN/Face_Pipeline.md | 120 ++ docs_v1.0/DESIGN/GUN_DETECTION_REPORT.md | 45 + docs_v1.0/DESIGN/GUN_DETECTOR_SCAN_REPORT.md | 73 + docs_v1.0/DESIGN/MARKBASE_DESIGN_V2.0.md | 995 +++++++++++ docs_v1.0/DESIGN/MARKBASE_DESIGN_v1.0.0.md | 730 +++++++++ .../MODULE_STANDARDIZATION_SPECIFICATION.md | 647 ++++++++ docs_v1.0/DESIGN/MOMENTRY_RAG_PRESENTATION.md | 353 ++++ docs_v1.0/DESIGN/NON_HUMAN_SOUND_DETECTION.md | 94 ++ .../DESIGN/PROCESSOR_MECHANISMS_REVIEW.md | 134 ++ docs_v1.0/DESIGN/RELEASE_PHASES.md | 240 +++ .../DESIGN/TMDb_Identity_File_System_V1.0.md | 361 ++++ docs_v1.0/DESIGN/TRACE_SEARCH_API_DESIGN.md | 101 ++ docs_v1.0/DESIGN/VIDEO_PROCESSING_SPEC.md | 1453 +++++++++++++++++ docs_v1.0/DESIGN/VIDEO_REGISTRATION.md | 264 +++ docs_v1.0/DESIGN/VISION_AGENT_API.md | 201 +++ .../VISUALIZATION_TOOL_CHOICES_V1.0.0.md | 105 ++ docs_v1.0/DESIGN/VOICE_TECH_CHOICES_V1.0.0.md | 114 ++ docs_v1.0/DESIGN/VOICE_TEST_RESULTS_V1.0.0.md | 36 + .../DESIGN/ZERO_SHOT_DETECTION_RESEARCH.md | 190 +++ docs_v1.0/DESIGN/ZERO_SHOT_GUN_TEST_PLAN.md | 49 + docs_v1.0/DESIGN/ZERO_SHOT_GUN_TEST_REPORT.md | 67 + .../DESIGN/ZERO_SHOT_VS_FINETUNE_SELECTION.md | 115 ++ docs_v1.0/GUIDES/API_ACCESS.md | 230 +++ docs_v1.0/GUIDES/API_ENDPOINTS.md | 321 ++++ docs_v1.0/GUIDES/API_ERROR_CODES.md | 106 ++ docs_v1.0/GUIDES/API_INDEX.md | 129 ++ docs_v1.0/GUIDES/API_QUICK_REFERENCE.md | 532 ++++++ docs_v1.0/GUIDES/API_REFERENCE.md | 310 ++++ docs_v1.0/GUIDES/API_TRAINING_MARCOM.md | 427 +++++ docs_v1.0/GUIDES/DEMO_RUNNER_V1.0.0.md | 159 ++ docs_v1.0/GUIDES/Demo_EndToEnd.md | 864 ++++++++++ docs_v1.0/GUIDES/M5API_Pipeline_Demo.md | 493 ++++++ .../PLAYGROUND_BINARY_IMPLEMENTATION.md | 412 +++++ docs_v1.0/GUIDES/PORTAL_API_DEMO_GUIDE.md | 418 +++++ docs_v1.0/GUIDES/PORTAL_DEVELOPMENT_PLAN.md | 122 ++ docs_v1.0/GUIDES/Pipeline_API_Demo.md | 472 ++++++ docs_v1.0/GUIDES/USER_MANUAL.md | 499 ++++++ .../Momentry_Core_API.postman_collection.json | 127 ++ docs_v1.0/INTEGRATIONS/N8N_API_FIX_SUMMARY.md | 106 ++ .../INTEGRATIONS/N8N_VIDEO_SEARCH_SUCCESS.md | 321 ++++ .../INTEGRATIONS/n8n_workflow_core_v1.2.json | 818 ++++++++++ .../INTEGRATIONS/n8n_workflow_simple.json | 123 ++ .../n8n_workflow_simple_test.json | 89 + .../n8n_workflow_video_rag_mcp.json | 109 ++ .../n8n_workflow_video_search.json | 138 ++ docs_v1.0/INTEGRATIONS/test_all.sh | 100 ++ docs_v1.0/INTEGRATIONS/test_momentry_api.sh | 33 + docs_v1.0/INTEGRATIONS/test_workflow.sh | 104 ++ .../M4_workspace/2026-05-17_file_uuid_rule.md | 69 + .../OPERATIONS/API_3002_VS_3003_COMPARISON.md | 57 + .../OPERATIONS/Brew_To_Source_Migration.md | 139 ++ docs_v1.0/OPERATIONS/DELIVERY_PROCEDURE.md | 371 +++++ docs_v1.0/OPERATIONS/DEVELOPMENT_LOG.md | 559 +++++++ docs_v1.0/OPERATIONS/JSON_OUTPUT_SPEC.md | 538 ++++++ .../M4_M5_COLLABORATION_PROTOCOL.md | 50 + .../M4_RELEASE_INCIDENT_2026-05-09.md | 31 + docs_v1.0/OPERATIONS/M4_VS_M5_COMPARISON.md | 77 + docs_v1.0/OPERATIONS/M5_SETUP_LOG.md | 259 +++ .../OPERATIONS/MOMENTRY_CORE_REDIS_KEYS.md | 303 ++++ docs_v1.0/OPERATIONS/PENDING_ISSUES.md | 830 ++++++++++ .../OPERATIONS/PHASE1_COMPLETION_REPORT.md | 150 ++ .../OPERATIONS/PHASE1_RELEASE_CHECKLIST.md | 63 + docs_v1.0/OPERATIONS/RELEASE_NOTES_v1.0.0.md | 111 ++ docs_v1.0/OPERATIONS/RELEASE_SOP_V2.0.md | 280 ++++ docs_v1.0/OPERATIONS/SERVICES.md | 1092 +++++++++++++ docs_v1.0/OPERATIONS/SFTPGO_DEMO_USER.md | 504 ++++++ docs_v1.0/OPERATIONS/SFTPGo_Lifecycle.md | 235 +++ docs_v1.0/OPERATIONS/SFTPGo_Setup.md | 237 +++ docs_v1.0/OPERATIONS/SFTPGo_Verification.md | 84 + docs_v1.0/OPERATIONS/Services_Inventory.md | 208 +++ docs_v1.0/OPERATIONS/VERSION_MANAGEMENT.md | 280 ++++ ...BASE_HTML_PREVIEW_SCREENSHOT_2026_05_15.md | 321 ++++ docs_v1.0/STANDARDS/USER_DOCS_STANDARD.md | 372 +++++ 79 files changed, 23293 insertions(+) create mode 100644 docs_v1.0/DESIGN/API_KEY_DESIGN.md create mode 100644 docs_v1.0/DESIGN/ASR_MODEL_SELECTION_REPORT.md create mode 100644 docs_v1.0/DESIGN/ASR_SEGMENTATION_ENHANCEMENT.md create mode 100644 docs_v1.0/DESIGN/DETECTOR_REGISTRY.md create mode 100644 docs_v1.0/DESIGN/DETECTOR_SELECTION_SOP.md create mode 100644 docs_v1.0/DESIGN/DOCUMENT_EMBEDDING_STRATEGY.md create mode 100644 docs_v1.0/DESIGN/Face_Pipeline.md create mode 100644 docs_v1.0/DESIGN/GUN_DETECTION_REPORT.md create mode 100644 docs_v1.0/DESIGN/GUN_DETECTOR_SCAN_REPORT.md create mode 100644 docs_v1.0/DESIGN/MARKBASE_DESIGN_V2.0.md create mode 100644 docs_v1.0/DESIGN/MARKBASE_DESIGN_v1.0.0.md create mode 100644 docs_v1.0/DESIGN/MODULE_STANDARDIZATION_SPECIFICATION.md create mode 100644 docs_v1.0/DESIGN/MOMENTRY_RAG_PRESENTATION.md create mode 100644 docs_v1.0/DESIGN/NON_HUMAN_SOUND_DETECTION.md create mode 100644 docs_v1.0/DESIGN/PROCESSOR_MECHANISMS_REVIEW.md create mode 100644 docs_v1.0/DESIGN/RELEASE_PHASES.md create mode 100644 docs_v1.0/DESIGN/TMDb_Identity_File_System_V1.0.md create mode 100644 docs_v1.0/DESIGN/TRACE_SEARCH_API_DESIGN.md create mode 100644 docs_v1.0/DESIGN/VIDEO_PROCESSING_SPEC.md create mode 100644 docs_v1.0/DESIGN/VIDEO_REGISTRATION.md create mode 100644 docs_v1.0/DESIGN/VISION_AGENT_API.md create mode 100644 docs_v1.0/DESIGN/VISUALIZATION_TOOL_CHOICES_V1.0.0.md create mode 100644 docs_v1.0/DESIGN/VOICE_TECH_CHOICES_V1.0.0.md create mode 100644 docs_v1.0/DESIGN/VOICE_TEST_RESULTS_V1.0.0.md create mode 100644 docs_v1.0/DESIGN/ZERO_SHOT_DETECTION_RESEARCH.md create mode 100644 docs_v1.0/DESIGN/ZERO_SHOT_GUN_TEST_PLAN.md create mode 100644 docs_v1.0/DESIGN/ZERO_SHOT_GUN_TEST_REPORT.md create mode 100644 docs_v1.0/DESIGN/ZERO_SHOT_VS_FINETUNE_SELECTION.md create mode 100644 docs_v1.0/GUIDES/API_ACCESS.md create mode 100644 docs_v1.0/GUIDES/API_ENDPOINTS.md create mode 100644 docs_v1.0/GUIDES/API_ERROR_CODES.md create mode 100644 docs_v1.0/GUIDES/API_INDEX.md create mode 100644 docs_v1.0/GUIDES/API_QUICK_REFERENCE.md create mode 100644 docs_v1.0/GUIDES/API_REFERENCE.md create mode 100644 docs_v1.0/GUIDES/API_TRAINING_MARCOM.md create mode 100644 docs_v1.0/GUIDES/DEMO_RUNNER_V1.0.0.md create mode 100644 docs_v1.0/GUIDES/Demo_EndToEnd.md create mode 100644 docs_v1.0/GUIDES/M5API_Pipeline_Demo.md create mode 100644 docs_v1.0/GUIDES/PLAYGROUND_BINARY_IMPLEMENTATION.md create mode 100644 docs_v1.0/GUIDES/PORTAL_API_DEMO_GUIDE.md create mode 100644 docs_v1.0/GUIDES/PORTAL_DEVELOPMENT_PLAN.md create mode 100644 docs_v1.0/GUIDES/Pipeline_API_Demo.md create mode 100644 docs_v1.0/GUIDES/USER_MANUAL.md create mode 100644 docs_v1.0/INTEGRATIONS/Momentry_Core_API.postman_collection.json create mode 100644 docs_v1.0/INTEGRATIONS/N8N_API_FIX_SUMMARY.md create mode 100644 docs_v1.0/INTEGRATIONS/N8N_VIDEO_SEARCH_SUCCESS.md create mode 100644 docs_v1.0/INTEGRATIONS/n8n_workflow_core_v1.2.json create mode 100644 docs_v1.0/INTEGRATIONS/n8n_workflow_simple.json create mode 100644 docs_v1.0/INTEGRATIONS/n8n_workflow_simple_test.json create mode 100644 docs_v1.0/INTEGRATIONS/n8n_workflow_video_rag_mcp.json create mode 100644 docs_v1.0/INTEGRATIONS/n8n_workflow_video_search.json create mode 100755 docs_v1.0/INTEGRATIONS/test_all.sh create mode 100755 docs_v1.0/INTEGRATIONS/test_momentry_api.sh create mode 100755 docs_v1.0/INTEGRATIONS/test_workflow.sh create mode 100644 docs_v1.0/M4_workspace/2026-05-17_file_uuid_rule.md create mode 100644 docs_v1.0/OPERATIONS/API_3002_VS_3003_COMPARISON.md create mode 100644 docs_v1.0/OPERATIONS/Brew_To_Source_Migration.md create mode 100644 docs_v1.0/OPERATIONS/DELIVERY_PROCEDURE.md create mode 100644 docs_v1.0/OPERATIONS/DEVELOPMENT_LOG.md create mode 100644 docs_v1.0/OPERATIONS/JSON_OUTPUT_SPEC.md create mode 100644 docs_v1.0/OPERATIONS/M4_M5_COLLABORATION_PROTOCOL.md create mode 100644 docs_v1.0/OPERATIONS/M4_RELEASE_INCIDENT_2026-05-09.md create mode 100644 docs_v1.0/OPERATIONS/M4_VS_M5_COMPARISON.md create mode 100644 docs_v1.0/OPERATIONS/M5_SETUP_LOG.md create mode 100644 docs_v1.0/OPERATIONS/MOMENTRY_CORE_REDIS_KEYS.md create mode 100644 docs_v1.0/OPERATIONS/PENDING_ISSUES.md create mode 100644 docs_v1.0/OPERATIONS/PHASE1_COMPLETION_REPORT.md create mode 100644 docs_v1.0/OPERATIONS/PHASE1_RELEASE_CHECKLIST.md create mode 100644 docs_v1.0/OPERATIONS/RELEASE_NOTES_v1.0.0.md create mode 100644 docs_v1.0/OPERATIONS/RELEASE_SOP_V2.0.md create mode 100644 docs_v1.0/OPERATIONS/SERVICES.md create mode 100644 docs_v1.0/OPERATIONS/SFTPGO_DEMO_USER.md create mode 100644 docs_v1.0/OPERATIONS/SFTPGo_Lifecycle.md create mode 100644 docs_v1.0/OPERATIONS/SFTPGo_Setup.md create mode 100644 docs_v1.0/OPERATIONS/SFTPGo_Verification.md create mode 100644 docs_v1.0/OPERATIONS/Services_Inventory.md create mode 100644 docs_v1.0/OPERATIONS/VERSION_MANAGEMENT.md create mode 100644 docs_v1.0/OPERATIONS/maintenance_records/rca/_completed/RCA_MARKBASE_HTML_PREVIEW_SCREENSHOT_2026_05_15.md create mode 100644 docs_v1.0/STANDARDS/USER_DOCS_STANDARD.md diff --git a/docs_v1.0/DESIGN/API_KEY_DESIGN.md b/docs_v1.0/DESIGN/API_KEY_DESIGN.md new file mode 100644 index 0000000..9a6a393 --- /dev/null +++ b/docs_v1.0/DESIGN/API_KEY_DESIGN.md @@ -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 +momentry api-key revoke --reason "安全原因" + +# 輪換管理 +momentry api-key rotate # 正常輪換 +momentry api-key force-rotate # 強制輪換 +momentry api-key rotation-status # 查看輪換狀態 + +# 異常管理 +momentry api-key suspend --reason "異常使用" +momentry api-key unsuspend +momentry api-key blacklist # 列入黑名單 + +# 審計 +momentry api-key audit --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 \ + --password \ + --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 \ + --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 \ + --password \ + --token-name +``` + +#### 查詢本地記錄 + +```bash +# 查詢已納管的 Token 記錄 +momentry gitea verify --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: " https://n8n.example.com/api/v1/workflows +``` + +### 12.5 CLI 命令 + +#### 建立 API Key + +```bash +# 基本用法 +momentry n8n create \ + --api-key \ + --label \ + --expires-in-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 +``` + +**輸出範例:** +``` +📋 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 \ + --label +``` + +#### 查詢本地記錄 + +```bash +# 查詢已納管的 API Key 記錄 +momentry n8n verify --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/) diff --git a/docs_v1.0/DESIGN/ASR_MODEL_SELECTION_REPORT.md b/docs_v1.0/DESIGN/ASR_MODEL_SELECTION_REPORT.md new file mode 100644 index 0000000..f1f4360 --- /dev/null +++ b/docs_v1.0/DESIGN/ASR_MODEL_SELECTION_REPORT.md @@ -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:40–28:40 | 3 min | Fast back-and-forth dialogue, Cary & Audrey | +| B — Normal | 10:00–13:00 | 3 min | Normal conversation pace | +| C — Complex | 73:20–76: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** — 55–59 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 | 47–64 segs/clip, clean sentence boundaries | +| Speed | 14–22s per 3-min clip (real-time 0.1×) | +| Stability | Never hangs, consistent across all scenes | +| Text capture | 90–98% 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. diff --git a/docs_v1.0/DESIGN/ASR_SEGMENTATION_ENHANCEMENT.md b/docs_v1.0/DESIGN/ASR_SEGMENTATION_ENHANCEMENT.md new file mode 100644 index 0000000..21be0a8 --- /dev/null +++ b/docs_v1.0/DESIGN/ASR_SEGMENTATION_ENHANCEMENT.md @@ -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 diff --git a/docs_v1.0/DESIGN/DETECTOR_REGISTRY.md b/docs_v1.0/DESIGN/DETECTOR_REGISTRY.md new file mode 100644 index 0000000..9716d2f --- /dev/null +++ b/docs_v1.0/DESIGN/DETECTOR_REGISTRY.md @@ -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 check(bbox/keypoints 疊加原圖)。 +4. **跨 detector 比對**:同一 frame 的不同 detector 輸出 bbox,IoU 應合理(非零且非 1.0)。 +5. **Vision detector 鐵律**:任何使用 Apple Vision Framework 的 detector,必須確認 Y-flip 已實作。 + +--- + +## 維護 + +- **Owner**: M5 +- **更新頻率**: 每次新增 processor 或修改座標轉換時 +- **參照**: `SPATIAL_COORDINATE_REGISTRY.md`(上層座標系統) diff --git a/docs_v1.0/DESIGN/DETECTOR_SELECTION_SOP.md b/docs_v1.0/DESIGN/DETECTOR_SELECTION_SOP.md new file mode 100644 index 0000000..01dbf92 --- /dev/null +++ b/docs_v1.0/DESIGN/DETECTOR_SELECTION_SOP.md @@ -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 bbox,IoU 應合理(置信度加權) | — | +| 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/A(LLM) | ❌ 無選型文件 | +| 11 | TKG Builder | active ✅ | N/A(graph) | — | +| 12 | TMDB Matcher | active ✅ | N/A(cosine) | — | +| 13 | Identity Agent | active ✅ | N/A(clustering) | — | +| 14 | Embedding (llama.cpp) | active ✅ | N/A(vector) | ✅ | + +--- + +## 維護 + +- **Owner**: M5 +- **更新頻率**: 每次新增 detector 時 +- **稽核**: 每季度檢視一次所有 active detector 是否仍符合品質標準 diff --git a/docs_v1.0/DESIGN/DOCUMENT_EMBEDDING_STRATEGY.md b/docs_v1.0/DESIGN/DOCUMENT_EMBEDDING_STRATEGY.md new file mode 100644 index 0000000..00d3514 --- /dev/null +++ b/docs_v1.0/DESIGN/DOCUMENT_EMBEDDING_STRATEGY.md @@ -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 diff --git a/docs_v1.0/DESIGN/Face_Pipeline.md b/docs_v1.0/DESIGN/Face_Pipeline.md new file mode 100644 index 0000000..7da9abc --- /dev/null +++ b/docs_v1.0/DESIGN/Face_Pipeline.md @@ -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 +``` diff --git a/docs_v1.0/DESIGN/GUN_DETECTION_REPORT.md b/docs_v1.0/DESIGN/GUN_DETECTION_REPORT.md new file mode 100644 index 0000000..74887b8 --- /dev/null +++ b/docs_v1.0/DESIGN/GUN_DETECTION_REPORT.md @@ -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.16–0.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 / 6479s),confidence 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` — 整合搜尋 agent(gun detector 偵測結果僅供參考) diff --git a/docs_v1.0/DESIGN/GUN_DETECTOR_SCAN_REPORT.md b/docs_v1.0/DESIGN/GUN_DETECTOR_SCAN_REPORT.md new file mode 100644 index 0000000..757eb26 --- /dev/null +++ b/docs_v1.0/DESIGN/GUN_DETECTOR_SCAN_REPORT.md @@ -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) | diff --git a/docs_v1.0/DESIGN/MARKBASE_DESIGN_V2.0.md b/docs_v1.0/DESIGN/MARKBASE_DESIGN_V2.0.md new file mode 100644 index 0000000..1a8cd07 --- /dev/null +++ b/docs_v1.0/DESIGN/MARKBASE_DESIGN_V2.0.md @@ -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; + + /// 此模式支援的排序方式 + fn sort_options(&self) -> Vec; + + /// 此模式支援的過濾器 + fn filter_options(&self) -> Vec; +} +``` + +### 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 server(port 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 | SHA256(file_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) -> Result> { + 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.0(CC0/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) | + +不使用外部 CLI(ditto、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 mode(SHA256 驗證,非 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 { + /// 節點唯一 ID(UUIDv4) + pub node_id: String, + + /// 顯示名稱 + pub label: String, + + /// 多語言別名 + pub aliases: Aliases, + + /// 關聯的 file_uuid(Momentry Core 來源) + pub file_uuid: Option, + + /// 父節點 node_id(root 為 None) + pub parent_id: Option, + + /// 子節點列表 + pub children: Vec, + + /// 節點類型 + pub node_type: NodeType, + + /// 自訂圖示(emoji 或 SVG 路徑) + pub icon: Option, + + /// 文字顏色(CSS hex) + pub color: Option, + + /// 背景顏色(CSS hex) + pub bg_color: Option, + + /// 建立時間 + pub created_at: String, + + /// 最後修改時間 + pub updated_at: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Aliases { + /// 繁體中文 + pub zh_tw: Option, + /// 英文 + pub en_us: Option, + /// 日文 + pub ja_jp: Option, + /// 韓文 + pub ko_kr: Option, + /// 法文 + pub fr_fr: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum NodeType { + /// 虛擬資料夾(用戶建立,不對應實體路徑) + Folder, + /// 實體檔案(指向 file_uuid) + File, + /// 動態層級(auto-layer 產生) + DynamicLayer, +} +``` + +### 4.2 SQLite Schema(user.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`、`10–100MB`、`100MB–1GB`、`>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> { + 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** | 🌡️ | 10–500ms | 中等 | 中 | 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 Engine(rsync / Direct / S3 / SFS / CDN) + ↑ +AI Agent(監控 tier 用量、偵測冷熱模式、自動歸檔、預取) + ↑ +Storage Tiers(M2 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 | diff --git a/docs_v1.0/DESIGN/MARKBASE_DESIGN_v1.0.0.md b/docs_v1.0/DESIGN/MARKBASE_DESIGN_v1.0.0.md new file mode 100644 index 0000000..dce2f4f --- /dev/null +++ b/docs_v1.0/DESIGN/MARKBASE_DESIGN_v1.0.0.md @@ -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 native(Rust + axum + Tauri WebView)| +| 授權 | Momentry 專屬工具,隨 momentry_core 發布 | + +--- + +## 命名 + +**MarkBase** — Markdown + Display Base + +> 承載所有內容類型的顯示基底。 +> 簡短、好記、產品感。 + +--- + +## 階段規劃 + +### Phase 0:Demo Display(MVP — 立即價值) + +**目標**:取代 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 2:Knowledge Base + +**目標**:從閱讀器升級為個人知識庫管理器 + +| 功能 | 說明 | +|------|------| +| 多文件索引 | 監控目錄,自動索引所有 .md | +| 全文檢索 | 跨文件模糊搜尋 + 標題索引 | +| 標籤管理 | YAML frontmatter tags → 標籤雲 | +| Backlinks | 文件間的雙向連結([[wiki-link]])| +| 收藏/書籤 | 標記常用文件 | +| 閱讀歷史 | 最近開啟 / 最近搜尋 | + +### Phase 3:Collaboration + +**目標**:多人協作與發布 + +| 功能 | 說明 | +|------|------| +| 評論/註釋 | 段落層級註解 | +| 版本歷史 | 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 控制 API(display 模式下啟用) + +`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 [-o output.html] Markdown → HTML +markbase serve 檔案瀏覽 + 編輯器 (計畫中) +``` + +### 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-hop:filter speaker = "Cary Grant" +{"filter": {"must": [{"key": "speaker", "match": {"value": "Cary Grant"}}]}} + +// ❌ 2-hop:graph 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 | 評論、版本、靜態站點 | diff --git a/docs_v1.0/DESIGN/MODULE_STANDARDIZATION_SPECIFICATION.md b/docs_v1.0/DESIGN/MODULE_STANDARDIZATION_SPECIFICATION.md new file mode 100644 index 0000000..960f69a --- /dev/null +++ b/docs_v1.0/DESIGN/MODULE_STANDARDIZATION_SPECIFICATION.md @@ -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, + pub metadata: Option, + + // 模組特定字段 + // ... +} + +// 數據單元結構 +#[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 { + // 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 = Lazy::new(|| { + env::var("MOMENTRY_MODULE_TIMEOUT") + .unwrap_or_else(|_| "3600".to_string()) + .parse() + .unwrap_or(3600) + }); + + pub static MODULE_CHUNK_SIZE: Lazy = 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 { + // 使用 .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, + 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)* +*狀態: 草案* diff --git a/docs_v1.0/DESIGN/MOMENTRY_RAG_PRESENTATION.md b/docs_v1.0/DESIGN/MOMENTRY_RAG_PRESENTATION.md new file mode 100644 index 0000000..b0945db --- /dev/null +++ b/docs_v1.0/DESIGN/MOMENTRY_RAG_PRESENTATION.md @@ -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 +``` diff --git a/docs_v1.0/DESIGN/NON_HUMAN_SOUND_DETECTION.md b/docs_v1.0/DESIGN/NON_HUMAN_SOUND_DETECTION.md new file mode 100644 index 0000000..6276bbd --- /dev/null +++ b/docs_v1.0/DESIGN/NON_HUMAN_SOUND_DETECTION.md @@ -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 diff --git a/docs_v1.0/DESIGN/PROCESSOR_MECHANISMS_REVIEW.md b/docs_v1.0/DESIGN/PROCESSOR_MECHANISMS_REVIEW.md new file mode 100644 index 0000000..af4d7c0 --- /dev/null +++ b/docs_v1.0/DESIGN/PROCESSOR_MECHANISMS_REVIEW.md @@ -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 | 依賴 ASR,ASR 不完整 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::` + - 若 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 兩段式關閉** diff --git a/docs_v1.0/DESIGN/RELEASE_PHASES.md b/docs_v1.0/DESIGN/RELEASE_PHASES.md new file mode 100644 index 0000000..a62aa18 --- /dev/null +++ b/docs_v1.0/DESIGN/RELEASE_PHASES.md @@ -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** = 每部影片的產出 package(output_json + chunks + vectors) +- **Engine** = momentry core,吃 model 提供 API(search, trace, identity) + +每個影片可有多個 model 版本,命名保留升級空間: + +| Model 版本 | Qdrant Collection | 內容 | 觸發時機 | +|-----------|------------------|------|---------| +| `{uuid}_v1` | `momentry_dev_v1` | sentence chunk embedding(base) | 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 1:Sentence Chunk Embedding(base model) + +**觸發時機**: ASR + ASRX 完成 + Rule 1 Ingestion + vectorize 完成 + +**交付內容**: +- `{uuid}.asr.json` +- `{uuid}.asrx.json` +- chunks(chunk_type = 'sentence') +- chunk_vectors(sentence embedding) + +**用途**: 終端使用者可進行語意搜尋 + +### Phase 2:完整 Pipeline(v2 model) + +**觸發時機**: 全部 processor 完成 + Rule 3 Ingestion + 5W1H Agent + +**交付內容**: +- Phase 1 全部內容 +- 所有 `{uuid}.*.json`(cut, yolo, face, pose, ocr, ...) +- chunks(chunk_type = 'cut', 'visual', 'trace', 'story') +- chunk_vectors(summary 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 DB(ephemeral) | model package(permanent) | +| 修正方式 | 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 3:Object Identity(v3 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(高難度,需資料或新模型) diff --git a/docs_v1.0/DESIGN/TMDb_Identity_File_System_V1.0.md b/docs_v1.0/DESIGN/TMDb_Identity_File_System_V1.0.md new file mode 100644 index 0000000..c85c28e --- /dev/null +++ b/docs_v1.0/DESIGN/TMDb_Identity_File_System_V1.0.md @@ -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 store,filesystem 是可攜離線副本 + +--- + +## 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 +write_identity_file(file: &IdentityFile) -> Result<()> +list_identity_uuids() -> Result> +count_identity_files() -> usize + +// index +read_index() -> Result> +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 { + 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 { + // 原本 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 +``` diff --git a/docs_v1.0/DESIGN/TRACE_SEARCH_API_DESIGN.md b/docs_v1.0/DESIGN/TRACE_SEARCH_API_DESIGN.md new file mode 100644 index 0000000..6d1db97 --- /dev/null +++ b/docs_v1.0/DESIGN/TRACE_SEARCH_API_DESIGN.md @@ -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) | diff --git a/docs_v1.0/DESIGN/VIDEO_PROCESSING_SPEC.md b/docs_v1.0/DESIGN/VIDEO_PROCESSING_SPEC.md new file mode 100644 index 0000000..3ad8a03 --- /dev/null +++ b/docs_v1.0/DESIGN/VIDEO_PROCESSING_SPEC.md @@ -0,0 +1,1453 @@ +--- +document_type: "reference_doc" +service: "MOMENTRY_CORE" +title: "Video 解析行為規範" +date: "2026-04-27" +version: "V1.1" +status: "active" +owner: "Warren" +created_by: "OpenCode" +tags: + - "解析行為規範" + - "video" + - "processing_status" +ai_query_hints: + - "查詢 Video 解析行為規範 的內容" + - "Video 解析行為規範 的主要目的是什麼?" + - "如何操作或實施 Video 解析行為規範?" + - "processing_status 字段的 SQL 映射" +--- + +# Video 解析行為規範 + +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-03-16 | +| 文件版本 | V1.1 | + +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-16 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | +| V1.1 | 2026-04-27 | 添加 processing_status 字段 SQL 映射說明 | OpenCode | GLM-5 | + +--- + +本文檔定義 Momentry Core 系統中影片解析的完整行為規範,涵蓋觸發、狀態、輸出、斷點續傳、多語言處理及各種識別標示。 + +--- + +## 1. Video 檔案觸發規範 + +### 1.1 支援的影片格式 + +| 格式 | 副檔名 | 說明 | +|------|--------|------| +| MP4 | .mp4 | 最廣泛支援 | +| MOV | .mov | QuickTime 格式 | +| AVI | .avi | 傳統格式 | +| MKV | .mkv | Matroska 格式 | +| WebM | .webm | Web 格式 | +| WMV | .wmv | Windows Media | +| FLV | .flv | Flash 格式 | + +### 1.2 觸發方式 + +#### 1.2.1 指令列註冊 + +```bash +cargo run -- register /path/to/video.mp4 +``` + +#### 1.2.2 監控目錄自動觸發 + +```yaml +# monitor_config.yaml +watch: + directories: + - /path/to/watch + recursive: true + extensions: [".mp4", ".mov", ".avi", ".mkv", ".webm"] +``` + +#### 1.2.3 API 觸發 + +```bash +# POST /api/v1/register +curl -X POST http://localhost:3002/api/v1/register \ + -H "Content-Type: application/json" \ + -d '{"path": "/path/to/video.mp4", "auto_process": true}' +``` + +### 1.3 觸發前驗證 + +```rust +pub fn validate_video_file(path: &str) -> Result { + // 1. 檢查檔案存在 + // 2. 檢查副檔名 + // 3. 檢查檔案大小 > 0 + // 4. 檢查是否為有效的影片檔案 (魔數 Magic Number) + + // 回傳結構 + Ok(VideoValidation { + path: path.to_string(), + valid: true, + codec: "h264".to_string(), + has_video: true, + has_audio: true, + }) +} +``` + +### 1.4 影片UUID 生成 + +``` +UUID = MD5(檔案路徑)[0:16] + +範例: +"/media/videos/clip.mp4" → "3a2f1b9c4d5e6f0a" +``` + +--- + +## 2. Video 處理過程狀態顯示規範 + +### 2.1 處理狀態定義 + +```rust +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ProcessStatus { + Pending, // 等待處理 + Registered, // 已註冊 + Probing, // 探測中 + AsrProcessing, // ASR 處理中 + AsrxProcessing, // 說話者分離中 + OcrProcessing, // OCR 處理中 + YoloProcessing, // YOLO 處理中 + FaceProcessing, // 人臉偵測中 + PoseProcessing, // 姿態估計中 + Chunking, // 分塊處理中 + Completed, // 完成 + Failed, // 失敗 + Paused, // 暫停 + Resuming, // 恢復中 +} +``` + +#### 2.1.1 SQL 映射說明 + +ProcessStatus enum 映射到 PostgreSQL `videos` 表的 `processing_status` 字段: + +| Rust Enum | SQL 值 | 說明 | +|-----------|--------|------| +| `Pending` | `'PENDING'` | 等待處理(觸發後狀態) | +| `Registered` | `'REGISTERED'` | 已註冊(註冊後狀態) | +| `Probing` | `'PROBING'` | 探測中(ffprobe 分析) | +| `AsrProcessing` | `'ASR'` | ASR 處理中 | +| `AsrxProcessing` | `'ASRX'` | 說話者分離中 | +| `OcrProcessing` | `'OCR'` | OCR 處理中 | +| `YoloProcessing` | `'YOLO'` | YOLO 物體檢測中 | +| `FaceProcessing` | `'FACE'` | 人臉偵測中 | +| `PoseProcessing` | `'POSE'` | 姿態估計中 | +| `Chunking` | `'CUT'` | 分塊處理中 | +| `Completed` | `'COMPLETED'` | 完成 | +| `Failed` | `'FAILED'` | 失敗 | +| `Paused` | `'PAUSED'` | 暫停 | +| `Resuming` | `'RESUMING'` | 恢復中 | + +#### 2.1.2 SQL 約束 + +```sql +ALTER TABLE videos +ADD CONSTRAINT videos_processing_status_check +CHECK ( + processing_status IS NULL OR + processing_status IN ('REGISTERED', 'PENDING', 'PROBING', 'ASR', 'OCR', 'YOLO', 'FACE', 'POSE', 'CUT', 'ASRX', 'COMPLETED', 'FAILED', 'PAUSED', 'RESUMING') +); +``` + +#### 2.1.3 與 status 字段的關係 + +`processing_status` 字段與 `status` 字段協同工作: + +| status | processing_status | 說明 | +|--------|-------------------|------| +| `pending` | `REGISTERED` | 新註冊的視頻,尚未觸發處理 | +| `processing` | `PENDING` | 已觸發處理,等待作業分配 | +| `processing` | `PROBING` | ffprobe 分析中 | +| `processing` | `ASR`/`OCR`/`YOLO`... | 各處理器作業執行中 | +| `completed` | `COMPLETED` | 所有處理完成 | +| `failed` | `FAILED` | 處理失敗 | + +Portal 顯示優先使用 `processing_status`(詳細狀態),Fallback 使用 `status`(基本狀態)。 + +#### 2.1.4 processing_status JSONB 映射說明(V1.2 起) + +從 V1.2 起,`processing_status` 改為 **JSONB** 格式,詳見 `REFERENCE/PROCESSING_STATUS_JSONB_SPEC.md`。 + +##### JSONB 字段映射 + +| PostgreSQL 字段 | JSONB 路徑 | 說明 | +|-----------------|-----------|------| +| `phase` | `processing_status->>'phase'` | 當前階段(對應舊版 VARCHAR) | +| `active_processors` | `processing_status->'active_processors'` | 正在執行的處理器 | +| `pre_chunks_count` | `processing_status->'pre_chunks_summary'->>'total_records'` | pre_chunks 總數 | +| `chunks_count` | `processing_status->'chunks_summary'->>'total_chunks'` | chunks 總數 | +| `agent_status` | `processing_status->'agents'->'5w1h'->>'status'` | Agent 狀態 | + +##### SQL 查詢範例 + +```sql +-- 取得處理狀態 +SELECT + uuid, + processing_status->>'phase' as phase, + processing_status->'active_processors' as active, + processing_status->'pre_chunks_summary'->>'total_records' as pre_chunks_count, + processing_status->'chunks_summary'->>'total_chunks' as chunks_count +FROM videos WHERE uuid = '384b0ff44aaaa1f14cb2cd63b3fea966'; + +-- 更新處理器進度 +UPDATE videos +SET processing_status = jsonb_set( + processing_status, + '{progress,ASR}', + '{"current_frame": 500, "percentage": 12}'::jsonb +) +WHERE uuid = '384b0ff44aaaa1f14cb2cd63b3fea966'; +``` + +--- + +### 2.2 狀態輸出格式 + +#### 2.2.1 標準輸出 (stdout) + +``` +[REGISTER] Video registered: 1636719dc31f78ac +[PROBE] Starting probe for video.mp4 +[PROBE] Duration: 120.5s, FPS: 24/1, Resolution: 1920x1080 +[ASR_START] Loading Whisper model... +[ASR_LANGUAGE:en] Language detected: English (99.45%) +[ASR_PROGRESS:50] Processed 50 segments... +[ASR_PROGRESS:100] Processed 100 segments... +[ASR_COMPLETE:150] Completed! Total: 150 segments +[ASRX_START] Loading pyannote model... +[ASRX_PROGRESS] Speaker diarization: 3 speakers identified +[ASRX_COMPLETE] Speaker diarization complete +[OCR_START] Starting OCR processing... +[OCR_PROGRESS:30/60] Frame 30/60 processed +[OCR_COMPLETE] OCR complete: 25 text regions found +[YOLO_START] Starting YOLO processing... +[YOLO_PROGRESS:60/120] Frame 60/120 processed +[YOLO_COMPLETE] YOLO complete: 189 objects detected +[FACE_START] Starting face detection... +[FACE_PROGRESS:60/120] Frame 60/120 processed +[FACE_COMPLETE] Face detection complete: 5 unique faces +[POSE_START] Starting pose estimation... +[POSE_PROGRESS:60/120] Frame 60/120 processed +[POSE_COMPLETE] Pose estimation complete: 12 persons detected +[CHUNK_START] Creating chunks... +[CHUNK_COMPLETE] 450 chunks created +[COMPLETE] Video processing complete! +``` + +#### 2.2.2 狀態訊息前綴 + +| 處理階段 | 前綴 | 範例 | +|----------|------|------| +| 註冊 | `[REGISTER]` | `[REGISTER] Video registered: 1636719dc31f78ac` | +| 探測 | `[PROBE]` | `[PROBE] Duration: 120.5s` | +| ASR | `[ASR_*]` | `[ASR_START]`, `[ASR_PROGRESS:50]` | +| ASRx | `[ASRX_*]` | `[ASRX_START]`, `[ASRX_COMPLETE]` | +| OCR | `[OCR_*]` | `[OCR_START]`, `[OCR_PROGRESS:30/60]` | +| YOLO | `[YOLO_*]` | `[YOLO_START]`, `[YOLO_COMPLETE]` | +| Face | `[FACE_*]` | `[FACE_START]`, `[FACE_PROGRESS:60/120]` | +| Pose | `[POSE_*]` | `[POSE_START]`, `[POSE_COMPLETE]` | +| Chunk | `[CHUNK_*]` | `[CHUNK_START]`, `[CHUNK_COMPLETE]` | +| 完成 | `[COMPLETE]` | `[COMPLETE] Video processing complete!` | +| 錯誤 | `[ERROR]` | `[ERROR] ASR processing failed` | +| 警告 | `[WARN]` | `[WARN] No audio track detected` | + +### 2.3 即時進度報告 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProcessProgress { + pub uuid: String, + pub status: ProcessStatus, + pub current_processor: String, + pub total_frames: i64, + pub processed_frames: i64, + pub progress_percentage: f64, + pub elapsed_seconds: f64, + pub estimated_remaining_seconds: f64, + pub last_checkpoint: Option, +} +``` + +#### 範例輸出 + +```json +{ + "uuid": "1636719dc31f78ac", + "status": "asr_processing", + "current_processor": "asr", + "total_frames": 3000, + "processed_frames": 1500, + "progress_percentage": 50.0, + "elapsed_seconds": 120.5, + "estimated_remaining_seconds": 120.5, + "last_checkpoint": { + "timestamp": 60.0, + "segments_processed": 50, + "output_file": "1636719dc31f78ac.asr.partial.json" + } +} +``` + +--- + +## 3. Video 處理輸出規範 + +### 3.1 輸出檔案命名 + +``` +{UUID}.{處理類型}.json + +範例: +1636719dc31f78ac.probe.json # 探測結果 +1636719dc31f78ac.asr.json # ASR 結果 +1636719dc31f78ac.asrx.json # 說話者分離結果 +1636719dc31f78ac.ocr.json # OCR 結果 +1636719dc31f78ac.yolo.json # YOLO 結果 +1636719dc31f78ac.face.json # 人臉偵測結果 +1636719dc31f78ac.pose.json # 姿態估計結果 +1636719dc31f78ac.chunks.json # 分塊結果 +``` + +### 3.2 輸出目錄結構 + +``` +momentry_core/ +├── output/ +│ ├── {uuid}/ +│ │ ├── {uuid}.probe.json +│ │ ├── {uuid}.asr.json +│ │ ├── {uuid}.asrx.json +│ │ ├── {uuid}.ocr.json +│ │ ├── {uuid}.yolo.json +│ │ ├── {uuid}.face.json +│ │ ├── {uuid}.pose.json +│ │ └── thumbnails/ +│ │ ├── thumb_000.jpg +│ │ ├── thumb_001.jpg +│ │ └── ... +│ └── checkpoints/ +│ └── {uuid}/ +│ ├── {uuid}.asr.partial.001.json +│ ├── {uuid}.asr.partial.002.json +│ └── ... +``` + +### 3.3 完整處理結果 JSON 結構 + +```json +{ + "uuid": "1636719dc31f78ac", + "video_path": "/path/to/video.mp4", + "video_info": { + "duration": 120.5, + "fps": "24/1", + "fps_value": 24.0, + "width": 1920, + "height": 1080, + "has_video": true, + "has_audio": true, + "has_music": false, + "audio_codec": "aac", + "video_codec": "h264" + }, + "processing": { + "status": "completed", + "started_at": "2026-03-16T10:00:00Z", + "completed_at": "2026-03-16T10:05:00Z", + "elapsed_seconds": 300.0, + "processors": { + "asr": { + "status": "completed", + "language": "en", + "language_probability": 0.9945, + "segments_count": 150, + "duration_seconds": 120.0 + }, + "asrx": { + "status": "completed", + "speakers_count": 3, + "segments_count": 150, + "duration_seconds": 60.0 + }, + "ocr": { + "status": "completed", + "text_regions_count": 25, + "duration_seconds": 45.0 + }, + "yolo": { + "status": "completed", + "objects_count": 189, + "unique_classes": ["person", "car", "dog"], + "duration_seconds": 30.0 + }, + "face": { + "status": "completed", + "unique_faces_count": 5, + "duration_seconds": 30.0 + }, + "pose": { + "status": "completed", + "persons_count": 12, + "duration_seconds": 15.0 + } + } + }, + "asr": { + "language": "en", + "language_probability": 0.9945855736732483, + "segments": [...] + }, + "asrx": { + "language": "en", + "segments": [...] + }, + "ocr": { + "segments": [...] + }, + "yolo": { + "segments": [...] + }, + "face": { + "segments": [...] + }, + "pose": { + "segments": [...] + } +} +``` + +--- + +## 4. Video 處理中分時輸出規範 (Checkpoint) + +### 4.1 分時輸出目的 + +- 避免處理異常中斷導致資料全部遺失 +- 提供中斷點,方便後續接續處理 +- 可設定輸出頻率(每 N 秒或每 N 幀) + +### 4.2 配置參數 + +```yaml +processing: + checkpoint: + enabled: true + interval_seconds: 60 # 每 60 秒輸出一次 + interval_frames: 1500 # 或每 1500 幀 (二選一) + output_dir: "checkpoints" + keep_partial: true # 保留部分完成檔案 +``` + +### 4.3 Checkpoint 結構 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Checkpoint { + pub uuid: String, + pub processor: String, + pub checkpoint_id: String, + pub timestamp: f64, + pub frame_number: i64, + pub total_frames: i64, + pub progress_percentage: f64, + pub partial_data: serde_json::Value, + pub created_at: DateTime, +} +``` + +### 4.4 分時輸出檔案命名 + +``` +{UUID}.{處理類型}.partial.{序號}.json + +範例: +1636719dc31f78ac.asr.partial.001.json # 第 1 次 checkpoint +1636719dc31f78ac.asr.partial.002.json # 第 2 次 checkpoint +1636719dc31f78ac.asr.partial.003.json # 第 3 次 checkpoint +``` + +### 4.5 分時輸出範例 (ASR) + +```json +{ + "uuid": "1636719dc31f78ac", + "processor": "asr", + "checkpoint_id": "partial_001", + "timestamp": 60.0, + "frame_number": 1440, + "total_frames": 3000, + "progress_percentage": 48.0, + "partial_data": { + "language": "en", + "language_probability": 0.9945, + "segments": [ + {"start": 0.0, "end": 5.0, "text": "Hello world"}, + {"start": 5.0, "end": 10.0, "text": "This is a test"}, + ... + ] + }, + "created_at": "2026-03-16T10:01:00Z" +} +``` + +### 4.6 分時合併邏輯 + +```rust +pub fn merge_checkpoints(checkpoints: Vec) -> serde_json::Value { + // 按 checkpoint_id 排序 + let mut sorted = checkpoints; + sorted.sort_by(|a, b| a.checkpoint_id.cmp(&b.checkpoint_id)); + + // 合併 segments + let mut merged_segments: Vec = vec![]; + for checkpoint in sorted { + if let Some(segments) = checkpoint.partial_data.get("segments") { + if let Some(seg_array) = segments.as_array() { + merged_segments.extend(seg_array.clone()); + } + } + } + + serde_json::json!({ + "segments": merged_segments + }) +} +``` + +--- + +## 5. Video 處理中斷接續規範 + +### 5.1 支援的中斷類型 + +| 中斷類型 | 說明 | 處理方式 | +|----------|------|----------| +| 程序崩潰 | 處理程序異常退出 | 從上次 checkpoint 恢復 | +| 系統關機 | 系統意外關機 | 從上次 checkpoint 恢復 | +| 資源不足 | OOM/磁碟空間不足 | 釋放資源後重試 | +| 用戶暫停 | 用戶主動暫停 | 顯示 Paused 狀態 | +| 網路中斷 | 遠端資源不可用 | 重試連線後繼續 | + +### 5.2 接續處理流程 + +``` +1. 檢測中斷 + │ + ▼ +2. 查找最新 checkpoint + │ + ▼ +3. 載入 partial data + │ + ▼ +4. 驗證數據完整性 + │ + ▼ +5. 從 checkpoint 繼續處理 + │ + ▼ +6. 輸出完整結果 +``` + +### 5.3 接續狀態檢測 + +```rust +pub async fn check_resume_status(uuid: &str) -> Result { + // 1. 查找所有 checkpoint 檔案 + let checkpoints = find_checkpoints(uuid)?; + + // 2. 查找最後處理的進度 + let last_checkpoint = checkpoints.last(); + + // 3. 檢查主要輸出檔案是否存在 + let main_output_exists = Path::new(&format!("{}.asr.json", uuid)).exists(); + + // 4. 判斷可恢復的處理器 + let resumeable = ResumeStatus { + can_resume: !checkpoints.is_empty() && !main_output_exists, + last_checkpoint: last_checkpoint.cloned(), + processed_processors: detect_processed_processors(uuid), + remaining_processors: detect_remaining_processors(uuid), + }; + + Ok(resumeable) +} +``` + +### 5.4 Resume Status 結構 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ResumeStatus { + pub can_resume: bool, + pub last_checkpoint: Option, + pub processed_processors: Vec, + pub remaining_processors: Vec, + pub suggested_action: String, +} +``` + +### 5.5 接續命令 + +```bash +# 自動檢測並恢復 +cargo run -- resume /path/to/video.mp4 + +# 強制從頭開始 +cargo run -- process /path/to/video.mp4 --force + +# 查看處理狀態 +cargo run -- status 1636719dc31f78ac + +# 查看可恢復的檢查點 +cargo run -- checkpoints 1636719dc31f78ac +``` + +### 5.6 衝突處理 + +```rust +pub fn resolve_conflict( + partial: &serde_json::Value, + main: &serde_json::Value, + strategy: ConflictStrategy, +) -> serde_json::Value { + match strategy { + ConflictStrategy::KeepMain => main.clone(), + ConflictStrategy::KeepPartial => partial.clone(), + ConflictStrategy::Merge => { + // 合併 segments,移除重複 + merge_segments(partial, main) + } + } +} +``` + +--- + +## 6. Video 處理多語種標示規範 + +### 6.1 語言代碼標準 + +使用 ISO 639-1 兩碼語言代碼: + +| 語言 | 代碼 | 範例 | +|------|------|------| +| 英語 | en | English | +| 國語/普通話 | zh | Chinese (Mandarin) | +| 粵語 | yue | Cantonese | +| 閩南語 | nan | Min Nan | +| 日語 | ja | Japanese | +| 韓語 | ko | Korean | +| 西班牙語 | es | Spanish | +| 法語 | fr | French | +| 德語 | de | German | +| 義大利語 | it | Italian | +| 俄語 | ru | Russian | +| 阿拉伯語 | ar | Arabic | +| 印地語 | hi | Hindi | +| 葡萄牙語 | pt | Portuguese | + +### 6.2 多語種偵測結果 + +```json +{ + "language": "multi", + "languages_detected": [ + { + "code": "en", + "name": "English", + "probability": 0.75, + "segments_count": 100 + }, + { + "code": "zh", + "name": "Chinese", + "probability": 0.20, + "segments_count": 30 + }, + { + "code": "ja", + "name": "Japanese", + "probability": 0.05, + "segments_count": 5 + } + ], + "primary_language": "en", + "segments": [ + { + "start": 0.0, + "end": 5.0, + "text": "Hello world", + "language": "en", + "language_probability": 0.99 + }, + { + "start": 5.0, + "end": 10.0, + "text": "你好世界", + "language": "zh", + "language_probability": 0.98 + } + ] +} +``` + +### 6.3 段落級語言標示 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AsrSegment { + pub start: f64, + pub end: f64, + pub text: String, + pub language: Option, // 段落語言 + pub language_probability: Option, // 段落語言機率 +} +``` + +### 6.4 資料庫儲存 + +```sql +CREATE TABLE asr_segments ( + id BIGSERIAL PRIMARY KEY, + uuid VARCHAR(16) NOT NULL, + segment_index INTEGER NOT NULL, + start_time DOUBLE PRECISION NOT NULL, + end_time DOUBLE PRECISION NOT NULL, + text TEXT NOT NULL, + language VARCHAR(10), + language_probability DOUBLE PRECISION, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(uuid, segment_index) +); + +CREATE INDEX idx_asr_language ON asr_segments(language); +``` + +--- + +## 7. Video 處理未識別成功語種標示規範 + +### 7.1 未識別狀態 + +| 狀態 | 代碼 | 說明 | +|------|------|------| +| Unknown | unknown | 無法判斷語言 | +| Uncertain | uncertain | 語言識別信心度過低 | +| NoSpeech | no_speech | 無語音內容 | +| Silent | silent | 完全是靜音 | + +### 7.2 未識別結果結構 + +```json +{ + "language": "unknown", + "language_probability": null, + "language_detection_failed": true, + "failure_reason": "low_confidence", + "min_confidence_threshold": 0.7, + "detected_language": "en", + "detected_probability": 0.45, + "segments": [ + { + "start": 0.0, + "end": 5.0, + "text": "", + "language": "no_speech", + "language_probability": 0.99, + "has_audio": false + } + ] +} +``` + +### 7.3 處理策略 + +```rust +pub fn handle_undetected_language( + audio_path: &str, + result: &AsrResult, +) -> AsrResult { + // 1. 檢測是否為靜音 + let is_silent = detect_silence(audio_path); + + // 2. 如果靜音,標示為 silent + if is_silent { + return AsrResult { + language: Some("silent".to_string()), + language_probability: Some(1.0), + segments: result.segments.iter().map(|s| AsrSegment { + language: Some("silent".to_string()), + has_audio: Some(false), + ..s.clone() + }).collect(), + }; + } + + // 3. 如果語言信心度低,標示為 uncertain + if result.language_probability.unwrap_or(0.0) < 0.7 { + return AsrResult { + language: Some("uncertain".to_string()), + language_probability: result.language_probability, + segments: result.segments.iter().map(|s| AsrSegment { + language: Some("uncertain".to_string()), + language_probability: Some(result.language_probability.unwrap_or(0.0)), + ..s.clone() + }).collect(), + }; + } + + result.clone() +} +``` + +### 7.4 靜音偵測 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SilenceDetection { + pub is_silent: bool, + pub silence_ratio: f64, # 靜音比例 (0.0 - 1.0) + pub audio_level_db: f64, # 平均音量大 (dB) + pub threshold_db: f64, # 閾值 (-40 dB) +} + +pub fn detect_silence(audio_path: &str, threshold_db: f64) -> SilenceDetection { + # 使用 ffmpeg 分析音量大 +} +``` + +--- + +## 8. Video 處理 Music 標示規範 + +### 8.1 音樂偵測結果 + +```json +{ + "has_music": true, + "music_segments": [ + { + "start": 0.0, + "end": 30.0, + "type": "background_music", + "confidence": 0.95, + "genre": "classical", + "tempo": 120, + "has_lyrics": false + }, + { + "start": 60.0, + "end": 90.0, + "type": "song_with_vocals", + "confidence": 0.88, + "artist": "Unknown", + "title": "Unknown", + "has_lyrics": true + } + ], + "audio_classification": { + "speech": 0.30, + "music": 0.60, + "ambient": 0.10 + } +} +``` + +### 8.2 音樂類型分類 + +| 類型 | 說明 | +|------|------| +| background_music | 背景音樂 | +| song_with_vocals | 帶歌詞的歌曲 | +| instrumental | 純音樂 | +| sound_effect | 音效 | + +### 8.3 結構定義 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MusicSegment { + pub start: f64, + pub end: f64, + pub music_type: String, // 音樂類型 + pub confidence: f64, // 偵測信心度 + pub genre: Option, // 音樂類型 (可選) + pub tempo: Option, # BPM (可選) + pub has_lyrics: bool, # 是否有歌詞 + pub artist: Option, # 藝術家 (可選) + pub title: Option, # 標題 (可選) +} +``` + +--- + +## 9. Video 處理無聲音標示規範 + +### 9.1 無聲音定義 + +| 狀態 | 說明 | +|------|------| +| no_audio_track | 影片無音軌 | +| all_silent | 有音軌但全為靜音 | +| audio_error | 音軌讀取錯誤 | + +### 9.2 無聲音結果 + +```json +{ + "has_audio": false, + "audio_status": "no_audio_track", + "audio_info": { + "has_audio_track": false, + "error_message": null, + "audio_codec": null, + "sample_rate": null, + "channels": null, + "duration": null + }, + "asr": { + "language": "no_speech", + "language_probability": 1.0, + "segments": [], + "segments_count": 0, + "total_speech_duration": 0.0, + "speech_ratio": 0.0 + } +} +``` + +### 9.3 處理流程 + +```rust +pub async fn process_video_no_audio(uuid: &str, video_path: &str) -> Result { + // 1. Probe 影片 + let probe = probe_video(video_path).await?; + + // 2. 判斷無聲音原因 + let audio_status = if !probe.has_audio_stream { + "no_audio_track" + } else if probe.audio_is_silent { + "all_silent" + } else { + "audio_error" + }; + + // 3. 產生結果 + Ok(ProcessingResult { + has_audio: false, + audio_status: audio_status.to_string(), + asr: AsrResult { + language: Some("no_speech".to_string()), + language_probability: Some(1.0), + segments: vec![], + }, + ..Default::default() + }) +} +``` + +--- + +## 10. Frame 物件識別標示規範 (YOLO) + +### 10.1 YOLO 偵測結果結構 + +```json +{ + "model": "yolov8x", + "model_version": "8.0", + "segments": [ + { + "start": 0.0, + "end": 1.0, + "frame_number": 0, + "objects": [ + { + "class": "person", + "class_id": 0, + "confidence": 0.92, + "box": { + "x1": 150, + "y1": 200, + "x2": 400, + "y2": 800 + }, + "tracking_id": "person_001" + }, + { + "class": "car", + "class_id": 2, + "confidence": 0.87, + "box": { + "x1": 800, + "y1": 400, + "x2": 1200, + "y2": 700 + }, + "tracking_id": "car_001" + } + ] + } + ], + "statistics": { + "total_objects": 189, + "unique_classes": ["person", "car", "dog", "bicycle"], + "class_counts": { + "person": 120, + "car": 45, + "dog": 15, + "bicycle": 9 + } + } +} +``` + +### 10.2 支援的類別 (COCO) + +| 類別 ID | 類別名稱 | +|---------|----------| +| 0 | person | +| 1 | bicycle | +| 2 | car | +| 3 | motorcycle | +| 4 | airplane | +| 5 | bus | +| 6 | train | +| 7 | truck | +| 8 | boat | +| 9 | traffic light | +| ... | ... | + +### 10.3 結構定義 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct YoloResult { + pub model: String, + pub model_version: String, + pub segments: Vec, + pub statistics: YoloStatistics, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct YoloSegment { + pub start: f64, + pub end: f64, + pub frame_number: i64, + pub objects: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct YoloObject { + pub class: String, + pub class_id: i32, + pub confidence: f64, + pub box: BoundingBox, + pub tracking_id: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BoundingBox { + pub x1: i32, + pub y1: i32, + pub x2: i32, + pub y2: i32, +} +``` + +### 10.4 處理配置 + +```yaml +processing: + yolo: + model: "yolov8x" + confidence_threshold: 0.25 + iou_threshold: 0.45 + max_det: 300 + device: "cuda" + batch_size: 8 + skip_frames: 1 # 每 N 幀處理一次 +``` + +--- + +## 11. Frame 文字識別標示規範 (OCR) + +### 11.1 OCR 偵測結果結構 + +```json +{ + "model": "easyocr", + "model_version": "1.7", + "language": ["en"], + "segments": [ + { + "start": 0.0, + "end": 1.0, + "frame_number": 0, + "texts": [ + { + "text": "EXAMPLE TEXT", + "text_normalized": "example text", + "boxes": [ + { + "x1": 100, + "y1": 50, + "x2": 400, + "y2": 100 + } + ], + "confidence": 0.95, + "language": "en" + }, + { + "text": "SUBTITLE HERE", + "text_normalized": "subtitle here", + "boxes": [ + { + "x1": 200, + "y1": 900, + "x2": 1720, + "y2": 1000 + } + ], + "confidence": 0.88, + "language": "en" + } + ] + } + ], + "statistics": { + "total_text_regions": 25, + "unique_texts": 18, + "languages_detected": ["en"] + } +} +``` + +### 11.2 結構定義 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OcrResult { + pub model: String, + pub model_version: String, + pub language: Vec, + pub segments: Vec, + pub statistics: OcrStatistics, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OcrSegment { + pub start: f64, + pub end: f64, + pub frame_number: i64, + pub texts: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OcrText { + pub text: String, + pub text_normalized: String, + pub boxes: Vec, + pub confidence: f64, + pub language: Option, +} +``` + +### 11.3 文字類型分類 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum TextType { + Subtitle, // 字幕 + Title, # 標題 + Sign, # 標牌/招牌 + Caption, # 說明文字 + Watermark, #浮水印 + SceneText, # 場景文字 + Unknown, # 未知 +} +``` + +### 11.4 處理配置 + +```yaml +processing: + ocr: + model: "easyocr" + languages: ["en", "zh", "ja"] + confidence_threshold: 0.5 + text_detection: true + text_recognition: true + batch_size: 16 + skip_frames: 30 # 每 30 幀處理一次 (字幕通常持續較久) +``` + +--- + +## 12. Frame Face 識別標示規範 (Face) + +### 12.1 人臉偵測結果結構 + +```json +{ + "model": "retinaface", + "model_version": "1.0", + "segments": [ + { + "start": 0.0, + "end": 1.0, + "frame_number": 0, + "faces": [ + { + "face_id": "face_001", + "box": { + "x1": 100, + "y1": 50, + "x2": 300, + "y2": 350 + }, + "embedding": [0.123, -0.456, ...], + "embedding_dim": 512, + "emotion": { + "dominant": "happy", + "scores": { + "happy": 0.75, + "neutral": 0.20, + "sad": 0.03, + "angry": 0.02 + } + }, + "age": 35, + "gender": "female", + "confidence": 0.95 + } + ] + } + ], + "statistics": { + "total_faces": 50, + "unique_faces": 5, + "face_tracks": [ + { + "face_id": "face_001", + "duration": 120.5, + "appearances": 3000, + "first_seen": 0.0, + "last_seen": 120.5 + } + ] + } +} +``` + +### 12.2 結構定義 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FaceResult { + pub model: String, + pub model_version: String, + pub segments: Vec, + pub statistics: FaceStatistics, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FaceSegment { + pub start: f64, + pub end: f64, + pub frame_number: i64, + pub faces: Vec, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Face { + pub face_id: String, + pub box: BoundingBox, + pub embedding: Option>, + pub embedding_dim: Option, + pub emotion: Option, + pub age: Option, + pub gender: Option, + pub confidence: f64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Emotion { + pub dominant: String, + pub scores: std::collections::HashMap, +} +``` + +### 12.3 人臉追蹤 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FaceTrack { + pub face_id: String, + pub duration: f64, + pub appearances: i32, + pub first_seen: f64, + pub last_seen: f64, + pub frames: Vec, + pub embedding_average: Vec, +} +``` + +### 12.4 處理配置 + +```yaml +processing: + face: + model: "retinaface" + recognition_model: "arcface" + detection_threshold: 0.5 + recognition_threshold: 0.6 + track_faces: true + detect_emotion: true + detect_age: true + detect_gender: true + skip_frames: 1 +``` + +--- + +## 13. 完整處理流程圖 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ VIDEO PROCESSING │ +├─────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ REGISTER │───▶│ PROBE │───▶│ ASR │───▶│ ASRx │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ UUID │ │ Video │ │ Language│ │Speaker │ │ +│ │ Generate│ │ Info │ │ Detect │ │Diariz. │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ OCR │───▶│ YOLO │───▶│ FACE │───▶│ POSE │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ Text │ │ Object │ │ Face │ │Pose │ │ +│ │ Detect │ │ Detect │ │ Track │ │Estimate │ │ +│ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ CHUNK │ │ +│ └──────────────┘ │ +│ │ │ +│ ▼ │ +│ ┌──────────────┐ │ +│ │ COMPLETE │ │ +│ └──────────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ CHECKPOINT (每分鐘輸出) │ │ +│ │ {uuid}.asr.partial.001.json │ │ +│ │ {uuid}.asrx.partial.001.json │ │ +│ │ ... │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 14. 處理器狀態矩陣 + +| 處理器 | 輸入 | 輸出 | 必要 | 可選參數 | +|--------|------|------|------|----------| +| Probe | 影片檔 | probe.json | ✅ | - | +| ASR | 影片/音軌 | asr.json | ✅ | model, language | +| ASRx | asr.json | asrx.json | ❌ | min_speakers, max_speakers | +| OCR | 影片幀 | ocr.json | ❌ | languages, threshold | +| YOLO | 影片幀 | yolo.json | ❌ | model, confidence | +| Face | 影片幀 | face.json | ❌ | recognition, track | +| Pose | 影片幀 | pose.json | ❌ | model, tracking | + +--- + +## 15. 錯誤處理 + +### 15.1 錯誤類型 + +```rust +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum ProcessError { + FileNotFound, + InvalidFormat, + NoAudioTrack, + NoVideoTrack, + ProcessingFailed { processor: String, message: String }, + OutOfMemory, + DiskFull, + Timeout, + Cancelled, +} +``` + +### 15.2 錯誤回應 + +```json +{ + "error": "processing_failed", + "processor": "asr", + "message": "Failed to load Whisper model", + "timestamp": "2026-03-16T10:00:00Z", + "retryable": false, + "suggestion": "Check GPU availability and model files" +} +``` + +--- + +## 16. 版本歷史 + +| 版本 | 日期 | 變更 | +|------|------|------| +| 1.0.0 | 2026-03-16 | 初始版本 | + +--- + +## 17. 相關文件 + +- [CHUNK_SPEC.md](./CHUNK_SPEC.md) - 影片分塊規範 +- [JSON_OUTPUT_SPEC.md](./JSON_OUTPUT_SPEC.md) - JSON 輸出規範 +- [RUST_DEVELOPMENT.md](./RUST_DEVELOPMENT.md) - Rust 開發規範 +- [AGENTS.md](../AGENTS.md) - 開發規範 diff --git a/docs_v1.0/DESIGN/VIDEO_REGISTRATION.md b/docs_v1.0/DESIGN/VIDEO_REGISTRATION.md new file mode 100644 index 0000000..4d6e310 --- /dev/null +++ b/docs_v1.0/DESIGN/VIDEO_REGISTRATION.md @@ -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::>() + .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 端點總覽 | diff --git a/docs_v1.0/DESIGN/VISION_AGENT_API.md b/docs_v1.0/DESIGN/VISION_AGENT_API.md new file mode 100644 index 0000000..c98b568 --- /dev/null +++ b/docs_v1.0/DESIGN/VISION_AGENT_API.md @@ -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/` + +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 | diff --git a/docs_v1.0/DESIGN/VISUALIZATION_TOOL_CHOICES_V1.0.0.md b/docs_v1.0/DESIGN/VISUALIZATION_TOOL_CHOICES_V1.0.0.md new file mode 100644 index 0000000..af4efdc --- /dev/null +++ b/docs_v1.0/DESIGN/VISUALIZATION_TOOL_CHOICES_V1.0.0.md @@ -0,0 +1,105 @@ +# 視覺呈現工具選型 v1.0.0 + +Momentry 前端視覺化工具選擇記錄。 + +## SVG(內建) + +| 項目 | 內容 | +|------|------| +| 用途 | Trace 時間軸、泳道圖、長條圖、矩陣 | +| 授權 | 瀏覽器內建,無授權問題 | +| 適用 | V1 TraceThumbnailTimeline、V2 IdentitySwimlane、V3 DurationHistogram、V4 SimilarityMatrix | +| 優點 | 零依賴、向量清晰、可互動 | +| 缺點 | 大規模節點時效能下降 | + +## Three.js + +| 項目 | 內容 | +|------|------| +| 用途 | 3D 臉部網格、3D 時空立方體 | +| 授權 | **MIT** — 可商用,需保留版權聲明 | +| 適用 | Face3DViewer(MediaPipe 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 以外需插件支援 | diff --git a/docs_v1.0/DESIGN/VOICE_TECH_CHOICES_V1.0.0.md b/docs_v1.0/DESIGN/VOICE_TECH_CHOICES_V1.0.0.md new file mode 100644 index 0000000..f5015c3 --- /dev/null +++ b/docs_v1.0/DESIGN/VOICE_TECH_CHOICES_V1.0.0.md @@ -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 使用文件 | diff --git a/docs_v1.0/DESIGN/VOICE_TEST_RESULTS_V1.0.0.md b/docs_v1.0/DESIGN/VOICE_TEST_RESULTS_V1.0.0.md new file mode 100644 index 0000000..b486c95 --- /dev/null +++ b/docs_v1.0/DESIGN/VOICE_TEST_RESULTS_V1.0.0.md @@ -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) diff --git a/docs_v1.0/DESIGN/ZERO_SHOT_DETECTION_RESEARCH.md b/docs_v1.0/DESIGN/ZERO_SHOT_DETECTION_RESEARCH.md new file mode 100644 index 0000000..dd87a8c --- /dev/null +++ b/docs_v1.0/DESIGN/ZERO_SHOT_DETECTION_RESEARCH.md @@ -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 (``) 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 | diff --git a/docs_v1.0/DESIGN/ZERO_SHOT_GUN_TEST_PLAN.md b/docs_v1.0/DESIGN/ZERO_SHOT_GUN_TEST_PLAN.md new file mode 100644 index 0000000..97c6d2e --- /dev/null +++ b/docs_v1.0/DESIGN/ZERO_SHOT_GUN_TEST_PLAN.md @@ -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 | diff --git a/docs_v1.0/DESIGN/ZERO_SHOT_GUN_TEST_REPORT.md b/docs_v1.0/DESIGN/ZERO_SHOT_GUN_TEST_REPORT.md new file mode 100644 index 0000000..1527877 --- /dev/null +++ b/docs_v1.0/DESIGN/ZERO_SHOT_GUN_TEST_REPORT.md @@ -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` diff --git a/docs_v1.0/DESIGN/ZERO_SHOT_VS_FINETUNE_SELECTION.md b/docs_v1.0/DESIGN/ZERO_SHOT_VS_FINETUNE_SELECTION.md new file mode 100644 index 0000000..ecf75eb --- /dev/null +++ b/docs_v1.0/DESIGN/ZERO_SHOT_VS_FINETUNE_SELECTION.md @@ -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**:可商用 + +### 缺點 +- **體積大**:891MB(vs YOLOv8n 的 6MB) +- **推論慢**:4.3s/frame(vs 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) +``` diff --git a/docs_v1.0/GUIDES/API_ACCESS.md b/docs_v1.0/GUIDES/API_ACCESS.md new file mode 100644 index 0000000..f5dd3db --- /dev/null +++ b/docs_v1.0/GUIDES/API_ACCESS.md @@ -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 個(句子 + 場景 + 時間) diff --git a/docs_v1.0/GUIDES/API_ENDPOINTS.md b/docs_v1.0/GUIDES/API_ENDPOINTS.md new file mode 100644 index 0000000..a641fef --- /dev/null +++ b/docs_v1.0/GUIDES/API_ENDPOINTS.md @@ -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 範例 diff --git a/docs_v1.0/GUIDES/API_ERROR_CODES.md b/docs_v1.0/GUIDES/API_ERROR_CODES.md new file mode 100644 index 0000000..282a437 --- /dev/null +++ b/docs_v1.0/GUIDES/API_ERROR_CODES.md @@ -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 diff --git a/docs_v1.0/GUIDES/API_INDEX.md b/docs_v1.0/GUIDES/API_INDEX.md new file mode 100644 index 0000000..e21da7b --- /dev/null +++ b/docs_v1.0/GUIDES/API_INDEX.md @@ -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 設計 diff --git a/docs_v1.0/GUIDES/API_QUICK_REFERENCE.md b/docs_v1.0/GUIDES/API_QUICK_REFERENCE.md new file mode 100644 index 0000000..741e93b --- /dev/null +++ b/docs_v1.0/GUIDES/API_QUICK_REFERENCE.md @@ -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 工作流程整合 diff --git a/docs_v1.0/GUIDES/API_REFERENCE.md b/docs_v1.0/GUIDES/API_REFERENCE.md new file mode 100644 index 0000000..62bb460 --- /dev/null +++ b/docs_v1.0/GUIDES/API_REFERENCE.md @@ -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` | diff --git a/docs_v1.0/GUIDES/API_TRAINING_MARCOM.md b/docs_v1.0/GUIDES/API_TRAINING_MARCOM.md new file mode 100644 index 0000000..0b1c6c0 --- /dev/null +++ b/docs_v1.0/GUIDES/API_TRAINING_MARCOM.md @@ -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 | diff --git a/docs_v1.0/GUIDES/DEMO_RUNNER_V1.0.0.md b/docs_v1.0/GUIDES/DEMO_RUNNER_V1.0.0.md new file mode 100644 index 0000000..52d9924 --- /dev/null +++ b/docs_v1.0/GUIDES/DEMO_RUNNER_V1.0.0.md @@ -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.5–6 秒) +- 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 內建 | +| webbrowser(Python)| 開啟瀏覽器 | 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 渲染工具 | diff --git a/docs_v1.0/GUIDES/Demo_EndToEnd.md b/docs_v1.0/GUIDES/Demo_EndToEnd.md new file mode 100644 index 0000000..b723c2e --- /dev/null +++ b/docs_v1.0/GUIDES/Demo_EndToEnd.md @@ -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** | 1–4 | System check, scan, register, probe | +| **處理中** | 5–6 | Submit job → Worker picks up → Each processor runs (pending→running→completed) | +| **處理後** | 7–9 | 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/.py + │ ├─ 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" +``` diff --git a/docs_v1.0/GUIDES/M5API_Pipeline_Demo.md b/docs_v1.0/GUIDES/M5API_Pipeline_Demo.md new file mode 100644 index 0000000..95a7cf8 --- /dev/null +++ b/docs_v1.0/GUIDES/M5API_Pipeline_Demo.md @@ -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 }" +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" +``` diff --git a/docs_v1.0/GUIDES/PLAYGROUND_BINARY_IMPLEMENTATION.md b/docs_v1.0/GUIDES/PLAYGROUND_BINARY_IMPLEMENTATION.md new file mode 100644 index 0000000..fe11df0 --- /dev/null +++ b/docs_v1.0/GUIDES/PLAYGROUND_BINARY_IMPLEMENTATION.md @@ -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 = Lazy::new(|| { + env::var("MOMENTRY_SERVER_PORT") + .unwrap_or_else(|_| "3002".to_string()) + .parse() + .unwrap_or(3002) +}); + +pub static REDIS_KEY_PREFIX: Lazy = 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, +``` + +**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 | diff --git a/docs_v1.0/GUIDES/PORTAL_API_DEMO_GUIDE.md b/docs_v1.0/GUIDES/PORTAL_API_DEMO_GUIDE.md new file mode 100644 index 0000000..6a01b67 --- /dev/null +++ b/docs_v1.0/GUIDES/PORTAL_API_DEMO_GUIDE.md @@ -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/ + +# 查詢遷移歷史 +curl http://localhost:3003/api/v1/files//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 diff --git a/docs_v1.0/GUIDES/PORTAL_DEVELOPMENT_PLAN.md b/docs_v1.0/GUIDES/PORTAL_DEVELOPMENT_PLAN.md new file mode 100644 index 0000000..8b652ba --- /dev/null +++ b/docs_v1.0/GUIDES/PORTAL_DEVELOPMENT_PLAN.md @@ -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. 是否需要匯出報表功能? diff --git a/docs_v1.0/GUIDES/Pipeline_API_Demo.md b/docs_v1.0/GUIDES/Pipeline_API_Demo.md new file mode 100644 index 0000000..692808e --- /dev/null +++ b/docs_v1.0/GUIDES/Pipeline_API_Demo.md @@ -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" +``` diff --git a/docs_v1.0/GUIDES/USER_MANUAL.md b/docs_v1.0/GUIDES/USER_MANUAL.md new file mode 100644 index 0000000..e10e285 --- /dev/null +++ b/docs_v1.0/GUIDES/USER_MANUAL.md @@ -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 +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 ` | 註冊影片 | +| `momentry process ` | 處理影片 | +| `momentry query ` | RAG 查詢 | + +### 3.2 影片管理 + +```bash +# 註冊影片 +momentry register /path/to/video.mp4 + +# 處理影片 +momentry process + +# 生成縮圖 +momentry thumbnails --count 6 + +# 查看狀態 +momentry status +``` + +### 3.3 API Key 管理 + +```bash +# 建立 Key +momentry api-key create --key-type --ttl + +# 列出 Keys +momentry api-key list + +# 驗證 Key +momentry api-key validate --key + +# 撤銷 Key +momentry api-key revoke --key + +# 請求輪換 +momentry api-key rotate --key + +# 統計資訊 +momentry api-key stats +``` + +### 3.4 Gitea 整合 + +```bash +# 建立 Token +momentry gitea create \ + --username \ + --password \ + --token-name \ + --scopes "read:repository,write:repository" + +# 列出 Tokens +momentry gitea list --username --password + +# 刪除 Token +momentry gitea delete \ + --username \ + --password \ + --token-name +``` + +### 3.5 n8n 整合 + +```bash +# 建立 API Key +momentry n8n create \ + --api-key \ + --label \ + --expires-in-days 90 + +# 列出 Keys +momentry n8n list --api-key + +# 刪除 Key +momentry n8n delete --api-key --label +``` + +### 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 +``` + +--- + +## 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: +``` + +### 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` | 版本記錄 | diff --git a/docs_v1.0/INTEGRATIONS/Momentry_Core_API.postman_collection.json b/docs_v1.0/INTEGRATIONS/Momentry_Core_API.postman_collection.json new file mode 100644 index 0000000..c366d62 --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/Momentry_Core_API.postman_collection.json @@ -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" + } + } + ] + } + ] +} diff --git a/docs_v1.0/INTEGRATIONS/N8N_API_FIX_SUMMARY.md b/docs_v1.0/INTEGRATIONS/N8N_API_FIX_SUMMARY.md new file mode 100644 index 0000000..b26071b --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/N8N_API_FIX_SUMMARY.md @@ -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", "", + "-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": "" + } + } + } +} +``` + +## Verified Endpoints + +### List Workflows +```bash +curl -H "X-N8N-API-KEY: " http://localhost:5678/api/v1/workflows +``` +**Result**: ✅ 30 workflows returned + +### List Executions +```bash +curl -H "X-N8N-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: ` +- Available endpoints: workflows, executions, credentials, users, etc. + +See full API reference: https://docs.n8n.io/api/ diff --git a/docs_v1.0/INTEGRATIONS/N8N_VIDEO_SEARCH_SUCCESS.md b/docs_v1.0/INTEGRATIONS/N8N_VIDEO_SEARCH_SUCCESS.md new file mode 100644 index 0000000..e483cba --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/N8N_VIDEO_SEARCH_SUCCESS.md @@ -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: " +``` + +--- + +## 服務資訊 + +| 服務 | 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 \ + -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`) diff --git a/docs_v1.0/INTEGRATIONS/n8n_workflow_core_v1.2.json b/docs_v1.0/INTEGRATIONS/n8n_workflow_core_v1.2.json new file mode 100644 index 0000000..9882924 --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/n8n_workflow_core_v1.2.json @@ -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" +} diff --git a/docs_v1.0/INTEGRATIONS/n8n_workflow_simple.json b/docs_v1.0/INTEGRATIONS/n8n_workflow_simple.json new file mode 100644 index 0000000..a9e6dd2 --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/n8n_workflow_simple.json @@ -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" +} \ No newline at end of file diff --git a/docs_v1.0/INTEGRATIONS/n8n_workflow_simple_test.json b/docs_v1.0/INTEGRATIONS/n8n_workflow_simple_test.json new file mode 100644 index 0000000..0b13a6f --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/n8n_workflow_simple_test.json @@ -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 +} diff --git a/docs_v1.0/INTEGRATIONS/n8n_workflow_video_rag_mcp.json b/docs_v1.0/INTEGRATIONS/n8n_workflow_video_rag_mcp.json new file mode 100644 index 0000000..59405c1 --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/n8n_workflow_video_rag_mcp.json @@ -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 +} \ No newline at end of file diff --git a/docs_v1.0/INTEGRATIONS/n8n_workflow_video_search.json b/docs_v1.0/INTEGRATIONS/n8n_workflow_video_search.json new file mode 100644 index 0000000..032249b --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/n8n_workflow_video_search.json @@ -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" +} \ No newline at end of file diff --git a/docs_v1.0/INTEGRATIONS/test_all.sh b/docs_v1.0/INTEGRATIONS/test_all.sh new file mode 100755 index 0000000..1da5dfd --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/test_all.sh @@ -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" diff --git a/docs_v1.0/INTEGRATIONS/test_momentry_api.sh b/docs_v1.0/INTEGRATIONS/test_momentry_api.sh new file mode 100755 index 0000000..d218cc0 --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/test_momentry_api.sh @@ -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 "==========================================" diff --git a/docs_v1.0/INTEGRATIONS/test_workflow.sh b/docs_v1.0/INTEGRATIONS/test_workflow.sh new file mode 100755 index 0000000..57244fa --- /dev/null +++ b/docs_v1.0/INTEGRATIONS/test_workflow.sh @@ -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 </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 "==========================================" diff --git a/docs_v1.0/M4_workspace/2026-05-17_file_uuid_rule.md b/docs_v1.0/M4_workspace/2026-05-17_file_uuid_rule.md new file mode 100644 index 0000000..e4e962c --- /dev/null +++ b/docs_v1.0/M4_workspace/2026-05-17_file_uuid_rule.md @@ -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 字元小寫 hex(0-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. 優先順序:en0(Wi-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()`),而非 birthtime(btime)。原因: + +- `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` diff --git a/docs_v1.0/OPERATIONS/API_3002_VS_3003_COMPARISON.md b/docs_v1.0/OPERATIONS/API_3002_VS_3003_COMPARISON.md new file mode 100644 index 0000000..98f522d --- /dev/null +++ b/docs_v1.0/OPERATIONS/API_3002_VS_3003_COMPARISON.md @@ -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` 欄位輔助) diff --git a/docs_v1.0/OPERATIONS/Brew_To_Source_Migration.md b/docs_v1.0/OPERATIONS/Brew_To_Source_Migration.md new file mode 100644 index 0000000..6787cce --- /dev/null +++ b/docs_v1.0/OPERATIONS/Brew_To_Source_Migration.md @@ -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 Leaves(user-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 ... +``` diff --git a/docs_v1.0/OPERATIONS/DELIVERY_PROCEDURE.md b/docs_v1.0/OPERATIONS/DELIVERY_PROCEDURE.md new file mode 100644 index 0000000..40b3a00 --- /dev/null +++ b/docs_v1.0/OPERATIONS/DELIVERY_PROCEDURE.md @@ -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 1:M4 回報(M4_workspace/) + +--- + +## Phase 1:M4 回報(M4_workspace/) + +M4 將問題寫入 `docs_v1.0/M4_workspace/YYYY-MM-DD_.md`,格式: + +```markdown +# {問題標題} +**Date**: YYYY-MM-DD +**From**: M4 +**To**: M5 + +--- + +## Bug / 建議 +(問題描述 + 影響分析) + +## Fix 建議 +(選擇性:程式碼範例或解決方向) +``` + +--- + +## Phase 2:M5 處理 + +| 步驟 | 動作 | 檢查點 | +|------|------|--------| +| 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__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.5:M5 內部自驗 + +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 3:Release 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 4:Deploy + +### 前置檢查 + +```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 5:Public 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 交付目錄 | diff --git a/docs_v1.0/OPERATIONS/DEVELOPMENT_LOG.md b/docs_v1.0/OPERATIONS/DEVELOPMENT_LOG.md new file mode 100644 index 0000000..e004b08 --- /dev/null +++ b/docs_v1.0/OPERATIONS/DEVELOPMENT_LOG.md @@ -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 Keys(UUID 必要)**: +- job:{uuid}, progress:{uuid}, metrics:{uuid} + +**Per-Processor Keys(UUID + 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 + +# 列出備份 +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 + +# 查詢進度 +curl http://127.0.0.1:3002/api/v1/progress/ +``` + +--- + +## 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 實時推送 +- [ ] 移動端響應式設計 diff --git a/docs_v1.0/OPERATIONS/JSON_OUTPUT_SPEC.md b/docs_v1.0/OPERATIONS/JSON_OUTPUT_SPEC.md new file mode 100644 index 0000000..788f534 --- /dev/null +++ b/docs_v1.0/OPERATIONS/JSON_OUTPUT_SPEC.md @@ -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) - 監控配置 diff --git a/docs_v1.0/OPERATIONS/M4_M5_COLLABORATION_PROTOCOL.md b/docs_v1.0/OPERATIONS/M4_M5_COLLABORATION_PROTOCOL.md new file mode 100644 index 0000000..8be51c7 --- /dev/null +++ b/docs_v1.0/OPERATIONS/M4_M5_COLLABORATION_PROTOCOL.md @@ -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 被 kill(SIGKILL) | partial 存為 `.json.partial`(非 `.json`) | + +## 違規後果 + +2026-05-09 事故:M4 release 打包未含 .json → 跨域操作 → M5 cleanup 誤刪 asr.json +→ 導致 ASR 需重跑(完整電影約 1.5hr) +→ YOLO 需重跑 +→ 損失已完成的 pipeline 進度 + +此類違規不可再發生。 diff --git a/docs_v1.0/OPERATIONS/M4_RELEASE_INCIDENT_2026-05-09.md b/docs_v1.0/OPERATIONS/M4_RELEASE_INCIDENT_2026-05-09.md new file mode 100644 index 0000000..c4443b2 --- /dev/null +++ b/docs_v1.0/OPERATIONS/M4_RELEASE_INCIDENT_2026-05-09.md @@ -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 diff --git a/docs_v1.0/OPERATIONS/M4_VS_M5_COMPARISON.md b/docs_v1.0/OPERATIONS/M4_VS_M5_COMPARISON.md new file mode 100644 index 0000000..5d1513a --- /dev/null +++ b/docs_v1.0/OPERATIONS/M4_VS_M5_COMPARISON.md @@ -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. diff --git a/docs_v1.0/OPERATIONS/M5_SETUP_LOG.md b/docs_v1.0/OPERATIONS/M5_SETUP_LOG.md new file mode 100644 index 0000000..cd95d48 --- /dev/null +++ b/docs_v1.0/OPERATIONS/M5_SETUP_LOG.md @@ -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. diff --git a/docs_v1.0/OPERATIONS/MOMENTRY_CORE_REDIS_KEYS.md b/docs_v1.0/OPERATIONS/MOMENTRY_CORE_REDIS_KEYS.md new file mode 100644 index 0000000..2b0b5db --- /dev/null +++ b/docs_v1.0/OPERATIONS/MOMENTRY_CORE_REDIS_KEYS.md @@ -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 Keys(UUID 必要) + +- 每個視頻獨立處理狀態 +- 即時進度追蹤 + +### 3.3 Per-Processor Keys(UUID + 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 狀態監控 diff --git a/docs_v1.0/OPERATIONS/PENDING_ISSUES.md b/docs_v1.0/OPERATIONS/PENDING_ISSUES.md new file mode 100644 index 0000000..7805e7f --- /dev/null +++ b/docs_v1.0/OPERATIONS/PENDING_ISSUES.md @@ -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", "", + "-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": "" + } + } + } +} +``` + +### 重要提醒 + +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 --key-type service --ttl 90 + +# 列出所有 Keys +momentry api-key list + +# 驗證 Key +momentry api-key validate --key + +# 撤銷 Key +momentry api-key revoke --key + +# 請求輪換 +momentry api-key rotate --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 用戶設定 diff --git a/docs_v1.0/OPERATIONS/PHASE1_COMPLETION_REPORT.md b/docs_v1.0/OPERATIONS/PHASE1_COMPLETION_REPORT.md new file mode 100644 index 0000000..4449d0b --- /dev/null +++ b/docs_v1.0/OPERATIONS/PHASE1_COMPLETION_REPORT.md @@ -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 diff --git a/docs_v1.0/OPERATIONS/PHASE1_RELEASE_CHECKLIST.md b/docs_v1.0/OPERATIONS/PHASE1_RELEASE_CHECKLIST.md new file mode 100644 index 0000000..717849a --- /dev/null +++ b/docs_v1.0/OPERATIONS/PHASE1_RELEASE_CHECKLIST.md @@ -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 diff --git a/docs_v1.0/OPERATIONS/RELEASE_NOTES_v1.0.0.md b/docs_v1.0/OPERATIONS/RELEASE_NOTES_v1.0.0.md new file mode 100644 index 0000000..f84afc9 --- /dev/null +++ b/docs_v1.0/OPERATIONS/RELEASE_NOTES_v1.0.0.md @@ -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) | diff --git a/docs_v1.0/OPERATIONS/RELEASE_SOP_V2.0.md b/docs_v1.0/OPERATIONS/RELEASE_SOP_V2.0.md new file mode 100644 index 0000000..1e55acd --- /dev/null +++ b/docs_v1.0/OPERATIONS/RELEASE_SOP_V2.0.md @@ -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 diff --git a/docs_v1.0/OPERATIONS/SERVICES.md b/docs_v1.0/OPERATIONS/SERVICES.md new file mode 100644 index 0000000..9251c06 --- /dev/null +++ b/docs_v1.0/OPERATIONS/SERVICES.md @@ -0,0 +1,1092 @@ +--- +document_type: "reference_doc" +service: "MOMENTRY_CORE" +title: "Momentry 系統服務安裝與管理指南" +date: "2026-03-18" +version: "V1.0" +status: "active" +owner: "Warren" +created_by: "OpenCode" +tags: + - "momentry" + - "系統服務安裝與管理指南" +ai_query_hints: + - "查詢 Momentry 系統服務安裝與管理指南 的內容" + - "Momentry 系統服務安裝與管理指南 的主要目的是什麼?" + - "如何操作或實施 Momentry 系統服務安裝與管理指南?" +--- + +# Momentry 系統服務安裝與管理指南 + +| 項目 | 內容 | +|------|------| +| 建立者 | Warren | +| 建立時間 | 2026-03-18 | +| 更新時間 | 2026-03-25 | +| 文件版本 | V1.2 | + +--- + +## 版本歷史 + +| 版本 | 日期 | 目的 | 操作人 | 工具/模型 | +|------|------|------|--------|-----------| +| V1.0 | 2026-03-18 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | +| V1.1 | 2026-03-24 | 更新所有服務 plist 狀態,統一使用自定義 plist | OpenCode | OpenCode / big-pickle | +| V1.2 | 2026-03-25 | 新增 Momentry Playground、Job Worker 說明 | OpenCode | OpenCode / GLM-5 | + +--- + +## 概述 + +本文檔記錄 momentry 系統所需的所有服務,包括安裝步驟、健康檢查和管理命令。 + +**重要**: 請勿使用 `brew services` 命令管理服務,否則可能導致 .plist 檔案還原為預設狀態,造成系統異常。請使用 `launchctl` 命令進行管理。 + +**2026-03-24 更新**: 所有服務已統一使用自定義 plist,存在於 `/Library/LaunchDaemons/` 目錄。Reboot 後會自動啟動。 + +--- + +## 服務清單 + +| 服務名稱 | 安裝方式 | 用途 | 狀態 | +|----------|----------|------|-------| +| PostgreSQL | 自定義 plist | 影片元資料儲存 | ✅ 正常 | +| Redis | 自定義 plist | 快取與工作佇列 | ✅ 正常 | +| Ollama | 自定義 plist | 本地 LLM 推論 | ✅ 正常 | +| Caddy | 自定義 plist | 網頁伺服器 | ✅ 正常 | +| Gitea | 自定義 plist | Git 服務 | ✅ 正常 | +| Gitea MCP Server | 自定義 plist | Gitea MCP 整合 | ✅ 正常 | +| Grafana | Homebrew | 監控儀表板 | ⚠️ Homebrew | +| Kafka | 手動安裝 | 訊息佇列 (可選) | ⚠️ 未遷移 | +| MariaDB | 自定義 plist | 資料庫 (可選) | ✅ 正常 | +| Netdata | Homebrew | 系統監控 | ⚠️ Homebrew | +| PHP | 自定義 plist | Web 後端 | ✅ 正常 | +| Prometheus | Homebrew | 指標收集 | ⚠️ Homebrew | +| SeaweedFS | 手動安裝 | 分散式儲存 (可選) | ⚠️ 未遷移 | +| SFTPGo | 自定義 plist | SFTP 服務 | ✅ 正常 | +| n8n | 自定義 plist | 工作流自動化 | ✅ 正常 | +| n8n Worker | 自定義 plist | 工作流 Worker | ✅ 正常 | +| MongoDB | 自定義 plist | 文件資料庫 | ✅ 正常 | +| Qdrant | 自定義 plist | 向量資料庫 | ✅ 正常 | +| Momentry API | 自定義 plist | 影片管理 API | ✅ 正常 | +| Momentry Playground | CLI | 開發測試用二進位 | ✅ 正常 | +| RustDesk HBBR | 自定義 plist | 遠端桌面橋接 | ✅ 正常 | +| RustDesk HBBS | 自定義 plist | 遠端桌面服務器 | ✅ 正常 | + +--- + +## Momentry 服務說明 + +### Momentry API vs Momentry Playground + +| 項目 | Momentry API | Momentry Playground | +|------|--------------|---------------------| +| 用途 | 生產環境 | 開發/測試環境 | +| Port | 3002 | 3003 | +| Redis Prefix | `momentry:` | `momentry_dev:` | +| 環境變數 | `.env` | `.env.development` | +| 啟動命令 | `cargo run --bin momentry -- server` | `cargo run --bin momentry_playground -- server` | + +### Job Worker + +Job Worker 是 Momentry Core 的背景處理系統,負責執行影片處理任務。 + +**架構**: +- 輪詢 `monitor_jobs` 表取得待處理任務 +- 最多同時執行 2 個 processor(可透過環境變數調整) +- 更新 `processor_results` 表記錄每個處理器狀態 + +**環境變數**: +| 變數 | 預設值 | 說明 | +|------|--------|------| +| `MOMENTRY_MAX_CONCURRENT` | 2 | 最大並行處理器數 | +| `MOMENTRY_POLL_INTERVAL` | 5 | 輪詢間隔(秒) | +| `MOMENTRY_WORKER_ENABLED` | true | 是否啟用 Worker | + +**狀態監控**: +```bash +# 查看待處理工作 +psql -U accusys -d momentry -c "SELECT * FROM monitor_jobs WHERE status = 'pending';" + +# 查看執行中工作 +psql -U accusys -d momentry -c "SELECT * FROM monitor_jobs WHERE status = 'running';" + +# 查看處理器狀態 +psql -U accusys -d momentry -c "SELECT * FROM processor_results WHERE job_id = ;" +``` + +--- + +## 服務健康檢查結果 (2026-03-24) + +### ✅ 正常運行的服務 + +| 服務 | 版本 | Port | 測試命令 | 結果 | +|------|------|------|----------|------| +| **PostgreSQL** | 18.1 | 5432 | `pg_isready -h 127.0.0.1 -p 5432` | ✅ 接受連線 | +| **Redis** | 7.4.x | 6379 | `redis-cli -a accusys ping` | ✅ PONG | +| **MongoDB** | 8.2.6 | 27017 | `mongosh --eval "db.adminCommand('ping')"` | ✅ { ok: 1 } | +| **Ollama** | - | 11434 | `curl -s http://localhost:11434/api/tags` | ✅ 模型可用 | +| **n8n** | 2.12.3 | 5678/5681/5682 | `curl -s http://localhost:5678/healthz` | ✅ {"status":"ok"} | +| **n8n Worker** | 2.12.3 | 5681/5690/5691 | - | ✅ 運行中 | +| **Momentry API** | 0.1.0 | 3002 | `curl -s http://localhost:3002/health` | ✅ OK | +| **Momentry Playground** | 0.1.0 | 3003 | `curl -s http://localhost:3003/health` | ✅ OK (開發環境) | +| **Qdrant** | 1.17.0 | 6333 | `curl -s http://localhost:6333/` | ✅ 版本資訊 | +| **Caddy** | 2.10.x | 443 | `curl -sI https://momentry.ddns.net` | ✅ HTTP 200 | +| **SFTPGo** | 2.7.x | 8080 | `curl -s http://localhost:8080/sftpgo/` | ✅ JSON 響應 | +| **Gitea** | - | 3000 | `curl -s http://localhost:3000/` | ✅ HTML 響應 | +| **Gitea MCP** | - | 8787 | `curl -s http://localhost:8787/` | ✅ 運行中 | +| **MariaDB** | 12.1.x | 3306 | `mariadb -u root -e "SELECT 1;"` | ✅ 正常 | +| **PHP-FPM** | 8.5.x | 9000 | `ps aux \| grep php-fpm` | ✅ 運行中 | + +### ⚠️ 需要配置的服務 + +| 服務 | 問題 | 解決方案 | +|------|------|----------| +| Grafana | 使用 Homebrew | 考慮遷移到自定義 plist | +| Prometheus | 使用 Homebrew | 考慮遷移到自定義 plist | +| Kafka | 未遷移 | 可選服務 | +| SeaweedFS | 未遷移 | 可選服務 | +| Netdata | 使用 Homebrew | 考慮遷移到自定義 plist | + +### ⚠️ Homebrew 管理的服務 (建議遷移) + +| 服務 | 風險 | +|------|------| +| homebrew.mxcl.grafana | Reboot 後可能自動啟動但使用預設設定 | +| homebrew.mxcl.prometheus | Reboot 後可能自動啟動但使用預設設定 | +| homebrew.mxcl.openwebui | 需要確認是否需要 | +| homebrew.mxcl.kafka | 需要確認是否需要 | +| homebrew.mxcl.seaweedfs | 需要確認是否需要 | +| homebrew.mxcl.netdata | 需要確認是否需要 | +| homebrew.mxcl.ddclient | 動態 DNS,可能需要 | +| homebrew.mxcl.shadowsocks-rust | VPN,可能需要 | + +### MCP Servers (2026-03-24) + +| Server | 安裝方式 | 路徑 | 狀態 | +|--------|----------|------|------| +| gitea | Homebrew | /opt/homebrew/bin/gitea-mcp-server | ✅ Connected | +| n8n | NPM | /opt/homebrew/bin/mcp-n8n | ✅ Connected | +| postgres | NPM | /opt/homebrew/bin/mcp-server-postgres | ✅ Connected | +| redis | NPM | /opt/homebrew/bin/mcp-server-redis | ✅ Connected | +| mongodb | NPM | /opt/homebrew/bin/mongodb-mcp-server | ✅ Connected | +| qdrant | Python | /opt/homebrew/bin/mcp-server-qdrant | ✅ Connected | +| filesystem | NPM | /opt/homebrew/bin/mcp-server-filesystem | ✅ Connected | +| sentry | NPM | /opt/homebrew/bin/sentry-mcp | ⏳ Pending Config | +| context7 | NPM | /opt/homebrew/bin/context7-mcp | ✅ Connected | +| playwright | NPM | /opt/homebrew/bin/playwright-mcp | ✅ Connected | + +**配置文件**: `~/.config/opencode/opencode.json` + +**驗證命令**: +```bash +opencode mcp ls +``` + +**詳細文檔**: [OpenCode MCP 安裝指南](./OPENCODE_MCP_INSTALL.md) + +### 測試腳本 + +```bash +#!/bin/bash +# services_health_check.sh + +echo "=== Momentry 服務健康檢查 ===" +echo "" + +# PostgreSQL +pg_isready -h 127.0.0.1 -p 5432 -U accusys > /dev/null 2>&1 && echo "✅ PostgreSQL" || echo "❌ PostgreSQL" + +# Redis +redis-cli -a accusys ping > /dev/null 2>&1 && echo "✅ Redis" || echo "❌ Redis" + +# MongoDB +mongosh --quiet --eval "db.adminCommand('ping')" > /dev/null 2>&1 && echo "✅ MongoDB" || echo "❌ MongoDB" + +# Ollama +curl -s http://localhost:11434/api/tags > /dev/null 2>&1 && echo "✅ Ollama" || echo "❌ Ollama" + +# n8n +curl -s http://localhost:5678/ > /dev/null 2>&1 && echo "✅ n8n" || echo "❌ n8n" + +# Momentry API +curl -s http://localhost:3002/health > /dev/null 2>&1 && echo "✅ Momentry API" || echo "❌ Momentry API" + +# Qdrant +curl -s http://localhost:6333/ > /dev/null 2>&1 && echo "✅ Qdrant" || echo "❌ Qdrant" + +# Caddy +curl -sI https://momentry.ddns.net > /dev/null 2>&1 && echo "✅ Caddy" || echo "❌ Caddy" + +# SFTPGo +curl -s http://localhost:8080/api/v2/healthz > /dev/null 2>&1 && echo "✅ SFTPGo" || echo "⚠️ SFTPGo (需配置)" +``` + +--- + +--- + +## 必要服務 (Momentry 核心) + +### 1. PostgreSQL + +#### 安裝 +```bash +# 檢查是否已安裝 +brew list postgresql@18 2>/dev/null || echo "Not installed" + +# 安裝 PostgreSQL 18 +brew install postgresql@18 +``` + +#### 資料目錄 +``` +/Users/accusys/momentry/var/postgresql +``` + +**重要**: 確保使用統一的資料目錄,避免與 homebrew plist 衝突。 + +#### 開機自動啟動 +```bash +# 建立 plist 檔案 +sudo tee /Library/LaunchDaemons/com.momentry.postgresql.plist > /dev/null <<'EOF' + + + + + Label + com.momentry.postgresql + UserName + accusys + EnvironmentVariables + + LC_ALL + en_US.UTF-8 + + WorkingDirectory + /Users/accusys/momentry/var/postgresql + ProgramArguments + + /opt/homebrew/opt/postgresql@18/bin/postgres + -D + /Users/accusys/momentry/var/postgresql + + RunAtLoad + + KeepAlive + + StandardErrorPath + /Users/accusys/momentry/log/postgresql.error.log + StandardOutPath + /Users/accusys/momentry/log/postgresql.log + + +EOF + +# 載入服務 +sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.postgresql.plist +``` + +#### 管理命令 +```bash +# 啟動 +sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.postgresql.plist + +# 停止 +sudo launchctl bootout system/com.momentry.postgresql.plist + +# 重新載入 +sudo launchctl bootout system/com.momentry.postgresql.plist +sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.postgresql.plist + +# 查看狀態 +launchctl list | grep com.momentry.postgresql +``` + +#### 健康檢查 +```bash +# 方法 1: 使用 pg_isready +pg_isready -h localhost -p 5432 -U accusys + +# 方法 2: 連線測試 +PGPASSWORD=n8n1234 psql -h localhost -U n8n -d n8n -c "SELECT 1;" + +# 方法 3: 檢查程序 +pgrep -a postgres + +# 方法 4: 檢查資料庫 +PGPASSWORD=n8n1234 psql -h localhost -U n8n -d n8n -c "SELECT COUNT(*) FROM workflow_entity;" +``` + +--- + +### 2. Redis + +#### 安裝 +```bash +# 檢查是否已安裝 +brew list redis 2>/dev/null || echo "Not installed" + +# 安裝 Redis +brew install redis +``` + +#### 設定密碼 +```bash +# 編輯 Redis 設定檔 +vim /opt/homebrew/etc/redis.conf + +# 找到 requirepass 行,修改為: +requirepass accusys + +# 或使用環境變數方式啟動 +``` + +#### 開機自動啟動 +```bash +# 建立 plist 檔案 +sudo tee /Library/LaunchDaemons/com.momentry.redis.plist > /dev/null <<'EOF' + + + + + Label + com.momentry.redis + UserName + accusys + WorkingDirectory + /Users/accusys/momentry/var/redis + ProgramArguments + + /opt/homebrew/opt/redis/bin/redis-server + /opt/homebrew/etc/redis.conf + + EnvironmentVariables + + REDIS_PASSWORD + accusys + + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/accusys/momentry/log/redis.log + StandardErrorPath + /Users/accusys/momentry/log/redis.error.log + + +EOF + +# 載入服務 +sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist +``` + +#### 管理命令 +```bash +# 啟動 +sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist + +# 停止 +sudo launchctl unload /Library/LaunchDaemons/com.momentry.redis.plist + +# 重啟 +sudo launchctl unload /Library/LaunchDaemons/com.momentry.redis.plist +sudo launchctl load /Library/LaunchDaemons/com.momentry.redis.plist + +# 查看狀態 +launchctl list | grep com.momentry.redis + +# 查看日誌 +tail -f /Users/accusys/momentry/log/redis.log +tail -f /Users/accusys/momentry/log/redis.error.log +``` + +#### 健康檢查 +```bash +# 方法 1: 使用 redis-cli ping +redis-cli -a accusys ping + +# 輸出應為: PONG + +# 方法 2: 檢查密碼認證 +redis-cli -a accusys AUTH accusys + +# 方法 3: 檢查程序 +pgrep -f redis-server + +# 方法 4: 檢查連線數 +redis-cli -a accusys INFO clients +``` + +--- + +### 3. Ollama + +#### 安裝 +```bash +# 檢查是否已安裝 +which ollama || echo "Not installed" + +# 安裝 Ollama +brew install ollama +``` + +#### 模型下載 +```bash +# 下載 Mistral (LLM) +ollama pull mistral:latest + +# 下載 Embedding 模型 +ollama pull nomic-embed-text:latest + +# 驗證模型 +ollama list +``` + +#### 開機自動啟動 +```bash +# 建立 plist 檔案 +sudo tee /Library/LaunchDaemons/com.momentry.ollama.plist > /dev/null <<'EOF' + + + + + Label + com.momentry.ollama + UserName + accusys + WorkingDirectory + /Users/accusys/momentry/var/ollama + ProgramArguments + + /opt/homebrew/bin/ollama + serve + + EnvironmentVariables + + OLLAMA_HOST + 0.0.0.0:11434 + OLLAMA_MODELS + /Users/accusys/momentry/var/ollama/models + + RunAtLoad + + KeepAlive + + StandardOutPath + /Users/accusys/momentry/log/ollama.log + StandardErrorPath + /Users/accusys/momentry/log/ollama.error.log + + +EOF + +# 載入服務 +sudo launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist +``` + +#### 管理命令 +```bash +# 啟動 +sudo launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist + +# 停止 +sudo launchctl unload /Library/LaunchDaemons/com.momentry.ollama.plist + +# 重啟 +sudo launchctl unload /Library/LaunchDaemons/com.momentry.ollama.plist +sudo launchctl load /Library/LaunchDaemons/com.momentry.ollama.plist + +# 查看狀態 +launchctl list | grep com.momentry.ollama + +# 查看日誌 +tail -f /Users/accusys/momentry/log/ollama.log +tail -f /Users/accusys/momentry/log/ollama.error.log +``` + +#### 健康檢查 +```bash +# 方法 1: API 測試 +curl -s http://localhost:11434/api/tags | jq '.models[].name' + +# 方法 2: 檢查程序 +pgrep -f ollama + +# 方法 3: 列出模型 +ollama list +``` + +--- + +### 4. n8n (工作流自動化) + +#### 安裝 +```bash +# 檢查是否已安裝 +which n8n || echo "Not installed" + +# 安裝 n8n +brew install n8n +``` + +#### 開機自動啟動 +```bash +# 複製 plist 到 LaunchDaemons 目錄 +sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.n8n.main.plist /Library/LaunchDaemons/ +sudo cp /Users/accusys/momentry_core_0.1/momentry_runtime/plist/com.momentry.n8n.worker.plist /Library/LaunchDaemons/ + +# 載入服務 +sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist +sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.worker.plist +``` + +#### 管理命令 +```bash +# 啟動 +sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.main.plist +sudo launchctl load /Library/LaunchDaemons/com.momentry.n8n.worker.plist + +# 停止 +sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.main.plist +sudo launchctl unload /Library/LaunchDaemons/com.momentry.n8n.worker.plist +``` + +#### 健康檢查 +```bash +# 方法 1: API 測試 +curl -s http://localhost:5678/ + +# 方法 2: 檢查程序 +ps aux | grep n8n | grep -v grep + +# 方法 3: 檢查端口 +lsof -i :5678 +lsof -i :5679 +``` + +--- + +## 可選服務 + +### 4. Caddy (網頁伺服器) + +```bash +# 安裝 +brew install caddy + +# 開機啟動 +cp /opt/homebrew.mxcl.caddy.plist /Library/LaunchDaemons/ +sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.caddy.plist + +# 健康檢查 +curl -s https://localhost:2019/config/ | head -5 +``` + +### 5. Grafana (監控) + +```bash +# 安裝 +brew install grafana + +# 開機啟動 +cp /opt/homebrew.mxcl.grafana.plist ~/Library/LaunchAgents/ +launchctl load ~/Library/LaunchAgents/homebrew.mxcl.grafana.plist + +# 健康檢查 +curl -s http://localhost:3000/api/health | jq '.' +``` + +### 6. Prometheus (監控) + +```bash +# 安裝 +brew install prometheus + +# 開機啟動 +cp /opt/homebrew.mxcl.prometheus.plist ~/Library/LaunchAgents/ +launchctl load ~/Library/LaunchAgents/homebrew.mxcl.prometheus.plist + +# 健康檢查 +curl -s http://localhost:9090/-/healthy +``` + +### 7. Netdata (系統監控) + +```bash +# 安裝 +brew install netdata + +# 開機啟動 +sudo brew services start netdata + +# 健康檢查 +curl -s http://localhost:19999/api/v1/info | jq '.version' +``` + +--- + +## 統一健康檢查腳本 + +建立 `/Users/accusys/momentry_core_0.1/scripts/health_check.sh`: + +```bash +#!/bin/bash + +# Momentry 系統健康檢查腳本 + +echo "========================================" +echo "Momentry 系統健康檢查" +echo "========================================" +echo "" + +# 顏色定義 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +check_service() { + local name=$1 + local check_cmd=$2 + + if eval "$check_cmd" > /dev/null 2>&1; then + echo -e "${GREEN}✓${NC} $name" + return 0 + else + echo -e "${RED}✗${NC} $name" + return 1 + fi +} + +total=0 +passed=0 + +# 1. PostgreSQL +total=$((total + 1)) +check_service "PostgreSQL (localhost:5432)" "pg_isready -h localhost -p 5432 -U accusys" && passed=$((passed + 1)) + +# 2. Redis +total=$((total + 1)) +check_service "Redis (localhost:6379)" "redis-cli -a accusys ping" && passed=$((passed + 1)) + +# 3. Ollama +total=$((total + 1)) +check_service "Ollama (localhost:11434)" "curl -s http://localhost:11434/api/tags > /dev/null" && passed=$((passed + 1)) + +# 4. n8n +total=$((total + 1)) +check_service "n8n (localhost:5678)" "curl -s http://localhost:5678/ > /dev/null" && passed=$((passed + 1)) + +# 5. Grafana (如果安裝) +if command -v grafana-server > /dev/null 2>&1; then + total=$((total + 1)) + check_service "Grafana (localhost:3000)" "curl -s http://localhost:3000/api/health > /dev/null" && passed=$((passed + 1)) +fi + +# 6. Prometheus (如果安裝) +if command -v prometheus > /dev/null 2>&1; then + total=$((total + 1)) + check_service "Prometheus (localhost:9090)" "curl -s http://localhost:9090/-/healthy > /dev/null" && passed=$((passed + 1)) +fi + +# 7. Netdata (如果安裝) +if command -v netdata > /dev/null 2>&1; then + total=$((total + 1)) + check_service "Netdata (localhost:19999)" "curl -s http://localhost:19999/api/v1/info > /dev/null" && passed=$((passed + 1)) +fi + +echo "" +echo "========================================" +echo "結果: $passed/$total 服務正常" +echo "========================================" + +if [ $passed -eq $total ]; then + exit 0 +else + exit 1 +fi +``` + +使用方式: +```bash +chmod +x scripts/health_check.sh +./scripts/health_check.sh +``` + +--- + +## 服務管理速查表 + +### 啟動服務 (使用 launchctl bootstrap) +```bash +# 所有服務 +for plist in /Library/LaunchDaemons/com.momentry.*.plist; do + sudo launchctl bootstrap system "$plist" +done + +# PostgreSQL +sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.postgresql.plist + +# Redis, Ollama, Qdrant +sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.redis.plist +sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.ollama.plist +sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.qdrant.plist + +# n8n (main + worker) +sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.n8n.main.plist +sudo launchctl bootstrap system /Library/LaunchDaemons/com.momentry.n8n.worker.plist +``` + +### 停止服務 (使用 launchctl bootout) +```bash +# 所有 Momentry 服務 +for svc in $(launchctl list | grep com.momentry | awk '{print $3}'); do + sudo launchctl bootout system/$svc 2>/dev/null +done + +# PostgreSQL +sudo launchctl bootout system/com.momentry.postgresql.plist + +# Redis, Ollama, Qdrant +sudo launchctl bootout system/com.momentry.redis.plist +sudo launchctl bootout system/com.momentry.ollama.plist +sudo launchctl bootout system/com.momentry.qdrant.plist + +# n8n +sudo launchctl bootout system/com.momentry.n8n.main.plist +sudo launchctl bootout system/com.momentry.n8n.worker.plist +``` + +### 查詢服務狀態 +```bash +# 查看所有 Momentry 服務 +launchctl list | grep com.momentry + +# 查看特定服務 +launchctl list | grep com.momentry.postgresql +launchctl list | grep com.momentry.n8n +``` + +--- + +## 故障排除 + +### PostgreSQL 問題 + +```bash +# 查看日誌 +tail -f /opt/homebrew/var/postgresql@18/logfile + +# 重新初始化 +pg_ctl -D /opt/homebrew/var/postgresql@18 stop +rm -rf /opt/homebrew/var/postgresql@18 +initdb -D /opt/homebrew/var/postgresql@18 + +# 重建資料庫 +dropdb momentry +createdb -U accusys momentry +``` + +### Redis 問題 + +```bash +# 查看日誌 +tail -f /opt/homebrew/var/log/redis.log + +# 測試連線 +redis-cli -a accusys DEBUG SLEEP 1 + +# 重新整理 ACL +redis-cli -a accusys FLUSHALL +``` + +### Ollama 問題 + +```bash +# 查看日誌 +tail -f ~/.ollama/logs/server.log + +# 重新下載模型 +ollama pull mistral:latest +ollama pull nomic-embed-text:latest + +# 檢查 GPU 使用情況 +ollama list +``` + +--- + +## 自動化腳本 + +建立 `/Users/accusys/momentry_core_0.1/scripts/service_manager.sh`: + +```bash +#!/bin/bash + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PLIST_DIR="$SCRIPT_DIR/../momentry_runtime/plist" + +action=${1:-start} +service=${2:-all} + +start_postgresql() { + echo "Starting PostgreSQL..." + sudo launchctl load /Library/LaunchDaemons/com.momentry.postgresql.plist 2>/dev/null || \ + echo "PostgreSQL plist not found, skipping..." +} + +start_redis() { + echo "Starting Redis..." + launchctl load ~/Library/LaunchAgents/com.momentry.redis.plist 2>/dev/null || \ + echo "Redis plist not found, skipping..." +} + +start_ollama() { + echo "Starting Ollama..." + launchctl load ~/Library/LaunchAgents/com.momentry.ollama.plist 2>/dev/null || \ + echo "Ollama plist not found, skipping..." +} + +stop_postgresql() { + echo "Stopping PostgreSQL..." + sudo launchctl unload /Library/LaunchDaemons/com.momentry.postgresql.plist 2>/dev/null || true +} + +stop_redis() { + echo "Stopping Redis..." + launchctl unload ~/Library/LaunchAgents/com.momentry.redis.plist 2>/dev/null || true +} + +stop_ollama() { + echo "Stopping Ollama..." + launchctl unload ~/Library/LaunchAgents/com.momentry.ollama.plist 2>/dev/null || true +} + +case $action in + start) + case $service in + all) + start_postgresql + start_redis + start_ollama + ;; + postgresql|pgsql|pg) + start_postgresql + ;; + redis) + start_redis + ;; + ollama) + start_ollama + ;; + *) + echo "Unknown service: $service" + exit 1 + ;; + esac + ;; + stop) + case $service in + all) + stop_ollama + stop_redis + stop_postgresql + ;; + postgresql|pgsql|pg) + stop_postgresql + ;; + redis) + stop_redis + ;; + ollama) + stop_ollama + ;; + *) + echo "Unknown service: $service" + exit 1 + ;; + esac + ;; + restart) + $0 stop $service + sleep 2 + $0 start $service + ;; + status) + echo "Service Status:" + echo "===============" + launchctl list | grep -E "(postgres|redis|ollama)" || echo "No services found" + ;; + *) + echo "Usage: $0 {start|stop|restart|status} [service]" + echo "Services: all, postgresql, redis, ollama" + exit 1 + ;; +esac +``` + +--- + +## 服務快速參照 + +### Port 對照表 + +| Port | 服務 | 說明 | +|------|------|------| +| 11434 | Ollama | LLM API | +| 19999 | Netdata | 系統監控 | +| 2019 | Caddy | 管理 API | +| 21115-21119 | RustDesk | 遠端桌面 | +| 27017 | MongoDB | 文件資料庫 | +| 3000 | Gitea | Git 服務 | +| 3001 | Grafana | 監控儀表板 | +| 3002 | Momentry API | Rust API 伺服器 (生產環境) | +| 3003 | Momentry Playground | Rust API 伺服器 (開發環境) | +| 3306 | MariaDB | MySQL 相容資料庫 | +| 4096 | OpenCode | CLI 工具 | +| 5000-7000 | ControlCenter | 控制中心 | +| 5678 | n8n | 工作流自動化 (Main) | +| 5681 | n8n | Worker HTTP | +| 5682 | n8n | Worker Health Check | +| 5690 | n8n | Task Broker | +| 5691 | n8n | Runner Health | +| 6333-6334 | Qdrant | 向量資料庫 | +| 6379 | Redis | 快取/佇列 | +| 8080 | SFTPGo | HTTP/WebDAV | +| 8081 | Trunk | 轉發服務 | +| 8082 | SeaweedFS | Master | +| 8090 | SFTPGo | WebDAV | +| 8333 | SeaweedFS | Volume | +| 8388 | Shadowsocks | VPN | +| 8888 | SeaweedFS | Filer | +| 9000 | PHP-FPM | PHP 處理器 | +| 9090 | Prometheus | 指標收集 | +| 9092-9093 | Kafka | 訊息佇列 | +| 9333 | SeaweedFS | Volume | +| 18082 | SeaweedFS | Volume | +| 18333 | SeaweedFS | Volume | +| 18888 | SeaweedFS | Filer | +| 19333 | SeaweedFS | Volume | + +## Caddy 反向代理 URL 對照表 + +| URL | 內部服務 | 說明 | +|-----|----------|------| +| `n8n.momentry.ddns.net` | :5678 | n8n 工作流自動化 | +| `wp.momentry.ddns.net` | :9000 | WordPress 網站 | +| `seaweed.momentry.ddns.net` | :8888 | SeaweedFS Filer | +| `sftpgo.momentry.ddns.net` | :8080 | SFTPGo HTTP | +| `webdav.momentry.ddns.net` | :8090 | SFTPGo WebDAV | +| `qdrant.momentry.ddns.net` | :6333 | Qdrant 向量資料庫 | +| `gitea.momentry.ddns.net` | :3000 | Gitea Git 服務 | +| `chat.momentry.ddns.net` | :8085 | Open WebUI | +| `netdata.momentry.ddns.net` | :19999 | Netdata 監控 | +| `grafana.momentry.ddns.net` | :3001 | Grafana 儀表板 | +| `router5.momentry.ddns.net` | 192.168.5.1:80 | Router 5 | +| `router110.momentry.ddns.net` | 192.168.110.1:80 | Router 110 | +| `router0.momentry.ddns.net/admin/*` | 192.168.0.1:80 | Router 0 | +| `truenas.momentry.ddns.net` | 192.168.0.219:80 | TrueNAS | +| `:3200` | :3002 | Momentry Dashboard + API | + +**Caddy 管理**: https://localhost:2019/ + +--- + +## 目錄對照表 + +### /Users/accusys/momentry/var/ + +| 目錄 | 服務 | 說明 | +|------|------|------| +| `caddy/` | Caddy | 資料目錄 | +| `gitea/` | Gitea | Git 資料庫 | +| `mariadb/` | MariaDB | 資料庫檔案 | +| `mongodb/` | MongoDB | 文件資料庫 (自定義 plist) | +| `n8n/` | n8n | 工作流資料 | +| `ollama/` | Ollama | 模型快取 | +| `php/` | PHP | FastCGI 進程 | +| `postgresql/` | PostgreSQL | 主資料庫 (自定義 plist) | +| `qdrant/` | Qdrant | 向量資料庫 | +| `redis/` | Redis | 持久化檔案 | +| `rustdesk/` | RustDesk | ID 資料庫 | +| `sftpgo/` | SFTPGo | 使用者資料 | +| `sftpgo_backup/` | SFTPGo | 備份 | + +### /opt/homebrew/var/ (Homebrew 管理) + +| 目錄 | 服務 | 說明 | +|------|------|------| +| `mongodb/` | MongoDB | (舊) 文件資料庫 | +| `postgresql@18/` | PostgreSQL | (舊) 主資料庫 | + +### /Users/accusys/momentry/etc/ + +| 目錄 | 服務 | 說明 | +|------|------|------| +| `caddy/` | Caddy | 配置 | +| `gitea/` | Gitea | 配置 | +| `php/` | PHP | 配置 | +| `sftpgo/` | SFTPGo | 配置 | + +### /Users/accusys/momentry/log/ + +| 檔案 | 服務 | 說明 | +|------|------|------| +| `backup.log` | Backup | 備份日誌 | +| `redis.log` | Redis | 執行日誌 | +| `redis.error.log` | Redis | 錯誤日誌 | +| `ollama.log` | Ollama | 執行日誌 | +| `ollama.error.log` | Ollama | 錯誤日誌 | + +### /Users/accusys/momentry/backup/ + +| 目錄 | 說明 | +|------|------| +| `daily/` | 每日備份 | +| `weekly/` | 每週備份 | +| `monthly/` | 每月備份 | + +--- + +## 附錄: 服務版本對應表 + +| 服務 | 版本 | Port | 使用者 | plist 位置 | 資料目錄 | +|------|------|------|--------|-------------|----------| +| PostgreSQL | 18.1 | 5432 | accusys | /Library/LaunchDaemons/ | /Users/accusys/momentry/var/postgresql | +| Redis | 7.4.x | 6379 | accusys | /Library/LaunchDaemons/ | /opt/homebrew/var/redis | +| Ollama | 0.13.5 | 11434 | accusys | /Library/LaunchDaemons/ | /Users/accusys/momentry/var/ollama/models | +| n8n | 2.12.3 | 5678/5681 | accusys | /Library/LaunchDaemons/ | /Users/accusys/momentry/var/n8n | +| Node.js (n8n) | 22.22.1 | - | - | /opt/homebrew/opt/node@22/ | - | +| Python (Momentry) | 3.11.14 | - | - | /opt/homebrew/bin/python3.11 | - | +| Caddy | 2.10.x | 2019/443 | root | /Library/LaunchDaemons/ | /Users/accusys/momentry/var/caddy | +| Gitea | - | 3000 | accusys | /Library/LaunchDaemons/ | /Users/accusys/momentry/var/gitea | +| Gitea MCP | - | 8787 | accusys | /Library/LaunchDaemons/ | - | +| SFTPGo | 2.7.x | 8080/2022 | accusys | /Library/LaunchDaemons/ | /Users/accusys/momentry/var/sftpgo | +| Qdrant | 1.17.0 | 6333 | accusys | /Library/LaunchDaemons/ | /Users/accusys/.local/share/qdrant | +| MongoDB | 8.2.6 | 27017 | root | /Library/LaunchDaemons/ | /opt/homebrew/var/mongodb | +| MariaDB | 12.1.x | 3306 | accusys | /Library/LaunchDaemons/ | /Users/accusys/momentry/var/mariadb | +| RustDesk HBBR | - | 21115 | accusys | /Library/LaunchDaemons/ | - | +| RustDesk HBBS | - | 21115-21119 | accusys | /Library/LaunchDaemons/ | /Users/accusys/momentry/var/rustdesk | +| PHP | 8.5.x | 9000 | - | /Library/LaunchDaemons/ | - | +| Momentry API | 0.1.0 | 3002 | accusys | /Library/LaunchDaemons/ | - | +| Momentry Playground | 0.1.0 | 3003 | accusys | - | - | + +--- + +## 重要密碼與金鑰 + +| 服務 | 用途 | 預設值 | +|------|------|--------| +| **PostgreSQL** | 連線密碼 (accusys) | `accusys` | +| **Redis** | 認證密碼 | `accusys` | +| **Qdrant** | API Key | `Test3200Test3200Test3200` | +| **n8n** | 資料庫密碼 | `n8n` (PostgreSQL) | +| **SFTPGo** | 安裝碼 | `Test3200Test3200` | +| **SFTPGo** | DB 密碼 | `sftpgo_pass_2026` | +| **Momentry API** | 資料庫 | `postgres://accusys:accusys@127.0.0.1:5432/momentry` | diff --git a/docs_v1.0/OPERATIONS/SFTPGO_DEMO_USER.md b/docs_v1.0/OPERATIONS/SFTPGO_DEMO_USER.md new file mode 100644 index 0000000..fa3e045 --- /dev/null +++ b/docs_v1.0/OPERATIONS/SFTPGO_DEMO_USER.md @@ -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 <" + exit 1 +fi + +sshpass -p "$PASS" sftp -P $PORT $USER@$HOST < {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 <EnvironmentVariables + + SFTPGO_DEFAULT_ADMIN_USERNAME + admin + SFTPGO_DEFAULT_ADMIN_PASSWORD + Test3200Test3200 + +``` + +#### 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` 目錄 +- **監控**: 所有上傳都有日誌記錄 +- **生產環境**: 正式環境應使用更強的密碼和金鑰認證 diff --git a/docs_v1.0/OPERATIONS/SFTPGo_Lifecycle.md b/docs_v1.0/OPERATIONS/SFTPGo_Lifecycle.md new file mode 100644 index 0000000..be7e495 --- /dev/null +++ b/docs_v1.0/OPERATIONS/SFTPGo_Lifecycle.md @@ -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 +ProgramArguments + + /Users/accusys/bin/sftpgo + serve + -c + /Users/accusys/momentry/etc/sftpgo/ + +``` + +## ⑥ 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/` | diff --git a/docs_v1.0/OPERATIONS/SFTPGo_Setup.md b/docs_v1.0/OPERATIONS/SFTPGo_Setup.md new file mode 100644 index 0000000..c67e884 --- /dev/null +++ b/docs_v1.0/OPERATIONS/SFTPGo_Setup.md @@ -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 ` 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 + +ProgramArguments + + /Users/accusys/bin/sftpgo + serve + -c + /Users/accusys/momentry/etc/sftpgo/ + +``` + +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) | diff --git a/docs_v1.0/OPERATIONS/SFTPGo_Verification.md b/docs_v1.0/OPERATIONS/SFTPGo_Verification.md new file mode 100644 index 0000000..ebfb7a0 --- /dev/null +++ b/docs_v1.0/OPERATIONS/SFTPGo_Verification.md @@ -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` | ✅ | diff --git a/docs_v1.0/OPERATIONS/Services_Inventory.md b/docs_v1.0/OPERATIONS/Services_Inventory.md new file mode 100644 index 0000000..3ae8b0c --- /dev/null +++ b/docs_v1.0/OPERATIONS/Services_Inventory.md @@ -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 +``` diff --git a/docs_v1.0/OPERATIONS/VERSION_MANAGEMENT.md b/docs_v1.0/OPERATIONS/VERSION_MANAGEMENT.md new file mode 100644 index 0000000..ed31d50 --- /dev/null +++ b/docs_v1.0/OPERATIONS/VERSION_MANAGEMENT.md @@ -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 +``` diff --git a/docs_v1.0/OPERATIONS/maintenance_records/rca/_completed/RCA_MARKBASE_HTML_PREVIEW_SCREENSHOT_2026_05_15.md b/docs_v1.0/OPERATIONS/maintenance_records/rca/_completed/RCA_MARKBASE_HTML_PREVIEW_SCREENSHOT_2026_05_15.md new file mode 100644 index 0000000..4d47220 --- /dev/null +++ b/docs_v1.0/OPERATIONS/maintenance_records/rca/_completed/RCA_MARKBASE_HTML_PREVIEW_SCREENSHOT_2026_05_15.md @@ -0,0 +1,321 @@ +--- +document_type: "rca" +service: "MARKBASE" +title: "RCA:HTML 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" +--- + +# RCA:HTML 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 內的 `"; + +// After +h+=""; +``` + +**修復 3:page.html — quickPreview** +```javascript +// Before +inner=""; + +// After +inner=""; +``` + +**修復 4:page.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 `