W21B-w5 — Space-Type-Aware Floor Plan Prompt

설계 + 상세 수정 계획서 · 2026-06-01 · DOC-ONLY Codex 2R 합의 commit·push 0 / ahead 10 hold

비-실내 공간(옥상·외부·선박)이 floor_plan_prompt에서 아파트처럼 과분할되는 구조 결함을 근본 수정한다. 핵심은 surface_role 공간유형 신호를 FP 생성 입력으로 전파하고, FP 프롬프트를 공간유형별로 분기하는 것(신규 pack v8, opt-in). STEP5-B sparse FP sidecar의 upstream 수정이며 sidecar는 건드리지 않는다.

1. 개요 & 현재 상태

항목
브랜치feat/w19-w20-bg-planning-cleanup · ahead 10 (push 보류)
선행STEP5-B sparse FP sidecar 完 (84b9783, b7baf58) — 본 작업과 독립
이 문서설계 brief + 파일별 상세 수정 계획서 (구현 코드 0)
구현 트리거사용자 GO 후 착수. 코드/commit/push는 사용자 명시 전까지 0
완성도 gate육안 canary (TDD는 deterministic까지만)

2. 근본원인 (코드로 직접 검증)

메모리 주장을 코드로 전수 확인함(Codex 아닌 실측).

2.1 surface_role는 background에만, FP로 미전파

2.2 FP 시스템 프롬프트(v7)가 실내 주거 모델을 무조건 강제

prompts/_base/floor_plan_prompt/7.202605291624/system.md

Rule내용문제
Rule 2모든 sub-area를 shared / private / service / threshold / utility zone으로 색칠 분할실내 주거 zone 어휘 전제
Rule 5bed / sofa / table / door=arc with swing / window=double line in wall / sink / toilet전부 실내 건축 평면 관습, 외부/개방 글리프 없음
Rule 43"dwelling scale and room count", "compact single-space dwelling…"공간을 무조건 dwelling으로 전제
전반exterior / open / rooftop / deck / vehicle / cabin 개념어휘에 아예 없음 → 분기 0

→ LLM은 주어진 유일한 어휘(방·벽·여닫이문·zone)로 옥상·어선도 충실히 모델링. 프롬프트 설계 결함이지 LLM 오작동이 아님. 두 image model(NB2/gpt-image-2) 모두 동일 재현(갤러리 8820/8821).

3. 데이터 흐름 & 전파 지점

# background_master_plan (LLM) — surface_role 원천 (bg 단위)
plan.backgrounds[].surface_role  ∈ SURFACE_ROLE_ENUM
plan.backgrounds[].depends_on_fp  ──┐  (interior_room은 필수)
plan.floor_plans[]  (surface_role 없음) │
        │                              │
        ▼                              ▼
floor_plan_prompt_step._process  (floor_plan_prompt_step.py:158)
   job["applied_bgs"] = [bg for bg in plan.backgrounds
                         if fp_id in bg.depends_on_fp]   # ← surface_role 여기 도달!
        │
        │  ★ NEW: space_type, mix = derive_fp_space_type(applied_bgs)
        ▼
build_fp_user_prompt(fp_spec, applied_bgs, …, prompt_version, space_type=…)
        │   .replace("{space_type_block}", …)   # selector "8"만; 5/6/7은 토큰無=no-oprun_floor_plan_prompt → LLM → numbered_elements + camera_recommendations + t2i_prompt
        ▼
floor_plan_render.render_one_floor_plan(prompt=t2i_prompt, …)  # 얇은 렌더러, prompt만 받음
        ▼  → FP PNG  (프롬프트 고치면 spec+render 동시교정, render 코드 변경 불필요)
핵심: step이 이미 applied_bgs를 들고 있으므로 추가 checkpoint 로드 없이 applied_bgs[].surface_role로 fp 공간유형을 도출할 수 있다. 이것이 유일한 전파 지점.

4. 설계 결정 (Codex 2라운드 합의)

핵심 원칙 3
  1. applied_bgs role consensus가 SOT이고, priority rule은 inconsistent data일 때만 쓰는 fallback.
  2. empty applied_bgs는 interior_room fallback 금지open_safe로 처리(=현 버그 재현 방지).
  3. v8의 목적은 "FP를 없애는 것"이 아니라 "surface role에 맞는 다이어그램 형태로 바꾸는 것".

4.1 space_type 도출 규칙

입력 (applied_bgs의 surface_role 집합 S)결과근거
전원 동일 (|S|=1)그 role 그대로invariant 4/5상 same sub_location→same fp = 정상 케이스
불일치 (|S|>1, 이상치)conservative priority
interior_room > transition_zone > exterior_plate ≈ site_surface
+ surface_role_mix 전달(primary+exceptions)
interior bg가 closed-room skeleton 필요할 위험 최대
empty (참조 bg 0)open_safe (interior 금지)fp_jobs는 빈 applied_bgs도 job 생성 → 실발생. "불명을 닫힌 아파트로"가 현 버그

