# Phase 3 — `location_floor_plan` Step Design Spec

> **참조 v3 plan**: `next_session_floor_plan_architecture_implementation.md` (memory)
> **검증 spike**: `backend/scripts/spike_floor_plan_svg/test_06_auto_floor_plan.py` + `results/auto_floor_plan_prompt.txt`
> **선행 Phase**: P0(aacccd9), Phase 1b(553fea2), Phase 2(04840cc) 완료

## 1. 목표

`location` 단위로 **top-down architectural floor plan PNG 1장**을 자동 생성하는 step을 신설한다. 이 PNG가 chain_bg_render(Phase 4)와 scene_image(Phase 5)의 spatial layout authority가 되어, 가구/문/공간 배치 일관성을 보장한다.

## 2. 핵심 설계 결정 (사용자 명시 + spike 검증)

| 결정 | 근거 |
|---|---|
| 도면 prompt 자동 생성 (gpt-5.5) | spike test 6: 사람 v3 동급 정확 |
| 도면 PNG 생성 (gpt-image-2) | text-to-image, 1024x1024, quality=high |
| **사람 개입 0** | multi-candidate / vision validator / edit UI 모두 제거 |
| location 단위 | scene 단위가 아님. 같은 location의 모든 씬 함께 분석 |
| 1 location = 1 PNG | high/dusk variant는 chain_bg가 다룸 |
| spec.json/SVG/matplotlib 0 | 텍스트 layout은 어떤 형식이든 효과 0 (spike 4 variant 부정확 검증) |
| toggle: `background_mode` | enum: `off`, `chain_only`, `floor_plan_anchored` |
| schema_version + config_hash | resume 안전성 (Codex H2) |

## 3. 토글 아키텍처

```
settings.background_mode: Literal["off", "chain_only", "floor_plan_anchored"] = "off"
```

| mode | location_floor_plan | chain_bg | scene_image | 효과 |
|---|---|---|---|---|
| `off` (default) | skip | skip | nano-banana-2 | 회귀 0건 (PID c00bbe19 baseline 일치) |
| `chain_only` | skip | nano-banana-2 | nano-banana-2 | 기존 chain bg만 사용 (Phase 1b/2 토글과 결합 가능) |
| `floor_plan_anchored` | **run** | gpt-image-2 (Phase 4 후) | gpt-image-2 (Phase 5 후) | 도면 + chain bg + entity refs 전체 architecture |

Phase 3 범위에서는 `floor_plan_anchored` 모드일 때 `location_floor_plan` step만 실행하고 PNG를 생성한다. chain_bg_render와 scene_image의 도면 ref input 전환은 Phase 4/5에서 처리.

## 4. 컴포넌트 책임 매트릭스

| 단계 | 모델 | 입력 | 출력 |
|---|---|---|---|
| 4-A. prompt 생성 | gpt-5.5 | 시나리오 + selected shots per location | 영문 도면 prompt 1단락 (~1500~2000자) |
| 4-B. PNG 생성 | gpt-image-2 | 4-A의 prompt | 도면 PNG 1024x1024 |
| 4-C. DB 등록 | (코드) | PNG 파일 | `ImageAsset(asset_type='floor_plan', is_primary=1)` |

## 5. 데이터 모델

### 5.1 신규 자산

```
projects/<pid>/images/<eid>/
└── floor_plan/
    └── <location_short_id>.png      # 신규, 1 PNG per location
```

### 5.2 신규 체크포인트

```
checkpoints/episodes/<eid>/location_floor_plan/manifest.json

data:
  schema_version: 1
  config_hash: <project_config의 background_mode + 모델 + 프롬프트 버전 해시>
  locations:
    - id: <short_id, e.g. "L05">
      label: <location 이름>
      scene_indices: [5, 12, 14, 18, 25, 27]
      prompt_text: <자동 생성된 도면 prompt 전문>
      prompt_chars: 1578
      image_path: "projects/<pid>/images/<eid>/floor_plan/L05.png"
      image_bytes: 1234567
      gen_model: "gpt-image-2"
      gen_size: "1024x1024"
      gen_quality: "high"
      status: "ok" | "failed"
      failure_reason: <str or null>
  applicable_count: <총 location 수 — selected shot이 있는 location만>
  succeeded_count: <ok 카운트>
  failed_count: <failed 카운트>
```

### 5.3 ImageAsset 테이블

