# TheRoad Scene Lab — 전체 파이프라인 단계 상세

## 파이프라인 흐름도

```
[분석 Phase]
entity_style(1) → entity_review(2) → entity_detail_batch(3) → entity_t2i(4)
                                                                    ↓
scene_segmentation(5) → scene_split(6) ──→ scene_director(7) → scene_cinematography(8)
                                      └──→ scene_dependency(9)        ↓
                                                              outlook_extraction(10)
                                                                    ↓
                                                              scene_detail(11) → scene_verify(12)

[DB 동기화] ← 분석 완료 후 자동 (_sync_checkpoints_to_db)

[이미지 Phase]
world_guide(20) ──────────────────────────────────────────┐
ref_image_gen(21) → composite_image_gen(22) ──────────────┤
                                                          ↓
                                                scene_image_pipeline(23)
                                                  ├─ prompt_translation
                                                  ├─ scene_t2i_gen (Gemini Image)
                                                  ├─ scene_t2i_validation (GPT Vision)
                                                  ├─ prompt_sanitize
                                                  ├─ angle_recommend (GPT Vision)
                                                  ├─ fal_angle_apply (fal.ai)
                                                  └─ final_select (GPT Vision)

[보조 (on-demand)]
outlook_dedup(100), project_summary(101)
```

---

## 분석 Phase (12단계)

### Step 1: entity_style — 스타일 + 요소 이름

| 항목 | 내용 |
|------|------|
| **순서** | 1 |
| **모델** | GPT-5.5 |
| **의존** | 없음 |
| **병렬** | 단일 호출 (1회) |
| **처리** | 시나리오 전문 → LLM 1회 호출 → 요소 목록 + visual_world_rules 반환 |
| **Input** | 시나리오 전문 (fulltext) |
| **Output** | `{characters: [{name, description, visual_traits}], locations: [...], props: [...], visual_world_rules: [str]}` |
| **enum** | 없음 (첫 추출이라 제약 불가) |
| **프롬프트** | `prompts/_base/entity_extractor_v2/*/turn0_style.md` |

**Input 예시:**
```
시나리오 전문 텍스트 (63페이지, ~80,000자)
```

**Output 예시:**
```json
{
  "characters": [
    {"name": "동녘", "description": "젊은 성인 남성...", "visual_traits": ["갸름한 얼굴", "짙은 머리"]},
    {"name": "김의원", "description": "..."}
  ],
  "locations": [{"name": "클럽하우스 폐창고", "description": "..."}],
  "props": [{"name": "1인 캡슐", "description": "..."}],
  "visual_world_rules": ["소울라이드 시 접속자의 물리적 몸은 원래 장소에 있음", "..."]
}
```

---

### Step 2: entity_review — 요소 교차 검증

| 항목 | 내용 |
|------|------|
| **순서** | 2 |
| **모델** | GPT-5.5 |
| **의존** | entity_style |
| **병렬** | 단일 호출 (1회) |
| **처리** | Step 1 결과를 다른 관점에서 리뷰 → 누락/중복/오류 보정 |
| **Input** | Step 1 결과 + 시나리오 전문 |
| **Output** | 보정된 요소 목록 (같은 형식) |
| **enum** | 없음 |

---

### Step 3: entity_detail_batch — 요소 상세 추출

| 항목 | 내용 |
|------|------|
| **순서** | 3 |
| **모델** | GPT-5.5 |
| **의존** | entity_review |
| **병렬** | 단일 호출 (1회 배치) + 누락 재시도 1회 |
| **처리** | 전체 요소를 한 번에 보내서 시각적 상세 정보 추출 |
| **Input** | 요소 이름+타입 목록 + 시나리오 전문 |
| **Output** | `{entities: [{name, description, visual_traits: [str]}]}` |
| **enum** | 없음 |

---

### Step 4: entity_t2i — 요소 T2I 프롬프트 (병렬)

| 항목 | 내용 |
|------|------|
| **순서** | 4 |
| **모델** | GPT-5.4 Mini |
| **의존** | entity_detail_batch |
| **병렬** | **ThreadPoolExecutor** (max 10 workers) — 요소당 1회 호출 |
| **처리** | 각 요소별 T2I 생성 프롬프트 작성 (영어, 참조이미지용) |
| **Input** | 개별 요소 {name, type, description, visual_traits} |
| **Output** | `{name, description, visual_traits, t2i_prompt, entity_type}` |
| **enum** | 없음 |
| **DB 동기화** | 완료 후 `_sync_checkpoints_to_db` → `entity_canon` 테이블에 저장 + **short_id 자동 발급** (C01, L01, P01) |

