# Pipeline v3 전체 리팩토링 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** 17개 분석 + 4개 이미지 + 2개 보조 = 23단계 파이프라인으로 전면 재설계. visible_entities를 LLM 의존에서 확정 데이터 기반 자동 구축으로 전환.

**Architecture:** StepRunner 베이스 클래스는 유지. step_manifest.py를 v3 23단계로 교체. 각 단계별 Step 클래스를 역할군별 파일로 재편성 (text_steps, summary_steps, entity_steps, scene_steps, director_steps, outlook_steps, detail_steps, image_steps). 모든 프롬프트는 prompts/_base/{module}/{version}/ 외부 파일, 하드코딩 제로.

**Tech Stack:** Python 3.12, FastAPI, SQLAlchemy 2.0, PostgreSQL, LiteLLM Router (GPT-5.4/Mini/Nano + Gemini), Opik, pytest

**Spec:** `docs/superpowers/specs/2026-03-23-pipeline-v3-design.md`

---

## File Structure Map

### New Files (Create)
```
backend/app/core/steps/text_steps.py        # TextCleanupStep
backend/app/core/steps/summary_steps.py      # EpisodeSummaryStep, SceneSummaryStep, ProjectSummaryStep
backend/app/core/steps/entity_steps.py       # EntityExtractStep, EntityReviewStep, EntityDetailStep, EntityT2iStep
backend/app/core/steps/scene_steps.py        # SceneSegmentationStep, SceneSplitStep, SceneSaveStep
backend/app/core/steps/director_steps.py     # SceneDirectorStep, SceneCinematographyStep, SceneDependencyStep
backend/app/core/steps/outlook_steps.py      # OutlookExtractionStep, OutlookDedupStep
backend/app/core/steps/detail_steps.py       # SceneDetailStep, SceneVerifyStep

backend/app/modules/pipeline/text_cleaner.py       # clean_text()
backend/app/modules/pipeline/episode_summarizer.py  # summarize_episode()
backend/app/modules/pipeline/scene_summarizer.py    # summarize_scenes()
backend/app/modules/pipeline/visual_world_rules.py  # extract_visual_rules()
backend/app/modules/pipeline/entity_extractor_v4.py # 3턴 타입별 추출
backend/app/modules/pipeline/entity_reviewer.py     # cross-model 검증
backend/app/modules/pipeline/outlook_extractor_v2.py # 2단계 아웃룩 (phase1 + phase2)
backend/app/modules/pipeline/scene_director_v2.py   # V/A/H 분류
backend/app/modules/pipeline/scene_dependency_v2.py # 배경+인물 분리 연관

prompts/_base/text_cleanup/1.202603231200/system.md
prompts/_base/text_cleanup/1.202603231200/cleanup_schema.json
prompts/_base/episode_summary/1.202603231200/system.md
prompts/_base/episode_summary/1.202603231200/summary_schema.json
prompts/_base/visual_world_rules/1.202603231200/system.md
prompts/_base/visual_world_rules/1.202603231200/rules_schema.json
prompts/_base/scene_summary/1.202603231200/system.md
prompts/_base/scene_summary/1.202603231200/summary_schema.json
prompts/_base/entity_extract_v4/1.202603231200/{system,character,location,prop}.md + schemas
prompts/_base/entity_review_v4/1.202603231200/system.md + review_schema.json
prompts/_base/scene_director/4.202603231200/system.md + analyze_schema.json  (V/A/H)
prompts/_base/scene_dependency/2.202603231200/system.md + dependency_schema.json (배경+인물)
prompts/_base/outlook_extractor/6.202603231200/{phase1,phase2}.md + schemas
prompts/_base/scene_verify/1.202603231200/system.md + verify_schema.json (앞2씬)

backend/tests/test_step_manifest_v3.py
backend/tests/test_text_cleanup.py
backend/tests/test_episode_summary.py
backend/tests/test_visual_world_rules.py
backend/tests/test_scene_summary.py
backend/tests/test_entity_extract_v4.py
backend/tests/test_entity_review_v4.py
backend/tests/test_scene_director_v2.py
backend/tests/test_scene_dependency_v2.py
backend/tests/test_outlook_v2.py
backend/tests/test_scene_detail_v3.py
backend/tests/test_scene_verify_v2.py
backend/tests/test_sync_v3.py
backend/tests/test_pipeline_v3_e2e.py
```

### Modified Files
```
backend/app/core/step_manifest.py        # 23단계로 전면 교체
backend/app/core/steps/__init__.py       # 신규 step 클래스 등록
backend/app/core/steps/image_steps.py    # world_guide input 수정 (scene_summary 활용)
backend/app/core/config.py              # env 추가 (summary lengths)
backend/app/core/database.py            # DB 마이그레이션 (신규 컬럼)
backend/app/models/project.py           # SceneStill에 scene_summary, audio_entity_ids, hallucination_entity_ids
backend/app/api/v1/steps.py             # _sync_checkpoints_to_db v3 + _get_step_runner 확장
backend/app/api/v1/entities.py          # V/A/H entity 표시
backend/.env                            # 신규 env 변수
```

### Preserved (기존 모듈 삭제 금지)
```
backend/app/core/steps/analysis_steps.py    # 기존 유지 (v2 호환)
backend/app/modules/pipeline/entity_extractor_v2.py
backend/app/modules/pipeline/entity_extractor_v3.py
backend/app/modules/pipeline/scene_extractor_v2.py
backend/app/modules/pipeline/outlook_extractor.py  # 기존 유지
backend/app/modules/pipeline/scene_dependency_extractor.py  # 기존 유지
backend/app/modules/pipeline/scene_validator.py  # 기존 유지
```

---

## Phase 0: 인프라 준비

### Task 1: 새 브랜치 생성 + 기본 설정

**Files:**
- Modify: `backend/app/core/config.py`
- Modify: `backend/.env`

- [ ] **Step 1: 새 브랜치 생성**

```bash
git checkout main
git pull origin main
git checkout -b feat/pipeline-v3
```

- [ ] **Step 2: config.py에 env 변수 추가**

`backend/app/core/config.py`에 추가:
```python
# Pipeline v3 요약 길이 설정
max_episode_summary_length: int = 500
max_scene_summary_length: int = 200
max_project_summary_length: int = 500
```

- [ ] **Step 3: .env에 기본값 추가**

```
MAX_EPISODE_SUMMARY_LENGTH=500
MAX_SCENE_SUMMARY_LENGTH=200
MAX_PROJECT_SUMMARY_LENGTH=500
```

- [ ] **Step 4: 커밋**

```bash
git add backend/app/core/config.py backend/.env
git commit -m "chore: Pipeline v3 env 설정 추가 (summary lengths)"
git push -u origin feat/pipeline-v3
```

---

### Task 2: DB 마이그레이션

