# Experiment v4 - Floor-plan 기반 배경 생성 검토/보완 계획서

> 상태: 원본 `experiment_floor_plan_v4_plan.md` 재검토 반영본. 코드 작성 전 개발 방향 문서.
> 원본 기준: `backend/scripts/experiment_floor_plan_v4_plan.md`
> 목적: 도면 기반 배경 생성 부분을 실제 관련 코드와 연결해 구현 방법, 출력 계약, 검증, 운영 통합 방향을 명확히 한다.
> 범위: 기존 코드 수정 없음. 이 문서는 standalone v4 실험과 이후 `set_design` 통합 방향을 다룬다.

검토 코드:

- `backend/scripts/experiment_floor_plan_v3.py`
- `backend/scripts/experiment_plan_to_photo.py`
- `backend/app/modules/pipeline/set_design.py`
- `backend/app/core/steps/set_design_step.py`
- `backend/app/services/scene_checkpoint_loaders.py`
- `backend/app/services/scene_generation_coordinator.py`
- `backend/app/services/prompt_service.py`
- `backend/app/core/steps/scene_context_loader.py`
- `backend/app/core/steps/detail_steps.py`

절대 규칙:

1. 시나리오 의존 어휘(인명, 지명, 작품명)는 코드, 시스템 프롬프트, 출력 필드에 넣지 않는다.
2. LLM에 전달하는 데이터는 자르지 않는다. `[:N]` 방식 금지.
3. 한국어 일반명사는 도면 그래픽 라벨에서만 허용한다. T2I 본문은 일반명사 기반 영문으로 작성한다.
4. 도면 단계라도 이미지 moderation에 걸릴 수 있는 직접 폭력/사망/혈흔 어휘는 쓰지 않는다.

---

## 1. 결론

원본 v4 계획의 핵심 진단은 맞다. v3는 base 도면을 각각 독립 `generate`로 만들고, shot 도면만 matching base 위에 `edit`한다. 따라서 shot 내부 일관성은 생기지만 base 도면끼리, base 사진끼리 동일 건물/동일 공간이라는 보장이 없다.

다만 원본 계획의 단일 `anchor_id` 방식은 보완이 필요하다. `site_map`, `exterior`, `interior`는 시점과 이미지 도메인이 달라서 하나의 anchor로 모두 묶으면 실패할 가능성이 높다. v4는 `environment_canon`을 전역 identity로 유지하고, 이미지는 `visual_domain`별 `anchor_clusters`로 묶어야 한다.

도면 기반 배경 생성의 핵심은 "도면 JSON을 텍스트로 참고하는 것"이 아니다. 최소한 대상 `floor plan PNG`를 이미지 reference로 넣고, 같은 domain의 `photo anchor`를 함께 써야 한다. 그래야 사진이 도면의 문, 창문, 가구 배치와 실제로 대응된다.

운영 통합은 바로 하지 않는다. 먼저 standalone v4 실험을 만들고, 산출물을 기존 `set_design` checkpoint 계약으로 변환할 수 있는 `set_design_adapter_preview.json`을 생성한다. 이후 결과가 안정적일 때만 feature flag를 분리해 운영에 붙인다.

최종 scene image generation까지 배경 구조를 유지하려면 별도 작업이 필요하다. 현재 `prompt_service.resolve_ref_roles()`는 일반 background reference를 lighting, architecture, mood 참고로만 해석한다. 도면 기반 배경은 `exact_floor_plan_background` 의미를 전달하도록 분기를 추가해야 한다.

---

## 2. 원본 문서 반영 포인트

원본 `experiment_floor_plan_v4_plan.md`에서 이 문서에 반드시 반영해야 하는 핵심 변경은 다음이다.

| 항목 | 반영 내용 |
| --- | --- |
| anchor | 단일 `anchor_id` 대신 `visual_domain`별 `anchor_clusters` |
| photo reference | 텍스트만이 아니라 `target plan PNG + domain anchor photo` |
| safety | warn-only보다 sanitize 재호출 기본값 |
| output | `set_design_adapter_preview.json` 포함 |
| manifest | `validation`, `phase_durations`, `set_design_adapter` 포함 |
| gallery | plan-photo, shot-photo, domain grid, adapter preview 4축 이상 |
| 운영 통합 | Phase 4로 분리, 즉시 통합 금지 |
| final scene ref | `exact_floor_plan_background` semantics 필요 |
| prompt/schema | DB-only 의존 줄이고 file fallback 필요 |
| 결정사항 | D1~D10 사용자 리뷰 항목으로 유지 |

---

## 3. 현재 코드 검토 결과

### 3.1 v3 floor plan 실험

파일: `backend/scripts/experiment_floor_plan_v3.py`

현재 흐름:

1. Step 1: scene, shot, staging, fixed_elements, location을 모아 spatial JSON 생성.
2. Step 2: base plan prompt 생성.
3. Step 2b: 각 base plan을 독립 `gen_image()`로 생성.
4. Step 3: shot overlay JSON 생성.
5. Step 3b: `edit_image(base_path, shot_prompt)`로 shot 도면 생성.

문제:

- base plan 이미지끼리는 image reference가 없다.
- prompt에 같은 canon이 반복되지 않으면 같은 건물로 보일 근거가 약하다.
- shot 도면은 base에 묶이지만, base 자체가 서로 다른 건물처럼 나오면 shot도 후속 배경 reference로 쓰기 어렵다.

v4 방향:

- Step 2에서 `environment_canon`, `visual_domain`, `anchor_clusters`를 생성한다.
- Phase B에서 domain별 anchor plan을 먼저 만들고, 같은 domain의 non-anchor plan은 해당 domain anchor를 reference로 edit한다.
- shot plan은 기존처럼 matching base plan 위에 edit하되 `visual_domain` mismatch를 검증한다.

### 3.2 plan_to_photo 실험

파일: `backend/scripts/experiment_plan_to_photo.py`

현재 흐름:

1. v3 run directory에서 `step1_spatial`, `step2_base_plans`, `step3_shot_*` JSON을 읽는다.
2. LLM이 `base_photos`, `shot_photos` T2I prompt를 만든다.
3. 모든 사진은 독립 `client.images.generate()`로 만든다.

문제:

- 도면 PNG가 이미지 reference로 들어가지 않는다.
- 결과는 "도면 기반"이 아니라 "도면 설명 기반"이다.
- base photo끼리도 독립 generate라서 건물 visual identity가 흔들린다.
- shot photo가 matching base photo 위에 edit되지 않아 shot 간 공간 연속성이 약하다.

v4 방향:

- Step 7 photo specs는 `source_floor_plan_image_path`, `source_anchor_photo_id`, `matching_base_photo_id`, `reference_strategy`를 반드시 가진다.
- base photo는 target floor plan PNG를 reference로 쓴다.
- non-anchor base photo는 가능하면 target floor plan PNG와 domain anchor photo를 함께 reference로 쓴다.
- shot photo는 matching base photo를 기준으로 edit하고, 가능하면 shot overlay plan PNG도 함께 reference로 넣는다.

### 3.3 운영 set_design 파이프라인

파일: `backend/app/modules/pipeline/set_design.py`

현재 흐름:

- Phase 0: selected shot을 primary location 기준으로 grouping.
- Phase 1: location별 base image 1~3장 생성. 이전 base 한 장만 reference로 쓰는 순차 chaining.
- Phase 2: shot별 background image 생성. LLM이 base/prev shot 중 ref 선택.
- 위치별 병렬, 위치 내부 shot은 순차.

장점:

- `locations[loc_id].base_images[]`, `locations[loc_id].shot_backgrounds[]` 구조가 이미 있다.
- `set_design_step.py`가 checkpoint 저장과 ImageAsset 등록을 수행한다.
- `scene_checkpoint_loaders.py`와 `scene_generation_coordinator.py`가 set_design background를 최종 이미지 생성 reference로 연결한다.

한계:

- 전역 canon, domain별 anchor, floor plan geometry, plan PNG reference 개념이 없다.
- Phase 1은 이전 base 한 장만 참조하므로 domain이 다른 이미지 간 reference 오염 가능성이 있다.
- prompt/schema 파일 fallback이 repo에서 확인되지 않아 DB prompt 의존성이 높다.

v4 방향:

- standalone v4에서는 운영 `set_design`을 직접 수정하지 않는다.
- 대신 `set_design_adapter_preview.json`을 만들어 현재 checkpoint 구조와 호환 가능한지 확인한다.
- 운영 통합은 Phase 4에서 별도 승인 후 진행한다.

### 3.4 최종 scene generation 연결

관련 코드:

- `scene_checkpoint_loaders.load_set_design_bg_map()`
- `scene_generation_coordinator`
- `prompt_service.resolve_ref_roles()`
- `scene_context_loader._load_set_design_hints()`
- `detail_steps`

현재 연결:

1. loader가 `set_design` checkpoint의 `shot_backgrounds[]`에서 image bytes와 label을 읽는다.
2. coordinator가 해당 background ref를 `labeled_refs` 첫 번째에 삽입한다.
3. prompt_service는 label에 `background` 또는 `location`이 있으면 lighting, architecture, environment mood 참고 지시만 만든다.
4. scene_detail 단계에는 set_design의 `t2i`, `state_changes`가 힌트로 들어간다.

문제:

- 도면 기반 배경은 벽, 문, 창문, 가구 layout을 정확히 유지해야 한다.
- 현재 generic background reference는 exact layout 보존 지시가 아니다.
- `previous shot same room` label일 때만 exact background에 가까운 지시가 있다.

v4 운영 통합 방향:

- adapter의 `shot_backgrounds[].ref_type`을 `exact_floor_plan_background`로 기록한다.
- loader/coordinator/prompt_service 중 한 지점에서 이 의미가 최종 prompt까지 전달되게 한다.
- 권장 위치는 `prompt_service.resolve_ref_roles()` 분기 추가다.

---

## 4. 목표 / 비목표

### 4.1 목표

