# Momentry Core 數據管理設計文檔 (v4) | 項目 | 內容 | |------|------| | 建立者 | Warren | | 建立時間 | 2026-03-17 | | 文件版本 | V1.0 | --- ## 版本歷史 | 版本 | 日期 | 目的 | 操作人 | 工具/模型 | |------|------|------|--------|-----------| | V1.0 | 2026-03-17 | 創建文件 | Warren | OpenCode / MiniMax M2.5 | --- ## 0. 核心概念:雙 UUID 系統 為減少資料庫大小,在現有的 videos 表中增加內部 ID 映射: ### 0.1 設計原則 - **external_uuid**: 用戶可見的識別碼(如 UUID) - **id**: 資料庫自動產生的內部 ID (SERIAL),節省空間 - **映射關係**: 透過 videos 表的 `id` 欄位關聯 ### 0.2 videos 表 (檔案映射表) 現有結構,增加 `id` 作為內部 ID: ```sql -- 現有 videos 表結構 CREATE TABLE videos ( id SERIAL PRIMARY KEY, -- 內部 ID (自動產生) uuid VARCHAR(32) UNIQUE NOT NULL, -- 外部 UUID (用戶可見) file_name VARCHAR(255) NOT NULL, file_path TEXT, duration DOUBLE PRECISION, width INTEGER, height INTEGER, fps DOUBLE PRECISION, probe_json JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_videos_uuid ON videos(uuid); ``` ### 0.3 對照的好處 | 方式 | 儲存空間 (1000個視頻,每個1000個chunk) | |------|---------------------------------------| | 直接用 uuid (32字元) | ~32MB | | 使用 id (4字元) | ~4MB | ## 1. 數據流架構 ``` ┌─────────────────────────────────────────────────────────────────────────────┐ │ 輸入階段 │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ 視頻文件 │→ │ ffprobe │ │ ASR │ │ YOLO │ │ │ │ (.mp4) │→ │ (probe) │→ │ (asr) │→ │ (yolo) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ ASRX │ │ CUT │ │ OCR │ │ FACE │ │ │ │ (asrx) │→ │ (cut) │→ │ (ocr) │→ │ (face) │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────────────┐ │ Pre-Chunk / Frame 階段 │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ pre_chunks 表 │ │ │ │ file_id → videos.id (FK) │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ type=sentence │ from asr, asrx │ 句子邊界範圍 │ │ │ │ │ │ type=cut │ from cut detection │ 場景切換範圍 │ │ │ │ │ │ type=time │ from time split │ 固定時間範圍 (10s) │ │ │ │ │ │ type=trace │ from yolo trace │ 物件追蹤範圍 │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ frames 表 │ │ │ │ file_id → videos.id (FK) │ │ │ │ - yolo 每幀識別結果 │ │ │ │ - ocr 每幀識別結果 │ │ │ │ - face 每幀識別結果 (如需要) │ │ │ │ - 單一圖像識別結果 → 直接入 frame │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────────────┐ │ Chunk 階段 │ │ │ │ ┌─────────────────────────────────────────────────────────────────────┐ │ │ │ chunks 表 │ │ │ │ file_id → videos.id (FK) │ │ │ │ │ │ │ │ 組合規則1: pre_chunk → chunk (直接轉換) │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ sentence_pre_chunk → sentence_chunk │ │ │ │ │ │ cut_pre_chunk → cut_chunk │ │ │ │ │ │ time_pre_chunk → time_chunk │ │ │ │ │ │ trace_pre_chunk → trace_chunk │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ 組合規則2: pre_chunk + frame 內容 → chunk (集合內容) │ │ │ │ ┌─────────────────────────────────────────────────────────────┐ │ │ │ │ │ sentence_pre_chunk + 涵蓋範圍內的 frames → 豐富的 sentence_chunk │ │ │ │ │ │ time_pre_chunk + 涵蓋範圍內的 frames → 豐富的 time_chunk │ │ │ │ │ └─────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ ↓ ┌─────────────────────────────────────────────────────────────────────────────┐ │ Vector 階段 │ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │ │ PostgreSQL vectors │ │ Qdrant vectors │ │ │ │ (chunk_vectors) │ │ (chunk_v3) │ │ │ └──────────────────────┘ └──────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────────┘ ``` ## 2. Pre-Chunk 類型定義 ### 2.1 Pre-Chunk 來源與類型對照表 | 來源類型 | source_type | 產出 Pre-Chunk Type | 說明 | |---------|-------------|---------------------|------| | ASR ( Whisper ) | asr | sentence | 句子邊界 | | ASRX ( with timestamps ) | asrx | sentence | 帶時間戳的句子 | | CUT (場景檢測) | cut | cut | 場景切換點 | | TIME (固定時間) | time | time | 每 10 秒 | | YOLO Trace | yolo_trace | trace | 物件追蹤軌跡 | | YOLO (單幀) | yolo | **→ frame** | 不入 pre_chunk | | OCR (單幀) | ocr | **→ frame** | 不入 pre_chunk | | FACE (單幀) | face | **→ frame** | 不入 pre_chunk | | PROBE | probe | metadata | 視頻元數據 | ### 2.2 Pre-Chunk Schema ```sql CREATE TABLE pre_chunks ( id SERIAL PRIMARY KEY, -- 檔案識別 (使用 videos 表的內部 ID 以節省空間) file_id INTEGER NOT NULL REFERENCES videos(id), -- 來源識別 source_type VARCHAR(32) NOT NULL, -- 'asr', 'asrx', 'cut', 'time', 'yolo_trace', 'probe' source_file TEXT, -- 原始 JSON 文件路徑 -- Chunk 類型 chunk_type VARCHAR(32) NOT NULL, -- 'sentence', 'cut', 'time', 'trace' -- 時間範圍 start_time DOUBLE PRECISION NOT NULL, end_time DOUBLE PRECISION NOT NULL, -- Frame 範圍 (精確到 frame) start_frame INTEGER NOT NULL, end_frame INTEGER NOT NULL, -- FPS (用於 frame 計算) fps DOUBLE PRECISION NOT NULL, -- 原始 JSON 內容 raw_json JSONB NOT NULL, -- 解析後的文字內容 (如有) text_content TEXT, -- 處理狀態 processed BOOLEAN DEFAULT FALSE, chunk_id VARCHAR(64), -- 轉換後的 chunk_id created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(file_id, source_type, start_frame, end_frame) ); CREATE INDEX idx_pre_chunks_file_id ON pre_chunks(file_id); CREATE INDEX idx_pre_chunks_type ON pre_chunks(file_id, chunk_type); CREATE INDEX idx_pre_chunks_time ON pre_chunks(file_id, start_time, end_time); CREATE INDEX idx_pre_chunks_frame ON pre_chunks(file_id, start_frame, end_frame); CREATE INDEX idx_pre_chunks_processed ON pre_chunks(file_id, processed); ``` ## 3. Frame 管理原則 ### 3.1 哪些數據進入 Frame 只儲存**單一圖像識別**的結果: - YOLO 每幀檢測結果 - OCR 每幀識別結果 - FACE 每幀檢測結果 ### 3.2 Frame Schema ```sql CREATE TABLE frames ( id SERIAL PRIMARY KEY, -- 檔案識別 (使用 videos 表的內部 ID 以節省空間) file_id INTEGER NOT NULL REFERENCES videos(id), frame_number INTEGER NOT NULL, timestamp DOUBLE PRECISION NOT NULL, fps DOUBLE PRECISION NOT NULL, -- YOLO 結果 (JSONB 陣列) yolo_objects JSONB, -- OCR 結果 (JSONB 陣列) ocr_results JSONB, -- Face 結果 (JSONB 陣列) face_results JSONB, -- 原始幀圖像路徑 (可選) frame_path TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(file_id, frame_number) ); CREATE INDEX idx_frames_file_id ON frames(file_id); CREATE INDEX idx_frames_frame ON frames(file_id, frame_number); CREATE INDEX idx_frames_timestamp ON frames(file_id, timestamp); ``` ## 4. Chunk 組合規則 ### 4.1 組合規則 1: 直接轉換 (rule_1) 將 pre_chunk 直接轉換為 chunk: ``` sentence_pre_chunk → sentence_chunk (rule: "rule_1") cut_pre_chunk → cut_chunk (rule: "rule_1") time_pre_chunk → time_chunk (rule: "rule_1") trace_pre_chunk → trace_chunk (rule: "rule_1") ``` ### 4.2 組合規則 2: 集合內容 (rule_2) 將 pre_chunk 與其時間區間內的所有 frame 識別結果集合: ``` sentence_pre_chunk + frames[在 start_time~end_time 範圍內] → 豐富的 sentence_chunk (rule: "rule_2") time_pre_chunk + frames[在 start_time~end_time 範圍內] → 豐富的 time_chunk (rule: "rule_2") ``` ### 4.3 Chunk Schema ```sql CREATE TABLE chunks ( id SERIAL PRIMARY KEY, -- 檔案識別 (使用 videos 表的內部 ID 以節省空間) file_id INTEGER NOT NULL REFERENCES videos(id), chunk_id VARCHAR(64) NOT NULL, chunk_index INTEGER NOT NULL, chunk_type VARCHAR(32) NOT NULL, -- 'sentence', 'cut', 'time', 'trace' -- 組合規則 (payload 中記錄) -- rule: 'rule_1' = 直接轉換, 'rule_2' = 集合內容 -- 時間範圍 start_time DOUBLE PRECISION NOT NULL, end_time DOUBLE PRECISION NOT NULL, -- Frame 範圍 (精確到 frame) start_frame INTEGER NOT NULL, end_frame INTEGER NOT NULL, -- FPS fps DOUBLE PRECISION NOT NULL, -- 主要內容 text_content TEXT, -- 完整內容 (JSONB) - 包含 rule 欄位 content JSONB NOT NULL, -- 來源的 pre_chunk IDs pre_chunk_ids INTEGER[], -- 包含的 frame 數量 frame_count INTEGER DEFAULT 0, -- 向量 ID vector_id VARCHAR(64), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(file_id, chunk_id) ); CREATE INDEX idx_chunks_file_id ON chunks(file_id); CREATE INDEX idx_chunks_type ON chunks(file_id, chunk_type); CREATE INDEX idx_chunks_time ON chunks(file_id, start_time, end_time); CREATE INDEX idx_chunks_frame ON chunks(file_id, start_frame, end_frame); CREATE INDEX idx_chunks_vector ON chunks(vector_id); ``` ## 5. 處理流程範例 ### 5.1 輸入數據 假設視頻長度 30 秒,fps=30: | 來源 | 產出 | |------|------| | ASR | 3 個 sentence_pre_chunk (每句約 10s) | | CUT | 2 個 cut_pre_chunk (場景 1, 場景 2) | | TIME | 3 個 time_pre_chunk (0-10s, 10-20s, 20-30s) | | YOLO | 900 個 frame 記錄 (每幀) | | OCR | 依實際識別結果入 frame | ### 5.2 Chunk 產出 **使用規則 1 (直接轉換):** - rule: "rule_1" - 3 個 sentence_chunk - 2 個 cut_chunk - 3 個 time_chunk **使用規則 2 (集合內容):** - rule: "rule_2" - 3 個 sentence_chunk (各含涵蓋時間範圍內的 yolo/ocr 結果) - 3 個 time_chunk (各含涵蓋時間範圍內的 yolo/ocr 結果) ## 8. 數據示例 ### 8.1 videos 表 (檔案映射) ```json { "id": 1, "uuid": "abc123def456", "file_name": "video_001.mp4", "file_path": "/path/to/video_001.mp4", "duration": 300.5, "width": 1920, "height": 1080, "fps": 30.0 } ``` ### 8.2 pre_chunks 表 (使用 file_id 關聯 videos) ```json { "file_id": 1, "source_type": "asr", "chunk_type": "sentence", "start_time": 0.0, "end_time": 5.5, "start_frame": 0, "end_frame": 165, "fps": 30.0, "raw_json": {...}, "text_content": "This is the first sentence" } ``` ### 8.3 frames 表 (使用 file_id 關聯 videos) ```json { "file_id": 1, "frame_number": 300, "timestamp": 10.0, "fps": 30.0, "yolo_objects": [ {"class": "person", "confidence": 0.9, "bbox": [100, 50, 200, 150]}, {"class": "car", "confidence": 0.85, "bbox": [50, 100, 150, 180]} ], "ocr_results": [], "face_results": [] } ``` ### 8.4 chunks 表 (使用 file_id 關聯 videos) ```json { "file_id": 1, "chunk_id": "sentence_0001", "chunk_type": "sentence", "rule": "rule_2", "start_time": 10.0, "end_time": 15.5, "start_frame": 300, "end_frame": 465, "fps": 30.0, "text_content": "The second sentence from the audio", "content": { "rule": "rule_2", "asr_text": "The second sentence from the audio", "objects": [ {"class": "person", "first_frame": 300, "last_frame": 450, "appears_in_frames": [300, 310, 320, ...]}, {"class": "car", "first_frame": 350, "last_frame": 465, "appears_in_frames": [350, 360, ...]} ], "ocr": [...], "faces": [...] }, "pre_chunk_ids": [5], "frame_count": 301 } ``` ### 8.5 chunk_vectors 表 (使用 file_id 關聯 videos) ```json { "file_id": 1, "chunk_id": "sentence_0001", "chunk_type": "sentence", "start_time": 10.0, "end_time": 15.5, "embedding": "[0.1, 0.2, ...]", "metadata": {"text": "The second sentence..."} } ``` ### 8.6 Qdrant Payload ```json { "file_uuid": "abc123def456", "chunk_id": "sentence_0001", "chunk_type": "sentence", "start_time": 10.0, "end_time": 15.5, "text": "The second sentence from the audio" } ``` ## 7. 向量管理原則 ### 7.1 Vector Schema ```sql -- Chunk 向量表 (PostgreSQL) CREATE TABLE chunk_vectors ( id SERIAL PRIMARY KEY, -- 檔案識別 (使用 videos 表的內部 ID 以節省空間) file_id INTEGER NOT NULL REFERENCES videos(id), chunk_id VARCHAR(64) NOT NULL, chunk_type VARCHAR(32) NOT NULL, -- 向量數據 embedding TEXT, -- JSON 格式的向量 embedding_vector VECTOR(768), -- pgvector 類型 (如可用) -- 時間範圍 (用於時間查詢) start_time DOUBLE PRECISION, end_time DOUBLE PRECISION, -- Metadata metadata JSONB, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(chunk_id) ); -- 索引 CREATE INDEX idx_chunk_vectors_file_id ON chunk_vectors(file_id); ``` ### 7.2 Qdrant Collection - Collection 名稱: `chunks_v3` - Vector 維度: 768 (nomic-embed-text) - Payload 包含: `file_uuid`, `chunk_id`, `chunk_type`, `start_time`, `end_time`, `text` > **注意**: Qdrant 中仍使用 uuid (字串),因為需要可讀性和跨系統整合。PostgreSQL 內部使用 videos.id (整數) 以節省空間。 ## 9. 設計原則總結 1. **單一圖像識別 → Frame**: yolo, ocr, face 等單幀識別結果直接入 frame 表 2. **時間序列識別 → Pre-Chunk**: asr, asrx, cut, time, trace 等有時間範圍的結果入 pre_chunk 表 3. **組合規則 1 (直接)**: pre_chunk → chunk (保持原有邊界) 4. **組合規則 2 (集合)**: pre_chunk + frames → chunk (加入識別內容) 5. **精確到 Frame**: 所有時間範圍都記錄 start_frame, end_frame 6. **雙向量存儲**: 同時支持 PostgreSQL 和 Qdrant 7. **跨視頻搜索**: 透過 videos 表的 uuid 進行搜索,內部使用 id 節省空間 8. **空間優化**: 內部表使用 videos.id (4 bytes) 而非 uuid (32 bytes) ## 10. 查詢範例 ### 10.1 跨視頻搜索所有 chunk ```sql -- 搜索所有視頻中包含 "hello" 的 chunk SELECT c.*, v.uuid, v.file_name FROM chunks c JOIN videos v ON c.file_id = v.id WHERE c.text_content ILIKE '%hello%'; ``` ### 10.2 查詢特定視頻的 chunk ```sql -- 查詢 uuid 為 'abc123' 的視頻的所有 chunk SELECT c.* FROM chunks c JOIN videos v ON c.file_id = v.id WHERE v.uuid = 'abc123'; ``` ### 10.3 按時間範圍搜索 ```sql -- 搜索所有視頻在 10-20 秒範圍內的 chunk SELECT c.*, v.uuid FROM chunks c JOIN videos v ON c.file_id = v.id WHERE c.start_time >= 10.0 AND c.end_time <= 20.0; ```