---
document_type: "spec"
service: "MOMENTRY_CORE"
title: "Pipeline & Rule Architecture: Processor Lifecycle, Embedding, Search V2.0"
date: "2026-05-05"
version: "V2.0"
status: "deprecated"
owner: "Warren"
created_by: "OpenCode"
tags:
  - "momentry"
  - "core"
  - "chunk"
  - "embedding"
  - "qdrant"
  - "bm25"
  - "lifecycle"
  - "versioning"
  - "v1.0"
  - "deprecated"
ai_query_hints:
  - "⚠️ 歷史設計文件，非當前實作"
  - "Story (template) summarization 已由 5W1H+ Agent 取代"
  - "Qdrant 3-collection 架構已簡化為 1 collection + chunk_type 區分"
related_documents:
  - "../PROCESSORS/FACE_V1.0.0.md"
  - "../PROCESSORS/FACE_EMBEDDING_FLOW_V1.0.0.md"
  - "../VECTOR_SPEC_V1.0.0.md"
  - "../CHUNK_DEFINITION_V1.0.0.md"
  - "../PROCESSOR_SELECTION_V1.0.0.md"
---

# Pipeline & Rule Architecture: Processor Lifecycle, Embedding, Search V2.0

> ⚠️ **歷史設計文件** — 此文件描述 v1.0 早期開發階段的雙軌 pipeline 設計。Story (template) 與 LLM (on-demand) 兩條 pipeline 皆曾實作，後期因 M5 的 LLM 算力充足，template-based summarization 已被 5W1H+ Agent 取代。當前實作請參考：
>
> - `AGENTS/5W1H_AGENT_V1.0.0.md` — 5W1H+ 遞迴摘要
> - `AGENTS/IDENTITY_AGENT_V1.0.0.md` — Identity Agent
> - `VECTOR_SPEC_V1.0.0.md` — 向量化規範

## 架構概述

兩個獨立 pipeline，共用同一底層（Qdrant + BM25），chunk_type 區隔：

```
                        ASR + ASRX + CUT + Face + YOLO
                                    │
                    ┌───────────────┴───────────────┐
                    ▼                               ▼
         Pipeline 1: Story                    Pipeline 2: LLM
         (template, instant)                  (LLM, on-demand)
                    │                               │
         ┌──────────┼──────────┐         ┌──────────┼──────────┐
         ▼          ▼          ▼         ▼          ▼          ▼
      Parent     Child     Embedding   Parent     Child     Embedding
      Summary    Summary   (nomic)     Summary    Summary   (nomic)
         │          │          │         │          │          │
         └──────────┴──────────┘         └──────────┴──────────┘
                    │                               │
                    ▼                               ▼
        ┌───────────────────────┐     ┌───────────────────────┐
        │  Qdrant (vector)      │     │  Qdrant (vector)      │
        │  PG tsvector (BM25)   │     │  PG tsvector (BM25)   │
        │  chunk_type: story_*  │     │  chunk_type: llm_*    │
        └───────────────────────┘     └───────────────────────┘
```

## Pipeline 1: Story (Template-Based)

### 輸入
- `{uuid}.asr.json` — 1629 segments (text + timestamps)
- `{uuid}.asrx.json` — 10 speakers (speaker_id + segments)
- `{uuid}.cut.json` — 1331 scenes (start/end times)
- Identity mapping (SPEAKER_0→Cary Grant, SPEAKER_1→Audrey Hepburn, etc.)

### 處理流程

```
1. build_child_chunks()
   ├── CUT scenes 分組 ASR segments
   ├── ASRX 時間對齊 → speaker_id → identity name
   └── 產出: scenes[children: [{start, end, text, speaker_name}]]

2. generate_story_parent_summary(scene)
   └── Template: "[{start}-{end}, {dur}] Cast: X. Total: N lines, W words. Speakers: ..."

3. generate_story_child_summary(child, parent)
   └── Template: "[{start}-{end}] {name}: \"{text}\""

4. embed_text(summary) → EmbeddingGemma 300M (Python MPS, port 11436) → 768D vector

5. Store:
   ├── Qdrant: upsert (point_id=chunk_id, vector=768D, payload={chunk_type, file_uuid, text})
   └── PostgreSQL: chunks table (text_content, tsvector, parent_chunk_id)
```

### Chunk Types

| chunk_type | 說明 | 數量 (Charade) |
|------------|------|----------------|
| `story_parent` | Template parent scene summary | ~300 |
| `story_child` | Per-sentence summary | ~1600 |

### Embedding Target

```
chunk_summary text → EmbeddingGemma 300M (768D, port 11436) → Qdrant collection "momentry_dev"
                                                     → PostgreSQL chunks.embedding (VECTOR(768))
```

## Pipeline 2: LLM (On-Demand)

### 輸入

同 Pipeline 1 + LLM server (Gemma4/Qwen)

### 處理流程

```
1. build_child_chunks()  (同 Pipeline 1)

2. generate_llm_parent_summary(scene)
   └── LLM Prompt: "Summarize this scene: {dialogue}" → paragraph (60-100 words)

3. generate_llm_child_summary(child, parent_summary)
   └── LLM Prompt: "{parent_summary} → Summarize this line: {text}" → one sentence
```

### Chunk Types

| chunk_type | 說明 |
|------------|------|
| `llm_parent` | LLM parent scene summary |
| `llm_child` | LLM child sentence summary |

## Qdrant Storage

### Collection Architecture (3 Collections)

| Collection | Source Data | Content | Embedding Target | Point Count (Charade) | Dev Name |
|------------|------------|---------|-----------------|----------------------|----------|
| **rule1** | ASR raw + ASRX speaker (1:1) | 原始對白句 + speaker_id + timestamp | Raw ASR text | 1,629 | `momentry_dev_rule1` |
| **story** | Story Pipeline 1 (template) | Parent scene + Child sentence with identity | Child summary: `[{t}s] {name}: "{text}"` | 1,629 child + 1,313 parent | `momentry_dev_story` |
| **llm_summary** | Story Pipeline 2 (LLM, future) | Parent LLM narrative + Child LLM summary | LLM-generated child summary | TBD | `momentry_dev_llm_summary` |

### 各 Collection 資料結構對比

