# 복잡 구조물 샷 — 맵·마커 제거 + A/B VLM 선택 재설계 (2026-07-16)

## 배경 — 사용자 확정 지시 2건

1. **코드(PIL 등)로 이미지에 그리기 절대 금지** — 마커·콘·도형·텍스트 오버레이 전부.
   이미지의 시각 요소는 항상 이미지 모델 몫. 현행 위반 사례 =
   `render_marker_map`(outdoor_marker_map.py, E1/CAM 마커+FOV 콘 PIL 합성).
   과거 "마커=PIL 유지 권고"(marker_i2i 비교, Codex)는 이 지시로 폐기.
2. **복잡 구조물 샷 = 맵·마커·마커 스케치 제거** — 사용자 원문:
   "복잡한 구조물이 있는 경우에는 맵을 제외하자. 그냥 배경보고
   콘티+배경+엔티티→샷이미지 만든 것과 그냥 배경+엔티티에서 만든 것 중에
   하나 VLM이 선택해서 하자. 나머지는 그대로 가고."
   - A = [배경 플레이트 + 경량 콘티 + 엔티티] → 스틸
   - B = [배경 플레이트 + 엔티티] → 스틸
   - VLM 블라인드 비교 선택 = **기존 conti_ab 계약 재사용**(순서 뒤집기 2회,
     동점=콘티본)
   - 나머지 파이프(seed·플레이트·일반 샷·3롤+critique) 유지
   - "복잡 구조물" 판별 = LLM (하드코딩 금지)

## 현행 구조 (변경 전)

- `outdoor_lane_plan`(LLM, evidence-bound): 야외 선택 샷 →
  `map_marker`(레인1, 개활지) / `structure_plate`(레인2, 복잡 구조물) 바인딩.
- 레인2: `outdoor_structure_seed` = [1] seed 실사(순수 T2I 멀티롤) +
  [2] siteplan 맵(nb2 재투영, 참조=[seed]).
- 양 레인 공통(`shot_conti_light._run_lane_conti`): base 맵(레인1=canon
  SITE PLAN / 레인2=siteplan) → geometry LLM → **render_marker_map(PIL)** →
  마커 스케치(gpt-image) + leakage judge. lane 샷은 일반 콘티에서 제외.
- `outdoor_frame_mode`(flag, E2E6 ③): structure_plate 샷
  structure_dominant → 스케치 생략(direct_seed, 스틸=seed 직참조).
- `still_recipe_service`: lane 샷 refs=[스케치(+seed)]+prev+엔티티,
  플레이트 0, `_LANE_PACK` 프롬프트. `ab_active`(:723)는
  `not lane_used`로 lane 샷을 A/B에서 제외.

## 설계 결정

### D1. 판별 = outdoor_lane_plan 재사용 (신규 판별 없음)

"복잡 구조물 샷" SOT = lane plan의 `structure_plate` 바인딩(기존 LLM
evidence-bound 계약). 하드코딩·신규 judge 없음. 레인 분류 자체(스텝·팩·
검증·parity)는 무변경.

### D2. shot_conti_light — structure_plate 샷의 lane 파이프 제거

`_run_lane_conti`에서 `lane == "structure_plate"` 샷:

- 마커 geometry·PIL·마커 스케치·frame_mode 분기 **전부 제거**.
- entry = `{lane, group_id, segment_id, status: "ab_select", seed_path,
  seed_asset_id}` 만 기록. seed 결손 = fail-closed 유지(레인 배정 샷의
  상류 자산 부재는 조용한 degrade 금지 — 기존 계약).
- **일반 콘티 제외 필터에서 `ab_select` 샷은 제외하지 않음** → 일반 경량
  콘티 생성(플레이트 느슨 참조, 기존 run_shot_conti_light 그대로).
  플레이트 부재 시 일반 규칙(콘티 생략)과 동일.
- `map_marker`(레인1) 샷: 스케치 파이프 유지하되 D5(PIL 제거) 적용.
- depends_on에서 `outdoor_frame_mode` 제거, SCHEMA_VERSION bump,
  config_hash에 새 팩 버전 반영.

### D3. outdoor_structure_seed — siteplan 생성 제거