```python
ImageAsset(
    id=uuid,
    project_id=<pid>,
    episode_id=<eid>,
    asset_type="floor_plan",          # 신규 enum 값
    entity_id=<location EntityCanon.id>,
    file_path="projects/.../floor_plan/L05.png",
    prompt_used=<도면 prompt 전문>,
    generation_model="gpt-image-2",
    status="generated",
    variant_type=None,
    is_primary=1,                     # location당 1장만이라 항상 primary
    created_at=now,
)
```

UPSERT 키: `(project_id, episode_id, entity_id, asset_type='floor_plan')`. 재실행 시 file_path/prompt_used 갱신.

## 6. 의존성 그래프

```
shot_validator (selected shot description)
shot_selection (선택 인덱스)
scene_save (시나리오 원문)
entity_merge (location entity_canon, short_id)
visual_world_rules (선택, 시각적 세계관 컨텍스트)
   ↓
🆕 location_floor_plan
   ↓
(Phase 4) background_chain_planning + background_chain_render
   ↓
(Phase 5) scene_image_pipeline
```

~~`scene_director` 의존성 제외~~ — Task 5 code review에서 발견된 C1 버그로 재추가됨. shot_validator의 shot에는 `location_id` 필드가 없으므로 (spike test 6은 hardcoded scene index로 bypass), `scene_director.scenes[].primary_location` + `entity_merge.locations[].name → short_id` 매핑 fallback 패턴(`background_chain_planning._phase0_group_by_location`)을 사용해야 location 단위 그루핑 가능.

→ depends_on: `["shot_validator", "shot_selection", "scene_save", "entity_merge", "visual_world_rules", "scene_director"]`

## 7. Step manifest entry

```python
"location_floor_plan": {
    "label": "위치 도면 생성",
    "category": "image",                    # asset 분류 — PNG 생성
    "order": 21.5,                           # scene_verify(21) 후 / world_guide(22) 전 — image>analysis 불변식
    "default_model": "gpt",                  # text prompt 생성용 (gpt-image-2는 코드에서 직접 호출)
    "provider": "openai",
    "depends_on": [
        "shot_validator", "shot_selection",
        "scene_save", "entity_merge",
        "visual_world_rules", "scene_director",
    ],
    "fan_out": False,
    "applicability": "if_floor_plan_mode",   # 신규 validator
    "step_type": "asset",                    # PNG 생성 + DB 등록
    "lifecycle": "active",
},
```

`applicability="if_floor_plan_mode"` validator는 `settings.background_mode == "floor_plan_anchored"` 일 때만 True. mode가 `off`/`chain_only`이면 step이 not_applicable로 자동 제외.

## 8. 실행 흐름 (코드)

```
LocationFloorPlanStep._execute(mode="resume"):
  1. settings.background_mode 검증 (floor_plan_anchored 아니면 not_applicable)
  2. shot_validator + shot_selection 로드 (Phase 1b 패턴 동일)
  3. scene_save 로드 (시나리오 원문)
  4. entity_merge에서 location entity_canon 로드 (short_id, label)
  5. selected shot이 있는 location만 추출 → location_groups: Dict[short_id, scenes_meta]
  6. visual_world_rules 로드 (선택)
  7. 각 location 병렬 처리 (max_workers=4):
     7a. _build_user_prompt(location, scenes_meta) — spike test 6 패턴
     7b. call_text(gpt-5.5, system_prompt, user_prompt) → 영문 도면 prompt
     7c. _generate_image(prompt, gpt-image-2, 1024x1024) → PNG bytes
     7d. PNG 저장 → projects/<pid>/images/<eid>/floor_plan/<short_id>.png
     7e. result entry 작성
  8. 카운트 집계 + 결과 dict 반환
  9. _register_image_assets(result.locations) — DB UPSERT
```

병렬도 4: location 수가 보통 3~7개 정도라 적당. retry는 LLM 호출당 max=3 (shot_essence_extraction과 동일).

## 9. Prompt 구조

### 9.1 system.md (spike test 6의 `PROMPT_GEN_SYSTEM` 그대로)

