# Phase 2 — chain_bg LLM이 샷 가이드 함께 출력 (single-call)

작성일: 2026-04-29
작성자: brainstorming 세션 (Claude + 사용자)
배경 plan: `next_session_floor_plan_architecture_implementation.md` (v3 plan, Phase 2)
Codex 리뷰: `session_20260428_codex_review_floor_plan.md`

## 1. 목표

`scene_detail`이 만드는 shot t2i 프롬프트에서 발생하는 두 종류 사고를 줄인다.

1. **중복**: chain_bg에 이미 그려진 가구(TV, 소파, 창문)를 t2i가 또 그려서 결과 이미지에 두 번 보이는 사고.
2. **공간 이해 부족**: t2i가 chain_bg 안 가구 위치를 모르고 인물을 가구 위에 겹치게 배치하는 사고.

해결: shot t2i 프롬프트 작성 시 "chain_bg에 이미 무엇이 어디에 있는지" 자연어 가이드를 prepend.

## 2. 핵심 결정 (사용자 명시)

| 결정 | 근거 |
|---|---|
| vision LLM 사용 X | 사후 vision 추출은 물체 분류 신뢰성 낮음 |
| 좌표 9-zone 결정 후 gpt-image-2에 명령 X | gpt-image-2 명령 신뢰성 보장 안 됨 |
| 이미지 내 영어 약자(T1, S1) 라벨 인쇄 X | production scene 이미지로 라벨 새어나갈 위험 |
| **`background_chain_render` LLM이 같은 호출에서 샷별 가이드도 함께 output** | 추가 LLM 호출 0, 같은 컨텍스트에서 일관성 보장 |
| guide는 **단순 자연어 한 단락** | 좌표·DSL·JSON 구조화 모두 위 결정과 충돌 |
| toggle default off | 회귀 0건 |

## 3. 흐름

```
background_chain_render (변경 — 1 호출에서 t2i_prompt + shot_guides 함께 출력)
  │
  ├─ Phase 1 LLM (gpt-5.5)
  │     input: 노드 + parent + shots_in_node (각 샷 description 포함)
  │     output:
  │       {
  │         "t2i_prompt": "<PNG 생성용, 기존 그대로>",
  │         "shot_guides": [
  │           {"shot_id": "S5_Shot1",
  │            "guide": "TV occupies upper-left of frame, sofa-table cluster
  │                      in center, window on right with daylight.
  │                      Position character standing in front of sofa,
  │                      gaze toward TV. DO NOT redraw the TV/sofa/window —
  │                      they will appear in the chain_bg reference."},
  │           {"shot_id": "S5_Shot3", "guide": "..."}
  │         ]
  │       }
  │
  └─ Phase 2 image (변경 X) → gpt-image-2가 t2i_prompt로 PNG 생성

manifest 저장 (변경)
  data.locations[loc_id].nodes[].t2i_prompt    (기존)
  data.locations[loc_id].nodes[].shot_guides   (신규, list of {shot_id, guide})

scene_detail (Phase 2 통합 지점)
  ├─ scene_context_loader._load_chain_bg_guide_by_shot()
  │     chain_bg_render manifest 파싱
  │     모든 노드의 shot_guides를 펼쳐 (scene_index, shot_index) → guide str 매핑
  │
  └─ _analyze_one에서 user_prompt에 prepend (toggle 분기)
      if chain_bg_guide_enabled and (si, shi) in ctx.chain_bg_guide_by_shot:
        user_prompt += "[chain_bg reference에 이미 있음 — 다시 그리지 말 것]\n"
        user_prompt += f"{guide}\n\n"
```

## 4. 컴포넌트 책임

| 컴포넌트 | 책임 | 입력 | 출력 |
|---|---|---|---|
| `background_chain_render` (gpt-5.5 LLM) | 노드 PNG 생성 prompt + 노드 안 각 샷에 줄 spatial guide | 노드 + parent + shots_in_node | `t2i_prompt` + `shot_guides[]` |
| `background_chain_render` (gpt-image-2) | 변경 X | t2i_prompt + ref | PNG |
| `SceneContextLoader` | chain_bg_render manifest → shot_id → guide 매핑 | runner 인스턴스 | `chain_bg_guide_by_shot: Dict[(si,shi), str]` |
| `SceneDetailStep._analyze_one` | toggle on이면 guide를 user_prompt 맨 앞 prepend | ctx + shot_info | user_prompt |
| `chain_bg_guide_enabled` toggle | scene_detail 단에서 prepend 여부 제어 | env/settings | bool |

