# Phase 5: 이미지 생성

> 분석 결과를 바탕으로 엔티티 참조 이미지, 인물+아웃룩 합성 이미지, 인물 상태 변형(dead/injured) 이미지, 최종 씬 이미지를 생성한다.
> **현재 기준**: v0.6.0 (2026-04-21). 비전문가용 요약은 `docs/architecture-easy/05-image-making.md` + `06-editing.md` 참조.
>
> ⚠️ **코드 참조 주의 (2026-04-24, W5 F23 + F24.4 완료)**: 본문 중 `image_service.py:XXXX` 라인 번호는 분할 이전 기준이므로 **참고용**이다. 현재는 image facade 379 + scene facade 803 + reference 8-way split + scene_generation_coordinator 852로 재구성되었다. **reference 도메인은 G5 exit "≤500 LOC" 달성**(facade 188 / orch 407 / phase1 212 / phase2 213 / phase3 204 / ctx 52 / entity 171 / composite 186). scene 도메인 coordinator 852는 G5 미달 — 후속 세션 분리 대상.

## 현재 분할 상태 (2026-04-24, W5 F23/F24 완료)

### Image facade 및 scene 서비스

| 서비스 | LOC | 주 책임 |
|--------|----:|---------|
| `image_service.py` | 379 | facade — `_build_final_scene_prompt` + review/composer/upload 위임 |
| `scene_image_service.py` | 803 | scene 파이프라인 facade (generate_single_scene_image, generate_scene_with_variations) |
| `scene_generation_coordinator.py` | 852 | scene 루프 + prev-shot ref + 최종화 (**G5 미달**, 후속 분리 대상) |

### Reference 도메인 (W5 F24.4 — G5 exit ≤500 LOC 달성)

| 서비스 | LOC | 주 책임 |
|--------|----:|---------|
| `reference_image_service.py` | 188 | facade — base/composite/single_entity/regenerate 공개 API |
| `reference_pipeline_orchestrator.py` | 407 | Setup + Phase 1/2/3 dispatch + finalize |
| `reference_pipeline_context.py` | 52 | Phase 간 공유 dataclass (counters/ref_image_map/low_freq_skip_ids) |
| `reference_phase1_service.py` | 212 | Phase 1: base reference 병렬 생성 + early return |
| `reference_phase2_service.py` | 213 | Phase 2: outfit standalone (LLM 번역 캐시 + ThreadPool) |
| `reference_phase3_service.py` | 204 | Phase 3: composite (face+outfit → 전신) ThreadPool |
| `reference_entity_service.py` | 171 | generate_single_entity_image |
| `reference_composite_service.py` | 186 | generate_composite_image + regenerate_composites_for_entity |

**전체 image 도메인 합계**: 3,667 LOC (2026-04-22 기준 5,314 대비 **−1,647, −31%**). 이력: `session_20260423_w5_f23.md` / `session_20260424_w5_f24_4.md`.

## 주요 함수 현행 위치 (2026-04-24 실측)

| 함수 | 현행 위치 |
|------|-----------|
| `_build_final_scene_prompt` | `image_service.py` (facade 유지) |
| `_build_image_index` / `_rewrite_t2i_with_image_refs` | `scene_image_service.py` + `scene_generation_coordinator.py` 분산 |
| `_resolve_refs_for_prompt` | `scene_image_service.py` / `scene_generation_coordinator.py` |
| base/composite/single_entity/regenerate 공개 API | `reference_image_service.py` (facade → pipeline orchestrator/phase 서비스 위임) |

정확한 최신 위치는 `grep -n "def <symbol>" backend/app/services/`.
>
> **v0.5.1 주요 변경:** 참조 이미지 지시문을 `keep the face from image N` → `use image N as character appearance reference`로 중립화. 클로즈업 구도에서 얼굴 강제 삽입 방지를 위해 `scene_detail`에서 C## 사용 자체를 제한하고, `image_service`의 T2I 텍스트 기반 skip 로직은 전면 제거됨.
>
> **v0.5.14 주요 변경:** `image_service` 1.9.0에서 `zoom_in_detail` ref_usage 라벨(`SAME FRAME ZOOMED`) 추가. `reference_image_generator` 1.3.0, `scene_image_generator` 1.4.0, `world_guide_generator` 1.1.0에서 국가/시대 하드코딩 제거 — `world_guide_block`이 결정하도록 범용화.

## Active vs Legacy 구분표