| | rule1 | story | llm_summary |
|---|---|---|---|
| **輸入** | ASR 原始段 | ASR + CUT + identity + YOLO + TKG | ASR + CUT + identity + YOLO + TKG + LLM |
| **Parent** | ❌ 無 | ✅ scene summary (template) | ✅ LLM narrative paragraph |
| **Child** | 原始句子 | `[時間] 角色: "對白"` | LLM-generated 1-sentence summary |
| **Speaker** | speaker_id | resolved identity name | resolved identity name |
| **搜尋特色** | 精確字詞匹配 | 角色+上下文語意 | 高層次語意理解 |
| **產生成本** | Zero | Zero (template) | High (LLM inference) |
| **狀態** | ⬜ pending | ✅ implemented | ⬜ future |

## Metadata 信度 (Confidence Scores)

所有已識別的內容都附帶信度，提供給 Story/LLM processor 做加權參考：

### 信度來源

| 來源 | 欄位 | 值域 | 說明 |
|------|------|------|------|
| **Speaker identity** | `speaker_confidence` | 0-1 | MAR lip analysis: mouth open/close events matched to speaker |
| **Face identity** | `face_confidence` | 0-1 | Identity clustering composite score (similarity + speaker weight) |
| **YOLO object** | `object_confidence` | 0-1 | YOLO detection confidence per object class |
| **ASR text** | `asr_confidence` | 0-1 | Whisper transcription confidence (per segment) |
| **Scene boundary** | `scene_confidence` | — | CUT scene detection is deterministic (binary) |

### Parent Chunk Metadata

```json
{
  "start_time": 304,
  "end_time": 318,
  "characters": ["Cary Grant"],
  "speaker_confidence": {
    "Cary Grant": 0.85   // MAR lip: 4/4 events during SPEAKER_0
  },
  "face_identities": {
    "Cary Grant": 0.64    // clustering composite score
  },
  "yolo_objects": {
    "car": 0.72,
    "bottle": 0.55
  },
  "child_count": 1,
  "total_words": 13
}
```

### Child Chunk Metadata

```json
{
  "start": 308.51,
  "end": 317.35,
  "text": "Sylvia I'm getting a divorce...",
  "speaker_id": "SPEAKER_0",
  "speaker_name": "Cary Grant",
  "speaker_confidence": 0.85,
  "face_confidence": 0.64,
  "asr_confidence": 0.92,
  "language": "en",
  "yolo_objects": ["bottle", "car", "chair"],
  "scene_id": 48
}
```

### 信度使用方式

Story processor (template) 用信度做：
- 過濾低信度 objects（<0.5 排除，避免 "cell phone" 誤報）
- 標註高信度 speaker identity（>0.8 → 確定的角色名）

LLM processor (future) 用信度做：
- Prompt context: "Cary Grant (confidence 0.85) says: ..."
- 低信度內容放後段或不放入 prompt

### Collection 1: rule1

Source: ASR sentence + ASRX speaker (1:1, no parent grouping)

```json
{
  "point_id": "rul1_{uuid}_{start_sec}_{end_sec}",
  "vector": "<nomic-embed 768D>",
  "payload": {
    "file_uuid": "1a04db97...",
    "chunk_type": "rule1_sentence",
    "text": "Hello and welcome to the old-time movie show...",
    "speaker_id": "SPEAKER_4",
    "speaker_name": "Walter Matthau",
    "start_time": 1.7,
    "end_time": 18.9,
    "language": "en"
  }
}
```

### Collection 2: story

Source: Story parent-child chunks (Pipeline 1)

```json
{
  "point_id": "story_{uuid}_{start_sec}_{end_sec}",
  "vector": "<nomic-embed 768D from child summary text>",
  "payload": {
    "file_uuid": "1a04db97...",
    "chunk_type": "story_child",
    "text": "[309s-317s] Cary Grant: \"Sylvia I'm getting a divorce...\"",
    "speaker": "Cary Grant",
    "parent_chunk_id": "story_parent_xxx_304_318",
    "parent_summary": "[304s-318s, 14s] Cast: Cary Grant. Total dialogue: 1 lines, 13 words.",
    "start_time": 308.5,
    "end_time": 317.4
  }
}
```

### Collection 3: llm_summary

Source: LLM parent-child summaries (Pipeline 2, future)

```json
{
  "point_id": "llm_{uuid}_{start_sec}_{end_sec}",
  "vector": "<nomic-embed 768D from LLM child summary>",
  "payload": {
    "file_uuid": "1a04db97...",
    "chunk_type": "llm_child",
    "text": "Cary Grant discusses his divorce from Sylvia with Charles.",
    "speaker": "Cary Grant",
    "parent_chunk_id": "llm_parent_xxx_304_318",
    "parent_llm_summary": "In this scene, Cary Grant reveals...",
    "start_time": 308.5,
    "end_time": 317.4
  }
}
```

### Collection Initialization (Rust)

```rust
// In QdrantDb or config
pub fn rule1_collection() -> &str { "momentry_dev_rule1" }
pub fn story_collection() -> &str { "momentry_dev_story" }
pub fn llm_summary_collection() -> &str { "momentry_dev_llm_summary" }
```

### Search Strategy

```
Hybrid Search: "divorce Charles"
  ├── Qdrant rule1:     vector search raw sentences
  ├── Qdrant story:     vector search enriched child chunks
  └── PG tsvector:      BM25 across all three chunk_types
       ↓
  Merge scores → ranked results
```

### 對應 chunk_type (PostgreSQL)

| Collection | chunk_type | 說明 |
|------------|-----------|------|
| rule1 | `rule1_sentence` | 原始 1:1 ASR sentence |
| story | `story_child` | Story child chunk (with parent context) |
| story | `story_parent` | Story parent chunk |
| llm_summary | `llm_child` | LLM child summary (future) |
| llm_summary | `llm_parent` | LLM parent summary (future) |

## BM25 (PostgreSQL Full-Text Search)

### Strategy

利用 PostgreSQL 內建 `tsvector` + `tsquery`：

```sql
-- 已有 search_vector 欄位 (tsvector type)
-- 需 trigger 自動更新
ALTER TABLE dev.chunks ADD COLUMN IF NOT EXISTS search_vector tsvector;

-- Trigger: 自動從 text_content 產生 tsvector
CREATE OR REPLACE FUNCTION update_chunk_search_vector() RETURNS trigger AS $$
BEGIN
    NEW.search_vector := to_tsvector('english', COALESCE(NEW.text_content, ''));
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- BM25 查詢
SELECT *, ts_rank(search_vector, query) as bm25_score
FROM dev.chunks
WHERE file_uuid = $1
  AND chunk_type IN ('story_parent', 'story_child', 'llm_parent', 'llm_child')
  AND search_vector @@ to_tsquery('english', $2)
ORDER BY bm25_score DESC
LIMIT 20;
```