space_type / surface_role_mix / open_safeprompt-input diagnostic 값 — FP output schema(numbered_elements/camera_recommendations) 필드가 아니다. 도출은 enum 집합연산(글자/substring 파싱 0 = 절대규칙 준수).

4.2 v8 공간유형 분기 (system.md)

space_type다이어그램 규칙self-fidelity 표현
interior_room현 v7 그대로: enclosed rooms / walls / swing-door arcs / zone color fills / 실내 furniture 글리프dwelling scale / room count
exterior_plateopen plate: 가짜 둘러싼 벽 금지, parapet/난간=낮은 boundary edge(방벽 아님), swing-door 분할 금지, zone-cell 과분할 금지. 열린 표면 윤곽 + 고정 구조물(난간/계단/headhouse/개구부)만open boundary / edge / zone count
transition_zonefull room 아님. 문턱/계단/입구/복도=통로·경계. 독립 방 cell 금지passage / threshold geometry count
site_surfaceexterior와 같은 open-surface branch, 문구만 더 넓게: larger outdoor/site layout, paths/edges/zones, not enclosed architectureopen boundary / edge / zone count
open_safe (fallback)공간유형 불명. enclosed apartment 금지 + 데이터 명시 구조만 보수적으로, 임의 방 분할 금지invent 금지(보수적)
Self-fidelity(Rule 43)는 전 type 유지 — 표현만 분기. "주어진 inventory만 충실히, invent 금지"는 공통. interior도 "do not invent rooms/walls/openings to fill the space"를 유지 → 이것이 어선 선실 과분할 완화의 근거. (어선이 interior_room이면 interior branch로 가되 self-fidelity로 과분할만 줄임 — 완전 해결은 vehicle subtype 후속.)

5. ★ 수정 계획서 (파일별 상세)

Codex 5단계 순서 + A/B/C 확정 반영. 구현 순서 = 의존 역순(pure helper → builder → pack → wiring → canary)로, 각 단계가 독립 green.

v0 불변식 (Codex 명시): cp shape 불변 / SCHEMA_VERSION bump 없음 / default selector "5" 불변 / selector 5·6·7 byte-identical / STEP5-B sidecar 무변경.
1

derive_fp_space_type — pure helper (신규)

파일: backend/app/modules/pipeline/floor_plan_prompt.py (모듈 내 pure 함수, step에서 호출 / Codex A 확정)

_SPACE_TYPE_PRIORITY = ["interior_room", "transition_zone", "exterior_plate", "site_surface"]
_OPEN_SAFE = "open_safe"

def derive_fp_space_type(applied_backgrounds) -> tuple[str, list[str]]:
    """applied_bgs의 surface_role 집합으로 fp 공간유형 도출.
    반환: (space_type, surface_role_mix). 순수 enum 집합연산 — 글자 파싱 0."""
    roles = [b.get("surface_role", "") for b in applied_backgrounds
             if b.get("surface_role")]
    uniq = sorted(set(roles))
    if not uniq:                      # empty → open_safe (interior 금지)
        return _OPEN_SAFE, []
    if len(uniq) == 1:                  # consensus = SOT
        return uniq[0], uniq
    for r in _SPACE_TYPE_PRIORITY:        # 이상치만 conservative priority
        if r in uniq:
            return r, uniq
    return _OPEN_SAFE, uniq            # 미지 enum 방어

테스트: 전원동일→그 role / 불일치→priority(interior>transition>exterior>site)+mix / empty→open_safe / 미지값 방어.

2

build_fp_user_prompt 확장

파일: floor_plan_prompt.py (build_fp_user_prompt, 현 L94 / L158 .replace 체인)

def build_fp_user_prompt(fp_spec, applied_backgrounds, applied_shots,
        scene_segments, visual_world_rules, prompt_version="5",
        space_type="", surface_role_mix=None):     # ★ 신규 kwargs
    ...
    # space_type 비어있으면(=selector 5/6/7 경로) block은 "" → 토큰 없어 무영향
    space_type_block = _build_space_type_block(space_type, surface_role_mix) if space_type else ""
    return (template
        .replace("{fp_id}", …)
        … # 기존 7개 그대로
        .replace("{space_type_block}", space_type_block))  # ★ 추가
byte-identical 메커니즘: selector 5/6/7의 user_template엔 {space_type_block} 토큰이 없다. str.replace(없는토큰, x) = 원문 그대로. 따라서 기존 selector 출력은 완전 동일. (테스트로 lock.)

테스트: selector "5"/"6"/"7" 출력이 space_type 인자 유무와 무관하게 byte-identical / selector "8"은 block 삽입 확인.

3

prompt pack v8 추가 (신규 디렉토리, v7 덮어쓰기 금지)

신규: prompts/_base/floor_plan_prompt/8.<YYYYMMDDHHmm>/