```
You are a film production designer. Given the screenplay scenes that occur in
a single shooting LOCATION, write a precise English text-to-image prompt that
will produce a TOP-DOWN ARCHITECTURAL FLOOR PLAN diagram of that location.

The output prompt will be sent directly to gpt-image-2 (text-to-image).
It must produce a schematic floor plan (NOT a photorealistic interior).

REQUIREMENTS for the prompt:
1. Start with: "Top-down architectural floor plan of [location description]."
2. List ALL rooms/zones explicitly with their relative positions
   (corner, side, center).
3. For each room, specify: which wall has which furniture, the size of
   furniture relative to the room, and how furniture relates to other furniture.
4. Specify all doors: which wall, where it leads.
5. Specify windows: which wall, which room.
6. Specify external adjacency: what is OUTSIDE the front entry door.
7. Add explicit NEGATIVE constraints — what NOT to draw.
8. Use universal nouns. NO scenario proper nouns.
9. Korean common nouns are OK in parentheses for room labels
   (e.g., "main bedroom (안방)").
10. End with: "Schematic line drawing style, clean labels, no shading,
    white background."

CRITICAL: Read EVERY scene/shot carefully. Identify zone markers
(e.g., '/거실', '/안방', '/욕실', '/현관'). Every zone mentioned must appear
in the floor plan.

OUTPUT: One single block of English prompt text only. No JSON, no explanation,
no markdown headers.
```

### 9.2 schema.json (text-only output, no JSON)

`gpt-5.5`가 도면 prompt 영어 텍스트를 그대로 출력하므로 structured schema는 사용하지 않는다. `call_text(...)` (text completion) 사용.

대신 검증:
- 출력 길이 ≥ 500자 (너무 짧으면 retry)
- 출력 길이 ≤ 6000자 (gpt-image-2 prompt 한계 안에 충분)
- ASCII + 한글 한정 (zone marker가 한글일 수 있음)

### 9.3 user_template.md

```
LOCATION ID: {location_short_id}
LOCATION LABEL: {location_label}

── ALL SCENES OCCURRING IN THIS LOCATION ──

{scenes_text}

── SELECTED SHOTS IN THIS LOCATION (visible elements) ──

{selected_shots_text}

── VISUAL WORLD RULES (선택) ──

{visual_world_rules_excerpt}

Now write the floor plan text-to-image prompt for this location.
```

## 10. Fallback 전략 (사용자 개입 0)

| 상황 | 동작 |
|---|---|
| gpt-5.5 호출 실패 | retry 3회 (exponential backoff 2/4/6s). 최종 실패 → location 결과 status='failed' + checkpoint 보존 (downstream Phase 4가 fallback 결정) |
| 도면 prompt 길이 < 500자 또는 > 6000자 | retry 3회 (LLM 호출과 같은 retry budget 공유). 그래도 실패 → status='failed' |
| gpt-image-2 호출 실패 | retry 3회. 최종 실패 → status='failed' |
| OPENAI_API_KEY 미설정 | fail-fast (`AppError`) — backround_chain_render와 동일 |
| selected shot 없는 location | 결과에서 제외 (applicable_count에 미포함) |
| location entity_canon 없음 | warning + skip (DB sync 단계에서) |

`floor_plan_anchored` 모드에서 도면 생성이 모두 실패한 경우의 동작:
- Phase 3 범위에서는 status='failed' checkpoint만 남기고 종료. Phase 4/5에서 chain_only fallback 분기 추가 예정. **본 Phase 3은 도면 생성 자체만 책임지고, 그 결과를 사용하는 chain_bg/scene_image의 mode 분기는 다음 Phase의 책임으로 분리**.

## 11. 회귀 보장 (3중)

1. `background_mode = "off"` default → step이 not_applicable, 코드 변경 0건이 production에 영향 X
2. spike test 6 패턴 그대로 production화 — prompt + 모델 + size/quality/n=1까지 동일
3. 신규 step 단독 실행 — chain_bg_render / scene_image / scene_detail 모두 변경 0건 (Phase 4/5에서 처리)

## 12. 변경 파일 (10개)

| 파일 | 변경 |
|---|---|
| `backend/app/core/config.py` | `background_mode` Literal 필드 추가 |
| `backend/app/core/applicability.py` | `_if_floor_plan_mode` validator + 레지스트리 등록 |
| `backend/app/core/step_manifest.py` | `location_floor_plan` entry 추가 |
| `backend/app/core/steps/__init__.py` | `LocationFloorPlanStep` import |
| `backend/app/core/steps/location_floor_plan_step.py` | **신규** |
| `backend/app/modules/pipeline/location_floor_plan.py` | **신규** (도면 prompt + PNG 생성 로직) |
| `backend/app/modules/llm/llm_client.py` | `PIPELINE_STEPS["location_floor_plan"]` 등록 |
| `prompts/_base/location_floor_plan/1.<ts>/system.md` | **신규** (spike test 6 패턴) |
| `prompts/_base/location_floor_plan/1.<ts>/user_template.md` | **신규** |
| `backend/tests/core/test_location_floor_plan.py` | **신규** — 단위 테스트 |