## Search API

### Hybrid Search (Vector + BM25)

```
GET /api/v1/search/hybrid?q=query&file_uuid=xxx

1. Qdrant: vector search → top N candidates (cosine_score)
2. PostgreSQL: BM25 search → top N candidates (bm25_score)  
3. Merge: weighted_rank = 0.7 * cosine_score + 0.3 * bm25_score
4. Return: ranked results with parent context
```

### Implementation Status

| Component | Status | File |
|-----------|--------|------|
| Story processor (template) | ✅ | `scripts/parent_chunk_5w1h.py` |
| LLM processor | ⬜ | `scripts/parent_chunk_5w1h.py` (--mode llm) |
| ProcessorType::Story | ✅ | `src/core/db/postgres_db.rs` |
| Rust processor call | ✅ | `src/worker/processor.rs:ProcessorType::Story` |
| Qdrant collection: rule1 | ⬜ | `momentry_dev_rule1` |
| Qdrant collection: story | ⬜ | `momentry_dev_story` |
| Qdrant collection: llm_summary | ⬜ | `momentry_dev_llm_summary` |
| pgvector embedding | ✅ | `dev.chunks.embedding` (VECTOR) |
| BM25 trigger | ✅ | migration 031, `dev.chunks.search_vector` |
| Hybrid search API | ⬜ | `src/api/server.rs` |

## Migration Plan

### 031_add_chunk_search_trigger.sql

```sql
-- Add search_vector if not exists
ALTER TABLE dev.chunks ADD COLUMN IF NOT EXISTS search_vector tsvector;

-- Drop old trigger if exists
DROP TRIGGER IF EXISTS trg_chunk_search_vector ON dev.chunks;

-- Create trigger function
CREATE OR REPLACE FUNCTION update_chunk_search_vector() RETURNS trigger AS $$
BEGIN
    NEW.search_vector := to_tsvector('english', COALESCE(NEW.text_content, ''));
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Create trigger
CREATE TRIGGER trg_chunk_search_vector
    BEFORE INSERT OR UPDATE ON dev.chunks
    FOR EACH ROW EXECUTE FUNCTION update_chunk_search_vector();
```

## Processor Order (Pipeline DAG)

```
Cut ──→ Scene
Asr ──→ Asrx ──→ Story ──→ Embedding Pipeline
                   ↑
               requires: Asr, Asrx, Cut, Yolo, Face
                   ↓
              Qdrant + BM25
```

Story processor depends on: Asr, Asrx, Cut, Yolo, Face  
Position in `ProcessorType::all()`: **last** (after VisualChunk)

## Test Plan

1. `python parent_chunk_5w1h.py --file-uuid xxx --mode story` → produces story_story.json
2. Apply migration 031 → adds search_vector trigger
3. Implement `QdrantDb::upsert_chunk_embedding()`
4. Wire Story processor into Rust pipeline
5. Run `--mode story --embed` → verify Qdrant + BM25
6. Run `--mode llm --embed` → verify LLM pipeline (when resources allow)

## Metadata 版本與更新機制

### 問題

Metadata 信度（speaker_confidence, face_confidence, asr_confidence 等）依賴處理器的模型選型。當模型升級（如 Whisper small → medium，InsightFace → Vision+FaceNet），原有的信度值不再準確，需要重新計算。

### 設計：處理器版本追蹤

每個處理器記錄其模型版本，metadata 附帶 `source_version` 以便判斷是否需要更新。

```json
// chunk metadata 中的 source_version 欄位
{
  "source_versions": {
    "asr": "faster-whisper/small/v1",        // ← 處理器 + 模型 + 版本
    "asrx": "speechbrain/ecapa-tdnn/v1",
    "face": "apple-vision+coreml-facenet/v2",
    "cut": "pyscenedetect/default",
    "yolo": "yolov5-coreml/v2",
    "speaker_binding": "mar-lip/v1",
    "identity_clustering": "cosine-threshold/v1",
    "story_processor": "template/v2.0"
  },
  "generated_at": "2026-05-05T02:30:00Z"
}
```

### 處理器版本表

| Processor | 當前版本 | 上一版本 | 升級影響 |
|-----------|---------|---------|---------|
| ASR | `faster-whisper/small/v1` | — | 換 medium/large → asr_confidence 變更 |
| ASRX | `speechbrain/ecapa-tdnn/v1` | — | 換模型 → speaker_id 可能變更 |
| Face detection | `apple-vision/v2` | `insightface/buffalo-l/v1` | bbox, pose 變化 |
| Face embedding | `coreml-facenet/v2` | `insightface-arcface/v1` | embedding 空間完全變更 |
| YOLO | `yolov5-coreml/v2` | `yolov8/v1` | object_confidence 分布變更 |
| Speaker binding | `mar-lip/v1` | — | 換方案 → speaker_confidence 變更 |
| Identity clustering | `cosine-threshold/v1` | — | 換 DBSCAN → face_confidence 變更 |
| Story processor | `template/v2.0` | — | 換 LLM → summary 品質變化 |

### 更新策略

#### Tier 1: 軟更新（metadata only）

當處理器升級但輸出不變更時（如 YOLO v8 → v5），只需重新計算信度：

```
1. 更新 version table
2. 標記受影響的 chunk 為 stale
3. 重新計算 confidence，更新 metadata
4. 不重新生成 embedding（文本不變）
```

#### Tier 2: 硬更新（重新產生）

當處理器輸出結構變更時（如 InsightFace → Vision），需完整重跑：

```
1. 標記所有 downstream 為 stale
2. 重跑 Face → Trace → Identity → Story → Embedding
3. 更新所有 source_version
```

#### 觸發時機

| 事件 | 更新範圍 |
|------|---------|
| ASR 模型升級 | ASR chunks → Rule1 → Story → 重新 embed |
| Face 模型切換 | Face traces → Identity → Story → 重新 embed |
| Speaker binding 改進 | Speaker metadata → Story → 重新 embed |
| Story template 修改 | Story chunks → 重新 embed |

### 實作