**Files:**
- Modify: `backend/app/models/project.py`
- Modify: `backend/app/core/database.py`
- Test: `backend/tests/test_models.py`

- [ ] **Step 1: SceneStill 모델에 신규 컬럼 추가**

`backend/app/models/project.py` SceneStill 클래스에 추가:
```python
scene_summary = Column(Text)                    # 씬 요약 (200자)
audio_entity_ids = Column(Text, default="[]")   # V/A/H: 소리만 들리는 엔티티 [short_id]
hallucination_entity_ids = Column(Text, default="[]")  # V/A/H: 비물리적 표현 [short_id]
```

- [ ] **Step 2: database.py 마이그레이션 SQL 추가**

`backend/app/core/database.py` `_migrations` 리스트에 추가:
```python
# Pipeline v3 — scene_still 확장
"ALTER TABLE scene_still ADD COLUMN IF NOT EXISTS scene_summary TEXT",
"ALTER TABLE scene_still ADD COLUMN IF NOT EXISTS audio_entity_ids TEXT DEFAULT '[]'",
"ALTER TABLE scene_still ADD COLUMN IF NOT EXISTS hallucination_entity_ids TEXT DEFAULT '[]'",
```

- [ ] **Step 3: 마이그레이션 적용 확인**

```bash
cd backend && python -c "from app.core.database import init_db; init_db(); print('Migration OK')"
```
Expected: `Migration OK` (no errors)

- [ ] **Step 4: 커밋**

```bash
git add backend/app/models/project.py backend/app/core/database.py
git commit -m "db: Pipeline v3 마이그레이션 — scene_summary, audio/hallucination entity IDs"
git push
```

---

### Task 3: step_manifest.py v3 전면 교체

**Files:**
- Modify: `backend/app/core/step_manifest.py`
- Create: `backend/tests/test_step_manifest_v3.py`

- [ ] **Step 1: 테스트 작성**

```python
# backend/tests/test_step_manifest_v3.py
"""step_manifest v3 — 23단계 정의, 의존성 DAG, 순서 검증."""
import pytest
from app.core.step_manifest import (
    STEP_MANIFEST, get_depends_on, get_downstream_steps,
    get_all_downstream_recursive, get_ordered_steps,
)

def test_total_step_count():
    """v3는 정확히 23단계."""
    assert len(STEP_MANIFEST) == 23

def test_analysis_steps_count():
    analysis = [s for s in STEP_MANIFEST.values() if s["category"] == "analysis"]
    assert len(analysis) == 17

def test_image_steps_count():
    image = [s for s in STEP_MANIFEST.values() if s["category"] == "image"]
    assert len(image) == 4

def test_auxiliary_steps_count():
    aux = [s for s in STEP_MANIFEST.values() if s["category"] == "auxiliary"]
    assert len(aux) == 2

def test_new_steps_exist():
    """v3 신규 단계가 모두 등록되어 있는지."""
    new_steps = [
        "text_cleanup", "episode_summary", "visual_world_rules",
        "scene_save", "scene_summary",
    ]
    for sid in new_steps:
        assert sid in STEP_MANIFEST, f"Missing step: {sid}"

def test_order_is_unique():
    """order 값 중복 없어야 함."""
    orders = [s["order"] for s in STEP_MANIFEST.values()]
    assert len(orders) == len(set(orders))

def test_order_monotonic():
    """analysis(1-17) < image(20-23) < auxiliary(100+)."""
    ordered = get_ordered_steps()
    for i in range(len(ordered) - 1):
        assert ordered[i]["order"] <= ordered[i + 1]["order"]

def test_dependency_graph_no_cycles():
    """의존성 DAG에 순환 없어야 함."""
    visited = set()
    rec_stack = set()
    def _has_cycle(node):
        visited.add(node)
        rec_stack.add(node)
        for dep in get_downstream_steps(node):
            if dep not in visited:
                if _has_cycle(dep):
                    return True
            elif dep in rec_stack:
                return True
        rec_stack.discard(node)
        return False
    for sid in STEP_MANIFEST:
        if sid not in visited:
            assert not _has_cycle(sid), f"Cycle detected at {sid}"

def test_scene_director_depends_on_entity_and_scene():
    """scene_director는 entity_t2i + scene_split 이후."""
    deps = get_depends_on("scene_director")
    assert "entity_t2i" in deps
    assert "scene_split" in deps

def test_scene_detail_depends_on_all_prior():
    """scene_detail은 scene_dependency, outlook_extraction, scene_cinematography, entity_t2i 이후."""
    deps = get_depends_on("scene_detail")
    assert "scene_dependency" in deps
    assert "outlook_extraction" in deps
    assert "scene_cinematography" in deps
    assert "entity_t2i" in deps

def test_text_cleanup_has_no_dependencies():
    assert get_depends_on("text_cleanup") == []

def test_scene_segmentation_depends_on_text_cleanup():
    deps = get_depends_on("scene_segmentation")
    assert "text_cleanup" in deps

def test_entity_extract_depends_on_scene_save_and_visual_world_rules():
    deps = get_depends_on("entity_extract")
    assert "scene_save" in deps
    assert "visual_world_rules" in deps

def test_scene_summary_depends_on_scene_save_and_episode_summary():
    deps = get_depends_on("scene_summary")
    assert "scene_save" in deps
    assert "episode_summary" in deps
    assert "visual_world_rules" in deps

def test_downstream_of_text_cleanup():
    """text_cleanup 무효화 시 scene_segmentation 이후 전부 영향."""
    downstream = get_all_downstream_recursive("text_cleanup")
    assert "scene_segmentation" in downstream
    assert "scene_detail" in downstream

def test_vah_step_is_scene_director():
    """scene_director가 V/A/H 분류 담당."""
    assert STEP_MANIFEST["scene_director"]["label"] == "씬 감독 (V/A/H)"
```

- [ ] **Step 2: 테스트 실패 확인**

```bash
cd backend && python -m pytest tests/test_step_manifest_v3.py -v 2>&1 | head -40
```
Expected: FAIL (현재 manifest에 text_cleanup 등 없음)

- [ ] **Step 3: step_manifest.py v3 구현**