**Output 예시:**
```json
{
  "name": "동녘",
  "entity_type": "character",
  "description": "젊은 성인 남성...",
  "visual_traits": ["갸름한 얼굴", "짙은 머리"],
  "t2i_prompt": "Passport-style ID photo, young Korean man with slender face..."
}
```

---

### Step 5: scene_segmentation — 씬 세그먼테이션

| 항목 | 내용 |
|------|------|
| **순서** | 5 |
| **모델** | GPT-5.4 Nano |
| **의존** | 없음 (entity와 독립) |
| **병렬** | 단일 호출 (1회) |
| **처리** | 시나리오에서 `INT./EXT.` 헤딩으로 씬 분할 (정규식 + LLM 보정) |
| **Input** | 시나리오 전문 |
| **Output** | `{segments: [{scene_index, heading, start_char, end_char, length}]}` |
| **enum** | 없음 |

**Output 예시:**
```json
{"segments": [
  {"scene_index": 1, "heading": "INT. 클럽하우스 폐창고-DAY", "start_char": 55, "end_char": 828, "length": 773},
  {"scene_index": 2, "heading": "INT. 클럽하우스 VIP룸-DAY", "start_char": 828, "end_char": 1732, "length": 904}
]}
```

---

### Step 6: scene_split — 큰 씬 분할

| 항목 | 내용 |
|------|------|
| **순서** | 6 |
| **모델** | GPT-5.4 Mini |
| **의존** | scene_segmentation |
| **병렬** | **ThreadPoolExecutor** — 긴 씬(600자 초과)만 병렬 분할 |
| **처리** | 600자 넘는 씬을 논리적 2분할 (LLM 판단) |
| **Input** | 개별 긴 씬 텍스트 |
| **Output** | 업데이트된 segments (씬 수 증가) |
| **예시** | 33씬 → 46씬 (13개 분할) |

---

### Step 7: scene_director — 씬 감독 (공간/시간 분석)

| 항목 | 내용 |
|------|------|
| **순서** | 7 |
| **모델** | GPT-5.5 |
| **의존** | scene_split + entity_t2i |
| **병렬** | **단일 호출** (전체 씬 JSON 일괄 전송) |
| **처리** | 모든 씬을 한 번에 보내서 씬별 물리적 존재 엔티티 판별 |
| **Input** | 전체 씬 JSON + 엔티티 목록 (short_id + name + type + description) |
| **Output** | `{scenes: [{scene_index, present_entity_ids: [short_id], not_present: [{id, reason}]}]}` |
| **enum** | **YES** — `present_entity_ids`와 `not_present.id`에 DB short_id enum 동적 주입 |
| **프롬프트** | `prompts/_base/scene_director/3.202603231900/` |

**Input 예시 (LLM에 전달):**
```json
[엔티티 목록]
  {"id": "C01", "name": "덕현", "type": "character", "description": "어린 남자아이..."}
  {"id": "C12", "name": "동녘", "type": "character", "description": "젊은 성인 남성..."}
  {"id": "L14", "name": "클럽하우스 폐창고", "type": "location", "description": "어둡고 낡은..."}
  {"id": "P09", "name": "1인 캡슐", "type": "prop", "description": "사람 한 명이..."}

[전체 씬 목록]
[{"scene_index": 1, "heading": "INT. 클럽하우스 폐창고-DAY", "text": "...씬 텍스트..."},
 {"scene_index": 2, "heading": "INT. 클럽하우스 VIP룸-DAY", "text": "..."}]
```

**Schema enum 주입:**
```json
"present_entity_ids": {
  "type": "array",
  "items": {"type": "string", "enum": ["C01","C02",...,"L01","L02",...,"P01","P02",...]}
}
```

**Output 예시:**
```json
{
  "scenes": [{
    "scene_index": 5,
    "primary_location": "INT. 헬리콥터내부-DAY",
    "scene_type": "normal",
    "present_entity_ids": ["C12", "L15", "P09"],
    "not_present": [
      {"id": "C08", "reason": "49번 비히클에 빙의 중이므로 물리적 몸은 VIP룸에 있음"}
    ]
  }]
}
```

**후처리:**
- short_id → UUID 역매핑 (`scene_present_entities`)
- short_id → 이름 역매핑 (`scene_present_characters`)
- 이름 기반 fallback (LLM이 short_id 무시 시)

---

### Step 8: scene_cinematography — 촬영 감독 (기법 선택)