```rust
// processor_results table 加入 model_version 欄位
pub struct ProcessorResult {
    ...
    pub model_version: Option<String>,  // "faster-whisper/small/v1"
}

// chunks metadata 記錄所有上游版本
// 比對 current_versions vs source_versions → 決定是否需要更新
pub fn needs_refresh(chunk_versions: &HashMap<String,String>, 
                     current_versions: &HashMap<String,String>) -> bool {
    chunk_versions.iter().any(|(proc, ver)| {
        current_versions.get(proc) != Some(ver)
    })
}
```

### 處理器版本註冊

```sql
CREATE TABLE dev.processor_versions (
    processor VARCHAR(32) PRIMARY KEY,
    model_version VARCHAR(128) NOT NULL,
    updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
```

---

## Pipeline & Rule 架構定義

### 名詞定義

| 名詞 | 定義 | 責任 |
|------|------|------|
| **Pipeline** | 排程管理的工作流。包含入庫與出庫。 | Processor 執行順序、依賴、產出、寫入 DB、指定資料來源路徑與欄位 |
| **Processor** | 單一處理單元 | 產出原始數據（JSON, DB rows, embeddings） |
| **Agent** | 智能處理單元 | 推理、匹配、總結（TMDb, Identity, Story） |
| **Rule** | 搜尋 API 的內涵定義 | 定義如何將 Pipeline 產出組織成可搜尋結構 |
| **Search API** | 查詢介面 | 查詢 Rule 產生的 chunk（vector search, BM25, hybrid） |

### Pipeline → Rule 關係

```
Pipeline (排程管理)                    Rule (搜尋內涵)
─────────────────────                   ────────────────
CUT ──→ Scene
ASR ──→ ASRX ──┐                        Rule 1: Sentence Chunks
                │                         ├── 輸入: ASR + ASRX
YOLO ──────────┤                         ├── 產出: 1:1 sentence chunks
                │                         └── 搜尋: 原始對白 text search
OCR ───────────┤
                │                        Rule 2: Visual Chunks
Face ──→ Trace ─┐                         ├── 輸入: YOLO + OCR
                │                         ├── 產出: frame-level visual chunks
                ├──→ Identity Agent      └── 搜尋: 物件/文字位置查詢
Pose ──────────┘
                                         Rule 3: Scene Chunks
                                           ├── 輸入: CUT + ASR + Rule1
                                           ├── 產出: scene-level parent chunks
                                           └── 搜尋: 場景摘要

                                         Rule 4: Story Chunks (5W1H)
                                           ├── 輸入: Rule3 + Identity + YOLO
                                           ├── 產出: parent + child summaries
                                           └── 搜尋: 情節/角色語意搜尋
```

### Pipeline 做什麼

1. 管理 Processor 生命週期（Select → Schedule → Execute → Produce → Complete）
2. 處理依賴 DAG（ASRX 需等 ASR 完成）
3. 排程資源（max_concurrent, slot allocation）
4. 處理錯誤（retry, skip non-essential）
5. **入庫** — 產出數據並寫入儲存層
6. **出庫** — 指定數據來源路徑與欄位，供 Rule/API 查詢

### Pipeline 入庫/出庫 明細

#### 內部產出（from Path）

| Processor | 入庫 Path | 入庫 Table.Column | 出庫 |
|-----------|----------|-------------------|------|
| CUT | `{uuid}.cut.json` | — | Rule3: scenes |
| ASR | `{uuid}.asr.json` | — | Rule1: segments |
| ASRX | `{uuid}.asrx.json` | — | Rule1: speaker_id |
| YOLO | `{uuid}.yolo.json` | — | Rule2: detections |
| OCR | `{uuid}.ocr.json` | — | Rule2: texts |
| Face Detect | `{uuid}_detect.json` | — | Face Embed: bbox |
| Face Embed | `{uuid}.face.json` | — | Face Trace: embedding |
| Face Trace | `{uuid}.face_traced.json` | `face_detections` | Identity: trace_id |
| Identity Agent | — | `face_detections.identity_id` | Story: names |
| Story Agent | `{uuid}.story_story.json` | `chunks` | Search API |
| Embedding Agent | — | `chunks.embedding` | Qdrant sync |
| *All Processors* | — | `processor_results` (job_id, processor, status, started_at, completed_at, error_message) | 生命週期追蹤 |
| *All Processors* | — | `monitor_jobs` (uuid, status, processors, completed_processors) | Pipeline 排程 |

#### 外部取得（from URL）

每個唯一的 (URL + Method + 參數組合) 獨立列管。參數或模型不同 → 視為不同資料源。

| ID | 來源 | URL | Method | 參數/模型 | Auth | 入庫 | 出庫 |
|----|------|-----|--------|----------|------|------|------|
| **U01** | TMDb | `api.themoviedb.org/3/search/movie` | GET | `query={name}`, `language=en-US` | `TMDB_API_KEY` | `identities.tmdb_id` | Identity Agent |
| **U02** | TMDb | `api.themoviedb.org/3/movie/{id}/credits` | GET | `movie_id` | `TMDB_API_KEY` | `identities.metadata` | Identity Agent |
| **U03** | TMDb | `image.tmdb.org/t/p/w185/{path}` | GET | `profile_path` | — | `identities.tmdb_profile` | Identity Agent |
| **U04** | TMDb | `tmdb_embed_extractor.py` (local) | Py | `model=coreml-facenet/v2` | — | `identities.face_embedding (512D)` | Identity Agent |
| **U05** | EmbeddingGemma | `localhost:11436/v1/embeddings` | POST | `input=text`, `model=embeddinggemma-300m` | — | `chunks.embedding (768D)` | Qdrant search |
| **U07** | Ollama | `localhost:11434/api/chat` | POST | `model=qwen3:8b` (future) | — | `chunks.text_content` | Story LLM |
| **U08** | Ollama | `localhost:11434/api/chat` | POST | `model=gemma4` (future) | — | `chunks.text_content` | Story LLM |
| **U09** | Qdrant | `localhost:6333/collections/{name}/points` | PUT | `collection=momentry_dev_rule1` | `QDRANT_API_KEY` | rule1 vectors | Search |
| **U10** | Qdrant | `localhost:6333/collections/{name}/points` | PUT | `collection=momentry_dev_story` | `QDRANT_API_KEY` | story vectors | Search |
| **U11** | Qdrant | `localhost:6333/collections/{name}/points` | PUT | `collection=momentry_dev_llm_summary` | `QDRANT_API_KEY` | llm vectors | Search |

