# Phase 9.1 — chain_bg ↔ shot 카메라 일관성 (B+)

## 목적

scene_image_pipeline 결과물 13건 결함 중 7건 (Cat 1+2+4+5)이 다음 root cause에서 파생:

- **chain_bg PNG**의 카메라 (광각 35mm, wide perspective)와
- **shot t2i**의 카메라 (close/ECU/MS 등)가
- **합성 시점에 처음 마주쳐** scale/perspective 충돌 발생

→ 사용자 결과: "사람이 가짜 배경 위에 떠있다", "크기 안 맞음", "얼굴만 공중", 5건.

## 해결 (B+ 옵션)

흐름 변경 0. inject + prompt 룰 + 분기만으로 해결.

### 변경 요약

```
[T2 floor_plan_prompt] camera_recommendations[] 출력 (이미 존재, bg_id별)
   ↓
[T2 background_render] cp에 camera_recommendations 보존 (shot_id별 매핑 가능하게)
   ↓
[T3 scene_context_loader] chain_bg_camera_meta_by_shot loader 신설
   ↓
[T3 scene_detail] user_prompt에 chain_bg.camera_meta 텍스트 block prepend
                  + scene_detail prompt v10에 일치 룰 + 메타포 어휘 룰
   ↓
[T4 scene_image_pipeline] shot framing이 close/ECU일 때 chain_bg ref skip
```

## Task 분해

### Task 1: background_render cp에 camera_recommendations 보존
**파일**: `backend/app/core/steps/background_render_step.py`

floor_plan_prompt cp에서 fp_id별 camera_recommendations[]을 읽어, 각 group에 해당 bg_id의 cr을 함께 저장.

```python
# data.groups[bg_id]에 추가:
{
  "status": "ok",
  ...,
  "shot_guides": [...],
  "camera_recommendations": {  # 신규
    "camera_position": "남동쪽 출입구 근처 1.6m",
    "camera_height": "eye-level standing ~1.6m",
    "lens_hint": "35mm wide angle",
    "framing_notes": "전경 출입문, 중경 식탁 ..."
  }
}
```

### Task 2: dto + scene_context_loader
**파일**:
- `backend/app/core/dto/scene_analysis.py` — 필드 추가:
  ```python
  chain_bg_camera_meta_by_shot: Dict[Tuple[int, int], Dict[str, str]] = field(default_factory=dict)
  ```
- `backend/app/core/steps/scene_context_loader.py` — 신규 helper:
  ```python
  def _load_chain_bg_camera_meta_by_shot(self) -> Dict[Tuple[int, int], Dict[str, str]]:
      """background_render cp의 groups[bg_id].camera_recommendations를
      (scene_index, shot_index) → meta dict로 펼침. shot_guides loader와
      동일한 매핑 로직 재사용."""
  ```
- `load_all()`에서 호출 → ctx에 채움.

### Task 3: detail_steps user_prompt에 camera_meta block
**파일**: `backend/app/core/steps/detail_steps.py`

`_build_phase2_prepend_blocks()`에 chain_bg_camera_meta block 추가:

```python
if chain_bg_camera_meta_enabled:  # 새 토글
    meta = chain_bg_camera_meta_by_shot.get((si, shi)) or {}
    if meta:
        blocks.append(
            "[chain_bg reference 카메라 정보 — 일치시키거나 명시적 변경 명시]\n"
            f"position: {meta.get('camera_position', '')}\n"
            f"height: {meta.get('camera_height', '')}\n"
            f"lens: {meta.get('lens_hint', '')}\n"
            f"framing notes: {meta.get('framing_notes', '')}"
        )
```

`SceneDetailStep._execute()`에서 settings.chain_bg_camera_meta_enabled (default True) 전달.

### Task 4: scene_detail prompt v10 작성

**파일**: `prompts/_base/scene_detail/10.202604301430/system.md`

기존 9.202604201700 위에 4개 룰 추가:

#### 룰 A — 카메라 일관성
```
RULE: chain_bg reference 카메라 정보가 제공되면, t2i_prompt의 카메라 위치/렌즈/시선 방향은 reference와 일치하거나 명시적 deviation을 지시한다.
- 일치 예: "From the same southeast doorway position as the reference, ~"
- ECU 예: "Push in to extreme close-up of {object} within the same room scale; subject's anatomy fully connected to body in frame."
```