## 5. 데이터 모델

### 5.1 `prompts/_base/background_chain_render/<v+1>/schema.json` (변경)

기존:
```json
{
  "type": "object",
  "properties": {
    "t2i_prompt": {"type": "string"}
  },
  "required": ["t2i_prompt"]
}
```

신규:
```json
{
  "type": "object",
  "properties": {
    "t2i_prompt": {"type": "string"},
    "shot_guides": {
      "type": "array",
      "items": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "shot_id": {
            "type": "string",
            "description": "S{scene}_Shot{idx} 형식. 본 노드의 shot_ids 중 하나여야 함"
          },
          "guide": {
            "type": "string",
            "description": "이 chain_bg를 reference로 받을 shot t2i 작성자에게 줄 자연어 한 단락 (영어). 가구의 상대 위치 + 다시 그리지 말 것 명령 포함. 100~250단어 권장."
          }
        },
        "required": ["shot_id", "guide"]
      }
    }
  },
  "required": ["t2i_prompt", "shot_guides"]
}
```

### 5.2 `prompts/_base/background_chain_render/<v+1>/system.md` (변경)

기존 system 프롬프트에 다음 섹션 추가:

```markdown
## 출력에 shot_guides도 함께 포함

이 노드 안 각 샷(`shots_in_node`)에 대해, 본 chain_bg가 reference로 주어졌을 때
shot t2i 작성자가 알아야 할 자연어 가이드를 작성한다.

가이드 작성 원칙:
1. **가구 상대 위치 명시** — "TV is in the upper-left of the frame", "sofa cluster in
   the center", "window on the right side". 좌표/픽셀 X. 자연어로 frame 상대 위치만.
2. **DO NOT redraw 명령 포함** — "DO NOT redraw the TV/sofa/window — they will appear
   in the chain_bg reference". 영어 명령형으로 명시.
3. **샷 description 기반으로 인물/소품 배치 힌트** — "Position the character standing
   in front of the sofa, gazing toward TV". 인물 자세는 essence가 결정하므로 여기서는
   chain_bg 가구와의 spatial 관계만.
4. **영어로** — t2i prompt에 직접 prepend되므로 영어.
5. **100~250단어** — 짧고 명확. 카탈로그식 묘사 금지.
6. **shot_ids 1:1 대응** — 노드의 모든 shot_ids에 대해 guide 1개씩. 누락/추가 금지.
```

### 5.3 manifest 변경

`data.locations[loc_id].nodes[]`에 `shot_guides` 필드 추가:

```json
{
  "id": "anchor_root",
  "kind": "anchor_root",
  "label": "...",
  "description": "...",
  "shot_ids": ["S5_Shot1", "S5_Shot3"],
  "t2i_prompt": "...",          // 기존
  "image_path": "...",
  "render_status": "ok",
  "shot_guides": [               // 신규
    {"shot_id": "S5_Shot1", "guide": "..."},
    {"shot_id": "S5_Shot3", "guide": "..."}
  ]
}
```

### 5.4 `SceneAnalysisContext` (변경)

```python
# Phase 2 (chain_bg_guide consumer)
chain_bg_guide_by_shot: Dict[Tuple[int, int], str] = field(default_factory=dict)

# Phase 1b consumer 통합 (지금까지 ctx field 없었음)
# shot_essence_extraction 체크포인트의 data.shots[]에서 essence list 추출
# (shot_index 기준 매핑)
essence_by_shot: Dict[Tuple[int, int], List[str]] = field(default_factory=dict)
```

## 6. 변경 파일

| 파일 | 변경 종류 | 비고 |
|---|---|---|
| `prompts/_base/background_chain_render/2.<YYYYMMDDHHMM>/system.md` | 신규 (현 v1 다음 버전) | shot_guides 작성 지시 추가 |
| `prompts/_base/background_chain_render/2.<YYYYMMDDHHMM>/schema.json` | 신규 | shot_guides field 추가 |
| `backend/app/modules/pipeline/background_chain_render.py` | 수정 | `generate_node_prompt` 반환을 dict로(`{t2i_prompt, shot_guides}`); `render_one_location`이 enriched_nodes에 shot_guides 저장 |
| `backend/app/core/config.py` | 수정 | `chain_bg_guide_enabled: bool = False` (env: `CHAIN_BG_GUIDE_ENABLED`) |
| `backend/app/core/dto/scene_analysis.py` | 수정 | `chain_bg_guide_by_shot` + `essence_by_shot` (Phase 1b consumer 통합) field 추가 |
| `backend/app/core/steps/scene_context_loader.py` | 수정 | `_load_chain_bg_guide_by_shot()` + `_load_essence_by_shot()` (Phase 1b consumer 통합) 신설; `load_all`에서 호출 |
| `backend/app/core/steps/detail_steps.py` | 수정 | `_analyze_one`에 toggle 분기 prepend 2개 (essence + chain_bg_guide) |
| `backend/tests/core/test_chain_bg_guide.py` | 신규 | toggle on/off + 로드 + prepend + legacy fallback (chain_bg_guide + essence consumer 통합 둘 다) |

