# Phase II 재배선 v2 — 라벨 마커 항공뷰 + 스케치 파이프라인 (2026-06-29)

검증 완료된 새 설계를 production `outdoor_site_layout` producer 에 적용하는 구현 명세.
실험 자산(scratchpad/phase2_*.py, ~/tmp/phase2_*)은 **절대 커밋 금지**. 본 doc 만 커밋 대상.

## 0. 검증 완료 요약 (전 단계 PASS, 갤러리 8899)

```
location 공통 빈 항공뷰 base(T2I, 환경=원형 숫자 1/2/3)   ← phase2_newdesign2
  → 샷별 블로킹 I2I(엔티티=원형 글자 A/B/C + 카메라 1개 방향화살표/cone)
  → 카메라뷰 스케치 I2I(블로킹 + compute_camera_brief 좌표산술 → eye-level 라인아트)  ← phase2_sketch
  → nb2 still(+캐릭터 passport 정체성)  ← phase2_still_tier3 / phase2_still_char
```
- **set 일관성**: 같은 공통 base → 멀티앵글(real/altTop/altRight)에서 환경 숫자원 위치 보존, 셸터/도로/바다 동일. §4e-B harm(샷마다 셸터 발명) 해결.
- **스케치 원근**: top-down→eye-level 변환은 `compute_camera_brief`(near/far·좌우·horizon LEVEL)가 주 동력. brief-only 대조도 거의 동등 → **단순 구도는 brief 만으로 충분**(블로킹의 가치는 복잡 구도/다수 엔티티 정확 배치).
- **캐릭터**: 스케치(구도 SOT) + passport 2장(정체성) = ref 3개에서 **혼란 없음**(입력 라벨로 "구도는 스케치/정체성은 portrait" 역할 분리). 단 ref 가 더 늘면(배경 plate+소품+다수 캐릭터 5개+) 한계 가능 → **게이팅이 ref 예산 관점에서도 정당**.

## 1. 현 producer 구조 (재배선 대상)

`outdoor_site_layout_provider.py`:
- `_render_clean_birdseye_png(spec)` (456): PIL 결정론 birdseye(색도형+wedge+dot, 글자0).
  좌표를 literal 렌더 → 좌표 품질 문제(셸터가 도로 위) 그대로 노출(사용자 지적).
- `generate_shared_model_camera_guide(layout, shot_index, ...)` (516): birdseye(PIL) →
  gpt-image-2 **edit** → 카메라뷰 layout-only 스케치 1장. **샷마다** birdseye 새로.
- `SHARED_MODEL_CAMVIEW_SYSTEM`(419)/`build_shared_model_guide_prompt`(441): 카메라뷰
  스케치 프롬프트(eye-level 라인아트, 마네킹, 디자인 발명 금지) — **재사용 가능**.
- 593줄~ **이미 LLM 의미 게이트 존재**(샷레벨 분류기, "열린 외부 departure"만 통과,
  evidence-backed, 시나리오 명사 0) — Codex 3층의 일부 = 활용/확장 대상.

`outdoor_site_layout_plan.py`: `compute_camera_brief`(631, **그대로 사용**=스케치 주 동력),
`build_clean_birdseye_spec`(743, PIL 전용 → 새 설계서 미사용), 좌표 헬퍼.

`outdoor_site_layout_step.py`: 943 `if chain["mode"]=="sketch" and shared_model_enabled`
분기(NEW producer), manifest producer_kind/guide_generation_status, no-old-fallback. config flag.

## 2. 새 producer (3-stage) — 교체 명세

`generate_shared_model_camera_guide` 내부를 아래로 교체. PIL `_render_clean_birdseye_png`/
`build_clean_birdseye_spec`는 새 경로서 미호출(삭제 금지 — 다른 소비 가능, 미사용 표시).

### Stage A — location 공통 항공뷰 base (T2I, 캐싱)
- 입력: layout landmarks(환경). 출력: 빈 컬러 top-down 항공뷰 + 환경=원형 숫자 (1)(2)(3).
- 등록 엔티티/카메라/글자(숫자 외) 제외. 평행 물리 규칙. 비실사 도식.
- **캐싱**: location(group) 단위 **1회** 생성 → 모든 샷 공유. 절대 샷별 재생성 X(일관성).
  step 에서 group base 캐시(메모리/디스크) → B 단계에 ref 로 전달.
- 새 함수 `generate_location_aerial_base(layout, *, openai_client, model) -> (png, meta)`.
- 프롬프트: scratchpad `phase2_newdesign2_exp.base_prompt` 이식(generic, label 만 데이터).

### Stage B — 샷별 블로킹 (I2I on base)
- 입력: base png(ref) + 이 샷 figures(엔티티) + camera. 출력: base 위에 엔티티=원형 글자
  (A)(B)(C) + 카메라 1개(방향 화살표 + view-cone) 주입. base set 보존.