`backend/app/core/step_manifest.py` 전면 교체:
```python
"""파이프라인 Step Manifest v3 — 23단계 정의, 의존성, 설정.

이 파일이 파이프라인의 단일 진실 소스(Single Source of Truth).
v3: 17분석 + 4이미지 + 2보조 = 23단계.
"""

from typing import Any, Dict, List, Optional

STEP_STATUSES = [
    "pending", "blocked", "runnable", "running", "completed",
    "partial", "failed", "not_applicable", "stale", "cancelled",
]

STEP_MANIFEST: Dict[str, Dict[str, Any]] = {
    # ── 분석 Phase (17단계) ──

    # 1. 텍스트 정리
    "text_cleanup": {
        "label": "텍스트 정리",
        "category": "analysis",
        "order": 1,
        "default_model": "gpt-nano",
        "provider": "openai",
        "depends_on": [],
        "fan_out": False,
        "applicability": "always",
    },
    # 2. 씬 세그먼테이션 (헤딩 기반)
    "scene_segmentation": {
        "label": "씬 세그먼테이션",
        "category": "analysis",
        "order": 2,
        "default_model": "gpt-nano",
        "provider": "openai",
        "depends_on": ["text_cleanup"],
        "fan_out": False,
        "applicability": "always",
    },
    # 3. 에피소드 요약
    "episode_summary": {
        "label": "에피소드 요약",
        "category": "analysis",
        "order": 3,
        "default_model": "gpt-mini",
        "provider": "openai",
        "depends_on": ["text_cleanup"],
        "fan_out": False,
        "applicability": "always",
    },
    # 4. 시각적 세계관 규칙
    "visual_world_rules": {
        "label": "시각적 세계관 규칙",
        "category": "analysis",
        "order": 4,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["episode_summary"],
        "fan_out": False,
        "applicability": "always",
    },
    # 5. 씬 재분할 (큰 씬)
    "scene_split": {
        "label": "큰 씬 분할",
        "category": "analysis",
        "order": 5,
        "default_model": "gpt-mini",
        "provider": "openai",
        "depends_on": ["scene_segmentation"],
        "fan_out": True,
        "applicability": "if_large_scenes",
    },
    # 6. 씬 저장 (DB + JSON)
    "scene_save": {
        "label": "씬 저장",
        "category": "analysis",
        "order": 6,
        "default_model": "-",
        "provider": "-",
        "depends_on": ["scene_split"],
        "fan_out": False,
        "applicability": "always",
    },
    # 7. 씬별 요약
    "scene_summary": {
        "label": "씬별 요약 (병렬)",
        "category": "analysis",
        "order": 7,
        "default_model": "gpt-mini",
        "provider": "openai",
        "depends_on": ["scene_save", "episode_summary", "visual_world_rules"],
        "fan_out": True,
        "applicability": "always",
    },
    # 8. 요소 추출 (3턴: 인물→배경→소품)
    "entity_extract": {
        "label": "요소 추출 (3턴)",
        "category": "analysis",
        "order": 8,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_save", "visual_world_rules"],
        "fan_out": False,
        "applicability": "always",
    },
    # 9. 요소 교차 검증 (다른 모델)
    "entity_review": {
        "label": "요소 교차 검증",
        "category": "analysis",
        "order": 9,
        "default_model": "gemini-pro",
        "provider": "gemini",
        "depends_on": ["entity_extract"],
        "fan_out": False,
        "applicability": "always",
    },
    # 10. 요소 상세 + enum 확정
    "entity_detail": {
        "label": "요소 상세 + enum",
        "category": "analysis",
        "order": 10,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["entity_review"],
        "fan_out": False,
        "applicability": "always",
    },
    # 11. T2I 프롬프트 (병렬)
    "entity_t2i": {
        "label": "요소 T2I 프롬프트 (병렬)",
        "category": "analysis",
        "order": 11,
        "default_model": "gpt-mini",
        "provider": "openai",
        "depends_on": ["entity_detail"],
        "fan_out": True,
        "applicability": "always",
    },
    # 12. 씬 감독 (V/A/H)
    "scene_director": {
        "label": "씬 감독 (V/A/H)",
        "category": "analysis",
        "order": 12,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_split", "entity_t2i"],
        "fan_out": False,
        "applicability": "always",
    },
    # 13. 촬영 감독
    "scene_cinematography": {
        "label": "촬영 감독 (기법 선택)",
        "category": "analysis",
        "order": 13,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_director"],
        "fan_out": False,
        "applicability": "always",
    },
    # 14. 씬 연관 분석 (확장: 배경+인물)
    "scene_dependency": {
        "label": "씬 연관 분석 (배경+인물)",
        "category": "analysis",
        "order": 14,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_director"],
        "fan_out": False,
        "applicability": "always",
    },
    # 15. 아웃룩 추출 (2단계)
    "outlook_extraction": {
        "label": "아웃룩 추출 (2단계)",
        "category": "analysis",
        "order": 15,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_director"],
        "fan_out": False,
        "applicability": "always",
    },
    # 16. 씬 상세 분석 (병렬)
    "scene_detail": {
        "label": "씬 상세 분석 (병렬)",
        "category": "analysis",
        "order": 16,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_dependency", "outlook_extraction", "scene_cinematography", "entity_t2i"],
        "fan_out": True,
        "applicability": "always",
    },
    # 17. 교차 검증 (병렬, 앞2씬 컨텍스트)
    "scene_verify": {
        "label": "교차 검증 (앞2씬)",
        "category": "analysis",
        "order": 17,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["scene_detail"],
        "fan_out": True,
        "applicability": "if_multi_char_scenes",
    },

    # ── 이미지 Phase (4단계) ──

    "world_guide": {
        "label": "월드 가이드",
        "category": "image",
        "order": 20,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["entity_t2i", "scene_detail"],
        "fan_out": False,
        "applicability": "always",
    },
    "ref_image_gen": {
        "label": "요소 참조 이미지",
        "category": "image",
        "order": 21,
        "default_model": "gemini-image",
        "provider": "gemini",
        "depends_on": ["entity_t2i"],
        "fan_out": True,
        "applicability": "always",
    },
    "composite_image_gen": {
        "label": "인물+아웃룩 합성 이미지",
        "category": "image",
        "order": 22,
        "default_model": "gemini-image",
        "provider": "gemini",
        "depends_on": ["ref_image_gen", "outlook_extraction"],
        "fan_out": True,
        "applicability": "if_has_outlooks",
    },
    "scene_image_pipeline": {
        "label": "씬 이미지 생성 (compound)",
        "category": "image",
        "order": 23,
        "default_model": "mixed",
        "provider": "mixed",
        "depends_on": ["composite_image_gen", "scene_verify", "world_guide"],
        "fan_out": True,
        "sub_steps": [
            {"id": "prompt_translation", "label": "T2I 번역", "default_model": "gpt-mini", "provider": "openai"},
            {"id": "scene_t2i_gen", "label": "T2I 이미지 생성", "default_model": "gemini-image", "provider": "gemini"},
            {"id": "scene_t2i_validation", "label": "이미지 검증", "default_model": "gpt", "provider": "openai"},
            {"id": "prompt_sanitize", "label": "프롬프트 안전화", "default_model": "gpt", "provider": "openai"},
            {"id": "angle_recommend", "label": "앵글 추천", "default_model": "gpt", "provider": "openai"},
            {"id": "fal_angle_apply", "label": "fal.ai 앵글 적용", "default_model": "fal-ai", "provider": "fal"},
            {"id": "final_select", "label": "최종 선택", "default_model": "gpt", "provider": "openai"},
        ],
        "applicability": "always",
    },

    # ── 보조 (on-demand) ──

    "outlook_dedup": {
        "label": "아웃룩 중복 판별",
        "category": "auxiliary",
        "order": 100,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["outlook_extraction"],
        "fan_out": False,
        "applicability": "on_demand",
    },
    "project_summary": {
        "label": "프로젝트 요약",
        "category": "auxiliary",
        "order": 101,
        "default_model": "gpt",
        "provider": "openai",
        "depends_on": ["episode_summary"],
        "fan_out": False,
        "applicability": "on_demand",
    },
}
```
(get_step_info, get_depends_on, get_downstream_steps, get_all_downstream_recursive, get_ordered_steps 함수는 기존 유지)