| 구분 | Step ID | 실행 여부 |
|------|---------|-----------|
| **Active** | `world_guide`, `ref_image_gen`, `composite_image_gen`, **`character_state_variant` (신규)**, `scene_image_pipeline` | 5단계 |
| **Optional** | `composite_image_gen` | `if_has_outlooks` (아웃룩이 있을 때만) |
| **Optional** | `scene_image_pipeline`의 `fal_angle_apply` 서브스텝 | `FAL_AI_ENABLED=true`일 때만 |

## 단계 목록

| 순서 | Step ID | 이름 | 모델 | 병렬 | 의존 |
|------|---------|------|------|------|------|
| 22 | `world_guide` | 월드 가이드 | GPT | - | entity_t2i, scene_detail |
| 23 | `ref_image_gen` | 요소 참조 이미지 | Gemini Image | 병렬 | entity_t2i |
| 24 | `composite_image_gen` | 인물+아웃룩 합성 이미지 | Gemini Image | 병렬 | ref_image_gen, outlook_phase3 |
| **24.5** | **`character_state_variant`** | **인물 상태 변형 (신규)** | **Gemini Image** | **-** | **composite_image_gen, shot_staging** |
| 25 | `scene_image_pipeline` | 씬 이미지 생성 (compound) | mixed | 병렬 | composite_image_gen, world_guide, character_state_variant |

## 흐름도

```mermaid
flowchart TD
    ET[entity_t2i] --> WG[world_guide]
    ET --> RIG[ref_image_gen]

    RIG --> CIG[composite_image_gen]
    OP3[outlook_phase3] --> CIG

    CIG --> CSV["character_state_variant (신규)"]
    STG[shot_staging] --> CSV

    CIG --> SIP[scene_image_pipeline]
    WG --> SIP
    CSV --> SIP

    subgraph SIP_Sub["scene_image_pipeline 내부 (compound)"]
        S1[prompt_translation] --> S2[scene_t2i_gen]
        S2 --> S3[scene_t2i_validation]
        S3 --> S4[prompt_sanitize]
        S4 --> S5[angle_recommend]
        S5 --> S6["fal_angle_apply (FAL_AI_ENABLED=true시)"]
        S6 --> S7[final_select]
    end

    SIP --> SIP_Sub

    style RIG fill:#f3e5f5
    style CIG fill:#e1bee7
    style CSV fill:#fff59d,stroke:#f57f17
    style SIP fill:#ba68c8,color:#fff
```

---

## world_guide

**목적**: 프로젝트 전체의 시각 스타일 가이드 생성. 씬 이미지의 `style_context`로 사용.

- **입력**: entity_t2i + scene_detail
- **출력**: `WorldGuide` DB 레코드 (style_context)
- **모델**: GPT
- **코드**: `backend/app/core/steps/image_steps.py` (WorldGuideStep)

---

## ref_image_gen (요소 참조 이미지)

**목적**: 각 엔티티(인물/배경/소품)의 참조 이미지 생성.

- **입력**: entity_t2i의 `t2i_prompt`
- **출력**: ImageAsset (asset_type: `reference`)
- **모델**: Gemini Image (`gemini-3.1-flash-image-preview`)
- **병렬**: 엔티티별 병렬 생성
- **핵심 로직**:
  - `ReferenceImageGenerator` 호출
  - 엔티티 의존 그래프에 따라 토폴로지 정렬 후 순서대로 생성
  - 검증 실패 시 재생성 시도
- **코드**: `backend/app/core/steps/image_steps.py` (RefImageGenStep)

---

## composite_image_gen (인물+아웃룩 합성)

**목적**: 인물 참조 이미지 + 아웃룩 참조 이미지를 합성하여 의상 착용 전신 이미지 생성.

- **입력**: ref_image_gen 결과 + outlook_phase3 (아웃룩 목록)
- **출력**: ImageAsset (asset_type: `composite`)
- **모델**: Gemini Image
- **병렬**: 합성 쌍별 병렬
- **적용 조건**: `if_has_outlooks` (아웃룩이 있을 때만)
- **핵심 로직**:
  - CharacterOutlook DB에서 인물-아웃룩 매핑 로드
  - **O00 (Null Outlook)**인 캐릭터는 composite 생성 없이 원본 ref 사용
  - composite key: `composite:{char_id}:{outlook_id}`
- **코드**: `backend/app/core/steps/image_steps.py` (CompositeImageGenStep)

---

## character_state_variant (신규 — order 24.5)

**목적**: 사망/의식불명/부상 등 특수 상태의 인물 참조 이미지 생성. scene_image_pipeline에서 composite ref 대신 이 variant ref를 주입하여, 카메라 앞에 쓰러진 인물이 "서 있는 일상 포즈"로 그려지는 문제를 해결한다.