- seed 멀티롤(변형 3종 저작 포함, seed 2R v5) = **무변경 유지**.
- siteplan(맵) 생성·영속 제거 — 유일 소비처(레인2 마커 맵)가 소멸.
  CP에서 siteplan 키 제거(SCHEMA_VERSION bump). 기존 siteplan
  ImageAsset row는 DB 보존(삭제 금지 원칙).

### D4. still_recipe_service — 복잡 구조물 샷 = 일반 경로 병합 + A/B 상시

- `lane_conti[tag].status == "ab_select"` → `lane_used = False` 계열 신규
  분기 `complex_ab = True`. 플레이트/콘티/prev/bg_only 취급은 **일반 샷과
  완전 동일**.
- **ab_active 재설계**:
  ```
  ab_active = (conti_ab_on or complex_ab)
              and not lane_used and not bg_only
              and prev_sel is None and conti is not None
  ```
  복잡 구조물 샷은 `still_conti_ab_enabled` OFF여도 A/B **상시**(사용자
  확정 — A/B가 이 샷들의 파이프 자체). 일반 샷 A/B는 기존 opt-in 유지.
- **seed 참조 병기(권고)**: A/B 양 브랜치 refs에 seed를 STRUCTURE LOOK으로
  추가 부착 + lineage `structure_seed_look` 유지.
  근거: ①사용자 "seed 유지" 명시 — 스틸 참조가 유일한 잔여 소비처
  (없으면 seed 파이프 자체가 고아) ②seed=구조물 외관 드리프트 해소가
  실증된 존재 이유(S4sh1). A/B 정의는 사용자 문구 그대로(콘티 유무만
  차이), seed는 양쪽 공통이라 비교 순수성 유지.
- prev 샷(complex): 일반 prev 규칙(refs=[prev+엔티티], A/B 비대상).
  seed 미부착 — prev가 배경·구조물 look SOT(배경 권위 이중화 금지,
  E2E6 ⑦ 정신).
- bg_only 샷(complex): 플레이트+seed (+NO PEOPLE). A/B 비대상(콘티 없음).
- 프롬프트: still_recipe **팩 v4 신설**(기존 v3 복제 + structure look
  보조 절 — 플레이트=LOCATION 권위 유지, seed=구조물 외관 SOT 보조).
  `build_still_refs`에 플레이트와 공존하는 seed 조립 분기 추가(신규
  인자, 기존 lane 분기와 별개 — v1~v3 조립 byte-identical 유지).
- conti_ab 재사용: `resolve_or_run_outer`·records 키(`tag::ab_conti`/
  `ab_noconti`/`conti_ab_decision`)·durable persist·winner 복사 전부
  기존 그대로(지문에 refs 차이가 자동 반영).
- `lane_direct_seed`/`decide_direct_seed_location_ref_mode` 소비 분기:
  도달 불가(생산처 소멸) — 서비스 분기 제거, 모듈 함수는 존치.

### D5. 레인1(map_marker) — PIL 마커 제거 (지시 ① 파생 필수)

- `render_marker_map` 프로덕션 호출 제거(함수 존치 + deprecated 명시).
- marker_map_sketch **팩 v4 신설**: 스케치 참조=[클린 canon SITE PLAN 맵]
  (마커 굽기 없음), geometry(카메라 위치·heading·FOV·엔티티 슬롯 좌표)를
  **STAGING GEOMETRY 텍스트 절로 직렬화 주입**. 코드는 geometry JSON→
  영문 좌표 서술 직렬화만(의미 판단 없음 — 데이터 계약 기반).
- leakage judge 유지(스케치에 마커 심볼·텍스트 렌더 금지 — 텍스트
  geometry가 마커 코드를 언급해도 그리지 않도록 방어).
- `lane_control_*.png`(마커 굽힌 맵) 산출 소멸 — 사이드카·CP shape 변경,
  지문에 sketch pack v4.
- 품질 리스크: 좌표 텍스트 서술의 공간 충실도는 마커 굽기 대비 미검증 —
  E2E7 육안으로 검증(마커 굽기는 지시 ①로 어떤 형태든 불가,
  이미지 모델 i2i 굽기도 좌표 불충실 실측 + 사용자가 맵 위 굽힌
  라벨·텍스트 자체를 불신).