- [ ] **Step 4: 테스트 통과 확인**

```bash
cd backend && python -m pytest tests/test_step_manifest_v3.py -v
```
Expected: ALL PASS

- [ ] **Step 5: 커밋**

```bash
git add backend/app/core/step_manifest.py backend/tests/test_step_manifest_v3.py
git commit -m "feat: step_manifest v3 — 23단계 (17분석+4이미지+2보조)"
git push
```

---

## Phase 1: 기반 단계 (Steps 1-6)

### Task 4: text_cleanup 모듈 + Step

**Files:**
- Create: `backend/app/modules/pipeline/text_cleaner.py`
- Create: `prompts/_base/text_cleanup/1.202603231200/system.md`
- Create: `prompts/_base/text_cleanup/1.202603231200/cleanup_schema.json`
- Create: `backend/app/core/steps/text_steps.py`
- Create: `backend/tests/test_text_cleanup.py`

- [ ] **Step 1: 테스트 작성**

```python
# backend/tests/test_text_cleanup.py
"""text_cleanup 모듈 테스트 — PDF 추출 텍스트 정리."""
import pytest
from unittest.mock import patch, MagicMock

def test_clean_text_removes_page_numbers():
    from app.modules.pipeline.text_cleaner import clean_text
    # mock LLM call
    with patch("app.modules.pipeline.text_cleaner.call_text") as mock_call:
        mock_call.return_value = "정리된 시나리오 텍스트"
        result = clean_text("--- 페이지 1 ---\n시나리오 텍스트\n--- 페이지 2 ---")
        assert result == "정리된 시나리오 텍스트"
        mock_call.assert_called_once()

def test_clean_text_passes_fulltext_to_prompt():
    from app.modules.pipeline.text_cleaner import clean_text
    with patch("app.modules.pipeline.text_cleaner.call_text") as mock_call:
        mock_call.return_value = "cleaned"
        clean_text("원본 텍스트", project_config={"override_model": {}})
        args, kwargs = mock_call.call_args
        assert kwargs["step"] == "text_cleanup"

def test_clean_text_loads_external_prompt():
    from app.modules.pipeline.text_cleaner import clean_text
    with patch("app.modules.pipeline.text_cleaner.call_text") as mock_call, \
         patch("app.modules.pipeline.text_cleaner.load_prompt") as mock_prompt:
        mock_call.return_value = "cleaned"
        mock_prompt.return_value = "system prompt"
        clean_text("원본")
        mock_prompt.assert_called_once()
```

- [ ] **Step 2: 테스트 실패 확인**

```bash
cd backend && python -m pytest tests/test_text_cleanup.py -v
```
Expected: FAIL (모듈 없음)

- [ ] **Step 3: 프롬프트 파일 생성**

`prompts/_base/text_cleanup/1.202603231200/system.md`:
```markdown
# 텍스트 정리

시나리오 PDF에서 추출된 원본 텍스트를 정리하는 역할.

## 제거 대상
- 페이지 번호 (예: "- 1 -", "페이지 3")
- 구분선 (예: "===", "---", "***")
- 헤더/푸터 (반복되는 문서 제목, 날짜)
- 빈 줄 과다 (3줄 이상 → 2줄로)
- PDF 변환 아티팩트 (깨진 문자, 불필요 공백)

## 유지 대상
- 씬 헤딩 (S#, 장소, 시간)
- 대사 및 지문
- 인물 이름 표기
- 전환 지시 (CUT TO, FADE IN 등)

## 출력
정리된 시나리오 텍스트를 그대로 출력. JSON이 아닌 순수 텍스트.
```

- [ ] **Step 4: text_cleaner.py 구현**

```python
# backend/app/modules/pipeline/text_cleaner.py
"""텍스트 정리 모듈 — PDF 추출 텍스트에서 불필요 요소 제거."""

import logging
from typing import Dict, Optional

from app.modules.llm.llm_client import call_text
from app.modules.prompt_loader import load_prompt

logger = logging.getLogger(__name__)

_MODULE = "text_cleanup"


def clean_text(
    fulltext: str,
    project_config: Optional[Dict] = None,
    opik_metadata: Optional[Dict] = None,
) -> str:
    system = load_prompt(_MODULE, "system")
    user_prompt = (
        "아래 시나리오 텍스트를 정리해주세요. "
        "페이지 번호, 구분선, 헤더/푸터를 제거하고 순수 시나리오만 남겨주세요.\n\n"
        f"{fulltext}"
    )

    result = call_text(
        step="text_cleanup",
        system_prompt=system,
        user_prompt=user_prompt,
        project_config=project_config,
        opik_metadata=opik_metadata,
    )
    return result.strip()
```

- [ ] **Step 5: TextCleanupStep 구현**

```python
# backend/app/core/steps/text_steps.py
"""텍스트 Phase StepRunner — text_cleanup."""

import logging
from typing import Any, Dict

from app.core.step_runner import StepRunner

logger = logging.getLogger(__name__)


class TextCleanupStep(StepRunner):
    """Step 1: PDF 텍스트 정리."""

    def _execute(self, mode="resume") -> Dict[str, Any]:
        from app.models.project import Episode
        from sqlalchemy.orm import undefer

        ep = self.db.query(Episode).options(undefer(Episode.fulltext)).filter(
            Episode.id == self.episode_id
        ).first()
        if not ep or not ep.fulltext:
            from app.core.errors import AppError
            raise AppError(code="step.no_fulltext", message="시나리오 텍스트가 없습니다.", status_code=400)

        from app.modules.pipeline.text_cleaner import clean_text

        cleaned = clean_text(
            fulltext=ep.fulltext,
            project_config=self.project_config,
            opik_metadata=self.build_opik_metadata(),
        )

        return {
            "completed_count": 1,
            "applicable_count": 1,
            "failed_count": 0,
            "data": {"cleaned_text": cleaned, "original_length": len(ep.fulltext), "cleaned_length": len(cleaned)},
        }
```

- [ ] **Step 6: 테스트 통과 확인**

```bash
cd backend && python -m pytest tests/test_text_cleanup.py -v
```
Expected: ALL PASS