#### Ollama 模型變體影響

| Model | Dim | 用途 | 影響範圍 |
|-------|-----|------|---------|
| `EmbeddingGemma 300M` | 768 | 多語言 embedding | 所有 chunk embedding 需重算 |
| `mxbai-embed-large` | 1024 | 英文為主（已棄用） | 改 dim → Qdrant collection 重建 |
| `qwen3:8b` | — | LLM summarization | Story parent/child summary 文本變更 |
| `qwen3:14b` | — | 同上，品質較好 | 同上 |
| `gemma4:4b` | — | 同上，較輕量 | 同上 |

#### 參數變更觸發規則

| 變更類型 | 觸發 | 範例 |
|---------|------|------|
| 換 model | 所有 downstream stale | `EmbeddingGemma 300M`（768D）取代 `nomic-embed-text-v2-moe`（768D），dim 不變 |
| 同 model 參數變更 | 只影響該層 | Qdrant collection rename |
| API endpoint 變更 | 重試策略 + 通知 | TMDb API v3 → v4 |

### Rule 做什麼

1. 讀取 Pipeline 產出的原始數據
2. 組織成父子 chunk 結構
3. 生成 summary text
4. 呼叫 Embedding (EmbeddingGemma 300M, Python MPS, port 11436)
5. 存入 Qdrant + PostgreSQL (vector + BM25)
6. 提供 Search API 查詢

### 五階段運作模型

每個 Processor/Agent 遵循標準五階段：

```
1. 選擇 ──→ 2. 排程 ──→ 3. 執行 ──→ 4. 產出 ──→ 5. 完成
(Select)   (Schedule)  (Execute)  (Produce)  (Complete)
```

| 階段 | 英文 | 責任 | 實例（Face V2.0） |
|------|------|------|-------------------|
| **1. 選擇** | Select | 決定模型/演算法/版本 | `face_detection=apple-vision/v2`, `face_embedding=coreml-facenet/v2` |
| **2. 排程** | Schedule | 依賴檢查、slot allocation、max_concurrent | Face depends on [] → slot available → queued |
| **3. 執行** | Execute | 執行 scripts / agent logic、監控資源、timeout | `python face_processor.py video out.json --sample 60` |
| **4. 產出** | Produce | 寫入輸出 JSON、DB table、觸發 post-processing | face.json → face_detections DB → store_traced_faces.py |
| **5. 完成** | Complete | 標記 DONE、記錄 source_versions、觸發 downstream | `processor_results.status='completed'`, 通知下游 StoryAgent |

### 每階段記錄

```json
{
  "processor": "face",
  "run_id": "run_20260505_001",
  "select": {
    "model_version": "apple-vision+coreml-facenet/v2",
    "sample_interval": 60,
    "config": {"detector": "Vision", "embedder": "FaceNet"}
  },
  "schedule": {
    "queued_at": "2026-05-05T00:00:00Z",
    "started_at": "2026-05-05T00:00:05Z",
    "wait_reason": null
  },
  "execute": {
    "process_pid": 16490,
    "cpu_percent": 30,
    "memory_mb": 50,
    "duration_sec": 433,
    "error": null
  },
  "produce": {
    "output_file": "face.json",
    "output_size_bytes": 115343360,
    "frames_processed": 4008,
    "embeddings_generated": 6182,
    "db_rows_inserted": 6182
  },
  "complete": {
    "completed_at": "2026-05-05T00:07:18Z",
    "status": "DONE",
    "source_versions": {
      "face_detection": "apple-vision/v2",
      "face_embedding": "coreml-facenet/v2"
    }
  }
}
```

### 以 Face V2.0 為例

```
1. SELECT: 選擇 apple-vision (detection) + coreml-facenet (embedding)
           sample_interval=60, max_concurrent=1
           ↓
2. SCHEDULE: Face 無依賴，slot available → 排入 processor pool
           ↓
3. EXECUTE: swift_face (Vision ANE detection, 26% CPU, ~3min)
           face_processor.py (CoreML embedding, 0% CPU, ~4min)
           total: 433s, CPU 30%, memory 50MB
           ↓
4. PRODUCE: face.json (110MB, 4008 frames, 6182 embeddings)
           face_detections DB (6182 rows with trace_id)
           face_traced.json → store_traced_faces.py
           ↓
5. COMPLETE: status=DONE, source_versions written
            ↓ 觸發下游
           TMDbAgent → IdentityAgent → StoryAgent → EmbeddingAgent
```

### State Machine

```
                    ┌─────────┐
                    │ PENDING │ ← queued, waiting for dependencies
                    └────┬────┘
                         │ dependencies met, slot available
                    ┌────▼────┐
              ┌─────│ RUNNING │─────┐
              │     └────┬────┘     │
              │          │          │
         crash/timeout  success   error
              │          │          │
         ┌────▼────┐ ┌───▼───┐ ┌───▼───┐
         │ STALE   │ │DONE   │ │FAILED │
         └────┬────┘ └───┬───┘ └───┬───┘
              │          │          │
              │    version change   │
              │          │          │
              └──────────┼──────────┘
                         │
                    ┌────▼────┐
                    │ Refresh │ → mark downstream stale → re-run
                    └─────────┘
```

### Processor 實作類別

三種實作語言，不同執行方式：

| 類別 | 執行方式 | 特點 | 範例 |
|------|---------|------|------|
| **Python** | `PythonExecutor.run(script, args)` | 彈性最大，模型多 | ASR, YOLO, Face embedding, Story |
| **Swift** | compiled binary, `subprocess.call(binary)` | ANE 原生，極低 CPU | Face detection, OCR, Pose |
| **Rust** | native tokio task | DB 操作，pipeline 排程 | Rule1/3 ingest, IdentityAgent, TMDbAgent |

### 各 Processor 類別