#### 룰 B — 조명·색조가 신체 변형하지 않게
```
RULE: lighting/color 효과는 신체 일부에 직접 적용 금지. 반드시 표면(피부 일부, 옷, 벽)에만 한정.
- BAD: "red light cuts across her eyes"
- BAD: "crimson spill cuts across her features"
- GOOD: "red light reflects on her cheek and jacket"
- GOOD: "cool red ambient washes the side of her face"
```

#### 룰 C — chain_bg에 이미 있는 객체 다시 그리지 말 것
```
RULE: chain_bg reference에 이미 있는 환경 객체(문, 창, 가구)는 t2i_prompt에서 다시 새로 그리도록 묘사하지 말 것. 그 객체에 카메라가 close-up할 때는 "use the {object} from the reference image; do not generate a new one"으로 명시.
```

#### 룰 D — 큰 prop은 본체 fully visible
```
RULE: vehicle, bicycle, boat 등 큰 prop의 일부(handlebar, wheel)만 묘사하면 본체가 누락되어 부유체로 합성됨. 인물이 그 prop을 잡거나 타고 있으면 본체(frame, body)도 명시할 것.
- BAD: "hands locked around handlebar"
- GOOD: "hands locked around handlebar of the bicycle, the bicycle frame and front wheel visible below"
```

#### `detail_steps.py` PROMPT_VERSION bump → "10.202604301430" 같은 hash invalidate.

### Task 5: scene_generation_coordinator framing-aware 분기
**파일**: `backend/app/services/scene_generation_coordinator.py`

`background_chain_bg_map.get(_bc_key)` 분기 직후, shot framing 확인:

```python
# shot_staging cp의 camera_direction에서 framing 키워드 추출
import re
_cam_dir = (staging.get("camera_direction") or "").upper()
# ECU=Extreme Close-Up, CU=Close-Up, MCU=Medium Close-Up
_is_close = bool(re.search(r"\b(ECU|EXTREME CLOSE|CLOSE-?UP|CU\b|MCU)\b", _cam_dir))

if _bc_bg and _bc_bg.get("image_bytes") and not _is_close:
    labeled_refs.insert(0, (_bc_bg["label"], _bc_bg["image_bytes"]))
    logger.info("Scene %d Shot %d: background_chain ref injected (framing=non-close)", ...)
elif _is_close:
    logger.info("Scene %d Shot %d: chain_bg ref SKIPPED (close framing — avoid wide-bg/close-subject scale conflict)", ...)
    # entity ref만 사용 — fallback to prev_shot_ref logic 적용
```

### Task 6: 회귀 테스트 + force 재실행
- pytest 회귀 (변경 영역 + 기존 background_chain 단위 테스트)
- scene_detail force → shot_dependency_t2i → scene_image_pipeline force
- 결과 viewer 재생성

### Task 7: 듀얼 리뷰 (Codex + Claude)
- BLOCKING/IMPORTANT 모두 fix → push

## 비용/시간

- scene_detail force: ~$3, 10-20분
- scene_image_pipeline force: ~$10-15, 20-40분
- 총 ~$13-18, 30-60분

## 결함 해결 예상

| Cat | shot | 해결 메커니즘 |
|---|---|---|
| 1 | S5_Shot13, S12_Shot8, S20_Shot5, S27_Shot2, S28_Shot4 | 카메라 일치 + close framing chain_bg skip |
| 2 | S12_Shot16, S13_Shot6 | 룰 B (조명/색조 신체 변형 금지) |
| 4 | S10_Shot6 | 룰 C (chain_bg 객체 중복 금지) |
| 5 | S24_Shot2 | 룰 D (vehicle 본체 명시) |

10건 해결 예상. Cat 3 (S6, S20 motion freeze) — shot_validator prompt 별도 (이번 phase 9.1에서 제외, 시간 들면 9.2로).
Cat 6/7 (S23 entity ref, S29 outfit) — trace 후 별도.