**prompts schema 신규 1개 (chain_bg_render v2). planning 변경 X. step 신설 X.**

**버전 형식**: 본 프로젝트의 prompt 버전 규칙은 `<버전번호>.<YYYYMMDDHHMM>`. chain_bg_render는 현재 `1.202604271700`이므로 신규는 `2.<작업시 시각>`. 작업 시 시간 결정.

## 7. Phase 1b essence 통합

Phase 1b에서 만든 `shot_essence_extraction` step은 체크포인트만 생산했고 `scene_detail` consumer는 미통합 상태. Phase 2에서 함께 통합.

`_analyze_one` user_prompt 시작 직후 (line 330 부근) 두 prepend 블록을 추가:

```python
# Phase 1b — shot_essence_extraction consumer
if settings.shot_essence_enabled and ctx.essence_by_shot.get((si, shi)):
    essence = ctx.essence_by_shot[(si, shi)]
    user_prompt += "[샷 핵심 시각 요소 — 반드시 t2i_prompt에 포함]\n"
    user_prompt += "\n".join(f"- {e}" for e in essence) + "\n\n"

# Phase 2 — chain_bg shot guide consumer
if settings.chain_bg_guide_enabled and ctx.chain_bg_guide_by_shot.get((si, shi)):
    guide = ctx.chain_bg_guide_by_shot[(si, shi)]
    user_prompt += "[chain_bg reference에 이미 있음 — 다시 그리지 말 것]\n"
    user_prompt += f"{guide}\n\n"

# 기존 user_prompt 시작 (씬 정보)
user_prompt += "..."
```

토글은 **독립적**. essence는 chain_bg 없이도 작동, chain_bg_guide는 essence 없이도 작동.

## 8. 토글 매트릭스 (회귀 보장)

| `chain_bg_guide_enabled` | `shot_essence_enabled` | 효과 |
|---|---|---|
| F (default) | F (default) | **기존 동작 그대로 — 회귀 0** |
| F | T | essence prepend만 |
| T | F | chain_bg guide prepend만 |
| T | T | 둘 다 prepend (Phase 1b consumer 완성) |

## 9. 회귀 보장 메커니즘 (3중)

1. **settings default off** — `chain_bg_guide_enabled = False`
2. **legacy 체크포인트 호환** — `shot_guides` 필드 누락 manifest는 빈 dict fallback (warning 로그). Phase 1b의 패턴 그대로 재사용.
3. **chain_bg_render LLM은 항상 shot_guides 출력** (prompt 새 버전 자동 선택). 출력 토큰 ~$0.01/EP 추가, **PNG 생성 결과는 0 변화** — chain_bg 자체 회귀 0.

## 10. Edge Cases

| 케이스 | 동작 |
|---|---|
| `chain_bg_render` 미실행 | `chain_bg_guide_by_shot` 빈 dict, scene_detail prepend 0 |
| 노드의 shot_guides에 누락된 shot_id 있음 | 누락된 shot은 prepend 없음 (warning 로그). 다른 shot은 정상. |
| 노드의 shot_guides에 input shot_ids에 없는 extra shot_id 있음 | extra는 무시 (warning 로그) |
| 노드의 shot_guides에 중복 shot_id | 첫 번째만 사용, 나머지 무시 (warning 로그) — Phase 1b의 `_merge_with_input_keys` 패턴 |
| guide가 빈 문자열 | prepend 0, warning 로그 |
| chain_bg_render manifest legacy (shot_guides 필드 없음) | 빈 dict fallback, scene_detail 정상 동작 |
| `chain_bg_guide_enabled=True` but chain_bg_render 미실행 | warning 로그, prepend 0 |
| location이 `skip_chain=true` (outdoor) | 노드 0개, shot_guides 0개. prepend 0 (정상) |
| `force` 모드로 chain_bg_render 재실행 | shot_guides 자동 갱신. 의존 step (scene_detail)은 기존 dependency invalidation 정책 따름 |

