# Phase 3: 샷/비트 분석

> 씬을 beat(상태변화 단위)로, beat를 shot(스틸컷 단위)으로 분해하고, 검증·재작성을 거친 뒤 이미지 생성 가치가 높은 shot을 선별한다.
>
> **shot-more 브랜치 핵심 원칙: "한 샷 = 한 찰나"** — 카메라 셔터가 한 번 눌린 1/1000초만 담는다.
> **현재 기준**: v0.6.0 (2026-04-21). 비전문가용 요약은 `docs/architecture-easy/03-shot-design.md` 참조.

## Active vs Legacy 구분표

| 구분 | Step ID | 실행 여부 |
|------|---------|-----------|
| **Active** | `beat_extract`, `shot_extract`, `shot_validator` (v0.5.4), `shot_selection` | 항상 실행 |

이 Phase에는 레거시 step이 없다.

## 단계 목록

| 순서 | Step ID | 이름 | 모델 | 병렬 | 의존 |
|------|---------|------|------|------|------|
| 7.1 | `beat_extract` | Beat 추출 | Gemini Pro | 병렬 (번들) | scene_save, visual_world_rules, entity_character_list |
| 7.2 | `shot_extract` | Shot 추출 | Gemini Pro | 순차 | beat_extract, visual_world_rules, entity_character_list |
| **7.25** | **`shot_validator`** | **Shot 검증 (한 찰나 재작성)** | **Gemini Pro (GPT fallback)** | **병렬 (씬별)** | **shot_extract** |
| 15.5 | `shot_selection` | 중요 샷 선택 | GPT Mini | 병렬 | **shot_validator** |

## 흐름도

```mermaid
flowchart TD
    Segments[scene_save segments] --> BE[beat_extract]
    VWR[visual_world_rules] --> |director_notes| BE
    ECL[entity_character_list] --> |인물 목록| BE

    BE --> |beats| SE[shot_extract]
    VWR --> |director_notes + t2i_context| SE
    ECL --> |인물 enum| SE

    SE --> |shots| SV["shot_validator (신규)"]
    SV --> |validated shots| SS[shot_selection]

    BE -.-> |"병렬 (번들 단위)"| BE
    SE -.-> |"순차 (이전 shot 누적)"| SE
    SV -.-> |"병렬 (씬별)"| SV
    SS -.-> |"병렬 (씬별)"| SS

    style BE fill:#e8f5e9
    style SE fill:#c8e6c9
    style SV fill:#fff59d,stroke:#f57f17
    style SS fill:#a5d6a7
```

## 핵심 개념: beat→shot 계층

```mermaid
flowchart LR
    Scene[씬 원문] --> Beat1[Beat 1: 상태변화]
    Scene --> Beat2[Beat 2: 상태변화]
    Scene --> Beat3[Beat 3: 상태변화]

    Beat1 --> Shot1[Shot 1]
    Beat1 --> Shot2[Shot 2]
    Beat2 --> Shot3[Shot 3]
    Beat3 --> Shot4[Shot 4]
    Beat3 --> Shot5[Shot 5]

    Shot1 --> |선택| Selected[Selected Shots]
    Shot3 --> |선택| Selected
    Shot5 --> |선택| Selected
```

- **Beat**: 인물 상태가 변화하는 최소 단위 (before_state → after_state)
- **Shot**: beat에 기반한 단일 스틸컷 (한 찰나, 한 시공간)
- **Selected Shot**: 이미지 생성 가치가 높은 shot (씬당 최대 N개)

---

## beat_extract

**목적**: 원본 씬에서 인물 상태 변화의 최소 단위(beat)를 추출.

- **입력**: scene_save segments + visual_world_rules (director_notes) + entity_character_list (인물 목록)
- **출력**: `scenes[].beats[]` (scene_index, beats: [{beat_index, change_type, before_state, after_state, description}])
- **모델**: Gemini Pro
- **병렬**: 번들 단위 병렬 처리 (ThreadPool, MAX_WORKERS=4)

### 번들링 전략

씬 텍스트를 BUNDLE_TARGET(3000자) 단위로 묶어 LLM 호출 횟수를 줄인다.

```mermaid
flowchart LR
    S1[씬1: 800자] --> B1[번들1: 2600자]
    S2[씬2: 900자] --> B1
    S3[씬3: 900자] --> B1
    S4[씬4: 3500자] --> B2[번들2: 3500자]
    S5[씬5: 1200자] --> B3[번들3: 2400자]
    S6[씬6: 1200자] --> B3
```

각 번들에는 앞쪽 씬의 참조 텍스트(REF_MAX=2000자)도 포함하여 문맥 유지.

### 주입 데이터