- [ ] **Step 7: 커밋**

```bash
git add backend/app/modules/pipeline/text_cleaner.py backend/app/core/steps/text_steps.py \
  prompts/_base/text_cleanup/ backend/tests/test_text_cleanup.py
git commit -m "feat: text_cleanup step — PDF 텍스트 정리 (Step 1)"
git push
```

---

### Task 5: episode_summary 모듈 + Step

**Files:**
- Create: `backend/app/modules/pipeline/episode_summarizer.py`
- Create: `prompts/_base/episode_summary/1.202603231200/system.md`
- Create: `prompts/_base/episode_summary/1.202603231200/summary_schema.json`
- Create: `backend/app/core/steps/summary_steps.py`
- Create: `backend/tests/test_episode_summary.py`

- [ ] **Step 1: 테스트 작성**

```python
# backend/tests/test_episode_summary.py
"""episode_summary 모듈 테스트."""
from unittest.mock import patch

def test_summarize_episode_returns_string():
    from app.modules.pipeline.episode_summarizer import summarize_episode
    with patch("app.modules.pipeline.episode_summarizer.call_structured") as mock:
        mock.return_value = {"summary": "요약 텍스트", "key_events": ["이벤트1"]}
        result = summarize_episode("시나리오 전문", max_length=500)
        assert "summary" in result
        assert len(result["summary"]) > 0

def test_summarize_episode_respects_max_length():
    from app.modules.pipeline.episode_summarizer import summarize_episode
    with patch("app.modules.pipeline.episode_summarizer.call_structured") as mock:
        mock.return_value = {"summary": "A" * 600, "key_events": []}
        result = summarize_episode("전문", max_length=500)
        # 모듈은 LLM 응답을 그대로 반환, 길이 제한은 프롬프트에서 지시
        assert result["summary"] is not None

def test_summarize_episode_loads_external_prompt():
    from app.modules.pipeline.episode_summarizer import summarize_episode
    with patch("app.modules.pipeline.episode_summarizer.call_structured") as mock, \
         patch("app.modules.pipeline.episode_summarizer.load_prompt") as mock_p:
        mock.return_value = {"summary": "ok", "key_events": []}
        mock_p.return_value = "system"
        summarize_episode("전문")
        mock_p.assert_called_once()
```

- [ ] **Step 2: 테스트 실패 확인**

```bash
cd backend && python -m pytest tests/test_episode_summary.py -v
```

- [ ] **Step 3: 프롬프트 파일 생성**

`prompts/_base/episode_summary/1.202603231200/system.md`:
```markdown
# 에피소드 요약

시나리오 전문을 분석하여 핵심 줄거리를 요약.

## 규칙
- 최대 {max_length}자
- 주요 인물, 핵심 사건, 갈등 포인트 포함
- 시각적 설정 변화 (장소 이동, 시간대) 반영
- 스포일러 포함 OK (내부 분석용)
- 고유명사 그대로 사용
```

`prompts/_base/episode_summary/1.202603231200/summary_schema.json`:
```json
{
  "type": "object",
  "properties": {
    "summary": {"type": "string", "description": "에피소드 요약"},
    "key_events": {
      "type": "array",
      "items": {"type": "string"},
      "description": "핵심 이벤트 목록 (5개 이내)"
    }
  },
  "required": ["summary", "key_events"],
  "additionalProperties": false
}
```

- [ ] **Step 4: episode_summarizer.py 구현**

```python
# backend/app/modules/pipeline/episode_summarizer.py
"""에피소드 요약 모듈."""

import logging
from typing import Dict, Optional

from app.modules.llm.llm_client import call_structured
from app.modules.prompt_loader import load_prompt, load_schema

logger = logging.getLogger(__name__)

_MODULE = "episode_summary"


def summarize_episode(
    fulltext: str,
    max_length: int = 500,
    project_config: Optional[Dict] = None,
    opik_metadata: Optional[Dict] = None,
) -> Dict:
    system = load_prompt(_MODULE, "system", max_length=max_length)
    schema = load_schema(_MODULE, "summary_schema")

    user_prompt = (
        f"아래 시나리오를 {max_length}자 이내로 요약해주세요.\n\n"
        f"{fulltext}"
    )

    result = call_structured(
        step="episode_summary",
        system_prompt=system,
        user_prompt=user_prompt,
        response_schema=schema,
        project_config=project_config,
        schema_name="episode_summary",
        opik_metadata=opik_metadata,
    )
    return result
```

- [ ] **Step 5: EpisodeSummaryStep 구현**

`backend/app/core/steps/summary_steps.py`:
```python
"""요약 Phase StepRunner — episode_summary, scene_summary, project_summary."""

import json
import logging
from concurrent.futures import ThreadPoolExecutor, as_completed
from typing import Any, Dict, Optional

from app.core.step_runner import StepRunner

logger = logging.getLogger(__name__)


class _SummaryStepMixin:
    def _load_fulltext(self) -> str:
        from app.models.project import Episode
        from sqlalchemy.orm import undefer
        ep = self.db.query(Episode).options(undefer(Episode.fulltext)).filter(
            Episode.id == self.episode_id
        ).first()
        if not ep or not ep.fulltext:
            from app.core.errors import AppError
            raise AppError(code="step.no_fulltext", message="시나리오 텍스트가 없습니다.", status_code=400)
        return ep.fulltext

    def _load_cleaned_text(self) -> str:
        """text_cleanup 결과가 있으면 사용, 없으면 원본."""
        cp = self._load_prev_checkpoint("text_cleanup")
        if cp and cp.get("data", {}).get("cleaned_text"):
            return cp["data"]["cleaned_text"]
        return self._load_fulltext()

    def _load_prev_checkpoint(self, step_id: str) -> Optional[Dict]:
        from pathlib import Path
        from app.core.config import settings
        cp = Path(settings.projects_dir) / self.project_id / "checkpoints" / "episodes" / self.episode_id / step_id / "manifest.json"
        if cp.exists():
            return json.loads(cp.read_text(encoding="utf-8"))
        return None


class EpisodeSummaryStep(_SummaryStepMixin, StepRunner):
    """Step 3: 에피소드 요약 (500자)."""

    def _execute(self, mode="resume") -> Dict[str, Any]:
        fulltext = self._load_cleaned_text()

        from app.core.config import settings
        from app.modules.pipeline.episode_summarizer import summarize_episode

        result = summarize_episode(
            fulltext=fulltext,
            max_length=settings.max_episode_summary_length,
            project_config=self.project_config,
            opik_metadata=self.build_opik_metadata(),
        )

        # episode.summary 에 저장
        from app.models.project import Episode
        ep = self.db.query(Episode).filter(Episode.id == self.episode_id).first()
        if ep:
            ep.summary = result.get("summary", "")
            self.db.commit()

        return {
            "completed_count": 1,
            "applicable_count": 1,
            "failed_count": 0,
            "data": result,
        }
```
(SceneSummaryStep, ProjectSummaryStep도 같은 파일에 후속 Task에서 추가)