1. base 도면 간 visual identity 일관성 확보.
2. base 도면과 base photo의 공간 매칭 확보.
3. shot plan/photo가 matching base에 묶이도록 보장.
4. 모든 산출물에 후속 파이프라인 통합 가능한 구조화 metadata 포함.
5. 시나리오 의존 어휘 0건 검증.
6. unsafe image words는 warn-only가 아니라 sanitize 재호출로 제거.
7. `set_design` checkpoint 계약과 호환 가능한 adapter preview 생성.
8. `gallery.html`에서 plan-photo, shot-photo, domain-grid, adapter preview를 한 번에 검토 가능하게 한다.

### 4.2 비목표

- 메인 파이프라인 즉시 통합.
- 운영 DB 또는 기존 checkpoint 수정.
- 기존 `set_design` 동작 변경.
- 모든 시나리오 자동 처리.
- 100% photo-realistic accuracy. 이번 목표는 구조/일관성 우선이다.

---

## 5. 핵심 설계

### 5.1 Environment Canon

Step 1은 공간 그룹뿐 아니라 visual identity를 고정하는 `environment_canon`을 만들어야 한다.

```jsonc
{
  "environment_canon": {
    "building": {
      "stories": "string",
      "primary_material": "string",
      "color_palette": ["string"],
      "exterior_stairs": "string",
      "rooftop_features": ["string"],
      "window_pattern": "string",
      "weathering": "string"
    },
    "interior": {
      "wall_finish": "string",
      "floor_finish": "string",
      "ceiling": "string",
      "lighting_fixtures": ["string"],
      "general_clutter_level": "string"
    },
    "exterior": {
      "surface_finish": "string",
      "roof_edge": "string",
      "utility_features": ["string"]
    },
    "site_context": {
      "density": "string",
      "neighboring_rooftops": "string"
    },
    "shared_furniture_styles": [
      {"category": "string", "style_note": "string"}
    ]
  }
}
```

규칙:

- 시나리오 고유명사는 canon에도 들어가면 안 된다.
- 데이터에서 추론하되, 데이터가 부족하면 일반적이고 보수적인 기본값만 쓴다.
- 모든 base plan/photo prompt 첫 단락에 canon 응축본을 넣는다.

### 5.2 Visual Domain과 Anchor Clusters

단일 anchor는 폐기하고, 각 `visual_domain`마다 anchor를 둔다.

```jsonc
{
  "anchor_clusters": [
    {
      "id": "cluster_site",
      "visual_domain": "site_map",
      "anchor_plan_id": "dense_residential_site_map",
      "purpose": "global spatial relationship"
    },
    {
      "id": "cluster_exterior",
      "visual_domain": "exterior",
      "anchor_plan_id": "rooftop_exterior",
      "purpose": "building facade, rooftop, stairs, exterior material"
    },
    {
      "id": "cluster_interior",
      "visual_domain": "interior",
      "anchor_plan_id": "rooftop_room_interior",
      "purpose": "room layout, furniture, doors, windows"
    }
  ]
}
```

검증:

- `anchor_clusters[].anchor_plan_id`는 `base_plans[].id` 중 하나여야 한다.
- 각 visual_domain마다 `is_anchor=true` base는 정확히 1개여야 한다.
- shot의 `visual_domain`은 matching base의 `visual_domain`과 같아야 한다.

### 5.3 도면 기반 사진의 정의

v4에서 "도면 기반 사진"은 다음 조건을 만족해야 한다.

1. `spatial JSON`과 `elements_meta`가 prompt에 반영된다.
2. `floor plan PNG`가 photo generation/edit reference로 들어간다.
3. 같은 domain의 `photo anchor`가 visual identity reference로 들어간다.

우선순위:

1. 구조 정확도: target floor plan PNG와 elements_meta.
2. visual identity: environment_canon과 domain anchor photo.
3. shot 대응: shot overlay camera/additions와 matching base photo.

### 5.4 Reference Strategy

사진 단계는 모든 이미지에 `reference_strategy`를 기록한다.

허용값:

- `plan_png+domain_anchor_photo`
- `plan_png_only`
- `domain_anchor_photo_only`
- `base_photo+shot_overlay`
- `base_photo_only`

기본값:

- base photo: `plan_png+domain_anchor_photo`, fallback `plan_png_only`.
- shot photo: `base_photo+shot_overlay`, fallback `base_photo_only`.

---

## 6. 새 흐름

```text
[Inputs] project-id, episode-id, scene-indices, location-ids, target-shots, banned words
     |
     v
Phase A - 분석 (LLM only)
  Step 1 spatial + environment_canon
  Step 2 plan_specs + anchor_clusters
  Step 3 shot_specs per target shot
     |
     v
Phase B - 도면 이미지
  Step 4 domain anchor base plan generate
  Step 5 non-anchor base plan edit(domain anchor)
  Step 6 shot plan edit(matching base plan)
     |
     v
Phase C - 사진
  Step 7 photo_specs
  Step 8 domain anchor photo from anchor plan PNG
  Step 9 non-anchor photo from target plan PNG + domain anchor photo
  Step 10 shot photo from matching base photo + optional shot overlay plan PNG
     |
     v
Phase D - set_design adapter
  Step 11 set_design_adapter_preview.json
     |
     v
Output: manifest, step JSON, plan PNG, photo PNG, gallery.html, adapter preview
```

호출 수 예시:

- LLM: `Step 1 + Step 2 + Step 3 × N + Step 7 + sanitize 재호출`
- 도면 이미지: `D anchor generate + (B-D) edit + N shot edit`
- 사진 이미지: `D anchor generate/edit + (B-D) edit + N shot edit`

B는 base plan 수, D는 visual_domain 수, N은 target shot 수다.

---

## 7. 데이터 모델

### 7.1 `step1_spatial.json`

```jsonc
{
  "space_groups": [
    {
      "id": "snake_case",
      "label": "한영",
      "covers_location_ids": ["L##"],
      "scale_estimate": "approx Nm x Mm",
      "type": "interior|exterior|site_map",
      "visual_domain": "interior|exterior|site_map",
      "rooms": [
        {
          "name": "string",
          "walls": ["string"],
          "doors": [{"connects_to": "string", "note": "string"}],
          "windows": [{"position": "string", "note": "string"}],
          "furniture": [{"name": "string", "position": "string", "note": "string"}],
          "fixtures": [{"name": "string", "position": "string"}]
        }
      ]
    }
  ],
  "shared_elements": [
    {
      "id": "string",
      "appears_in_scenes": [0],
      "in_space_group_id": "string",
      "in_room": "string",
      "description": "English common nouns only",
      "note": "Korean common noun helper only"
    }
  ],
  "environment_canon": {}
}
```

### 7.2 `step2_plan_specs.json`

```jsonc
{
  "anchor_clusters": [
    {
      "id": "cluster_interior",
      "visual_domain": "interior",
      "anchor_plan_id": "rooftop_room_interior",
      "purpose": "room layout, furniture, doors, windows"
    }
  ],
  "base_plans": [
    {
      "id": "snake_case",
      "label": "한영",
      "visual_domain": "interior",
      "anchor_cluster_id": "cluster_interior",
      "is_anchor": true,
      "canon_refs": ["building.primary_material", "interior.floor_finish"],
      "t2i_prompt": "English prompt with canon paragraph",
      "legend": [{"symbol": "", "meaning": ""}],
      "elements_meta": [
        {
          "id": "string",
          "kind": "wall|door|window|furniture|fixture",
          "label": "string",
          "position_hint": "string",
          "note": "string"
        }
      ],
      "annotations": [{"text": "string", "placement": "string"}]
    }
  ],
  "shot_assignment": [
    {
      "scene_index": 12,
      "shot_index": 4,
      "base_plan_id": "rooftop_room_interior",
      "visual_domain": "interior"
    }
  ]
}
```

### 7.3 `step3_shot_*.json`

v3 schema를 유지하되 다음을 추가한다.

```jsonc
{
  "scene_index": 12,
  "shot_index": 4,
  "base_plan_id": "rooftop_room_interior",
  "visual_domain": "interior",
  "camera": {},
  "characters": [],
  "additions": [],
  "t2i_prompt": "English overlay prompt",
  "legend_updates": []
}
```

규칙:

- `base_plan_id`는 반드시 `base_plans[].id` 중 하나다.
- `visual_domain`은 base plan과 같아야 한다.
- `characters[].name`은 metadata로 보존 가능하지만 `t2i_prompt`에는 넣지 않는다.

### 7.4 `step7_photo_specs.json`

```jsonc
{
  "anchor_clusters": [
    {
      "id": "cluster_interior_photo",
      "visual_domain": "interior",
      "anchor_plan_id": "rooftop_room_interior",
      "source_floor_plan_image_path": "base_plan_rooftop_room_interior.png",
      "reference_strategy": "plan_png_only"
    }
  ],
  "base_photos": [
    {
      "id": "photo_base_rooftop_room_interior",
      "source_plan_id": "rooftop_room_interior",
      "visual_domain": "interior",
      "source_floor_plan_image_path": "base_plan_rooftop_room_interior.png",
      "source_anchor_photo_id": "photo_base_rooftop_room_interior",
      "reference_strategy": "plan_png+domain_anchor_photo",
      "label": "clean photo label, not drawn",
      "lighting": "string",
      "camera_note": "string",
      "t2i_prompt": "English photo prompt with canon paragraph"
    }
  ],
  "shot_photos": [
    {
      "id": "photo_shot_S12_Shot4",
      "source_shot": {"scene_index": 12, "shot_index": 4},
      "source_plan_id": "rooftop_room_interior",
      "matching_base_photo_id": "photo_base_rooftop_room_interior",
      "shot_overlay_plan_image_path": "shot_plan_S12_Shot4.png",
      "reference_strategy": "base_photo+shot_overlay",
      "lighting": "string",
      "camera_note": "string",
      "t2i_prompt": "English prompt"
    }
  ]
}
```

### 7.5 `set_design_adapter_preview.json`