- 새 함수 `inject_shot_blocking(base_png, layout, shot_index, *, openai_client, model)`.
- 프롬프트: `phase2_newdesign2_exp.inject_prompt` 이식. 엔티티 위치=환경 원 기준 상대 +
  grid. 이름 누출 0(익명 글자). 카메라 aim=`_camera_aim`.
- ★조건부 검토(Codex 의논): 단순 구도(brief 로 충분)는 B 생략하고 base→C 직행 가능.
  복잡 구도(다수 엔티티)만 B. judge 의 guide_scope/복잡도로 분기?

### Stage C — 카메라뷰 스케치 (I2I)
- 입력: 블로킹 png(또는 base, B 생략 시) ref + `compute_camera_brief`(좌표산술). 출력:
  eye-level 라인아트 구도 스케치. **기존 SHARED_MODEL_CAMVIEW_SYSTEM 변형 재사용**
  (ref 가 birdseye→블로킹으로 바뀜, "numbered/lettered circles + camera = blocking" 해석 추가).
- 프롬프트: `phase2_sketch_exp.SKETCH_SYS` 이식(블로킹 읽기 + brief 깊이 + 마네킹 + 디자인 발명 0).
- 이 스케치가 최종 composition guide → prompt_service 가 nb2 still 에 ref(현 경로 그대로).

### 비용
- 현: 샷당 1 edit(PIL 무료). 새: location당 base 1(T2I) + 샷당 B 1 + C 1 = 샷당 2 I2I.
  ~2-3배. **게이팅으로 대상 샷 축소 + B 조건부 생략으로 상쇄**. (Codex 의논 포인트)

## 3. 게이팅 (Codex 3층 — code candidate → LLM judge → no-guide)

기존 593줄 LLM 게이트를 Codex 설계로 정비:
1. **결정론 candidate(코드, candidate-only)**: selected outdoor shot + valid layout/cameras +
   같은 scene_index·location group 에 selected 2+ 멤버 + 각 member camera/spatial_summary +
   공통 landmark + 카메라/프레이밍 **구조적** 차이(pos/look_at·enum, 의미판정 아님). zoom/
   immobilized 가 더 강한 SOT 면 그 feature 우선, outdoor 는 진단만. 단일샷 복잡도는 코드
   확정 X → signal 로만.
2. **LLM judge**: 구조화 입력(group meta + per-shot staging/spatial_contract/entity +
   layout/continuity/risk signals) → schema `{needs_shared_model_guide, decision_type
   (cross_shot_continuity|single_shot_complexity|both|no_guide), confidence, evidence[shot_key,
   source_field,quote], reasons[enum], guide_scope, risk_notes}`. 가드: confidence low·evidence
   비면 deny, prereq 없으면 no-guide+diag, judge 는 route 만(셸터 모양 결정 금지).
3. **조합 AND**: prereq 없거나 simple single-shot → LLM 호출 안 함, no-guide. candidate →
   judge. attach = candidate AND judge true AND confidence>=medium. 생성 실패 → old fallback
   금지, no-guide+diag.
- **v1 보수적**: cross-shot continuity group **만** production attach, single-shot complexity 는
  **diagnostic only**(per-shot guide harm 재발 방지). scratchpad 서만 single-shot 실험.

## 4. flag / manifest

- `outdoor_composition_guide_shared_model_enabled` default OFF(유지).
- `outdoor_shared_model_guide_judge_enabled` = shared_model ON 내부 옵션.
- gate/guide version 을 config_hash 에 접기(ON 일때만 — OFF byte-identical).
- manifest 전 단계: deterministic_signals, judge_decision(+evidence/confidence), deny reason,
  base_hash(location 공통), per-shot blocking_hash, camera_view sketch_hash, final attach 여부.
- force-all 은 test hook(`_shared_guide_override`)만, production config 금지.

## 5. 구현 순서

1. plan.py: base_prompt/inject_prompt/sketch 프롬프트 helper 이식(generic, 시나리오토큰0) +
   결정론 candidate 판정(순수함수, TDD 가볍게).
2. provider.py: `generate_location_aerial_base`/`inject_shot_blocking` 추가, `generate_shared_
   model_camera_guide`를 3-stage 로 재구성(C 는 기존 프롬프트 변형). PIL 경로 미사용 표시.
3. judge: LLM judge provider(schema, evidence-backed) — 기존 593줄 게이트 확장/대체.
4. step.py: base 캐싱(group 단위) + B/C 배선 + 게이팅(candidate→judge→attach) + manifest.
5. config flag.
6. 결정론 테스트(candidate/flag OFF byte-identical/no-fallback/manifest) — 가볍게.
7. canary(osl-l11 실 호출) → 8899 육안 → Codex 리뷰 → 커밋 GO(사용자).