## 13. 테스트 전략

### 13.1 단위 테스트 (mock 기반, real LLM 호출 X)

```
test_settings_background_mode_default_off()
test_applicability_off_returns_false()
test_applicability_chain_only_returns_false()
test_applicability_floor_plan_anchored_returns_true()
test_load_location_groups_filters_by_selection()
test_load_location_groups_skips_no_short_id()
test_build_user_prompt_includes_zone_markers()
test_build_user_prompt_excludes_non_selected_shots()
test_generate_prompt_validates_length_min()
test_generate_prompt_validates_length_max()
test_generate_prompt_retry_on_failure()
test_generate_image_retry_on_failure()
test_step_no_selected_shots_returns_zero()
test_step_failed_location_records_status()
test_step_db_register_upsert()
test_checkpoint_schema_v1_fields()
test_checkpoint_config_hash_changes_on_mode_flip()
```

### 13.2 회귀 테스트 (기존 99 PASSED 유지)

- `test_chain_bg_guide.py` (Phase 2, 19) → 변경 없음
- `test_shot_essence_extraction.py` (Phase 1b, 10) → 변경 없음
- `test_step_manifest_v3.py` (22) → location_floor_plan entry로 23이 됨 (1 추가 기대)
- `test_manifest_fields.py` (18) → 동일 (신규 step도 필드 검증 통과해야)
- `test_step_catalog.py` (13) → 1 추가 기대
- `test_background_chain_render.py` (17) → 변경 없음

총 99 → 약 117 (단위 테스트 17 + manifest 추가 1) PASSED 목표.

### 13.3 production 검증 (별도 세션)

`background_mode=floor_plan_anchored` E2E는 Phase 6의 5 시나리오 검증에서 일괄 처리. Phase 3 단독 검증은:
1. 옥탑방 location 1건만 step 실행
2. PNG 결과를 spike test 6의 `auto_floor_plan.png`와 시각 비교
3. ImageAsset DB row 확인

## 14. 비용 (per EP, floor_plan_anchored)

| 단계 | 모델 | 호출 수 | 비용 |
|---|---|---|---|
| location_floor_plan prompt | gpt-5.5 | ~5 location | $0.05 |
| location_floor_plan image | gpt-image-2 | ~5 location | $0.40 |
| **Phase 3 total** | | | **~$0.45 / EP** |

기존 `off` 모드는 변경 없음.

## 15. 의사결정 사항 (확정 — 추가 질문 없음)

| 항목 | 결정 |
|---|---|
| location 단위 vs scene 단위 | location 단위 (1 PNG per location) |
| 시간대 variant (day/dusk) | 본 Phase에선 1장만. chain_bg_render가 시간대 처리 |
| sub_steps (prompt+image 분리) | 단일 step 내부 두 phase. UI에는 단일 step으로 노출 |
| order 21.5 | scene_verify(21) 후 / world_guide(22) 전. image>analysis 불변식 만족 — Task 7 review C1 fix |
| applicability validator 이름 | `if_floor_plan_mode` |
| ImageAsset.asset_type | 신규 enum 값 `floor_plan` |
| primary key 정책 | location당 1장이라 is_primary=1 항상 |
| force 재실행 시 | DB UPSERT, 파일은 같은 경로 덮어쓰기 |

## 16. 다음 Phase 연계

- **Phase 4**: `background_chain_render`가 도면 PNG를 ref로 받아 chain_bg PNG 생성 (gpt-image-2 전환). `background_chain_planning`도 도면 prompt를 input context로 받아 더 정확한 plan 생성.
- **Phase 5**: `scene_image_pipeline`이 도면 + chain_bg + entity refs를 ref로 받아 scene 이미지 생성 (mode 분기로 nano-banana-2 vs gpt-image-2).
- **Phase 6**: 5 시나리오 E2E + baseline 비교 + chain_only fallback 검증.

본 Phase 3은 후속 단계의 입력만 만든다. **Phase 3만 머지해도 production 회귀 0건 보장** (mode default off).