- [ ] **Step 6: 테스트 통과 확인**

```bash
cd backend && python -m pytest tests/test_episode_summary.py -v
```

- [ ] **Step 7: 커밋**

```bash
git add backend/app/modules/pipeline/episode_summarizer.py backend/app/core/steps/summary_steps.py \
  prompts/_base/episode_summary/ backend/tests/test_episode_summary.py
git commit -m "feat: episode_summary step — 에피소드 요약 500자 (Step 3)"
git push
```

---

### Task 6: visual_world_rules 모듈 + Step

**Files:**
- Create: `backend/app/modules/pipeline/visual_world_rules.py`
- Create: `prompts/_base/visual_world_rules/1.202603231200/system.md`
- Create: `prompts/_base/visual_world_rules/1.202603231200/rules_schema.json`
- Modify: `backend/app/core/steps/summary_steps.py` (VisualWorldRulesStep 추가)
- Create: `backend/tests/test_visual_world_rules.py`

- [ ] **Step 1: 테스트 작성** — visual_world_rules 모듈 + LLM mock
- [ ] **Step 2: 테스트 실패 확인**
- [ ] **Step 3: 프롬프트 파일 생성** — system.md (비히클/변신/영혼 가이드), rules_schema.json
- [ ] **Step 4: visual_world_rules.py 구현** — `extract_visual_rules(fulltext, summary, project_config, opik_metadata) -> Dict`
- [ ] **Step 5: VisualWorldRulesStep 구현** — _SummaryStepMixin 재사용, episode_summary 체크포인트에서 요약 로드
- [ ] **Step 6: 테스트 통과 확인**
- [ ] **Step 7: 커밋**

```bash
git commit -m "feat: visual_world_rules step — 시각적 세계관 규칙 (Step 4)"
git push
```

---

### Task 7: scene_segmentation + scene_split + scene_save 리팩토링

**Files:**
- Create: `backend/app/core/steps/scene_steps.py` (SceneSegmentationStep, SceneSplitStep, SceneSaveStep)
- Create: `backend/tests/test_scene_steps_v3.py`
- 기존 `backend/app/modules/pipeline/scene_extractor_v2.py`의 세그먼테이션 로직 재활용

- [ ] **Step 1: 테스트 작성** — scene_segmentation (cleaned text 사용), scene_split (조건부), scene_save (DB 저장)
- [ ] **Step 2: 테스트 실패 확인**
- [ ] **Step 3: scene_steps.py 구현**
  - `SceneSegmentationStep`: text_cleanup 결과에서 세그먼테이션. 기존 scene_extractor_v2._segment_scenes() 재활용
  - `SceneSplitStep`: 기존 로직 유지, check_applicability() 오버라이드
  - `SceneSaveStep`: 분할 결과를 DB scene_still + checkpoint JSON에 저장 (LLM 호출 없음, 순수 저장)
- [ ] **Step 4: 테스트 통과 확인**
- [ ] **Step 5: 커밋**

```bash
git commit -m "feat: scene_steps v3 — segmentation + split + save (Steps 2,5,6)"
git push
```

---

## Phase 2: 요소 단계 (Steps 7-11)

### Task 8: scene_summary 모듈 + Step

**Files:**
- Create: `backend/app/modules/pipeline/scene_summarizer.py`
- Create: `prompts/_base/scene_summary/1.202603231200/system.md`
- Create: `prompts/_base/scene_summary/1.202603231200/summary_schema.json`
- Modify: `backend/app/core/steps/summary_steps.py` (SceneSummaryStep 추가)
- Create: `backend/tests/test_scene_summary.py`

- [ ] **Step 1: 테스트 작성** — ThreadPool 병렬 요약, 앞3씬 + 에피소드 요약 + rules 입력
- [ ] **Step 2: 테스트 실패 확인**
- [ ] **Step 3: 프롬프트 + 스키마 생성** — 200자 제한, 시각적 요소 중심
- [ ] **Step 4: scene_summarizer.py 구현** — `summarize_scenes(segments, episode_summary, visual_rules, max_length, project_config) -> List[Dict]`
- [ ] **Step 5: SceneSummaryStep 구현** — ThreadPoolExecutor, scene_save + episode_summary + visual_world_rules 체크포인트 로드
- [ ] **Step 6: 테스트 통과 확인**
- [ ] **Step 7: 커밋**

```bash
git commit -m "feat: scene_summary step — 씬별 200자 요약 (Step 7)"
git push
```

---

### Task 9: entity_extract v4 (3턴 타입별) 모듈 + Step

**Files:**
- Create: `backend/app/modules/pipeline/entity_extractor_v4.py`
- Create: `prompts/_base/entity_extract_v4/1.202603231200/{system,character,location,prop}.md` + schemas
- Create: `backend/app/core/steps/entity_steps.py`
- Create: `backend/tests/test_entity_extract_v4.py`

- [ ] **Step 1: 테스트 작성** — 3턴 순차 (인물→배경→소품), 2회+ 등장 필터, visual_world_rules 기반
- [ ] **Step 2: 테스트 실패 확인**
- [ ] **Step 3: 프롬프트 파일 생성** — 타입별 프롬프트 (character.md, location.md, prop.md) + system.md + character_schema.json 등
- [ ] **Step 4: entity_extractor_v4.py 구현**
  - `extract_entities_by_type(fulltext, entity_type, visual_rules, project_config) -> List[Dict]`
  - 3턴: 인물→배경→소품 순차 호출
  - 필터: 2회 이상 다른 씬 등장만
  - visual_world_rules 기반 판단 (비히클 상태에서 외모 판별 등)
- [ ] **Step 5: EntityExtractStep 구현** — visual_world_rules 체크포인트 로드, 3턴 순차 실행
- [ ] **Step 6: 테스트 통과 확인**
- [ ] **Step 7: 커밋**

```bash
git commit -m "feat: entity_extract v4 — 3턴 타입별 추출 + 2회+ 등장 필터 (Step 8)"
git push
```

---

### Task 10: entity_review (교차 모델) + entity_detail + entity_t2i

**Files:**
- Create: `backend/app/modules/pipeline/entity_reviewer.py`
- Create: `prompts/_base/entity_review_v4/1.202603231200/system.md` + `review_schema.json`
- Modify: `backend/app/core/steps/entity_steps.py` (EntityReviewStep, EntityDetailStep, EntityT2iStep 추가)
- Create: `backend/tests/test_entity_review_v4.py`