## 11. 비용 / 작업량

- **추가 LLM 호출**: 0 (chain_bg_render 1회 호출에서 output 토큰만 ~$0.01/EP 추가)
- **추가 자산**: 0 (이미지 생성 변경 없음)
- **작업량**: 0.5~1일

## 12. 검증 / 테스트

### 12.1 단위 테스트 (`backend/tests/core/test_chain_bg_guide.py`)

**chain_bg_guide 부분**:
1. `test_settings_toggle_default_false` — `settings.chain_bg_guide_enabled is False`
2. `test_loader_returns_empty_when_no_chain_bg_render_checkpoint` — manifest 없으면 빈 dict
3. `test_loader_parses_shot_guides_from_manifest` — manifest 파싱 → (si, shi) → guide
4. `test_loader_handles_legacy_checkpoint_without_shot_guides` — 필드 누락 → 빈 dict + warning
5. `test_loader_handles_extra_shot_ids` — 입력 shot_ids에 없는 extra 무시 + warning
6. `test_loader_handles_duplicate_shot_ids` — 중복 시 첫 번째만 (Phase 1b 패턴)
7. `test_analyze_one_prepends_chain_bg_guide_when_toggle_on` — toggle on + guide present → user_prompt에 prepend
8. `test_analyze_one_skips_chain_bg_guide_when_toggle_off` — toggle off → prepend 0 (회귀 보장)
9. `test_analyze_one_skips_chain_bg_guide_when_no_guide_for_shot` — guide 없는 shot → prepend 0

**essence consumer 부분 (Phase 1b 통합)**:
10. `test_loader_parses_essence_by_shot_from_manifest` — shot_essence_extraction 체크포인트 → (si, shi) → essence list
11. `test_loader_returns_empty_when_no_shot_essence_checkpoint` — Phase 1b step 미실행 시 빈 dict
12. `test_analyze_one_prepends_essence_when_toggle_on` — `shot_essence_enabled` on + essence present → prepend
13. `test_analyze_one_skips_essence_when_toggle_off` — toggle off → prepend 0
14. `test_analyze_one_prepends_both_essence_and_guide_when_both_on` — 둘 다 on → 두 블록 prepend (essence 먼저, guide 뒤)
15. `test_analyze_one_handles_failed_status_essence` — Phase 1b의 `status="failed"` shot은 essence list 빈 채로 fallback (prepend 0)

### 12.2 회귀 baseline

기존 manifest 베이스라인 테스트 영향 없음 (step 신설 X). Phase 1b의 51 단계 baseline 유지.

### 12.3 integration smoke

PID `c00bbe19` (현 baseline)에서:
1. toggle off → scene_detail 결과 baseline 동일 (해시 비교)
2. toggle on + chain_bg_render 재실행 → manifest에 shot_guides 채워짐
3. toggle on + scene_detail 재실행 → user_prompt에 prepend 확인 (DEBUG 로그 또는 sampled 출력)

## 13. 듀얼 코드 리뷰 (Codex + Claude)

구현 완료 후 commit 전 듀얼 리뷰 필수 (CLAUDE.md 절대 규칙):
- Codex 리뷰
- Claude 리뷰
- 둘 다의 High 이상 수정 후 commit

## 14. v3 plan과의 정합성

v3 plan (`next_session_floor_plan_architecture_implementation.md`) Phase 2는 원래:
- chain_bg → guide.txt **별도 파일** + vision LLM 호출

본 design은 그것과 다음과 같이 다름:
- 별도 파일 X → `chain_bg_render` manifest 안에 인라인 (Codex L1 권장)
- vision LLM 호출 X → 같은 chain_bg_render LLM이 single-call에서 함께 출력 (사용자 결정: vision 신뢰성 우려)

본 design은 **기존 v3 plan을 단순화**한 것으로, Phase 2의 본질(샷 t2i에 chain_bg 정보 prepend)은 동일하나 구현 비용/복잡도가 ~1/3로 축소됨.

## 15. 다음 작업

1. spec 자체 검토 (placeholder/일관성/범위/모호성)
2. 사용자 검토 게이트
3. 승인되면 `writing-plans` skill 호출하여 구현 plan 작성
4. plan 작성 후 코드 변경 시작