## 6. Codex 의논 포인트 (재배선 착수 전)
- (a) Stage B(블로킹) 필요성/조건부: 단순 구도는 brief-only 스케치 직행이 동등 → B 를
  복잡 구도(judge guide_scope=single_shot_complexity or 다수 엔티티)에만? 비용 절감.
- (b) base 캐싱 단위: location vs group(같은 location 다른 group 가능성).
- (c) 비용(샷당 2 I2I) 수용 범위 + 게이팅 축소 효과 추정.
- (d) 기존 593줄 LLM 게이트 재사용 vs 새 judge 로 대체.
```

## 7. 구현 완료 (2026-06-29, flag OFF, 미커밋)

§5 순서대로 구현 완료 — Codex §6 정렬 + 코드리뷰 보강 전부 반영. 5파일(II-1 PIL birdseye
producer 를 이 v2 가 대체):

- **plan.py**: `build_aerial_base_prompt`(Stage A) / `build_shot_blocking_prompt`(Stage B) /
  `build_blocking_sketch_prompt`(Stage C, marker-leakage 가드 — diagram 숫자/글자/카메라
  아이콘 복사 금지·출력 마커 0) 프롬프트 빌더(generic, 시나리오 토큰 0) +
  `shared_model_candidate_groups`(cross-shot: same scene+loc, valid cam 2+, `_cameras_non_
  identical` eps=1.5 **기술 dedup tolerance**[의미 단정 아님]) + `shot_complexity_signals`
  (entity_count_bucket/depth_planes/has_motion) + `shot_blocking_recommended`. PIL
  `build_clean_birdseye_spec` 는 LEGACY(미사용, 삭제 X).
- **provider.py**: `generate_location_aerial_base`(T2I, group 공통) / `inject_shot_blocking`
  (I2I) / `generate_shared_model_camera_guide`(base_png+use_blocking 받는 B 조건부→C 재구성)
  + `judge_shared_model_guide`(route schema `{needs_shared_model_guide, decision_type, confidence,
  evidence[shot_key,source_field,quote], reasons[enum], guide_scope, risk_notes}`, route only).
  PIL `_render_clean_birdseye_png`/`SHARED_MODEL_CAMVIEW_SYSTEM`/`build_shared_model_guide_
  prompt` 는 LEGACY 표시(삭제 X). `SHARED_MODEL_GUIDE_VERSION`=`shared_model_aerial_v2.*`.
- **step.py**: guide 블록 최상단 **fork** — `if ... and shared_model_enabled:
  _generate_shared_model_guides()` / `elif ...: <OFF legacy departure+G1G2+chain 그대로>`.
  OFF 본문 **byte-identical**(diff 상 OFF body 변경 0). 게이팅 = candidate AND judge.needs
  AND decision∈{cross_shot_continuity,both} AND conf>=medium AND evidence non-empty.
  single_shot_complexity = diagnostic only(attach X). group base 캐시(key=group_id+loc+
  layout_hash+base_prompt_ver+model+member_set_hash). Stage B 트리거 = 구조신호(다수 figure/
  depth_planes≥2) or judge reason figure_placement_continuity; blocking_stage∈{used,
  skipped_simple,skipped_no_figures}+reason. anchor 만 3-stage 생성, 나머지 continuity_anchor
  불변. 실패=no-guide+diagnostic(old fallback 금지). manifest 전단계 hash + judge decision +
  candidate signals + predicted/actual image call count.
- **config.py**: `outdoor_shared_model_guide_judge_enabled`(default True, shared_model ON 일
  때만 config_hash fold — OFF byte-identical).

### 검증
- 결정론 테스트 34 passed (신규 5: candidate cross-shot-only / ON-uses-v2-not-old[old brief/
  sketch/G1·G2 judge 미호출 lock] / failure-no-fallback / judge-default-deny 4변형 /
  judge-disabled-candidate-only). 회귀 142 passed (+1 pre-existing `test_ref_contract_dry_run`
  surface_role = 내 모듈 import 0, baseline 서도 동일 실패 = 무관).
- OFF byte-identical: config_hash 가 judge flag 토글에 불변(OFF), ON 일 때만 변동. step.py OFF
  본문 diff 0.
- canary(production provider 함수, osl-l11 base→블로킹→스케치, 멀티앵글 합성 alt + B 생략
  대조): 8899/phase2_rebase_canary 육안 — set 일관성 / marker leakage 0 / eye-level 원근 확인.
  ★osl-l11 은 실 데이터상 단일 샷이라 게이팅상 cross-shot 후보 0(정상) — canary 는 producer
  품질 확인용 합성 멀티앵글.

## 미커밋 현황
- HEAD=b389101d. 미커밋: 재배선 v2 5파일(위, flag OFF, II-1 PIL birdseye 를 대체). 섣불리
  커밋 X — canary 육안 + Codex 코드리뷰 통과 + 사용자 GO 후 통합 커밋(scope=Phase II v2).