파일변경
schema.jsonv7 byte-identical 복제 (output contract 불변 → validator 재사용)
user_template.mdv7 + 신규 {space_type_block} 섹션 (예: ## Space type (read first) 자리)
system.mdv7 + 공간유형 conditional 분기(§4.2 표) + Rule43 type별 표현 분기
★Codex Required #1 (system-only 금지): surface_role 분기는 system 규칙만으로는 모델이 적용 못 함 — 실제 space_type 값이 user prompt({space_type_block})에 들어가야 LLM이 구조적으로 인지. system.md + user_template.md 둘 다 변경 필수.
4

selector wiring

파일변경
floor_plan_prompt.pyPROMPT_VERSION_MAP["8"] = "8.<ts>" · _V6_COMPATIBLE_SELECTORS = frozenset({"6","7","8"}) (Codex 답변(1) 확정 — v8이 v6 output contract 유지하므로 _validate_fp_prompt_extras 그대로 적용)
floor_plan_prompt_step.py (_process)space_type, mix = derive_fp_space_type(job["applied_bgs"])build_fp_user_prompt(…, space_type=space_type, surface_role_mix=mix)
config.py:116floor_plan_prompt_version: Literal["5","6","7","8"] = "5" (default 불변, "8" 추가)
test syncconfig Literal / PROMPT_VERSION_MAP / _V6_COMPATIBLE_SELECTORS lock 테스트 갱신
Codex C 확정 — config_hash: selector "8"이 _resolved_prompt_version()로 pack명을 바꿔 config_hash에 자동 반영 → re-run 전파. space_type은 upstream background_master_plan 데이터에서 나온 prompt input이므로 별도 hash 필드 불필요(master_plan 변경은 upstream_revision으로 잡힘).
5

canary (육안 visual gate, 별도)

§7 참조. 코드 green과 분리 — 완성도는 여기서 판정.

6. 테스트 계획 (deterministic = TDD)

대상케이스
derive_fp_space_type전원동일(interior/exterior/transition/site 각각) · 불일치→priority+mix · empty→open_safe · 미지 enum 방어
build_fp_user_promptselector 5/6/7 byte-identical(space_type 인자 무시 확인) · selector 8 block 삽입 · open_safe block 문구
validator_validate_fp_prompt_extras가 selector "8"에 v6 검사 적용(_V6_COMPATIBLE_SELECTORS)
sync lockconfig Literal == PROMPT_VERSION_MAP keys, schema-sync 가드
회귀tests/pipeline 전체 green, default selector "5" 무변경 실증

※ 절대규칙: PASS 카운트는 deterministic 안전망일 뿐 — 프롬프트/렌더 품질 검증 아님. "테스트 통과 = 동작 검증"으로 보고 금지.

7. canary 검증 계획 (완성도 gate)

  1. 1단계 — surface_role 확인: 각 canary fp의 실제 surface_role을 background_master_plan cp에서 직접 조회(cp 불변 유지, Codex B). 옥상이 정말 exterior_plate로 부여됐는지 등 확인.
  2. 2단계 — selector "8" 재생성 + 갤러리 비교:
대상기대 surface_role성공 기준
옥상exterior_plateopen plate — 가짜 둘러싼 벽·방 cell 없음, parapet=낮은 edge
site_surface 케이스(있으면)site_surfaceexterior와 분리 — paths/edges/zones 중심, enclosed architecture 없음
어선surface_role 의존open deck면 open / 선실이 interior_room이면 enclosed cabin. vehicle 특화는 후속 — 이번엔 2zone/2중문 과분할 완화까지(완전해결 아님)
경찰서 · 아파트interior_room현행 회귀 없음(v7 대비 동등)

비용: canary image 소액(자율 범위). 결과는 사용자 visual gate. image model은 기존 gpt-image-2(sidecar와 무관, FP 본 렌더).

8. 경계 lock & 절대규칙 준수

건드리지 않음 (lock)

  • STEP5-B sidecar (skeleton 렌더) — 사용자 명시
  • default selector "5" — 변경 0
  • selector 5/6/7 출력 — byte-identical
  • v7 pack 파일 — 덮어쓰기 금지(신규 v8 dir)
  • floor_plan_render — 무변경(prompt만 받음)
  • master_plan surface_role 부여 — 소비만
  • cp shape / SCHEMA_VERSION — bump 없음

절대규칙 준수 증명

  • 특정 시나리오 하드코딩 0 — generic surface_role enum 기반
  • 글자/substring 패턴 0 — space_type 도출은 enum 집합연산
  • 프롬프트 문구 generic descriptor만(roof/deck/threshold), 작품 노운 0
  • 완성도 = 육안 canary, TDD는 deterministic까지만
  • 프롬프트 파일 덮어쓰기 금지 → 신규 v8 dir

9. Codex 협업 기록 (2026-06-01, gpt-5.5 xhigh)

Codex 의견은 모두 검증 후 채택. 특히 Q1은 Codex의 "interior 무조건 우선"을 "이상치에만 발동"으로 좁혔고, 어선 overclaim은 내가 검증해 수정함(verify-codex-not-blindly).

10. open items / 후속


관련: brief.md(동일 폴더, 텍스트 원본) · 선행 진입점 메모리 session-20260601-w21b-w5-fp-spacetype-brief.