| 항목 | 내용 |
|------|------|
| **순서** | 8 |
| **모델** | GPT-5.5 |
| **의존** | scene_director |
| **병렬** | **단일 호출** (전체 씬 일괄) |
| **처리** | DB의 30개 촬영 기법 중 씬당 2개 선택 |
| **Input** | 전체 씬 요약 + 촬영 기법 목록 (DB `shot_type` 테이블) |
| **Output** | `{scenes: [{scene_index, shot_1, shot_2, shot_1_focus, shot_2_focus}]}` |
| **enum** | 없음 (촬영 기법은 자유 텍스트지만 DB 목록에서 선택) |

---

### Step 9: scene_dependency — 씬 연관 분석

| 항목 | 내용 |
|------|------|
| **순서** | 9 |
| **모델** | GPT-5.5 |
| **의존** | scene_split |
| **병렬** | **단일 호출** (전체 씬 일괄) |
| **처리** | 각 씬의 시각적 연관 씬 (같은 장소, 연속 장면) 판별 |
| **Input** | 전체 씬 JSON (heading + text 200자) |
| **Output** | `{dependencies: {scene_index: {prev_ref: int, next_ref: int, reason: str}}}` |

---

### Step 10: outlook_extraction — 아웃룩 추출

| 항목 | 내용 |
|------|------|
| **순서** | 10 |
| **모델** | GPT-5.5 |
| **의존** | scene_director |
| **병렬** | **단일 호출** (전체 씬 + 캐릭터 일괄) |
| **처리** | 각 씬에서 캐릭터가 입는 의상(아웃룩) 판별 + 씬별 배정 |
| **Input** | 캐릭터 목록 (short_id + name) + 전체 씬 JSON + visual_world_rules |
| **Output** | `{outlooks: [{name, description, is_shared}], scene_assignments: [{scene_index, characters: [{character_id, outlook_name, character_name}]}]}` |
| **enum** | **YES** — `character_id`에 캐릭터 short_id enum 동적 주입 |
| **프롬프트** | `prompts/_base/outlook_extractor/5.202603231800/` |

**Input 예시 (캐릭터 목록):**
```
- C01 (덕현)
- C08 (김의원)
- C12 (동녘)
- C15 (강의원)
```

**Output 예시:**
```json
{
  "outlooks": [
    {"name": "캡슐비히클내의1", "description": "빛바랜 회백색 민소매 내의...", "is_shared": false},
    {"name": "배틀로얄군복", "description": "짙은 올리브 드랍 전투복...", "is_shared": true}
  ],
  "scene_assignments": [
    {"scene_index": 1, "characters": [
      {"character_id": "C12", "outlook_name": "캡슐비히클내의1", "character_name": "동녘"}
    ]},
    {"scene_index": 2, "characters": [
      {"character_id": "C08", "outlook_name": "VIP클론정장2", "character_name": "김의원"},
      {"character_id": "C15", "outlook_name": "VIP클론정장1", "character_name": "강의원"}
    ]}
  ]
}
```

**DB 동기화:** 아웃룩 → `entity_canon` (type=outlook, short_id=O01~), `CharacterOutlook` 연결 테이블

---

### Step 11: scene_detail — 씬 상세 분석 (병렬)

| 항목 | 내용 |
|------|------|
| **순서** | 11 |
| **모델** | GPT-5.5 |
| **의존** | scene_dependency + outlook_extraction + scene_cinematography + entity_t2i |
| **병렬** | **ThreadPoolExecutor** (max 10 workers) — 씬당 1회 호출 |
| **처리** | 각 씬에 확정된 요소로 T2I 프롬프트 N개 + 대표 순간 생성 |
| **Input** | 씬 텍스트 + 확정 요소 블록 (short_id) + 촬영 기법 + 연관 씬 컨텍스트 |
| **Output** | `{scene_index, heading, beat_title, representative_moment, t2i_variations: [{variant_label, camera_effect, t2i_prompt}], scene_type}` |
| **enum** | 없음 (스키마에 visible_entities 제거 — 코드에서 자동 구축) |
| **프롬프트** | `prompts/_base/scene_extractor_v2/9.202603232000/` |

**visible_entities 자동 구축 (LLM 의존 X):**
```
인물: scene_assignments (outlook_extraction 확정)
배경+소품: scene_present_entities_map (scene_director 확정)
```

**LLM에 전달되는 엔티티 블록 예시:**
```
이 씬의 인물 (아래 목록만 사용. 추가 금지!):
  - C12 (동녘)
  - C08 (김의원)
이 씬의 배경 (아래 목록만 사용. 추가 금지!):
  - L14 (클럽하우스 폐창고: 어둡고 낡은 대형 창고...)
이 씬의 물체 (아래 목록만 사용. 추가 금지!):
  - P09 (1인 캡슐)
  - P31 (캡슐 상태창)
```

