# W21B-W8 — outdoor_site_layout production brief (2026-06-12)

> Codex W21B_W8_SITE_LAYOUT_DESIGN_REVIEW = **APPROVED_WITH_NARROW_SCOPE** 합의 설계의 구현 brief.
> 선행 실측: site-layout spike + ablation + 모델 비교 (갤러리 8896/geom_site_layout/spike_gallery.html,
> 세션 메모리 21~24절). 사용자 확정: **production 이미지 모델은 gemini flash 유지** — 모델 교체 없음.

## 0. 문제와 실측 근거

- B-run 피드백: S15 sh5/sh7 "가는 방향/멀어짐 구분 못함" — 멀어지는 인물(수리영)이
  머무는 인물(혜수)보다 크게 렌더.
- ablation 으로 원인 분리 확정 (3차 재확인):
  1. **텍스트 위치 구절이 지배 변수** — scene_detail 이 staging 의 "tracking from
     behind + foreground" 연출을 받아써 "middle-center foreground" 류 구절을 생성
     → 모델이 멀어지는 인물을 전경 크기로 렌더.
  2. **top-down layout 이미지는 이미지 ref 채널로는 기하 명세 효과 없음**
     (06-04 clay 교훈과 동계열) — 이미지 ref 부착은 production 금지.
  3. 위치 구절을 layout 좌표에 위임한 v2 작문이면 production gemini flash 로 충분.
- 따라서 W8 = **좌표는 텍스트 생성의 입력** (FP→VLM→T2I 패턴과 동일): site layout
  좌표 emit → 코드가 카메라-인물 거리/방향 deterministic 요약 → 위치 구절만 최소
  재작문 → image-phase prompt override 로 소비.

## 1. 신규 step: `outdoor_site_layout` (order 21.76)

- 위치: t2i_review(21.72)·zoom_continuity_anchor(21.73) 이후, image phase(22) 이전.
  scene_detail t2i 의 최종본(t2i_review 반영)을 입력으로 보장.
- opt-in: `settings.outdoor_site_layout_enabled` (default **False**) +
  `outdoor_site_layout_group_cap` (default 8). OFF 시 not_applicable — default 경로 영향 0.
- depends_on: scene_detail, t2i_review(ordering), scene_save, shot_validator,
  shot_selection, shot_staging, shot_director(VE), background_classify(is_indoor),
  shot_dependency_t2i(zoom 멤버 제외 신호 — soft read).

### 1.1 seed (전부 deterministic 조인 — 단어/regex 의미 판별 0)

selected shot 중:

- **outdoor**: shot VE(shot_director visible_entity_ids)의 L## 이
  background_classify members 의 `is_indoor == false` (structured) 에 속함.
- **2인+**: shot_staging `character_angles` 항목 수 ≥ 2 (structured).
- **제외 + diagnostic**:
  - zoom continuity 멤버(shot_dependency_t2i `ref_usage == "zoom_in_detail"` pair 의
    zoom/source 양쪽) → `zoom_continuity_member` (이중 재작문 방지 — merge 우선순위가
    2차 방어).
  - 1인 + 이동(frame_spatial_contract constraint: `target_kind == "character"` AND
    `gesture_action == "moves_toward"` — structured enum, 이동 동사 regex 금지)
    → `single_figure_moving` (v1 범위 밖임을 기록).
  - 같은 shot 에 outdoor L## 2개 이상 → 정렬 첫 번째 채택 + `multiple_outdoor_locations`.
- **그룹핑**: outdoor loc_id 별 1 그룹(= LLM1 콜 1) — cap 8, 초과는
  `cap` skipped 진단 (silent drop 금지).

### 1.2 LLM1 — location 별 sparse site layout 좌표