```jsonc
{
  "locations": {
    "L##": {
      "location_name": "common noun location name",
      "floor_plan": {
        "environment_canon": {},
        "anchor_clusters": [],
        "space_groups": []
      },
      "base_images": [
        {
          "base_id": "base_rooftop_room_interior",
          "angle_description": "floor-plan guided interior base",
          "image_path": ".../photo_base_rooftop_room_interior.png",
          "floor_plan_image_path": ".../base_plan_rooftop_room_interior.png",
          "source_plan_id": "rooftop_room_interior",
          "visual_domain": "interior",
          "anchor_cluster_id": "cluster_interior",
          "is_anchor": true,
          "t2i": "...",
          "covers_shots": ["S12_Shot4"]
        }
      ],
      "shot_backgrounds": [
        {
          "scene_index": 12,
          "shot_index": 4,
          "shot_label": "S12_Shot4",
          "ref_type": "exact_floor_plan_background",
          "ref_id": "base_rooftop_room_interior",
          "source_plan_id": "rooftop_room_interior",
          "visual_domain": "interior",
          "image_path": ".../photo_shot_S12_Shot4.png",
          "floor_plan_image_path": ".../shot_plan_S12_Shot4.png",
          "t2i": "...",
          "state_changes": "..."
        }
      ]
    }
  }
}
```

호환성:

- 기존 `load_set_design_bg_map()`은 `shot_backgrounds[].scene_index`, `shot_index`, `image_path`, `ref_type`만으로 기본 동작 가능하다.
- 추가 metadata는 기존 loader가 무시하므로 preview 단계에서는 안전하다.
- 운영 통합 시 exact layout 의미를 살리려면 loader label 또는 prompt_service 분기가 필요하다.

### 7.6 `manifest.json`

```jsonc
{
  "run_id": "run_YYYYMMDD_HHMMSS",
  "mode": "standalone_experiment",
  "args": {},
  "models": {"text": "gpt-5.5", "image": "gpt-image-2"},
  "stats": {
    "space_groups": 0,
    "base_plans": 0,
    "shot_plans": 0,
    "base_photos": 0,
    "shot_photos": 0
  },
  "phase_durations": {"phaseA": 0, "phaseB": 0, "phaseC": 0, "phaseD": 0},
  "validation": {
    "scenario_word_hits": [],
    "unsafe_word_hits": [],
    "missing_files": [],
    "schema_errors": [],
    "reference_integrity_errors": [],
    "canon_coverage": {"missing_in_prompts": []}
  },
  "set_design_adapter": {
    "enabled": true,
    "checkpoint_preview_path": "set_design_adapter_preview.json"
  }
}
```

---

## 8. 시스템 프롬프트 방향

### 8.1 Step 1 - spatial + canon

책임:

- 공간 그룹 분석.
- `environment_canon` 추출.
- `space_groups[].visual_domain` 지정.

강제 규칙:

- 모든 텍스트 필드에서 인명, 지명, 작품명 제거.
- 데이터 의존. 임의 가구/공간 추가 금지.
- 한국어 일반명사는 보조 note/도면 label 수준만.

### 8.2 Step 2 - plan_specs + anchor_clusters

책임:

- 각 `space_group`별 base plan spec 생성.
- domain별 anchor cluster 결정.
- 모든 base plan prompt에 canon 응축 단락 삽입.

강제 규칙:

- `anchor_cluster_id`, `visual_domain`, `is_anchor` 필수.
- 도면 prompt라도 unsafe words 직접 사용 금지.
- 그래픽 라벨은 일반명사만 허용.

### 8.3 Step 3 - shot_specs

책임:

- target shot별 camera/character/addition overlay 생성.
- `base_plan_id` 매핑.

강제 규칙:

- matching base의 `visual_domain`과 shot `visual_domain` 일치.
- 인물 이름은 metadata에만 허용, prompt 본문 금지.
- 폭력/사망/혈흔/도구 직접 단어 금지.

### 8.4 Step 7 - photo_specs

책임:

- plan/photo reference 관계를 명시한 photo specs 생성.
- base photo와 shot photo prompt 작성.

강제 규칙:

- `source_floor_plan_image_path` 필수.
- base photo는 `source_anchor_photo_id` 또는 anchor 자기 자신 명시.
- shot photo는 `matching_base_photo_id` 필수.
- `reference_strategy` 필수.
- 사진 prompt에는 `no people`, `empty space`, `no overlaid text`, `no diagram lines` 포함.

---

## 9. 안전 어휘 / Sanitization

### 9.1 도면 단계 금지/대체

| 의미 | 대체 | 금지 |
| --- | --- | --- |
| 사망 | `motionless figure marker`, `still figure outline` | `dead`, `corpse`, `deceased`, `victim`, `body` |
| 혈흔 | `dark fluid stippled region`, `reddish marker stain` | `blood`, `bloodstain`, `bloody`, `gore` |
| 상처 | `condition note`, `state indicator` | `wound`, `trauma`, `injury`, `torn flesh` |
| 행위 | `altercation marker`, `approach trail` | `kill`, `stab`, `strangle`, `attack`, `assault`, `beat`, `slash` |
| 도구 | `elongated object marker`, `triangular object marker` | `knife`, `blade`, `weapon`, `gun` |

### 9.2 사진 단계 추가 규칙

- 사진은 인물 0명. `no people`, `no characters`, `no figures`.
- 사망/외상은 사진 prompt에서 직접 묘사하지 않는다.
- 바닥 흔적은 `dark dried floor stain` 정도로만 둔다.
- 다음 위험 조합은 한 prompt 안에 동시에 넣지 않는다.
  - `low angle through doorway` + `bedroom` + `floor stain`
  - `doll` + `backpack` + `dim bedroom` + `stain/footprint`
  - `open door` + `dark bedroom interior` + `reddish stain`