### D6. 폐기·dormant

- `outdoor_frame_mode` 스텝: 소비처 소멸 → applicability 항상
  not_applicable(스텝·모듈 파일 존치 — 삭제 금지 원칙), 플래그
  deprecated 주석. E2E7 ON 목록에서 제외.
- 구 lane 스케치 산출·siteplan 산출: 파일·asset 보존, 신규 실행에서
  생성 안 함.
- `outdoor_map_conti`(구 폐기 경로): 이미 dormant — 무변경.

### D7. 플래그 — 신규 없음

complex A/B는 `outdoor_lane_pipe_enabled` ON의 기본 동작(lane pipe 자체가
opt-in이므로 별도 플래그 불요). `still_conti_ab_enabled`는 일반 샷 A/B
opt-in으로 의미 유지. E2E7 ON 필요 플래그: STRUCTURE_SEED_VARIANTS +
OUTDOOR_LANE_PLAN/PIPE + STILL_PLATE_SELECT (+일반 샷 A/B 원하면
STILL_CONTI_AB) — OUTDOOR_FRAME_MODE 제외.

### D8. 테스트 (결정론 영역만 — TDD)

- ab_active 진리표(complex_ab×conti_ab_on×bg_only×prev×conti 유무).
- `_run_lane_conti` 라우팅: structure_plate→ab_select entry(+seed
  fail-closed), map_marker→스케치(PIL 무호출), 일반 콘티 제외 필터에
  ab_select 미포함.
- `build_still_refs` seed 병기 조립(v4) + v1~v3 byte-identical 잠금.
- geometry→텍스트 절 직렬화 순수 함수 + marker_map_sketch v4 조립.
- structure_seed CP shape(siteplan 키 제거) + schema bump.
- frame_mode not_applicable, parity·재검증 계약 유지, config_hash 드리프트.
- 이미지 품질 = E2E7 육안(테스트 PASS≠동작 검증 — 기존 원칙).

## 변경 파일 (예상)

- `backend/app/core/steps/shot_conti_light_step.py` — D2, D5 배선
- `backend/app/core/steps/outdoor_structure_seed_step.py` — D3
- `backend/app/core/steps/outdoor_frame_mode_step.py`·`applicability.py`·
  `step_manifest.py` — D6, depends_on
- `backend/app/services/still_recipe_service.py` — D4
- `backend/app/modules/pipeline/still_recipe.py` — refs/prompt v4
- `backend/app/modules/pipeline/outdoor_marker_map.py` — geometry 텍스트
  직렬화 헬퍼, render_marker_map deprecated
- `prompts/_base/still_recipe/4.*` · `prompts/_base/marker_map_sketch/4.*`
  — 신규 버전 디렉토리(덮어쓰기 금지)

## 열린 쟁점 — Codex 의논 결과 확정 (2026-07-16, NEEDS_REVISION 전건 수용)

1. seed 병기 범위 = **A/B 양쪽+bg_only 부착, prev 미부착** (합의).
2. A/B eligible complex 샷의 콘티/플레이트 결손 = **fail-closed** (B 단독
   진행 금지 — 사용자 확정 A/B 파이프가 아님). bg_only·prev만 명시 bypass.
3. geometry = 구조화 JSON → **ID-free normalized planning prose** (아래 R5).
4. direct_seed 서비스 live 분기 **제거**, helper=deprecated 존치. 구 CP는
   schema/hash 무효화로 소비 금지. frame_mode는 applicability 뿐 아니라
   consumer/hash/dependency 전부 제거.

## Codex 설계 리뷰 반영 (R1~R7 — 구현 계약)

### R1 (BLOCKING-1) 플레이트 권위 선행 고정

`still_plate_select`가 스틸 시점에 플레이트를 교체하면 A=[Y플레이트+
X플레이트로 그린 콘티+엔티티] vs B=[Y+엔티티]가 되어 콘티 유무 외 변수가
오염된다(일반 샷 A/B에도 잠재하던 결함).