- [ ] **Step 1: 테스트 작성** — cross-model 검증 (GPT→Gemini 또는 반대), 필터링
- [ ] **Step 2: 테스트 실패 확인**
- [ ] **Step 3: entity_reviewer.py 구현** — `review_entities(entities, fulltext, project_config) -> Dict`
  - entity_extract가 GPT → review는 gemini-pro (default_model 설정으로 자동)
  - 다른 모델이 동일 텍스트로 교차 확인
- [ ] **Step 4: EntityReviewStep, EntityDetailStep, EntityT2iStep 구현**
  - EntityDetailStep: 기존 entity_detail_batch 로직 재활용, short_id 확정
  - EntityT2iStep: 기존 ThreadPool 병렬 T2I 로직 재활용
- [ ] **Step 5: 테스트 통과 확인**
- [ ] **Step 6: 커밋**

```bash
git commit -m "feat: entity_review + detail + t2i — 교차 검증, 상세, T2I (Steps 9-11)"
git push
```

---

## Phase 3: 씬 분석 단계 (Steps 12-17)

### Task 11: scene_director v2 (V/A/H)

**Files:**
- Create: `backend/app/modules/pipeline/scene_director_v2.py`
- Create: `prompts/_base/scene_director/4.202603231200/system.md` + `analyze_schema.json`
- Create: `backend/app/core/steps/director_steps.py`
- Create: `backend/tests/test_scene_director_v2.py`

- [ ] **Step 1: 테스트 작성** — V/A/H 3분류, short_id enum, audio_entity_ids, hallucination_entity_ids
- [ ] **Step 2: 테스트 실패 확인**
- [ ] **Step 3: 프롬프트 파일 생성 (V/A/H 가이드)**
  - V (Visual): 물리적으로 존재하고 카메라에 보임
  - A (Audio): 소리만 (V.O., 전화, 방송)
  - H (Hallucination): 보이지만 물리적 존재 아님 (귀신, 투영체, 회상)
- [ ] **Step 4: scene_director_v2.py 구현**
  - `direct_scenes(segments, fulltext, entities, project_config) -> Dict`
  - 각 씬별: present_entity_ids (V), audio_entity_ids (A), hallucination_entity_ids (H)
  - short_id enum constraint
- [ ] **Step 5: SceneDirectorStep 구현** — director_steps.py
- [ ] **Step 6: 테스트 통과 확인**
- [ ] **Step 7: 커밋**

```bash
git commit -m "feat: scene_director v2 — V/A/H 3분류 (Step 12)"
git push
```

---

### Task 12: scene_cinematography + scene_dependency v2

**Files:**
- Create: `backend/app/modules/pipeline/scene_dependency_v2.py`
- Create: `prompts/_base/scene_dependency/2.202603231200/system.md` + `dependency_schema.json`
- Modify: `backend/app/core/steps/director_steps.py` (SceneCinematographyStep, SceneDependencyStep 추가)
- Create: `backend/tests/test_scene_dependency_v2.py`

- [ ] **Step 1: 테스트 작성** — 배경 중심 앞2씬 + 인물 중심 앞2씬
- [ ] **Step 2: 테스트 실패 확인**
- [ ] **Step 3: 프롬프트 파일 생성** — 배경/인물 분리 의존성
- [ ] **Step 4: scene_dependency_v2.py 구현** — `extract_dependencies(segments, entities, director_result) -> Dict`
  - 배경 중심: 같은/유사 장소 앞 2개 씬
  - 인물 중심: 같은 인물 등장 앞 2개 씬
- [ ] **Step 5: SceneCinematographyStep 기존 로직 이전, SceneDependencyStep 구현**
- [ ] **Step 6: 테스트 통과 확인**
- [ ] **Step 7: 커밋**

```bash
git commit -m "feat: scene_cinematography + dependency v2 — 배경+인물 분리 연관 (Steps 13-14)"
git push
```

---

### Task 13: outlook_extraction v2 (2단계)

**Files:**
- Create: `backend/app/modules/pipeline/outlook_extractor_v2.py`
- Create: `prompts/_base/outlook_extractor/6.202603231200/{phase1,phase2}.md` + schemas
- Create: `backend/app/core/steps/outlook_steps.py`
- Create: `backend/tests/test_outlook_v2.py`

- [ ] **Step 1: 테스트 작성** — 1단계 목록 추출 + 2단계 씬별 C+O enum 매핑
- [ ] **Step 2: 테스트 실패 확인**
- [ ] **Step 3: 프롬프트 파일 생성** — phase1 (아웃룩 목록), phase2 (씬별 매핑)
- [ ] **Step 4: outlook_extractor_v2.py 모듈 구현**
  - `extract_outlooks_phase1(segments, characters, project_config) -> Dict` — 전체 씬에서 아웃룩 목록
  - `extract_outlooks_phase2(segments, outlooks, characters, project_config) -> Dict` — 씬별 C+O enum 매핑
- [ ] **Step 5: OutlookExtractionStep 구현** — outlook_extractor_v2 모듈에 위임, 2단계 순차 호출
- [ ] **Step 6: OutlookDedupStep 이전**
- [ ] **Step 6: 테스트 통과 확인**
- [ ] **Step 7: 커밋**

```bash
git commit -m "feat: outlook_extraction v2 — 2단계 아웃룩 추출 (Step 15)"
git push
```

---

### Task 14: scene_detail + scene_verify (확장)

**Files:**
- Create: `backend/app/core/steps/detail_steps.py`
- Create: `prompts/_base/scene_verify/1.202603231200/system.md` + `verify_schema.json`
- Create: `backend/tests/test_scene_detail_v3.py`
- Create: `backend/tests/test_scene_verify_v2.py`

- [ ] **Step 1: 테스트 작성** — scene_detail (확정 visible_entities 기반), scene_verify (앞2씬 컨텍스트)
- [ ] **Step 2: 테스트 실패 확인**
- [ ] **Step 3: scene_verify 프롬프트 생성** — 앞 2씬 텍스트 포함, 소울라이드/빙의 추적
- [ ] **Step 4: SceneDetailStep 구현**
  - visible_entities: scene_director 결과(V)에서 자동 구축 (LLM 의존 X)
  - 입력: scene_dependency + outlook_extraction + scene_cinematography + entity_t2i + scene_summary
  - ThreadPool 병렬
- [ ] **Step 5: SceneVerifyStep 구현** — 앞2씬 텍스트 함께 전달, cross-model 검증
- [ ] **Step 6: 테스트 통과 확인**
- [ ] **Step 7: 커밋**

```bash
git commit -m "feat: scene_detail + scene_verify — 확정데이터 기반 + 앞2씬 컨텍스트 (Steps 16-17)"
git push
```

---

## Phase 4: 통합 + Step 등록 + Sync

### Task 15: Step 등록 + _get_step_runner 업데이트

**Files:**
- Modify: `backend/app/core/steps/__init__.py`
- Modify: `backend/app/api/v1/steps.py`

- [ ] **Step 1: __init__.py에 모든 v3 step 클래스 등록**