| Processor | 類別 | 執行內容 |
|-----------|------|---------|
| CUT | **Python** | `cut_processor.py` (PySceneDetect) |
| ASR | **Python** | `asr_processor.py` (faster-whisper) |
| ASRX | **Python** | `asrx_processor_custom.py` (SpeechBrain) |
| OCR | **Swift** | `swift_ocr` → Python wrapper |
| YOLO | **Python** | `yolo_processor.py` (CoreML YOLOv5) |
| Face detection | **Swift** | `swift_face` (Vision ANE) |
| Face embedding | **Python** | `face_processor.py` (CoreML FaceNet) |
| Face trace | **Python** | `store_traced_faces.py` → `face_tracker.py` |
| Pose | **Swift** | `swift_pose` (Vision ANE) |
| Scene | **Python** | `scene_classification.py` |
| VisualChunk | **Rust** | `visual_chunk.rs` (native) |
| Story | **Python** | `parent_chunk_5w1h.py` (template/LLM) |
| Rule1 Ingest | **Rust** | `rule1_ingest.rs` (DB + embedding) |
| Rule3 Ingest | **Rust** | `rule3_ingest.rs` + LLM client |
| TMDbAgent | **Rust** | `tmdb/probe.rs` + `tmdb/face_agent.rs` |
| IdentityAgent | **Python** | `experiments/identity_clustering/runner_v2.py` |
| EmbeddingAgent | **Python** | Ollama API call → pgvector write |

---

## Processor/Agent 登錄冊

### CUT — Scene Detection

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-04 / OpenCode |
| **類別** | Python |
| **簡要說明** | PySceneDetect 偵測視覺場景切換。輸出 scene boundaries。1331 scenes for Charade 113min。 |
| **依賴** | 無 |
| **選型測試** | `scripts/swift_processors/swift_cut_test.swift` (Swift AVFoundation — slower, not adopted) |
| **相關文件** | `docs_v1.0/.../PROCESSORS/CUT_V1.0.0.md` |
| **輸入** | `video_path` (MP4/MOV file) |
| **產出** | `{uuid}.cut.json`: `{scenes: [{scene_number, start_frame, end_frame, start_time, end_time}]}` |

### ASR — Speech Recognition

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-04 / OpenCode |
| **類別** | Python |
| **簡要說明** | faster-whisper small 語音轉文字。1,629 segments, 113 min for Charade。支援 language detection。 |
| **依賴** | 無 |
| **選型測試** | `scripts/swift_processors/asr_swift.swift` (Apple Speech Framework — quality insufficient) |
| **相關文件** | `docs_v1.0/.../PROCESSORS/ASR_V1.0.0.md` |
| **輸入** | `video_path`, `{uuid}.cut.json`, `output_path` |
| **產出** | `{uuid}.asr.json`: `{language, segments: [{start, end, text, language}]}` |

### ASRX — Speaker Diarization

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-04 / OpenCode |
| **類別** | Python |
| **簡要說明** | SpeechBrain ECAPA-TDNN 192D speaker embedding + clustering。10 speakers in Charade。 |
| **依賴** | ASR |
| **選型測試** | `scripts/swift_processors/asrx_swift.swift`, `speaker_test.swift` (Apple Speech Framework — no speaker embedding API) |
| **相關文件** | `docs_v1.0/.../PROCESSORS/ASRX_V1.0.0.md` |
| **輸入** | `video_path`, `{uuid}.asr.json`, `output_path` |
| **產出** | `{uuid}.asrx.json`: `{segments: [{start_time, end_time, speaker_id}], embeddings: [192D], speaker_stats}` |

### OCR — Text Recognition

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 PaddleOCR → V2.0 Swift Vision / 2026-05 |
| **類別** | Swift + Python wrapper |
| **簡要說明** | VNRecognizeTextRequest, 30+ languages, ANE accelerated。 |
| **依賴** | 無 |
| **選型測試** | `scripts/swift_processors/swift_ocr.swift`, `vision_ocr_test.swift` |
| **相關文件** | `docs_v1.0/.../PROCESSORS/OCR_V1.0.0.md` |
| **輸入** | `video_path`, `output_path`, `--sample-interval` |
| **產出** | `{uuid}.ocr.json`: `{frames: [{frame, timestamp, texts: [{text, confidence, bbox}]}]}` |

### YOLO — Object Detection

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 YOLOv8 → V2.0 CoreML YOLOv5 / 2026-05 |
| **類別** | Python |
| **簡要說明** | CoreML YOLOv5 80-class, ANE accelerated。328K frames for Charade。 |
| **依賴** | 無 |
| **選型測試** | `scripts/swift_processors/vision_object_test.swift` (Vision — no general object API) |
| **相關文件** | `docs_v1.0/.../PROCESSORS/YOLO_V1.0.0.md` |
| **輸入** | `video_path`, `output_path`, `--uuid` |
| **產出** | `{uuid}.yolo.json`: `{frames: {frame_num: {detections: [{class_name, confidence, x1,y1,x2,y2}]}}}` |

### Face Detection — 人臉偵測

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 InsightFace → V2.0 Apple Vision / 2026-05 |
| **類別** | Swift |
| **簡要說明** | VNDetectFaceRectanglesRequest + VNDetectFaceLandmarksRequest。ANE, 0% CPU。Bbox + pose + lip landmarks。 |
| **依賴** | 無 |
| **選型測試** | `scripts/swift_processors/face_vision_test.swift`, `face_compare_test.swift` |
| **相關文件** | `docs_v1.0/.../PROCESSORS/FACE_V1.0.0.md` |
| **輸入** | `video_path`, `output_detect.json`, `--sample-interval` |
| **產出** | `{uuid}_detect.json`: `{frames: [{frame, timestamp, faces: [{bbox, confidence, pose, lips}]}]}` |

### Face Embedding — 人臉向量

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 InsightFace ArcFace → V2.0 CoreML FaceNet / 2026-05 |
| **類別** | Python |
| **簡要說明** | CoreML FaceNet InceptionResnetV1 512D embedding, MIT license, ANE。 |
| **依賴** | Face Detection |
| **選型測試** | `scripts/swift_processors/face_vision_test.swift` (VNFaceprint — private API, unusable) |
| **相關文件** | `docs_v1.0/.../PROCESSORS/FACE_EMBEDDING_FLOW_V1.0.0.md` |
| **輸入** | `video_path`, `{uuid}_detect.json`, `output_path` |
| **產出** | `{uuid}.face.json`: `FaceResult {frame_count, fps, frames: [{frame, timestamp, faces: [{x,y,w,h, embedding(512D), pose_angle, lips}]}]}` |