**T2I 프롬프트 Output 예시:**
```
Photorealistic cinematic still. C12O02 stands inside the last P09, eyes open,
while P31 on the side panel displays active readouts, and cold light stretches
across the dark warehouse floor of [L14: 어둡고 낡은 대형 창고 내부...]
```

**DB 동기화 시 visible_entities_json:**
```json
[
  {"entity_type": "character", "short_id": "C12", "outlook_short_id": "O02", "entity_name": "동녘", "id": "uuid..."},
  {"entity_type": "character", "short_id": "C08", "outlook_short_id": "O06", "entity_name": "김의원", "id": "uuid..."},
  {"short_id": "L14", "entity_type": "location", "entity_name": "클럽하우스 폐창고", "id": "uuid..."},
  {"short_id": "P09", "entity_type": "prop", "entity_name": "1인 캡슐", "id": "uuid..."}
]
```

---

### Step 12: scene_verify — 교차 검증 (병렬)

| 항목 | 내용 |
|------|------|
| **순서** | 12 |
| **모델** | GPT-5.5 |
| **의존** | scene_detail |
| **병렬** | **ThreadPoolExecutor** — 인물 2명 이상 씬만 |
| **처리** | visible_entities가 실제로 물리적으로 보이는지 재확인 |
| **Input** | 씬 텍스트 + visible_entities 목록 (short_id + entity_name) |
| **Output** | `{scene_index, verified_entities: [{entity_name, entity_type, physically_visible: bool, reason}]}` |
| **후처리** | physically_visible=false인 엔티티 제거 + T2I 마커 정리 (C01O02 / [[name]] 양 패턴) |

---

## 이미지 Phase (4단계)

### Step 20: world_guide — 월드 가이드

| 항목 | 내용 |
|------|------|
| **순서** | 20 |
| **모델** | GPT-5.5 |
| **의존** | entity_t2i + scene_detail |
| **병렬** | 단일 호출 |
| **처리** | 프로젝트 전체의 시각적 스타일 규칙 생성 (색감, 조명, 분위기) |
| **Input** | 엔티티 요약 + 씬 목록 요약 |
| **Output** | 스타일 가이드 텍스트 (이미지 생성 시 공통 적용) |

---

### Step 21: ref_image_gen — 요소 참조 이미지

| 항목 | 내용 |
|------|------|
| **순서** | 21 |
| **모델** | **Gemini 3.1 Flash Image** (이미지 생성) + GPT-5.5 (LVM 검증) |
| **의존** | entity_t2i |
| **병렬** | **ThreadPoolExecutor** (max 15 workers) |
| **처리** | 각 요소별 참조 이미지 생성 → GPT Vision 검증 → 필요 시 재생성 |

| 하위 흐름 | 모델 | 설명 |
|-----------|------|------|
| T2I 생성 | Gemini Image | t2i_prompt → 이미지 (1K, 1:1 또는 16:9) |
| LVM 검증 | GPT Vision | 생성 이미지가 description과 일치하는지 |
| 재생성 비교 | GPT Vision | 2개 이미지 중 더 나은 것 선택 |
| Moderation 실패 시 | GPT-5.5 | 프롬프트 안전화 후 재시도 |

**Aspect Ratio:**
- character, outlook: 1:1
- composite: 16:9
- location, prop: 16:9

---

### Step 22: composite_image_gen — 인물+아웃룩 합성 이미지

| 항목 | 내용 |
|------|------|
| **순서** | 22 |
| **모델** | **Gemini 3.1 Flash Image** |
| **의존** | ref_image_gen + outlook_extraction |
| **병렬** | **ThreadPoolExecutor** (max 15 workers) |
| **처리** | 얼굴 참조 + 아웃룩 참조 → 전신 합성 이미지 생성 |
| **Input** | 얼굴 이미지(bytes) + 아웃룩 이미지(bytes) + 합성 프롬프트 |
| **Output** | 합성 이미지 (16:9) |

---

### Step 23: scene_image_pipeline — 씬 이미지 생성 (compound)

| 항목 | 내용 |
|------|------|
| **순서** | 23 |
| **모델** | Mixed (GPT + Gemini Image + fal.ai) |
| **의존** | composite_image_gen + scene_verify + world_guide |
| **병렬** | **ThreadPoolExecutor** — 씬별 병렬, variation별 내부 병렬 |