### 9.3 Sanitization 기본값

원본 계획의 D6 권장안대로 `--moderation-precheck sanitize`를 기본값으로 한다.

흐름:

1. Step 2/3/7 LLM 응답 직후 `unsafe_word_grep()` 실행.
2. hit가 있으면 같은 system prompt로 재호출하되 금지 단어 목록을 명시.
3. 재호출 후에도 남으면 manifest.validation에 기록하고 해당 항목은 fail 또는 generic 대체.
4. scenario banned words도 동일하게 검증하되, 인명/지명/작품명은 자동 추출 + 수동 override를 병행한다.

---

## 10. 코드 구조 제안

파일:

- 신규: `backend/scripts/experiment_floor_plan_v4.py`
- 보존: `experiment_floor_plan_v3.py`, `experiment_plan_to_photo.py`

함수 구조:

```python
# Context
def collect_context(...) -> dict: ...
def auto_extract_banned_words(ctx) -> list[str]: ...

# Message builders
def msg_step1(ctx) -> str: ...
def msg_step2(ctx, spatial) -> str: ...
def msg_step3(ctx, spatial, plans, shot) -> str: ...
def msg_step7(ctx, spatial, plans, shots, plan_image_paths, base_photo_paths) -> str: ...

# LLM
def call_llm_json(client, model, system, user) -> dict: ...
def call_llm_json_with_sanitize(client, model, system, user, banned_words) -> dict: ...

# Image
def gen_image(client, model, prompt, size, quality, out_path) -> Path: ...
def edit_image(client, model, ref_paths, prompt, size, quality, out_path) -> Path: ...
def _decode_or_raise(resp, op) -> bytes: ...

# Phases
def phase_a(...) -> tuple[dict, dict, list[dict]]: ...
def phase_b(...) -> dict: ...
def phase_c(...) -> dict: ...
def phase_d(...) -> Path: ...

# Validation
def verify_no_scenario_words(text, banned_words, label) -> list[str]: ...
def verify_unsafe_words(text, label) -> list[str]: ...
def verify_schema(obj, required_keys, label) -> list[str]: ...
def verify_reference_integrity(spatial, plans, shots, photos) -> list[str]: ...
def verify_canon_coverage(canon, prompts) -> list[str]: ...

# Adapter / Gallery
def build_set_design_adapter_preview(spatial, plans, shots, photos, paths) -> dict: ...
def write_gallery_html(out_dir, manifest, paths) -> Path: ...
```

주의:

- `edit_image()`는 multi-reference 입력을 받을 수 있게 `ref_paths` list를 받는다.
- multi-reference가 API에서 실패하면 `reference_strategy` fallback에 따라 단일 reference로 재시도한다.
- 이미지 실패는 전체 run을 즉시 중단하지 말고 manifest에 기록 후 다음 항목으로 진행한다.

---

## 11. CLI

```text
--project-id <uuid>              required
--episode-id <uuid>              required
--scene-indices N [N ...]        required
--location-ids L## [L## ...]     required
--target-shots S:S [S:S ...]     required
--text-model gpt-5.5
--image-model gpt-image-2
--image-size-plan 1024x1024
--image-size-photo 1536x1024
--image-quality high
--out-name <run_id>
--scenario-banned-words "w1,w2,..."
--reference-strategy auto|plan_png_only|plan_png+domain_anchor_photo
--anchor-strategy llm|first|biggest
--moderation-precheck warn|sanitize|skip
--skip-photo
--skip-shots
--skip-adapter
--max-image-retries 3
```

기본값:

- `--reference-strategy auto`
- `--anchor-strategy llm`
- `--moderation-precheck sanitize`
- `--max-image-retries 3`

---

## 12. 출력 디렉토리

```text
backend/scripts/output/floor_plan_v4/<run_id>/
  context.json
  step1_spatial.json
  step2_plan_specs.json
  step3_shot_<S##_Shot#>.json
  step7_photo_specs.json
  base_plan_<plan_id>.png
  shot_plan_<S##_Shot#>.png
  photos/
    photo_base_<plan_id>.png
    photo_shot_<S##_Shot#>.png
  set_design_adapter_preview.json
  gallery.html
  manifest.json
```

---

## 13. 검증 계획

### 13.1 자동 검증

필수:

- 모든 JSON schema 검증.
- 모든 image path 존재 검증.
- `base_plan_id`, `source_plan_id`, `visual_domain`, `anchor_cluster_id` 참조 무결성.
- `anchor_clusters[].anchor_plan_id`가 `base_plans[].id` 중 하나인지 검증.
- `is_anchor=true` base가 domain별 정확히 1개인지 검증.
- scenario banned words grep.
- unsafe image words grep.
- photo prompt에 `no people`, `no overlaid text` 존재 검증.
- `shot_backgrounds[]`가 target shot 수와 일치하는지 검증.
- adapter preview의 `base_images[]`, `shot_backgrounds[]` 무결성.

권장:

- canon keyword coverage.
- domain별 anchor 최소 1개.
- visual_domain mismatch 검증.
- generated files size 0 방어.

### 13.2 수동 검증

`gallery.html`에서 확인할 축:

1. base plan ↔ base photo.
2. shot plan ↔ shot photo.
3. 같은 visual_domain 내 base photo grid.
4. v3 baseline ↔ v4 비교.
5. adapter preview sample.

체크리스트:

- 같은 건물 재질, 색, 창문 패턴이 유지되는가.
- 실내 문/창문/가구 배치가 plan과 photo에서 대응되는가.
- shot photo가 base photo 구조를 깨지 않는가.
- 사진에 도면 라벨 텍스트나 선이 남지 않는가.
- moderation block이나 unsafe word hit가 없는가.

---

## 14. Gallery 구성

`gallery.html` 섹션:

1. Header: run_id, models, validation badges.
2. Anchor Clusters: domain별 anchor base plan + anchor base photo.
3. Base Plan ↔ Photo: 모든 base를 좌/우 비교.
4. Shot Plan ↔ Photo: 모든 target shot을 좌/우 비교.
5. Visual Domain Grid: interior/exterior/site_map별 photo 묶음.
6. Adapter Preview Sample: `set_design_adapter_preview.json`의 `shot_backgrounds[]` 일부.
7. JSON Metadata Links: step JSON, manifest, adapter preview 링크.

---

## 15. 운영 통합 Path

운영 통합은 standalone v4 결과가 안정적일 때 별도 승인 후 진행한다.

### 15.1 Feature flag

- 기존 `SET_DESIGN_ENABLED`는 유지.
- 신규 후보: `FLOOR_PLAN_SET_DESIGN_ENABLED`.
- 두 flag를 동시에 켰을 때 우선순위를 명확히 해야 한다.

### 15.2 `prompt_service.resolve_ref_roles()` 분기

현재:

- `background`/`location` label → lighting, architecture, mood 참고.

추가:

- `exact_floor_plan_background` label → exact wall/door/window/furniture layout 유지 지시.

권장 지시:

```text
use image N as the exact background layout reference; preserve wall positions,
door/window placement, furniture layout, surface materials, and lighting unless
the shot prompt explicitly changes them.
```

### 15.3 `scene_generation_coordinator` label

adapter에서 넘어온 `ref_type=exact_floor_plan_background`가 prompt_service까지 전달되어야 한다.

단기:

- label 문자열에 `exact_floor_plan_background` 포함.

장기:

- `labeled_refs`를 `(label, bytes)`에서 typed reference 객체로 확장.

### 15.4 `set_design_step` DB 등록

확장 후보:

- `source_plan_id`
- `visual_domain`
- `floor_plan_image_path`
- `anchor_cluster_id`
- `ref_type`

테이블 컬럼 추가는 피하고, 가능하면 JSON metadata 필드 또는 prompt/variant naming으로 처리한다.

### 15.5 prompt/schema file fallback

운영 재현성을 위해 DB prompt만 의존하지 않도록 fallback 파일을 추가한다.

후보 경로:

```text
backend/prompts/_base/set_design/<version>/system.md
backend/prompts/_base/set_design/<version>/phase1_base.md
backend/prompts/_base/set_design/<version>/phase1_schema.json
backend/prompts/_base/set_design/<version>/phase2_shot_bg.md
backend/prompts/_base/set_design/<version>/phase2_schema.json
```

원본 문서의 `backend/app/prompts/_base/...` 후보와 실제 `prompt_loader.PROMPTS_BASE` 경로가 다르다. 현재 코드상 fallback base는 `backend/prompts/_base`이므로 구현 전 경로를 확정해야 한다.

---

## 16. 비용 / 시간 예상

옥탑방 케이스 가정: B=4 base plans, N=3 target shots, D=3 visual domains.

| Phase | 항목 | 횟수 | 단위 시간 | 누적 |
| --- | --- | --- | --- | --- |
| A | LLM Step 1 | 1 | ~60s | ~1분 |
| A | LLM Step 2 | 1 | ~60s | ~1분 |
| A | LLM Step 3 | 3 | ~30s | ~1.5분 |
| A | sanitize 재호출 | ~1 | ~30s | ~0.5분 |
| B | anchor plan generate | 3 | ~3분 | ~9분 |
| B | non-anchor plan edit | 1 | ~3분 | ~3분 |
| B | shot plan edit | 3 | ~3분 | ~9분 |
| C | LLM Step 7 | 1 | ~60s | ~1분 |
| C | anchor photo generate/edit | 3 | ~3분 | ~9분 |
| C | non-anchor photo edit | 1 | ~3분 | ~3분 |
| C | shot photo edit | 3 | ~3분 | ~9분 |
| D | adapter preview | 1 | ~1s | <1분 |
| Total |  |  |  | ~47분 |

---

## 17. 리스크와 대응