### Pose — 姿態估計

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 MediaPipe → V2.0 Apple Vision / 2026-05 |
| **類別** | Swift + Python wrapper |
| **簡要說明** | VNDetectHumanBodyPoseRequest, 19 joints, ANE。8,190 poses for Charade。 |
| **依賴** | 無 |
| **選型測試** | `scripts/swift_processors/swift_pose.swift`, `pose_benchmark.swift` |
| **相關文件** | `docs_v1.0/.../PROCESSORS/POSE_V1.0.0.md` |
| **輸入** | `video_path`, `output_path`, `--sample-interval` |
| **產出** | `{uuid}.pose.json`: `{frame_count, fps, frames: [{frame, timestamp, persons: [{keypoints, bbox}]}]}` |

### Face Trace — 人臉追蹤

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-05 / OpenCode |
| **類別** | Python |
| **簡要說明** | IoU + embedding cosine cross-frame tracking → trace_id。存入 DB + pgvector。 |
| **依賴** | Face Detection, Face Embedding |
| **選型測試** | `scripts/utils/face_tracker.py` (553 frames → 2 traces) |
| **相關文件** | `docs_v1.0/.../FACE_TRACKER_GUIDE.md` |
| **輸入** | `{uuid}.face.json`, `--file-uuid` |
| **產出** | `{uuid}.face_traced.json`, DB: `face_detections` (trace_id + bbox + embedding) |

### Speaker Binding Agent — 語者綁定

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-05 / OpenCode |
| **類別** | Python |
| **簡要說明** | MAR (Mouth Aspect Ratio) lip movement → correlate with speaker segments → identify who is speaking。 |
| **依賴** | ASRX, Face Detection |
| **選型測試** | Inline: solo-frame overlap + MAR open/close event counting |
| **相關文件** | `docs_v1.0/.../DUAL_EMBEDDING_PIPELINE_V1.0.0.md` (Metadata 信度) |
| **輸入** | `{uuid}.face.json` (lip landmarks), `{uuid}.asrx.json` (speaker segments) |
| **產出** | Speaker→identity mapping + confidence scores。Stored in `identities.metadata`。 |

### TMDb Agent — 電影資料庫

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-04 / OpenCode |
| **類別** | Rust |
| **簡要說明** | TMDb API search → cast download → identity creation with face embeddings。15 cast members, 9 with embeddings for Charade。 |
| **依賴** | 無 (external API) |
| **選型測試** | N/A (API integration) |
| **相關文件** | `src/core/tmdb/probe.rs`, `src/core/tmdb/face_agent.rs` |
| **輸入** | `TMDB_API_KEY` env var, video filename |
| **產出** | `identities` table (name, tmdb_id, tmdb_profile, face_embedding) |

### Identity Agent — 身份聚類

| 項目 | 內容 |
|------|------|
| **出生登記** | V2.0 / 2026-05 / OpenCode |
| **類別** | Python |
| **簡要說明** | Multi-stage face trace clustering: TMDb bind → video reference enrichment → iterative cosine matching → 99.4% coverage。 |
| **依賴** | Face Trace, TMDb Agent, Speaker Binding |
| **選型測試** | `experiments/identity_clustering/configs/exp_001-008.json` |
| **相關文件** | `experiments/identity_clustering/README.md`, `docs_v1.0/.../FACE_TO_IDENTITY_FLOW.md` |
| **輸入** | `face_detections` DB, `tkg_edges` SPEAKS_AS data |
| **產出** | `labels.json` (trace→identity mapping), DB: `face_detections.identity_id` |

### Story Agent — 故事摘要

| 項目 | 內容 |
|------|------|
| **出生登記** | V2.0 / 2026-05 / OpenCode |
| **類別** | Python |
| **簡要說明** | Template parent-child chunk generation + embedding。300 parents, 1,175 children for Charade。支援 --mode llm (future)。 |
| **依賴** | ASR, ASRX, CUT, YOLO, Identity Agent |
| **選型測試** | `experiments/identity_clustering/runner_v2.py` (identity binding for speaker resolution) |
| **相關文件** | `docs_v1.0/.../DUAL_EMBEDDING_PIPELINE_V1.0.0.md`, `docs_v1.0/.../CHUNK_DEFINITION_V1.0.0.md` |
| **輸入** | `{uuid}.asr.json`, `{uuid}.asrx.json`, `{uuid}.cut.json`, `{uuid}.yolo.json` |
| **產出** | `{uuid}.story_story.json`, DB: `chunks` table (parent + child), pgvector (768D) |

### Embedding Agent — 向量化

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-05 / OpenCode |
| **類別** | Python |
| **簡要說明** | EmbeddingGemma 300M（Python MPS, port 11436）→ 768D vector → pgvector + Qdrant。 |
| **依賴** | Story Agent |
| **選型測試** | N/A (API integration) |
| **相關文件** | `docs_v1.0/.../VECTOR_SPEC_V1.0.0.md` |
| **輸入** | `chunks` table (text_content column) |
| **產出** | `chunks.embedding` (VECTOR 768D) |

### Scene — 場景分類

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-04 / OpenCode |
| **類別** | Python |
| **簡要說明** | Places365 場景分類模型，為每個 CUT scene 分配場景標籤（室內/室外、街道/辦公室等）。 |
| **依賴** | CUT |
| **選型測試** | — |
| **相關文件** | `docs_v1.0/.../PROCESSORS/SCENE_V1.0.0.md` |
| **輸入** | `video_path`, `{uuid}.cut.json`, `output_path` |
| **產出** | `{uuid}.scene.json`: `{scenes: [{scene_id, labels, confidence}]}` |

### VisualChunk — 視覺分塊

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-04 / OpenCode |
| **類別** | Rust |
| **簡要說明** | YOLO 物件偵測結果 → 視覺 chunk（相似物件組合）。目前 fixed-frame + similarity-based 兩種策略。 |
| **依賴** | YOLO |
| **選型測試** | `src/core/processor/visual_chunk.rs` (Jaccard similarity) |
| **相關文件** | — |
| **輸入** | `{uuid}.yolo.json`, DB |
| **產出** | `pre_chunks` table (chunk_type='visual') |

