# 5W1H 摘要設計方案比較

## 問題描述

每個 scene 獨立呼叫 LLM 時沒有劇情上下文，LLM 無法理解人物關係與事件演進。

現行做法：
```
Scene 1 → LLM(context="") → summary_1
Scene 2 → LLM(context="") → summary_2   ← 不知道 scene_1 發生什麼
```

## 方案比較

### 方案 A：全片對白當 context

```
LLM call for scene_42:
  Input: [全部 1709 句對白] + [scene_42 的對話]
  Output: scene_42 summary
```

| 面向 | 評估 |
|------|------|
| Context token | 1709 句 × 15 詞 ≈ 34K tokens（僅對白） |
| Model 限制 | Gemma4 26B 僅 32K context — 超出 |
| Token 成本 | 721 calls × 34K = 24.5M input tokens |
| 注意力稀釋 | 模型看到 1709 句，只問其中 2-3 句，signal/noise 極低 |

**結論：不可行。** Token 成本高、超過 context limit、注意力稀釋嚴重。

---

### 方案 B：遞迴式（✅ 採用）

每段 scene 帶入前面所有 scene 的摘要作為 context：

```
Scene 1 → LLM(context="")                  → summary_1
Scene 2 → LLM(context=summary_1)           → summary_2
Scene 3 → LLM(context=summary_1+summary_2) → summary_3
```

#### Prompt 結構

```
Scene time: 1860s–1920s

Dialogue:
[1] When you start to eat like this...
[2] It's just that I'm too miserable...

Actors present (face detection): Audrey Hepburn, Cary Grant
Objects detected: person, car, tie
Face traces: trace_42 (Audrey Hepburn), trace_57 (Cary Grant)
Active speakers: SPEAKER_2 (Audrey Hepburn), SPEAKER_4 (Cary Grant)

Story so far (previous scenes):
Scene 1 (t=0s): [summary_1]
Scene 2 (t=35s): [summary_2]
...
```

#### Token 成本分析

假設每段 scene summary 約 50-80 tokens（5 句），5W1H 約 100 tokens：

| 位置 | 前情 tokens | 當下 scene tokens | 每 call 總 input |
|------|------------|-------------------|------------------|
| Scene 1 | 0 | ~200 | ~200 |
| Scene 10 | 9×150 = 1,350 | ~200 | ~1,550 |
| Scene 100 | 99×150 = 14,850 | ~200 | ~15,050 |
| Scene 721 | 720×150 = 108,000 | ~200 | ~108,200 |

**問題**：後期 scene 的 context 會超過模型 limit。

#### 對策：Context Window 管理

```rust
const MAX_CONTEXT_SCENES: usize = 50; // 可調

// 只保留最近 N 個 scene
let recent: Vec<&str> = prev_context.iter()
    .rev()
    .take(MAX_CONTEXT_SCENES)
    .rev()
    .collect();
let context = recent.join("\n");
```

或更精確的按 token 數 truncate：

```rust
let mut context = String::new();
for s in prev_context.iter().rev() {
    if context.len() + s.len() > 2000 { break; } // ~500 tokens
    context = format!("{}\n{}", s, context);
}
```

採用第二種按 token 數 truncate 的策略，保留最近且不超過 500 tokens 的前情。

#### 實際評估

| 項目 | 值 |
|------|-----|
| 總前情 tokens | ~108K（全保留）→ ~500（truncate 後） |
| 每 call 總 input | ~700 tokens avg |
| 721 calls 總 input | ~500K tokens |
| 預估執行時間 | ~12-25 分鐘（Gemma4 26B） |
| 記憶體 | 同上，無額外開銷 |

#### 優點

- Token 成本低（~500K vs 方案 A 的 24.5M）
- 保留近期劇情連貫性（最近 ~3-5 個 scene 的摘要）
- 容易實作，單一 accumulator vector
- 不影響獨立 scene 處理能力

#### 缺點

- 極早期的 scene 會被 truncate 掉
- 長距離劇情依賴（如 call back）會遺失
- 需要略長的 time budget

---

### 方案 C：鄰近場景視窗

```
Scene 42: context = summaries[37..41] + scene_42
```

固定取前 5 個 scene，不隨著場景數成長：

| 面向 | 評估 |
|------|------|
| Context token | 固定 ~750 tokens（5 × 150） |
| Token 成本 | 721 × 750 = 540K |
| 連貫性 | 只看前 5 個 scene |
| 優點 | Token 恆定，不 truncate |
| 缺點 | 無法看到劇情大轉折的前因後果 |

方案 B 的 truncate 實務上就是方案 C 的行為（只保留最近 N 個摘要），只是 N 由 token budget 動態決定而非固定數字。

---

### 方案 D：分幕處理

```
Movie → LLM → movie_summary (5 句)
         ↓
Act 1 → LLM(context=movie_summary) → act1_summary
Act 2 → LLM(context=movie_summary+act1_summary) → act2_summary
         ↓
Scene within Act → LLM(context=act_summary+scene_dialogue) → scene_summary
```

| 面向 | 評估 |
|------|------|
| Token 成本 | 1 + N_acts + N_scenes 次 call |
| 實現複雜度 | 需要知道 movie 的分幕結構 |
| 適用場景 | 有明確幕/章節結構的電影 |
| Charade 適用性 | 無內建分幕，需額外推論 |

**結論**：對 Charade 而言缺少先驗的分幕資訊。未來可考慮加入 scene clustering 自動分幕後再套用此方案。

---

## 決定

**採用方案 B（遞迴式）**，實作方式：

1. `summarize_one_scene` 新增 `prev_context: &str` 參數，放進 prompt 的 `Story so far` 區塊
2. `analyze_5w1h` / `batch_analyze_5w1h` / `run_5w1h_agent` 改為依序傳遞累積 context
3. 每個 scene 完成後將 `scene_summary` append 到 context accumulator
4. truncate 策略：保留最近最多 2000 bytes（~500 tokens）的前情摘要

## 程式變更

| 檔案 | 說明 |
|------|------|
| `src/api/five_w1h_agent_api.rs` | 3 個 entry points + `summarize_one_scene` |

## 相關文件

- `M5_workspace/2026-05-07_session_summary_v2.md`
- `M4_workspace/2026-05-07_pipeline_progress_report_template.md` (v2)