```python
# backend/app/core/steps/__init__.py
from app.core.steps.text_steps import TextCleanupStep
from app.core.steps.summary_steps import EpisodeSummaryStep, SceneSummaryStep, ProjectSummaryStep, VisualWorldRulesStep
from app.core.steps.entity_steps import EntityExtractStep, EntityReviewStep, EntityDetailStep, EntityT2iStep
from app.core.steps.scene_steps import SceneSegmentationStep, SceneSplitStep, SceneSaveStep
from app.core.steps.director_steps import SceneDirectorStep, SceneCinematographyStep, SceneDependencyStep
from app.core.steps.outlook_steps import OutlookExtractionStep, OutlookDedupStep
from app.core.steps.detail_steps import SceneDetailStep, SceneVerifyStep
from app.core.steps.image_steps import WorldGuideStep, RefImageGenStep, CompositeImageGenStep, SceneImagePipelineStep

STEP_CLASSES = {
    "text_cleanup": TextCleanupStep,
    "scene_segmentation": SceneSegmentationStep,
    "episode_summary": EpisodeSummaryStep,
    "visual_world_rules": VisualWorldRulesStep,
    "scene_split": SceneSplitStep,
    "scene_save": SceneSaveStep,
    "scene_summary": SceneSummaryStep,
    "entity_extract": EntityExtractStep,
    "entity_review": EntityReviewStep,
    "entity_detail": EntityDetailStep,
    "entity_t2i": EntityT2iStep,
    "scene_director": SceneDirectorStep,
    "scene_cinematography": SceneCinematographyStep,
    "scene_dependency": SceneDependencyStep,
    "outlook_extraction": OutlookExtractionStep,
    "scene_detail": SceneDetailStep,
    "scene_verify": SceneVerifyStep,
    "world_guide": WorldGuideStep,
    "ref_image_gen": RefImageGenStep,
    "composite_image_gen": CompositeImageGenStep,
    "scene_image_pipeline": SceneImagePipelineStep,
    "outlook_dedup": OutlookDedupStep,
    "project_summary": ProjectSummaryStep,
}
```

- [ ] **Step 2: steps.py _get_step_runner 업데이트**

`_get_step_runner()`에서 v3를 우선, 기존 fallback (클래스 이름 충돌 방지):
```python
from app.core.steps import STEP_CLASSES as V3_CLASSES
from app.core.steps.analysis_steps import ANALYSIS_STEP_CLASSES
from app.core.steps.image_steps import IMAGE_STEP_CLASSES

# v3 wins on overlapping keys, legacy remains for keys not yet replaced
step_classes = {**ANALYSIS_STEP_CLASSES, **IMAGE_STEP_CLASSES, **V3_CLASSES}
```
주의: `__init__.py`는 v3 클래스만 import. analysis_steps.py의 같은 이름 클래스와 충돌 방지를 위해 `__init__.py`에서 analysis_steps를 import하지 않음.

- [ ] **Step 3: 커밋**

```bash
git commit -m "feat: v3 step 등록 — 23개 step 클래스 매핑"
git push
```

---

### Task 16: _sync_checkpoints_to_db v3 업데이트

**Files:**
- Modify: `backend/app/api/v1/steps.py`
- Create: `backend/tests/test_sync_v3.py`

- [ ] **Step 1: 테스트 작성** — V/A/H sync (audio_entity_ids, hallucination_entity_ids), scene_summary sync
- [ ] **Step 2: 테스트 실패 확인**
- [ ] **Step 3: _sync_checkpoints_to_db 수정**
  - scene_director 체크포인트에서 audio_entity_ids, hallucination_entity_ids 로드 → scene_still에 저장
  - scene_summary 체크포인트에서 scene_summary 로드 → scene_still.scene_summary에 저장
  - mid-pipeline sync 포인트: entity_t2i 후 + scene_save 후
- [ ] **Step 4: 테스트 통과 확인**
- [ ] **Step 5: 커밋**

```bash
git commit -m "feat: sync v3 — V/A/H entity IDs + scene_summary DB 동기화"
git push
```

---

### Task 17: image_steps 마이너 업데이트

**Files:**
- Modify: `backend/app/core/steps/image_steps.py`

- [ ] **Step 1: WorldGuideStep input 업데이트** — scene_summary 체크포인트도 활용
- [ ] **Step 2: 기존 image step 테스트 통과 확인**
- [ ] **Step 3: 커밋**

```bash
git commit -m "refactor: image_steps — scene_summary 입력 추가"
git push
```

---

## Phase 5: E2E 테스트 + 리뷰 + 문서

### Task 18: E2E 통합 테스트

**Files:**
- Create: `backend/tests/test_pipeline_v3_e2e.py`

- [ ] **Step 1: E2E 테스트 작성** — 전체 analysis 파이프라인 (mock LLM), step 순서 검증, checkpoint 생성 확인, DB sync 확인
- [ ] **Step 2: 테스트 통과 확인**
- [ ] **Step 3: 커밋**

```bash
git commit -m "test: Pipeline v3 E2E 통합 테스트"
git push
```

---

### Task 19: 전체 Codex 코드 리뷰

- [ ] **Step 1: Codex로 전체 브랜치 diff 리뷰**

```bash
git diff main...feat/pipeline-v3 -- backend/ prompts/
```

- [ ] **Step 2: 리뷰 결과 기반 수정**
- [ ] **Step 3: 수정 커밋**

---

### Task 20: 문서 업데이트 + 머지

- [ ] **Step 1: 설계서 업데이트** — `docs/superpowers/specs/2026-03-23-pipeline-v3-design.md` 완료 표시
- [ ] **Step 2: CLAUDE.md 업데이트** — 완료된 Phase 목록에 v3 추가
- [ ] **Step 3: PR 생성**

```bash
gh pr create --base main --head feat/pipeline-v3 \
  --title "feat: Pipeline v3 — 23단계 전체 리팩토링" \
  --body "$(cat <<'EOF'
## Summary
- 17분석 + 4이미지 + 2보조 = 23단계 파이프라인
- visible_entities: LLM 의존 → 확정 데이터 자동 구축
- V/A/H 3분류 (Visual/Audio/Hallucination)
- 3턴 타입별 요소 추출 + 교차 모델 검증
- 2단계 아웃룩 추출
- scene_verify 앞2씬 컨텍스트
- 에피소드/씬 요약 + 시각적 세계관 규칙
- 모든 프롬프트 외부 파일, 하드코딩 제로
- TDD: 모든 step에 단위 테스트

## Test plan
- [ ] 모든 단위 테스트 통과 (`pytest backend/tests/`)
- [ ] step_manifest 23단계 검증
- [ ] E2E mock pipeline 통과
- [ ] EP1 실제 분석 테스트 (이미지 없이)

🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```

- [ ] **Step 4: Codex 최종 리뷰 → 수정 → 머지**
