---
document_type: "architecture_design"
service: "MOMENTRY_CORE"
title: "Momentry Core 數據管理設計文檔 (v4)"
date: "2026-03-17"
version: "V1.0"
status: "active"
owner: "Warren"
created_by: "OpenCode"
tags:
  - "momentry"
  - "core"
  - "數據管理設計文檔"
ai_query_hints:
  - "查詢 Momentry Core 數據管理設計文檔 (v4) 的內容"
  - "Momentry Core 數據管理設計文檔 (v4) 的主要目的是什麼？"
  - "如何操作或實施 Momentry Core 數據管理設計文檔 (v4)？"
---

# 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' (ChunkType::Sentence), 'cut' (ChunkType::Cut), 'time' (ChunkType::TimeBased), 'trace' (ChunkType::Trace), 'story' (ChunkType::Story)
    
    -- 時間範圍
    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' (ChunkType::Sentence), 'cut' (ChunkType::Cut), 'time' (ChunkType::TimeBased), 'trace' (ChunkType::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;
```