- **선택을 shot_conti_light 로 선행**: flag ON + 콘티 대상 샷(person-
  visible·no-prev·플레이트 존재·같은 location 후보 2+)은 콘티 생성 **전**
  plate_select 판정 → 선택 plate path/asset_id/판정 record 를 conti
  records·CP 에 영속. 콘티는 선택된 플레이트로 생성.
- still_recipe 는 콘티 있는 샷에서 **영속 기록을 재사용**(재판정 금지 —
  A/B 양 브랜치와 콘티가 정확히 같은 plate path/asset_id 소비).
  bg_only 샷(콘티 없음)만 스틸 시점 판정 유지. prev·맵 플레이트 샷
  비대상(기존).
- plate_select 계약(CURRENTLY ASSIGNED 마킹·불명확=배정 유지·오류=
  fail-open+기록)은 무변경 — 판정 시점만 이동.

### R2 (BLOCKING-2) 정책 entry / 자산 entry 분리 + unique counts

`lane_conti`에 남긴 ab_select entry 와 일반 콘티 재투입이 겹치면
`applicable_count = result.applicable_count + len(lane_contis)` 가 같은
샷을 이중 가산하고, 기존 "lane_contis 태그 = 일반 콘티 제외" 필터와도
충돌한다. **2단계 finalize**:

- (a) structure_plate 바인딩 = pending **policy** entry 만 기록, 일반
  콘티 대상에서 **제외하지 않음**.
- (b) 일반 콘티 완료 후 finalize: A/B eligible(person-visible·no-prev)
  샷은 seed+authoritative plate+conti 전부 존재 시 `status=
  ab_select_ready`, 하나라도 결손이면 `failed`(fail-closed).
- (c) bg_only/prev 샷은 `status=ab_select_bypass` + 명시 reason
  (bg_only=플레이트 강제 / prev=배경 SOT) — 기존 규칙 유지.
- (d) counts 는 **unique shot 기준** — ready/bypass 샷을 일반 콘티
  집계에 이중 가산하지 않음(structure 샷은 일반 콘티 집계에 이미 포함,
  policy entry 는 카운트 제외; map_marker 샷만 lane 가산 유지).

### R3 (BLOCKING-3) config hash·provenance 완전성

complex A/B 는 `still_conti_ab_enabled=False` 여도 강제 — 현재 scene
hash 는 global flag ON 일 때만 conti_ab 팩을 스탬프하므로 불완전.

- SceneImagePipelineStep hash: **lane pipe ON 이면** resolved still_recipe
  v4 팩 + conti_ab 팩 + judge alias·물리 모델 + complex AB 계약/스키마
  버전을 스탬프(기존 completed still/scene CP 무효화 의도).
- still_recipe branch `extra_fingerprint` 에 v4 계약 반영.
- `save_single_scene_asset` 의 prompt_type/code_version/
  prompt_file_version **v1 하드코딩 → 샷별 실제 팩 버전** provenance.
- shot_conti_light / outdoor_structure_seed SCHEMA_VERSION+hash bump.

### R4 (HIGH-4) D5 lineage — control asset 소멸

- `register_intermediate_assets`(shot_conti_light.py 모듈 — 변경 파일에
  추가)의 `lane_marker_control`(deterministic_pil) 등록 제거. 신규
  스케치 asset 의 input_image_ids = **clean canon map UUID 직결**.
- 신규 role 은 marker 아닌 중립 이름(`lane_storyboard_sketch`), 구 row
  는 보존만.
- 테스트: fresh run 에서 lane_marker_control 생성 0 · render_marker_map
  호출 0 · canon map→storyboard sketch edge 1.

### R5 (HIGH-5) marker_map_sketch v4 = 전면 계약 갱신 + ID-free 직렬화

v3 head/no_marker/leakage_judge 는 "첨부 맵에 E1/CAM/blue wedge 가
있다"를 전제 — clean map 경로에서 유지하면 금지 대상을 프라이밍.

- 내부 geometry JSON 의 E-slot 은 유지하되, 이미지 모델에 보내는
  STAGING GEOMETRY 텍스트는 **ID-free**: 역할명(subject)+normalized
  x/y, camera origin/look-target/FOV 수치 + "planning data only —
  numbers/coordinates/labels/arrows/cones 를 렌더하지 말라".
  E1/CAM/marker/wedge 토큰 금지. 수직 각도=camera_direction SOT 유지.