1. **director_notes**: 물리적 존재 판단 기준 (빙의/영혼 등)
2. **인물 목록**: entity_character_list에서 로드, 인물 언급 시 이 목록의 이름 사용 강제
3. **scene_index enum**: 스키마에 동적 주입하여 LLM 출력을 제한

### 검증 및 재시도

- 반환된 scene_index가 expected와 불일치하면 순서 매핑 또는 재시도 (최대 5회)
- 최종 실패 시 빠진 씬은 빈 beats로 채움
- **코드**: `backend/app/core/steps/beat_shot_steps.py:97` (`BeatExtractStep`)

---

## shot_extract — "한 샷 = 한 찰나" (v10, shot-more 브랜치)

**목적**: beat 기반으로 각 씬의 스틸컷(shot)을 추출. 한 shot = 한 찰나.

- **입력**: beat_extract 결과 + scene_save segments + visual_world_rules + entity_character_list
- **출력**: `scenes[].shots[]` (shot_index, description, characters, based_on_beat)
- **모델**: Gemini Pro (`gemini-3.1-pro-preview`)
- **순차 실행**: 이전 번들의 shot 결과를 누적하여 다음 번들에 전달 (인물 호칭/존재 일관성)
- **프롬프트 버전**: `shot_extractor` v2.0.0 (v10 프롬프트, 2026-04-15)

### 한 샷 = 한 찰나 원칙 (v10 프롬프트)

shot_extract는 기존의 "장면을 적당히 요약한 한 장면"이 아니라 **1/1000초 단위의 정지 순간**으로 샷을 정의한다.

1. **시간 연결어 절대 금지**: `"and then"`, `"while ~ing"`, `"after ~ing"`, `"as ~"`, `"before ~"`, `"~하자"`, `"~하며"`, `"~한 뒤"`
2. **연속 동작 분해 강제**: 하나의 동작(예: "문을 열고 들어가 앉는다")은 before / during / after 중 하나의 찰나만 담아 1~3개의 샷으로 분해.
3. **수 제한 제거**: 예전 v9 이하에서는 씬당 샷 수를 제한했지만, v10에서는 "한 찰나 = 한 샷" 원칙 위반을 막기 위해 수 제한을 없앴다. shot_selection이 이후에 가치 있는 샷만 선별.
4. **두 개 이상의 동작 금지**: 같은 샷 안에 "서 있다 + 손을 뻗는다" 같은 복수 동작을 넣지 않는다.
5. **앞뒤 샷 내용 금지**: 다음/이전 샷에서 일어날 일을 현재 샷 설명에 섞지 않는다.

### 순차 실행의 이유

shot_extract는 **번들 간 순차** 실행한다. 이전 씬들의 shot 결과를 `prev_shots_ctx`로 누적하여 다음 번들 프롬프트에 주입함으로써:
- 인물 호칭 일관성 유지
- 존재 판단 일관성 유지
- 연속 행동의 샷 배분 일관성 확보

```mermaid
sequenceDiagram
    participant Code
    participant LLM as Gemini Pro

    Code->>LLM: 번들1 (씬1~3) + ref_text
    LLM-->>Code: shots (씬1~3)

    Note over Code: prev_shots_ctx 누적

    Code->>LLM: 번들2 (씬4~6) + prev_shots_ctx
    LLM-->>Code: shots (씬4~6)

    Note over Code: prev_shots_ctx 계속 누적

    Code->>LLM: 번들3 (씬7~9) + prev_shots_ctx
    LLM-->>Code: shots (씬7~9)
```

### 주입 데이터

1. **beat 정보**: 각 씬의 beat 목록 (beat_index, change_type, before→after)
2. **visual_world_rules**: director_notes + t2i_context
3. **인물 enum**: shot의 characters 필드에 entity_character_list의 이름만 허용 (스키마 enum 주입)
4. **이전 shot 결과**: `_build_prev_shots_context()` (전체 전달, 잘라내기 없음 -- PREV_SHOTS_MAX=0)

- **코드**: `backend/app/core/steps/beat_shot_steps.py:297` (`ShotExtractStep`)

---

## shot_validator — "한 찰나" 원칙 후처리 검증 (v0.5.4 신규)

**목적**: `shot_extract`가 뽑은 shot description을 LLM으로 재검토하여 "한 찰나" 원칙을 위반한 것(시간 연결어/연속 동작/복수 시점)을 재작성. 원본은 `original_description` 필드로 백업되고 체크포인트 구조는 `shot_extract`와 동일해서 다운스트림은 로더 경로만 `shot_validator`로 교체하면 된다.