### Rule1 Ingest — 句塊入庫

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-04 / OpenCode |
| **類別** | Rust |
| **簡要說明** | ASR sentence + ASRX speaker → sentence chunks → DB + Qdrant。未實作（Sentence chunks = 0 for Charade）。 |
| **依賴** | ASR, ASRX |
| **選型測試** | `src/core/chunk/rule1_ingest.rs` (time overlap matching) |
| **相關文件** | `docs_v1.0/.../CHUNK_DEFINITION_V1.0.0.md` |
| **輸入** | `{uuid}.asr.json`, `{uuid}.asrx.json`, DB |
| **產出** | `chunks` table (chunk_type='sentence', rule='rule_1'), Qdrant `momentry_dev_rule1` |

### Rule3 Ingest — 場景塊入庫

| 項目 | 內容 |
|------|------|
| **出生登記** | V1.0 / 2026-04 / OpenCode |
| **類別** | Rust |
| **簡要說明** | CUT scene + Rule1 children → parent scene chunks + LLM 5W1H summary。 |
| **依賴** | CUT, ASR, Rule1 |
| **選型測試** | `src/core/chunk/rule3_ingest.rs` (LLM generate_5w1h_summary) |
| **相關文件** | `docs_v1.0/.../CHUNK_DEFINITION_V1.0.0.md` |
| **輸入** | `{uuid}.cut.json`, `chunks` table (rule1) |
| **產出** | `chunks` table (chunk_type='cut', rule='rule_3'), Qdrant |

### Processor 生命週期

| State | 觸發 | 動作 |
|-------|------|------|
| **PENDING** | job created, dependencies not met | 等待 processor pool slot |
| **RUNNING** | dependencies met, slot acquired | Python script / agent running |
| **DONE** | script exit 0, output valid | → trigger downstream processors |
| **FAILED** | script exit ≠0, timeout, OOM | → retry or skip (non-essential) |
| **STALE** | upstream processor version changed | → needs refresh |

### Agent 生命週期

Agents（TMDb Agent, Identity Agent, Story Agent）與 Processor 共用生命週期，但有額外特性：

| Agent | 觸發條件 | 生命週期特點 |
|-------|---------|-------------|
| **TMDbAgent** | Face DONE | 外部 API call，網路 unstable → retry 次數限制 |
| **IdentityAgent** | Face DONE + ASRX DONE | 多階段（Stage 1→2→3），每階段獨立狀態 |
| **StoryAgent** | All processor DONE | 可選 LLM mode（資源允許時），template mode always available |
| **EmbeddingAgent** | Story DONE | 純 CPU task，批次處理，可 resume |

### 依賴圖 (DAG)

```
CUT ──────→ Scene
               │
ASR ──→ ASRX ─┤
               │
YOLO ─────────┤
               │
OCR ──────────┤
               │
Face ──→ Trace ──→ IdentityAgent ─┐
               │                   │
               ├───────────────────┤
Pose ─────────┤                   │
               │                   │
VisualChunk ──┘                   │
               │                   │
               └─── StoryAgent ────┤
                         │         │
                    EmbeddingAgent │
                         │         │
                    Qdrant + BM25  │
                                   │
                         └─ TMDbAgent
```

### 下游傳播規則

當上游版本變更時：

```
ASR version change (v1 → v2)
  → ASRX: STALE (depends on ASR)
  → Rule1 chunks: STALE
  → StoryAgent: STALE (depends on ASR + Rule1)
  → Embedding: STALE
  
Face version change (InsightFace → Vision)  
  → Trace: STALE
  → IdentityAgent: STALE (embeddings changed)
  → TKG: STALE (trace_id changed)
  → StoryAgent: STALE (identity names may change)
  → Embedding: STALE

Template change (Story template v1 → v2)
  → Story chunks: STALE
  → Embedding: STALE (text changed)
```

### Stale Detection Logic

```python
def check_stale(file_uuid, current_versions):
    """Check which processors/agents need refresh"""
    chunks = db.query("SELECT source_versions, chunk_type FROM chunks WHERE file_uuid=?", file_uuid)
    
    stale_agents = set()
    for chunk in chunks:
        for proc, ver in current_versions.items():
            if chunk.source_versions.get(proc) != ver:
                stale_agents.add(proc)
                # Propagate downstream
                stale_agents.update(get_downstream(proc))
    
    return stale_agents  
```

### Refresh Pipeline

```
1. check_stale() → identify affected processors/agents
2. Mark affected chunks as STALE  
3. Re-run stale processors/agents in dependency order
4. Re-embed updated chunks
5. Update source_versions in chunk metadata
```

### Current Version Registry (Charade)

| Processor | Model Version | Status |
|-----------|--------------|--------|
| CUT | `pyscenedetect/default` | ✅ |
| ASR | `faster-whisper/small/v1` | ✅ |
| ASRX | `speechbrain/ecapa-tdnn/v1` | ✅ |
| OCR | `apple-vision/v1` | ✅ |
| YOLO | `yolov5-coreml/v2` | ✅ |
| Face detection | `apple-vision/v2` | ✅ |
| Face embedding | `coreml-facenet/v2` | ✅ |
| Pose | `apple-vision/v1` | ✅ |
| Trace | `iou+embedding/v1` | ✅ |
| TMDbAgent | `tmdb-api/v1` | ✅ |
| IdentityAgent | `cosine-threshold/v1` | ✅ |
| StoryAgent | `template/v2.0` | ✅ |
| EmbeddingAgent | `embeddinggemma-300m/v1` | ✅ |

## Schema 隔離原則

`dev` 與 `public` 完全獨立，禁止交叉污染。

| 規則 | 說明 |
|------|------|
| 資料隔離 | dev 不讀取 public table，反之亦然 |
| 擴展獨立 | pgvector 兩邊各自安裝 |
| Migration | 標明 target schema（`dev.table` vs `public.table`） |
| Sequence | 各自獨立，不共用 |
| Index | 各自維護 |
| Qdrant | `momentry_dev_*` vs `momentry_*` |
| EmbeddingGemma | embedding server 共用（port 11436，不分 dev/prod） |

## Version History

| Version | Date | Purpose | Author |
|---------|------|---------|--------|
| V1.0 | 2026-05-05 | Initial design | OpenCode |
| V1.1 | 2026-05-05 | 3-collection Qdrant + metadata confidence + version tracking | OpenCode |
| V1.2 | 2026-05-07 | EmbeddingGemma 300M 取代 nomic-embed-text-v2-moe（768D, Python MPS, port 11436） | OpenCode |
| V2.0 | 2026-05-07 | ⚠️ 標記為 deprecated — Story template pipeline 已由 5W1H+ Agent 取代 | OpenCode |