- serializer 계약 버전 별도 상수 → fingerprint/config hash 스탬프.
- leakage_judge v4: 최종 이미지의 annotation/text/flat-plan leakage 만
  판정("원본에 빨간 마커" 서술 제거).

### R6 (HIGH-6) seed 관할·tie-break 명문화 + 공용 branch-ref helper

- 관할: **plate = 현재 샷 subspace/layout/surroundings/time/lighting
  SOT**, **seed = target 고정 구조물 identity/shape/proportion/
  openings/material/color SOT**. target 구조물 충돌=seed 우선, 주변=
  plate 우선 — still_recipe 팩 v4 에 명시(현 plate_label 이
  architecture/material 을 SOT 로 잡아 그대로면 중첩).
- A/B 두 브랜치는 **동일 seed path/label** 공유(비교 순수성). 현재
  refs_b 가 `prompt_version="1"` 재조립이므로 **공용 branch-ref
  helper** 신설 — B 브랜치 seed/v4 라벨 누락 구조적 차단.
- bg_only 조기 return 전에도 seed 포함. seed lineage 는 항상 기록,
  conti edge 는 winner=A 일 때만. prev/bypass 사유 records 명시.

### R7 (NARROW) siteplan 완전 제거 + 전역 감사

- D3: siteplan fn/config/model/CP key/신규 ImageAsset 생성 전부 제거
  (구 파일·row 삭제 금지). fresh E2E acceptance 에
  `structure_seed_siteplan 신규 생성 0 · downstream edge 0` 포함.
- 지시 ① 전역 준수: 이번 TASK acceptance = "새 3레인 생성 참조에
  code-rendered raster 0"(소스/lineage 감사). repo 전체 ImageDraw
  생산 코드는 **active generation refs 기준 inventory** 를 남김
  (review-only HTML/무손실 유틸과 생성 참조 구분).

## ImageDraw 전역 inventory (R7 — 2026-07-16 실사, 지시 ① 준수 감사)

이번 TASK acceptance = **새 3레인 생성 참조에 code-rendered raster 0**
(달성 — render_marker_map 호출 0, lane_marker_control asset 신규 생성 0,
스케치 참조=클린 canon 맵 직결). repo 전체 PIL ImageDraw 생산 코드:

| 위치 | 용도 | 생성 참조 여부 | 판정 |
|---|---|---|---|
| `outdoor_marker_map.render_marker_map` | 레인 마커 굽기 | (구) 스케치 참조 | **이번 TASK 로 호출 0 — deprecated 보존** |
| `outdoor_site_layout_provider._render_clean_birdseye_png` | birdseye 셋맵 | 없음 | LEGACY(v2 미사용) 보존 — 호출 0 |
| `dwelling_zone_map_step`(annotate) | 존 배정 검증 오버레이 | 없음("verification only, never a render anchor" 자체 명시) | review-only — 허용 범위 |
| `space_set_bg.mark_fp` → `space_set_bg_step` | 실내 FP 빨간 원+번호 마킹 → i2i 참조 `marked_fp:#N` | **있음 (active, 실내 레인3 파이프)** | ★지시 ① 관점 잠재 위반 — 레인3=기존 그대로(이번 범위 밖), **사용자 판단 필요** |
| `gemini_i2i_editor._create_camera_diagram` | 카메라 앵글 다이어그램 → i2i 참조 | **있음 (active, 앵글 variation 경로)** | ★동일 — 3레인 밖, **사용자 판단 필요** |

## 구현 순서 (Codex 합의)

P1 plate authority 선행 고정(R1) → P2 D2 policy/unique counts+
fail-closed(R2) → P3 D3 siteplan 제거(R7) → P4 D5 clean-map 텍스트
geometry+direct lineage(R4·R5) → P5 D4 v4 refs/A-B/hash/provenance
(R3·R6) → P6 frame_mode/direct_seed dormant 정리 → 결정론 테스트 +
2~3샷 canary + ImageDraw inventory.