| 리스크 | 영향 | 대응 |
| --- | --- | --- |
| 단일 anchor로 모든 domain 변환 | 시점 변환 실패 | visual_domain별 anchor cluster |
| 도면 PNG 라벨이 사진에 남음 | 사진 품질 저하 | no overlaid text/no diagram lines, 필요 시 label-free variant |
| 도면 JSON만으로 사진 생성 | geometry 손실 | target floor plan PNG reference |
| set_design ref가 mood reference로만 해석 | 최종 scene 구조 흔들림 | exact_floor_plan_background 분기 |
| unsafe words 잔존 | moderation block | sanitize 재호출 |
| prompt/schema DB-only | 재현성 저하 | file fallback 추가 |
| location별 병렬 처리에서 reference 오염 | 다른 공간 섞임 | location/domain scope 제한 |
| multi-reference edit 불안정 | 품질/실패 | reference_strategy fallback |
| OpenAI 500/timeout | run 중단 | max retry + manifest 기록 + continue |
| partial failure | 산출물 누락 | missing_files 기록, v4.1에서 partial resume |

---

## 18. Pre-implementation 확인사항

코드 작성 전 확인:

1. `gpt-image-2` image edit가 `image=[plan_png, anchor_photo]` 형태의 multi-reference를 안정적으로 받는지.
2. 도면 PNG를 photo reference로 넣었을 때 한영 라벨/선이 사진에 번지는지.
3. `prompt_loader.PROMPTS_BASE` 실제 경로 기준으로 set_design prompt/schema fallback 위치를 확정.
4. `exact_floor_plan_background` 의미를 label string으로 전달할지 typed reference로 확장할지.
5. scenario banned words 자동 추출 범위. 인명은 `entity_merge`, 지명/작품명은 별도 수동 override가 필요할 수 있다.

권장:

- 1, 2는 작은 샘플로 먼저 실행한다.
- 4는 단기적으로 `prompt_service` label 분기, 장기적으로 typed reference 객체가 낫다.

---

## 19. Decision 요약

| ID | 항목 | 옵션 | 권장 |
| --- | --- | --- | --- |
| D1 | anchor 방식 | 단일 anchor / visual_domain별 cluster / Responses conversational | visual_domain별 cluster |
| D2 | base photo reference | 텍스트만 / target plan PNG + domain anchor photo / target plan PNG only | target plan PNG + domain anchor photo, fallback target plan PNG only |
| D3 | shot photo reference | base photo only / base photo + shot overlay plan PNG | base photo + shot overlay plan PNG, fallback base photo only |
| D4 | 운영 통합 | 즉시 통합 / adapter preview 먼저 | adapter preview 먼저 |
| D5 | scene ref 의미 | label string hack / prompt_service 분기 / typed ref 객체 | prompt_service 분기 |
| D6 | safety 검증 | skip / warn / sanitize 재호출 | sanitize 재호출 |
| D7 | prompt/schema | DB only / file fallback | file fallback |
| D8 | scenario banned list | 자동 / 수동 / 자동 + 수동 override | 자동 + 수동 override |
| D9 | gallery v3 비교 | 별도 폴더 + 수동 / gallery 통합 | 별도 폴더 + 수동 |
| D10 | partial resume | 지금 추가 / v4.1로 미룸 | v4.1로 미룸 |

---

## 20. 작업 순서

### Phase 0 - 계약 확정

- D1~D10 사용자 선택 확정.
- multi-reference edit 샘플 테스트.
- 도면 PNG 라벨 번짐 샘플 테스트.
- prompt/schema fallback 경로 확정.
- 본 문서와 원본 계획서 lock.

### Phase 1 - Standalone v4 도면 실험

- `experiment_floor_plan_v4.py` 작성.
- Phase A/B 구현.
- 기존 v3와 같은 입력으로 실행.
- base plan과 shot plan의 일관성 확인.

### Phase 2 - 도면 기반 Photo Phase

- Step 7/Phase C 구현.
- target plan PNG + domain anchor photo reference 사용.
- shot photo는 matching base photo + shot overlay plan PNG reference 사용.
- gallery 4축 완성.

### Phase 3 - Adapter Preview

- `set_design_adapter_preview.json` 생성.
- `load_set_design_bg_map()` 형식으로 읽을 수 있는지 dry-run 검증.
- 운영 checkpoint/DB는 수정하지 않는다.

### Phase 4 - 운영 통합

- 별도 승인 후 진행.
- `FLOOR_PLAN_SET_DESIGN_ENABLED` flag 추가.
- `prompt_service.resolve_ref_roles()` 분기 추가.
- coordinator/loader label 전달 정리.
- `set_design_step` DB 등록 metadata 확장.
- prompt/schema fallback 추가.
- 기존 set_design 회귀 테스트.

### Phase 5 - 리뷰와 일반화

- 듀얼 코드 리뷰.
- 옥탑방 케이스 final run.
- 다른 location group으로 일반화 테스트.
- 실패 항목은 v4.1 partial resume/sanitizer 개선으로 분리.

---

## 21. 산출 위치

- 원본 계획: `backend/scripts/experiment_floor_plan_v4_plan.md`
- 본 보완 계획: `backend/scripts/experiment_floor_plan_v4_background_generation_plan.md`
- 예정 실험 코드: `backend/scripts/experiment_floor_plan_v4.py`
- 예정 출력: `backend/scripts/output/floor_plan_v4/<run_id>/`