- **입력**: shot_extract 결과 + scene_save segments (원문 컨텍스트)
- **출력**: `scenes[].shots[]` (description은 재작성됨, `original_description`에 원본 보존)
- **모델**: Gemini Pro → GPT fallback (실패 시)
- **병렬**: ThreadPool 씬별 병렬 (MAX_WORKERS=4)
- **프롬프트 버전**: `shot_validator` v1.2.0 (v3 프롬프트, 2026-04-30 — Phase 9.2: 동적 동사 freeze 시 motion direction 보존 룰 추가)
- **적용 효과**: v0.5.8 이후 모든 다운스트림(`entity_all_*`, `shot_selection`, `scene_camera_flow`, `shot_staging`, `scene_consistency`, `shot_director`, `scene_detail`)이 `shot_validator` 체크포인트를 읽음.
- **코드**: `backend/app/core/steps/shot_validator_step.py:27` (`ShotValidatorStep`)

### 재작성 예시

```
원본: "인물A가 문을 열고 들어와 테이블에 앉아 편지를 펼친다" (3개 동작 혼합)
→ 3개 shot으로 분해:
  [1] "인물A가 문손잡이를 돌리는 순간"
  [2] "인물A가 테이블 앞에 앉는 순간"
  [3] "인물A가 편지를 펼치는 순간"
```

---

## shot_selection — 이미지 생성 가치 기준 (v4, 2중 캡)

**목적**: 씬별로 이미지 생성 가치가 높은 shot을 최대 N개 선별. 비선택 샷도 UI/플로우 설계 자료로 DB에 보존.

- **입력**: shot_validator 결과 (v0.5.4 이후 validated shots)
- **출력**: `scenes[].selected_shot_indices` (선택된 shot index 목록)
- **모델**: GPT Mini
- **병렬**: 씬별 ThreadPool 병렬 (MAX_WORKERS=4)
- **환경변수**: `SHOT_SELECTION_MAX` (기본 5), `SHOT_SELECTION_ENABLED` (기본 true)
- **프롬프트 버전**: `shot_selector` v4.0.0 (2중 캡 + 이유 필수, 2026-04-19)

### v0.5.8 2중 캡 (dual cap)

v0.5.7까지는 절대 최대치(N개)만 적용 → 작은 씬(5 shot)에서 N=5이면 전부 선택되는 과선택 문제가 있었다. v0.5.8 이후 **절대 캡 + 비율 캡**의 `min()`을 적용:

- **절대 캡**: `SHOT_SELECTION_MAX` (기본 5)
- **비율 캡**: 전체 shot 수의 50% (반올림)
- **적용**: `ceil(total_shots * 0.5)`와 `SHOT_SELECTION_MAX` 중 작은 값
- **예외**: shot 1~2개 씬은 전부 선택 (LLM 호출 없음)

또한 v4 프롬프트는 각 선택에 **`reason` 필드 필수**로 요구하여 LLM이 선택 근거를 명시하게 한다.

### 선별 로직

```mermaid
flowchart TD
    Input[씬별 shots] --> Check{shot 수 확인}
    Check --> |1개| Auto1[자동 선택]
    Check --> |2~N개| AutoN[전부 선택]
    Check --> |N+1개 이상| LLM[LLM 판단]

    LLM --> |선택 결과| Valid[유효성 검증]
    Valid --> |빈 결과| Fallback[첫 N개 fallback]
    Valid --> |OK| Selected[선택 완료]

    Auto1 --> Quota[에피소드 최대 shot 수 적용]
    AutoN --> Quota
    Selected --> Quota
    Fallback --> Quota
```

- shot 1개: 자동 선택 (LLM 호출 없음)
- shot <= max_n: 전부 선택 (LLM 호출 없음)
- shot > max_n: LLM이 이미지 생성 가치 기준으로 선택
- `SHOT_SELECTION_ENABLED=false`: 모든 shot 자동 선택

### 비선택 샷 처리 (v0.5.0)

v0.5.0부터 비선택 샷도 `scene_still` 테이블에 `is_selected=false`로 저장된다. 이유:
- `scene_camera_flow`가 씬 전체 흐름(선택+비선택 포함)을 분석
- UI에서 사용자가 비선택 샷을 "+선택" 토글로 되살릴 수 있음
- 맥락 보존 — 씬 이해에 필요한 "이미지는 안 만들 샷"도 추적 가능

### 에피소드 최대 shot 수 제한

`EPISODE_MAX_SHOTS` 환경변수로 에피소드 전체 shot 수를 제한:
1. 비례 할당 (각 씬의 선택 수 기준)
2. 최소 1개 보장
3. 초과 시 가장 많은 씬에서 1개씩 삭감

- **코드**: `backend/app/core/steps/shot_selection_step.py:21` (`ShotSelectionStep`)

### "이미지 생성 가치" 기준

LLM에게 다음 기준으로 shot 평가를 요청:
- 시각적 임팩트가 큰 순간
- 인물 감정 전환점
- 내러티브 핵심 순간
- 공간/시간의 전환이 명확한 순간

---

## 참고: 레거시

이 Phase에는 on_demand/disabled 레거시 step이 없다.
