## v0.9.20260325_144654 ### Features - API Key Authentication System - Job Worker System - V2 Backup Versioning ### Bug Fixes - get_processor_results_by_job column mapping Co-authored-by: OpenCode
535 lines
22 KiB
Markdown
535 lines
22 KiB
Markdown
# 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;
|
||
```
|