- **입력**: composite_image_gen 결과 + shot_staging (character_angles의 `subject_state`)
- **출력**: ImageAsset (asset_type: `reference`, prompt_used에 `state_variant:{char_uuid}:{state_type}` 프리픽스)
- **모델**: Gemini Image
- **핵심 로직**:
  1. `shot_staging`을 스캔하여 `subject_state`가 `dead`, `severely_injured`, `unconscious` 중 하나인 인물을 helper `is_immobilized_state` 로 집계 → `affected: {char_name → {state_type}}`. (Area #2 (2026-05-17): legacy mixed gaze field → `subject_state` enum 분리.)
  2. DB의 EntityCanon에서 이름→UUID→description을 매핑.
  3. 각 (인물, 상태) 쌍에 대해 helper `get_visual_descriptor(state)` (`backend/app/core/subject_state.py`) 의 상태 묘사를 프롬프트에 주입하여 variant 이미지 생성. (Area #2 W4 (2026-05-17, commit `c9b537a`): 이전 image_steps 안 inline per-state prose dict 정의 → helper module 안 closed-world dict 로 이관. alive 제외 — alive 입력 시 AppError fail-fast.)
  4. resume 모드: 같은 prompt_used 프리픽스로 기존 variant가 있으면 스킵. force 모드: `state_variant:` → `state_variant_old:`로 마킹.
- **helper SOT** (`SUBJECT_STATE_VISUAL_DESCRIPTOR`, `backend/app/core/subject_state.py`):
  - `dead`: `lying motionless, pale/ashen skin, eyes fully closed, slack facial muscles, no signs of life`
  - `severely_injured`: `visible bruises and cuts, bloodied areas on face or clothing, pained or grimacing expression, disheveled appearance`
  - `unconscious`: `eyes closed, slack facial features, limp posture, head tilted to one side`
- **코드**: `backend/app/core/steps/image_steps.py:535` (`CharacterStateVariantStep`)

```mermaid
flowchart LR
    Staging["shot_staging.character_angles"] --> |"subject_state ∈ {dead, unconscious, severely_injured}"| Detect
    Detect --> |"(인물, 상태) 쌍"| Gen[Gemini Image 생성]
    Gen --> Asset["ImageAsset asset_type=reference, prompt_used=state_variant:{uuid}:{state}"]
```

---

## scene_image_pipeline (씬 이미지 생성, compound step)

**목적**: 최종 씬 이미지 생성. 7개 서브 단계를 포함하는 복합 단계.

- **입력**: composite_image_gen + world_guide + character_state_variant
- **출력**: SceneStill 이미지 (DB + 파일)
- **모델**: mixed (GPT Mini + Gemini Image + GPT Vision + fal.ai)

### 서브 단계

| 서브 단계 | 모델 | 역할 |
|-----------|------|------|
| `prompt_translation` | GPT Mini | 한국어 T2I → 영어 변환 |
| `scene_t2i_gen` | Gemini Image | T2I 이미지 생성 (참조 이미지 포함) |
| `scene_t2i_validation` | GPT Vision | 생성 이미지 품질 검증 |
| `prompt_sanitize` | GPT | 프롬프트 안전화 (콘텐츠 필터 대응) |
| `angle_recommend` | GPT Vision | N개 이미지 중 앵글 적용 후보 선택 |
| `fal_angle_apply` | fal.ai | 선택된 이미지에 카메라 앵글 적용 (`FAL_AI_ENABLED=true`시) |
| `final_select` | GPT Vision | N+1개 이미지 중 최종 대표 선택 |

---

## 핵심 함수

### 1. `_resolve_refs_for_prompt` (`backend/app/services/image_service.py:4471`)

T2I 프롬프트에서 엔티티 ID를 찾아 해당하는 참조 이미지를 매칭.

```mermaid
flowchart TD
    T2I["t2i_prompt: C01O03 sits..."] --> Parse["ID 파싱"]

    Parse --> |C01O03| Check1{"state_variant?"}
    Check1 --> |Yes| SV["state variant ref 제공 (composite 대신)"]
    Check1 --> |No| Check2{"O00?"}
    Check2 --> |Yes| BaseRef["캐릭터 base ref 직접 사용"]
    Check2 --> |No| Check3{"composite 존재?"}
    Check3 --> |Yes| CompRef["composite ref 제공"]
    Check3 --> |No| FaceOnly["face만 제공 (outfit 단독 금지)"]

    Parse --> |P01| PropCheck{"P## 언급?"}
    PropCheck --> |Yes| PropRef["소품 ref 제공"]
    PropCheck --> |No| Skip["스킵"]
```

주요 규칙:
- **state_variant 인물**: dead/unconscious 상태 variant ref 우선 제공. state_variant ref가 있으면 composite/base는 스킵.
- **state_variant ref가 없는 경우 (v0.5.1)**: 스킵하지 않고 일반 composite ref로 **fallback**한다 (이전에는 스킵했음).
- **O00 (Null Outlook)**: composite 없이 캐릭터 base ref 직접 사용.
- **outfit 단독 제공 금지**: composite가 없으면 face만 제공 (outfit 단독은 Gemini가 2명으로 해석).
- **P## 언급 체크**: T2I 프롬프트에 해당 소품 ID가 word-boundary regex로 언급된 경우에만 ref 주입.

### 2. `_build_image_index` (`image_service.py:4608`)

labeled_refs에 Image N 번호를 부여하고, short_id → Image N 매핑 생성.

```
labeled_refs:
  [0] ("character C01O03 in outfit", bytes)      → Image 1 (character reference): 인물이름 -- 인물설명
  [1] ("object P02", bytes)                      → Image 2 (object reference): 소품이름 -- 소품설명
  [2] ("BACKGROUND from previous shot", bytes)   → Image 3: BACKGROUND...
```

- 캐릭터 ref: `Image N (character reference): 이름 -- T2I 설명[:80]` (성별/외모 키워드로 Gemini 매칭 보강)
- 소품 ref: `Image N (object reference): 이름 -- T2I 설명[:60]`
- 기타: `Image N: 원본 label`

### 3. `_rewrite_t2i_with_image_refs` (`image_service.py:4663`)

T2I 프롬프트의 `C##O##`, `P##`을 `"the character from Reference image N"`, `"the object from Reference image N"`으로 치환.

```
입력: "C01O03 sits across from C02O01, holding P02"
출력: "the character from Reference image 1 sits across from the character from Reference image 3, holding the object from Reference image 2"
```

- short_id 길이 역순으로 치환 (C01O03 → C01보다 먼저)
- regex word boundary 기반 치환
- 치환 전 `[L01: 공간 묘사]` 같은 배경 대괄호는 미리 제거(내용만 남김)

### 4. `_build_final_scene_prompt` (`image_service.py:62`) — v0.5.1 중립화

최종 Gemini 이미지 생성 프롬프트를 조립. 참조 이미지 라벨 기반 지시문을 **중립화**하여 "반드시 얼굴을 복사하라" 대신 "정체성 레퍼런스로 참고하라"로 완화.

#### 라벨 분기 (v0.5.1 업데이트)

| 라벨 키워드 | ref_role | 지시문 |
|-------------|----------|--------|
| `outfit appearance` | `standalone outfit/costume reference` | `dress the character in the outfit shown in image N` |
| `wearing` / `outfit` | 원본 label | `use image N as character appearance reference — match the person's identity and outfit where visible in the scene` |
| `face` / `character` | 원본 label | `use image N as character appearance reference — match the person's identity where visible in the scene` |
| `prop` / `object` | 원본 label | `include the object shown in image N` |
| `previous shot` + `same room` | `BACKGROUND from a previous shot (SAME ROOM) — use as-is` | `use the background, furniture layout, walls, and lighting from image N as-is` + `do NOT copy any standing/moving people from image N` + label에 포함된 keep/ignore 지시 |
| **`SAME FRAME ZOOMED`** (v0.5.14) | `ZOOM reference (crop from previous shot)` | `this shot is a zoom-in crop of image N — preserve its background, lighting, framing, and only render the scene description's focus area` |
| `previous shot` / `previous scene` / `continuity` | `BACKGROUND/ENVIRONMENT from a previous shot at the same location` | `use ONLY the lighting, color palette, and environment mood from image N` + `do NOT copy characters` + `do NOT copy composition or character poses` |
| `background` / `location` | `background/environment reference.` | `use the lighting, architecture, and environment mood from image N` |

#### v0.5.1 중요 변경

1. **`keep the face from image N` → `use image N as character appearance reference`**
   - 이전 버전은 카메라 각도/구도와 무관하게 얼굴을 복사하도록 강제 → 뒷모습/클로즈업 프롬프트에서도 얼굴이 부자연스럽게 합성되는 문제.
   - 새 지시문은 `where visible in the scene` 조건을 붙여 "씬 안에서 얼굴이 보이는 구도일 때만" 정체성을 맞추도록 유도.
2. **클로즈업 얼굴 강제 삽입 방지**: `scene_detail` v2 프롬프트가 클로즈업에서 C## 자체를 쓰지 않도록 바뀌었고, 이미지 지시문도 `only render what the scene description asks for — if only a hand or wrist is described, do NOT add the character's face`로 보강됨.

#### 항상 추가되는 지시

- `do not copy poses or compositions from reference images`
- `do not alter character identities where their face is visible in the scene`
- `only render what the scene description asks for — if only a hand or wrist is described, do NOT add the character's face`

### 5. 참조 이미지 주입 우선순위

`_resolve_refs_for_prompt` + `scene_t2i_gen` 단계가 labeled_refs에 이미지를 쌓는 순서:

```mermaid
flowchart LR
    CV["state_variant ref (dead/injured 인물)"] --> Comp["composite ref (일반 인물+아웃룩)"]
    Comp --> Base["base ref (O00 또는 composite 없음)"]
    Base --> Prop["prop ref (P## 언급 시만)"]
```

> ⚠️ 2026-04-27 set_design 제거 + prev-shot 배경 ref OFF (사용자 지시, 코드 보존). 새 background_chain step(별도 PR)이 도입되면 첫 번째 ref 슬롯에 chain bg로 재연결될 예정. 현재는 첫 ref가 비어있는 상태로 동작.
- state_variant는 `_resolve_refs_for_prompt`가 C##O##를 만나면 먼저 체크 → 있으면 composite/base 스킵하고 variant만 제공.

### state_variant 처리 흐름 (dead/unconscious)

```mermaid
flowchart LR
    Normal[일반 composite ref] --> |shot_staging.subject_state ∈ immobilized 감지| StateVariant[character_state_variant 단계에서 variant 이미지 생성]
    StateVariant --> |_resolve_refs_for_prompt| Priority["variant ref 우선 제공"]
    Priority --> |label| Label["character C01O03 -- dead state reference"]
```

1. `character_state_variant` 단계에서 dead/unconscious/severely_injured 인물의 variant 이미지 생성.
2. `_resolve_refs_for_prompt`에서 해당 인물의 state_variant를 composite 대신 우선 제공.
3. `_build_final_scene_prompt`에서 variant ref의 역할을 지시.
4. **v0.5.1 fallback**: state_variant ref가 없어도(생성 실패 등) 일반 composite ref로 fallback하도록 변경 (skip하지 않음).

### prop P## 언급 체크

소품 참조 이미지는 T2I 프롬프트에 해당 소품의 Short ID (`P01`, `P02`)가 **word-boundary regex**로 명시적으로 언급된 경우에만 주입. 불필요한 참조 이미지를 줄여 Gemini의 이미지 품질을 개선.

```python
# image_service.py:4599
if prop_sid and not _re.search(rf'\b{_re.escape(prop_sid)}\b', t2i_prompt):
    continue  # T2I에 P## 언급 없으면 스킵
```

### fal.ai 앵글 적용

`FAL_AI_ENABLED=true` 환경변수가 설정된 경우에만 활성화:

1. GPT Vision이 N개 이미지 중 앵글 적용 후보 선택 + 앵글 추천
2. `horizontal_angle` (0-360), `vertical_angle` (-30-90), `zoom` (0-10) 결정
3. 최소 20도 이상 변화 강제 (trivial adjustment 방지)
4. fal.ai API로 이미지에 앵글 적용
5. GPT Vision이 N+1개(기존 + 앵글 적용) 중 최종 대표 선택

---

## 이미지 파일 저장 구조

```
projects/{project_id}/images/
  episodes/{episode_id}/
    entities/          # 참조 이미지 (ref_image_gen)
    composites/        # 합성 이미지 (composite_image_gen)
    state_variants/    # 상태 변형 이미지 (character_state_variant)
    scenes/            # 씬 이미지 (scene_image_pipeline)
```

---

## 참고: 레거시

이 Phase에는 on_demand/disabled 레거시 step이 없다. 모든 5개 단계가 Active 경로에서 호출된다.

단, v0.5.1 이전의 `_resolve_refs_for_prompt`에 있던 **T2I 텍스트 regex skip 로직**(예: `back_to_camera` / `over_shoulder` / `closed` 키워드로 ref를 스킵)은 오탐이 많아 전면 제거되었다. 현재는 C## 사용 규칙(scene_detail 프롬프트)으로만 제어한다.