- 입력: 멤버 샷이 속한 **씬 원문 전체**(자르지 않음) + 멤버 샷 description/staging 전체.
- 출력(generic schema — 작품/시나리오 토큰 0): 정규화 0–100 좌표.
  - `landmarks[]`: {id, label, kind(point|line|area), points[[x,y]…]} — 원문에 근거한
    큰 공간 anchor 만, 소품/장식 금지.
  - `figures[]`: {figure_id, label, entity_token(C## | null),
    positions[{shot_index, pos, moving_toward(이동 시) | null}]}.
  - `cameras[]`: {shot_index, pos, look_at}.
- **validator 는 shape/범위만** (좌표 수치 0–100, 구조) — 의미 게이트 없음, 의미는
  visual canary (Codex ⓔ).

### 1.3 코드 — 카메라-인물 deterministic 공간 요약

좌표에서 shot 별로 순수 계산 (LLM 0):

- 인물별 카메라 거리 + 최근접 대비 거리비 → 상대 겉보기 크기 (k× 거리 ≈ 1/k 높이).
- 이동 방향: `moving_toward` 가 카메라 기준 멀어지는지/다가오는지 (거리 비교).
- 프레임 좌/우: 카메라 시선축 기준 외적 부호.
- 산출: shot 별 영어 요약 텍스트 (LLM2 입력 + manifest 기록).

### 1.4 LLM2 — 위치 구절 최소 재작문 (Codex ⓑ 계약)

- 대상: 멤버 shot 의 scene_detail `t2i_variations` 전체 (variation index 보존).
- **위치/깊이/상대크기/카메라거리 구절만** 보정 — 카메라 위치·프레이밍·무드·환경·
  인물 묘사 등 나머지는 AS-IS.
- entity ID 불변. 원본에 없는 인물 추가 금지(ID/이름/평문 전부 — W-C1 S29 규칙 승계).
- **카메라워크 언어 금지** (tracking/dolly/pan 등) → 단일 스틸 구도 표현만.
- 출력: variation 별 `revised_prompt` + `edited_spans[{original, revised}]` +
  무수정 시 `no_edit_reason`.
- **deterministic audit (기존 재사용 + 보강)**: `revised_prompt_new_tokens`(신규 ID
  토큰 도입) + 기존 ID 토큰 누락 — 위반 variation 은 revised 폐기 + diagnostic (비차단).

### 1.5 산출 manifest

```
data = {
  schema_version: 1,
  groups: [{location_id, label, member_shots, layout, spatial_summaries{"si:shi"}}],
  prompt_overrides: {"si:shi": {location_id, revised{vi}, provenance{vi: hash쌍},
                                 edited_spans{vi}, no_edit_reason{vi}}},
  diagnostics: {seeds/skipped/audit…},
  review_html,
}
```

- scene_detail cp **불변** — image-phase prompt override 로만 소비 (zoom W-C2 와 동일
  계약: 실사용 프롬프트는 prompt_used, hash provenance 는 cp).
- **layout 이미지(top-down)는 review 갤러리 전용** — scene 생성 ref 로 부착 **금지**
  (ablation 실측: 이미지 ref 채널은 무력 + 오염 위험. Codex ⓓ 문서화 의무).
  review_html 에 inline SVG 로 렌더 (산출물 의존성 0).

## 2. 소비 — loader + 명시적 priority merge

- `load_outdoor_site_layout_context(project_id, episode_id)` (step 모듈 module-level,
  zoom 의 `load_zoom_continuity_context` 와 동형): flag OFF/cp 부재·미완료/override 0
  → `{}` (byte-identical no-op).
- **명시적 priority merge: custom > zoom > site > original** —
  `merge_prompt_overrides(zoom_overrides, site_overrides)` 순수 함수 (plan 모듈):
  - custom_prompt 는 기존 별도 분기가 선행 승리 (변경 없음).
  - (shot, variation_index) 단위로 zoom revised 가 site revised 에 우선.
  - entry 에 `prompt_source`/`prompt_source_by_index` stamp — override 적용 로그가
    출처를 정확히 기록.
  - 둘 다 빈 맵이면 빈 맵 → 소비자 전원 no-op.
- 배선 지점: scene_image_service 배치 entry + 단건 경로 (zoom_ctx 의 source_overrides
  를 merge 본으로 교체 — coordinator 의 기존 override 적용 코드는 shape 호환이라 무변경,
  로그만 prompt_source 동적화).

## 3. canary (S15 sh5/sh7, production 단건 — Codex ⓕ)

acceptance 4항 전부:
1. sh5: 수리영 < 혜수 (상대 크기) + 도로 깊이 방향으로 멀어짐이 읽힘.
2. sh7: 거리감 개선 (수풀 시점에서 두 인물 거리 관계).
3. 두 샷이 같은 장소로 읽힘 (도로/정류장/바다 방향 일관).
4. identity/outfit/action 회귀 0.

## 4. 제약 (사용자 명시 + Codex narrow scope)

- 시나리오 의존 코드/프롬프트 절대 금지 (spike 의 road/sea/bus_stop schema 는
  scenario-specific — production 은 generic landmarks 로 대체).
- production 이미지 모델 = gemini flash 유지 (사용자 확정. 모델 교체/혼용 없음).
- 구현 범위 = 새 step + loader + merge + minimal tests 만. spike 자산은 /tmp 유지.
- 테스트는 핵심 계약만 — LLM/이미지 품질은 canary + 육안 gate (TDD 비대상).

## 5. 구현 + canary 기록 (2026-06-12)

### 구현 파일
- 신규: `app/modules/pipeline/outdoor_site_layout_plan.py`(seed/validator/공간요약/
  audit/merge — 순수 deterministic) + `outdoor_site_layout_provider.py`(LLM1/LLM2,
  PROMPT_VERSION 1.202606130030) + `app/core/steps/outdoor_site_layout_step.py`
  (21.76, loader, review_html inline SVG) + `tests/pipeline/test_outdoor_site_layout_plan.py`(8건).
- 배선: config(2 flag)/applicability/steps __init__/step_manifest 21.76 +
  scene_image_service 단건·배치 merge 적용(zoom_ctx.source_overrides 를 merge 본으로
  교체) + coordinator override 로그 prompt_source 동적화.
- baseline 갱신: step 73/active 62/analysis 60/transform 60/applicability set.
- green: 신규 8 + 영향권(services/pipeline/core·steps) **2364**. 잔여 fail 44 는
  HEAD 임시 worktree 재현으로 **사전존재** 입증(실험 slice=untracked 테스트 파일,
  label/버전 drift 계열) — 회귀 0.

### 1차 canary 가 잡은 설계 보정 1건
- layout 좌표에서 유도한 **좌/우(lateral) 가 staging frame_spatial_contract 의
  screen_zone 과 충돌** (sh7: staging middle_left ↔ layout 유도 right). lateral
  구도의 SOT 는 staging 이므로 공간 요약에서 좌/우 출력 제거 + REWRITE 에
  "측면 배치 변경 금지" hard rule 추가 → 재실행에서 sh7 의 middle_left/right 가
  원문 그대로 보존되고 깊이/크기/방향만 보정됨 (edited_spans 실측).

### S15 canary (production 단건, acceptance 4항)
- step force: 4그룹(L01/L04/L12/L18)·override 5샷·layout shape 위반 0·audit 위반 0·
  1인 이동 진단 1(S6sh1)·zoom 멤버 16샷 제외.
- 단건 regen 2: override 적용 로그 실측(`prompt_source=outdoor_site_layout,
  group=osl-l12, revised_hash`) + prompt_used 에 revised 본문 그대로.
- 육안: ①sh5 수리영<혜수 + 도로 깊이로 멀어짐 (before 는 동일 크기) ✓
  ②sh7 거리 관계 명확 개선 ✓ ③같은 장소(정류장+해안도로+바다) ✓
  ④identity/outfit/action 유지 ✓.
- 갤러리: **8896/geom_osl_canary/canary_gallery.html** (before/after + edited_spans
  + 공간 요약 + step review_html 링크). canary 스크립트 ~/tmp/geum_osl_canary.py.

## 5.5 피드백 R2/R3 (2026-06-13, 커밋 724c99fd 이후 — working tree)

- 사용자 육안 지적: ①sh5 혜수 "귀신처럼 투영"(반투명 거대 흉상)+수리영 바다 방향
  ②sh7 혜수 시선 반대+수리영이 혜수 앞(깊이 미준수).
- 진단 (spike v2 승인 구절과 대조): R1 재작문이 (a)near 인물에 "in the nearest
  foreground" 삽입 → i2i 가 env ref 와 합성하며 고스트 (b)"moves away from the
  camera" 카메라 상대 동사 → env still 기하(도로 측향·바다 안쪽)와 결합해 바다
  방향으로 해석. spike v2 성공 구절은 near 인물 무수정+위치 진술("already
  partway down the road")이었음.
- fix (provider REWRITE 계약, PROMPT_VERSION 1.202606131230): ①NEAREST 인물 구절
  **verbatim 보존**(축소·이동도 금지 — R2 중간 실행에서 LLM 이 혜수를
  small/deep 으로 축소한 위반 실측 후 강화) ②이동=위치 진술형, "toward/away
  from the camera" 금지, 경로 표현은 원문 landmark 유지 ③summary 의 이동 문구도
  위치 진술형으로. 테스트 어서션 갱신(9 green).
- R3 결과: sh5 고스트 해소+도로 위 달리기 교정 ✓ / sh7 깊이 관계 명확(혜수
  근경·수리영 원경) ✓. **잔존**: sh5 수리영-혜수 크기 대비 약함 — 근본 원인 =
  bg ref "use as-is" 라벨이 측면 도로 구도를 잠금 ↔ staging 카메라는 도로
  방향(tracking from behind). spike 승인본은 identity-only 라벨("keep place
  identity, do NOT copy composition")이라 소실점 재구도가 가능했음. 후속 제안 =
  site-layout 멤버 샷의 bg ref 라벨 완화(별도 narrow scope, Codex 의논).

## 5.6 R3/R4 + nb2 구도 가이드 spike (2026-06-13, working tree — 커밋 대기)

- **R3** (사용자 2차 지적: sh5 바다 방향+혜수 고스트/sh7 깊이 미준수): REWRITE 계약
  보정 — NEAREST 인물 구절 verbatim 보존 + 이동=위치 진술형(카메라 상대 동사 금지).
  고스트/방향 해소, sh5 크기 대비는 잔존.
- **R4** (사용자 지시 "길 방향/정류장 위치 같은 지형을 설명하라"): 공간 요약을
  **SCENE GEOGRAPHY** 로 전면 교체 — landmark 가 프레임에서 어디서 어디로
  달리는지(주축 extent, kind 무관), 이동 인물이 무엇을 따라(점-선분 거리)
  어디로 가고 어디로는 안 가는지(최대 면적 area 1개) 전부 좌표 산술.
  + LLM1 staged screen zone 재현 강제 + `layout_side_conflicts` deterministic
  검증/1회 retry + spike 의 "떠나는 인물은 카메라보다 멀리" 제약 복원.
  R4 canary: sh7 충족, sh5 방향 교정·크기 대비 약함(bg ref "use as-is" 구도
  잠금이 소실점 차단 — 근본 한계 확인).
- **nb2 구도 가이드 spike** (~/tmp/geom_osl_nb2, 갤러리 8896/geom_osl_nb2/gallery.html):
  M0~M6 비교 — top-down(카메라 아이콘 포함 M1/T2I 지도 M2)은 방향 힌트까지,
  **카메라 시점 투영 가이드(M3~M6)는 구도를 지배**. M3 와이어=누출,
  M4/M5 실루엣=누출 0, **M6=T2I 가 SCENE GEOGRAPHY 텍스트만으로 구도 스케치를
  그려 첨부 — 최고 결과**(소실점+멀어짐+크기 대비, 작도 코드 불필요=일반화 유리).
  스케치/still 모두 nb2 였음 — flash 교차 미확인.
- **사용자 합의 방향**: 야외 v1 = 구도 중요/이동 샷에 스케치 가이드(structured
  신호로 선별), 스케치=구도 SOT/텍스트=내용·무드 SOT+fallback(둘 다 유지, 일치),
  LLM 복잡도 게이트는 v2, 실내는 v3.

## 5.7 R2~R4 Codex 리뷰 fix (2026-06-13)

- Codex W21B-W8_R2_R4_CODE_REVIEW = **NEEDS_REVISION_NARROW_1**: retry 후에도
  staged screen_zone 충돌이 남은 shot 에 SCENE GEOGRAPHY override 를 계속
  적용 — R4 요약은 frame 좌/우를 prompt 에 넣으므로 staging(lateral SOT)과
  충돌하는 지형 서술이 실제 주입될 수 있음. "비차단"의 의미는 원본
  scene_detail prompt fallback 이어야 한다(5.5 NARROW_1 과 동일 원칙).
  직접 대조로 타당성 검증 후 수용.
- fix: ①잔존 충돌 shot_index 는 override 생성 제외 + `spatial_summary_skipped`
  reason=`layout_side_conflict` 진단(같은 그룹의 충돌 없는 shot 은 유지)
  ②retry 채택 조건 `<=`→`<` (엄격 개선만) ③관사 중복 보정 `_definite`
  ("NOT heading into the the sea" 방지). 회귀 테스트
  `test_side_conflict_shot_excluded_from_overrides` 추가 (11 green).

## 7. composition guide v1 설계 (2026-06-13, Codex APPROVED_WITH_NARROW_GUARDS)

배경: 텍스트 override 만으로는 bg ref 의 composition lock("use as-is")을 못 이기는
케이스 잔존(R4 sh5 크기 대비). spike M6 + flash 교차 실험(8896/geom_osl_nb2
F 섹션)으로 레시피 검증: SCENE GEOGRAPHY 텍스트 → T2I 모노크롬 구도 스케치 1장 →
still 에 composition-only ref 부착. still=flash 5/5 충족, 스케치=nb2 누출 0
(flash 1/2 누출), nb2 변동성 3회 핵심 구도 안정, ablation(본문 구도문구 제거)=
제3 인물 중복 → **텍스트 override 와 스케치 병행 유지**.

설계 (모두 deterministic structured 신호, 시나리오 의존 0):

1. **적용 선별**: outdoor_site_layout cp 그룹 멤버 중 spatial_summary 존재 샷
   (side-conflict skip 샷 자동 제외) AND layout figure 에 moving_toward 존재하며
   그 figure 가 NEAREST 가 아닌(departing) 샷. v1 은 실측 검증된 **2인+ departing
   class 한정** — 1인 이동 샷으로 넓히지 않는다 (Codex guard).
2. **생성 위치**: outdoor_site_layout step 내, flag
   `outdoor_composition_guide_enabled` default OFF (W8 flag 와 독립). 스케치
   T2I 1콜/샷, cp 디렉토리에 PNG 저장 + manifest `composition_guides` 맵
   ("si:shi" → path/model/prompt_hash/source_summary_hash). 실패=비차단 진단만
   (가이드 없이 기존 text override 경로 fallback). base cp/spatial_summary 부재
   시 no-op 진단 — guide 가 layout 을 새로 의미판단하지 않는다 (Codex guard).
3. **스케치 모델**: settings `outdoor_composition_guide_model` 주입(기본 nb2
   gemini-3-pro-image — 누출 0 실측). still 은 `gemini_image_model`(flash) 유지.
4. **소비**: scene 생성 시 guide 존재 샷에 labeled ref 마지막 1장 부착 — **별도
   role `composition_guide`** (background/character/prop 계약에 불참,
   required_refs 충족 판단 제외 — Codex guard). 라벨=M6_LABEL 형(STORYBOARD
   SKETCH … composition only … do not reproduce its flat tones or shapes).
5. **bg ref 라벨 완화**: guide 부착 샷 한정 — 같은 샷의 same-room bg ref 지시를
   "use as-is" → place identity/material/lighting reference + do NOT copy
   camera/composition 계열로 완화. consumer attach 시점의 label/metadata
   override 로 처리 (bg plate provenance 불변, Codex guard).
6. **텍스트 병행**: SCENE GEOGRAPHY 재작문 override 유지(ablation 실증). 스케치
   입력=cp 의 spatial_summary 전문 (자르지 않음).
7. **variation 공유**: technique_1/2 는 같은 모멘트 — guide 1장 공유, map key
   는 (scene, shot) 단위 (variation 별 override 와 느슨 결합).
8. **덮어쓰기 조건**: departing 샷에서 가이드가 staging 카메라 플랜(tracking
   from behind 등)을 사실상 덮어씀 — 적용 조건이 위 structured 신호로 좁고,
   lateral SOT=staging 은 유지(스케치 입력=side-conflict 통과본만).

테스트 최소 4건(Codex 권고): ①selection(departing non-nearest 만, side-conflict
skip 제외) ②생성 실패 비차단(failed_count=0, text override 유지) ③consumer
attach(guide 샷만 ref 추가+bg 라벨 완화, 무 guide 샷 byte-identical)
④ref_contract 방어(guide ref 가 required_refs 판정에 불참).

## 7.1 composition guide v1 E2E (2026-06-13, push 후 — Codex ACCEPTED_WITH_CAVEATS)

- 전체 선택 샷 58개 재생성 (backend API 3병렬, ~/tmp/geum_osl_e2e_regen.py):
  **57/58 성공**. 유일 실패 S2sh4 = `ref_contract.violation`
  (reference_phrase_kinds 'character' 선언 ↔ character meta 미부착 — guide/
  override 비대상 샷의 사전존재 family, 별도 narrow triage 후보).
- guide 4샷 실측: attach 로그 4/4 + prompt_used sketch_ref 4/4 + as-is residue
  0/4 (relaxed 는 same-room bg ref 가 있는 S15 2샷만 발화 = 정상).
- 육안: S15sh5/sh7 = 2차 독립 생성에서도 구도 충족(레시피 변동성 acceptance
  통과) / S28sh9 충족 / S1sh4 = 본문 액션(몸싸움)이 구도 지배, 스케치는 배경
  깊이만 기여 — 내용 SOT=본문 설계 의도, 회귀 아님. 갤러리 8896/geom_osl_e2e.
- caveat 2건 (기록만, 코드 변경 불필요 — Codex 합의):
  ①스케치 2/4 에 라벨 텍스트(C 토큰/한국어 지명) — still 누출 0/4 로 무해.
    다음 touch 때 스케치 프롬프트 "no letters, no words, no captions, no
    labels, no symbols or written marks of any kind" 강화 후보.
  ②departing 선별이 접근 이동 샷(S1sh4)도 포함 — 무해(본문이 이김). 향후
    실품질 문제 시 distance ratio threshold 등으로 좁히는 방향, 지금 조이면
    S15 계열 재발 위험이 더 큼.

## 6. 판정 로그

- 2026-06-12 Codex W21B_W8_SITE_LAYOUT_DESIGN_REVIEW = APPROVED_WITH_NARROW_SCOPE
  (ⓐ seed 범위 ⓑ 재작문 계약 ⓒ loader+merge ⓓ layout 이미지 갤러리 전용
  ⓔ validator shape/범위만 ⓕ S15 canary ⓖ 구현 범위).
- 2026-06-12 Codex W21B-W8_SITE_LAYOUT_CODE_REVIEW = **NEEDS_REVISION_NARROW_1**:
  layout LLM/shape 실패를 failed_count 로 반환 → StepRunner final_status 가
  partial 저장 → active analysis step 이라 fresh run 하류(analyzed/image) 진입
  차단 가능 — 설계("비차단 quality override layer")와 모순. step_runner.py
  직접 대조로 타당성 검증 후 수용.
  → fix: failed_count=0 고정(applicable=len(selected_locs)/completed=len(groups)),
  실패는 diagnostics 로만 surface + 회귀 테스트
  `test_layout_failure_is_non_blocking` 추가 (신규 9 green, 영향권 2424 green).
- 2026-06-12 Codex W21B-W8_SITE_LAYOUT_REREVIEW = **APPROVED_TO_COMMIT**
  (반환 계약/회귀 테스트/scope guard 확인. push 는 사용자 GO 별도).
- 후속 공유 합의: 동일 failed_count 패턴이 committed 인
  zoom_continuity_anchor(21.73)에도 존재 — W8 과 분리한 별도 narrow follow-up.
  zoom flag 켠 fresh/image run 전에 우선순위 높게 (Codex 권고).