| 하위 단계 | 모델 | 병렬 | 설명 |
|-----------|------|------|------|
| prompt_translation | GPT-5.4 Mini | 씬별 | T2I 프롬프트(C01O02 + 한국어) → 영어 + image N 참조 변환 |
| scene_t2i_gen | **Gemini Image** | variation별 (max 3) | 영어 프롬프트 + 참조 이미지 → 씬 이미지 생성 (16:9, 1K) |
| scene_t2i_validation | GPT Vision | - | 생성 이미지 검증 |
| prompt_sanitize | GPT-5.5 | - | Moderation 실패 시 프롬프트 수정 |
| angle_recommend | GPT Vision | - | N개 이미지 중 앵글 조정 대상 선택 |
| fal_angle_apply | **fal.ai** | - | 선택된 이미지에 앵글 적용 (horizontal/vertical/zoom) |
| final_select | GPT Vision | - | 최종 대표 이미지 선택 |

**참조 이미지 매칭 (`_resolve_refs_for_prompt`):**
```
T2I: "C15O05 walks toward C08O06 in [L11: 고급 연회장...]"

→ C15O05 매칭:
  1) composite:{C15_uuid}:{O05_uuid} 이미지 (우선)
  2) fallback: face(C15) + outfit(O05) 분리 첨부

→ 최종 프롬프트:
  Reference image 1: character in outfit (강의원+VIP정장1)
  Reference image 2: character in outfit (김의원+VIP정장2)

  Generate one image:
  - keep the face from image 1
  - keep the face from image 2
  ...
```

**fal.ai 앵글 적용:**
```json
{
  "image_urls": ["data:image/png;base64,..."],
  "horizontal_angle": 15,
  "vertical_angle": -10,
  "zoom": 1.0
}
```

---

## 보조 단계 (on-demand)

### Step 100: outlook_dedup — 아웃룩 중복 판별

| 항목 | 내용 |
|------|------|
| **모델** | GPT-5.5 |
| **처리** | 유사 아웃룩 판별 → 병합 그룹 제안 → DB 적용 |
| **수동 실행** | UI에서 명시적 트리거 필요 |

### Step 101: project_summary — 프로젝트 요약

| 항목 | 내용 |
|------|------|
| **모델** | GPT-5.5 |
| **처리** | 시나리오 앞부분 3000자 → 5문장 요약 |
| **수동 실행** | UI에서 명시적 트리거 필요 |

---

## Short ID 체계

| 타입 | 접두어 | 예시 | 발급 시점 |
|------|--------|------|----------|
| Character | C | C01, C12 | entity_t2i 완료 → DB 동기화 시 |
| Location | L | L01, L14 | 동일 |
| Prop | P | P01, P09 | 동일 |
| Outlook | O | O01, O05 | outlook_extraction 완료 → DB 동기화 시 |

**복합 표현:** `C12O02` = 인물 C12(동녘)이 아웃룩 O02(배틀로얄군복) 착용

**enum 적용 단계:**
- scene_director: `present_entity_ids`, `not_present.id` → DB short_id 전체
- outlook_extraction: `character_id` → DB character short_id
- scene_detail: visible_entities는 코드에서 자동 구축 (enum 불필요)

**UI 변환:**
- 내부: `C12O02` (LLM 통신, 체크포인트)
- UI: `[[동녘]+[배틀로얄군복]]` (displayPrompt 함수에서 변환)

---

## DB 동기화 (`_sync_checkpoints_to_db`)

| 데이터 | 소스 | 타이밍 |
|--------|------|--------|
| entity_canon (C/L/P) | entity_t2i 체크포인트 | entity_t2i 완료 + scene_director 전 |
| entity_canon (O) | outlook_extraction 체크포인트 | 전체 완료 후 |
| entity_episode_link | 동시 생성 | 동일 |
| character_outlook | scene_assignments에서 추출 | 동일 |
| scene_still | scene_detail 체크포인트 | 동일 |
| visible_entities_json | 코드 자동 구축 (scene_assignments + scene_director) | 동일 |
| episode.status | "analyzed" | 전체 완료 후 |

---

## Opik 옵저버빌리티

| 구분 | 방식 | 그룹핑 |
|------|------|--------|
| 텍스트 LLM (call_structured/call_text) | LiteLLM → Opik callback 자동 | thread-local session_id |
| 이미지 생성 (Gemini Image) | ImageTracer 수동 span | 동일 session_id |
| fal.ai | ImageTracer 수동 span | 동일 session_id |

**session_id 형식:** `{project_name}_{episode_title}_{MMDD-HHMMSS}_{uuid[:8]}`
