비-실내 공간(옥상·외부·선박)이 floor_plan_prompt에서 아파트처럼 과분할되는 구조 결함을 근본 수정한다. 핵심은 surface_role 공간유형 신호를 FP 생성 입력으로 전파하고, FP 프롬프트를 공간유형별로 분기하는 것(신규 pack v8, opt-in). STEP5-B sparse FP sidecar의 upstream 수정이며 sidecar는 건드리지 않는다.
| 항목 | 값 |
|---|---|
| 브랜치 | 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까지만) |
메모리 주장을 코드로 전수 확인함(Codex 아닌 실측).
SURFACE_ROLE_ENUM = {interior_room, exterior_plate, transition_zone, site_surface} — background_master_plan.py:24. 4개(이전 메모리엔 3개로 잘못 기재 → 정정).floor_plans[] 스키마 = fp_id / loc_id / space_key_hint / sub_location / scope / depends_on_fp — surface_role 필드 없음 (bgmp.py:107).floor_plan_prompt.py / floor_plan_render.py 에 surface_role 참조 0건 → FP 생성·렌더는 공간유형을 전혀 모른다.bgmp.py:245): interior_room bg만 depends_on_fp 필수. exterior/transition/site는 fp 불필요(금지는 아님).prompts/_base/floor_plan_prompt/7.202605291624/system.md
| Rule | 내용 | 문제 |
|---|---|---|
| Rule 2 | 모든 sub-area를 shared / private / service / threshold / utility zone으로 색칠 분할 | 실내 주거 zone 어휘 전제 |
| Rule 5 | bed / 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).
# 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-op ▼ run_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 코드 변경 불필요)
applied_bgs를 들고 있으므로 추가 checkpoint 로드 없이 applied_bgs[].surface_role로 fp 공간유형을 도출할 수 있다. 이것이 유일한 전파 지점.open_safe로 처리(=현 버그 재현 방지).| 입력 (applied_bgs의 surface_role 집합 S) | 결과 | 근거 |
|---|---|---|
| 전원 동일 (|S|=1) | 그 role 그대로 | invariant 4/5상 same sub_location→same fp = 정상 케이스 |
| 불일치 (|S|>1, 이상치) | conservative priorityinterior_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_safe는 prompt-input diagnostic 값 — FP output schema(numbered_elements/camera_recommendations) 필드가 아니다. 도출은 enum 집합연산(글자/substring 파싱 0 = 절대규칙 준수).
| space_type | 다이어그램 규칙 | self-fidelity 표현 |
|---|---|---|
interior_room | 현 v7 그대로: enclosed rooms / walls / swing-door arcs / zone color fills / 실내 furniture 글리프 | dwelling scale / room count |
exterior_plate | open plate: 가짜 둘러싼 벽 금지, parapet/난간=낮은 boundary edge(방벽 아님), swing-door 분할 금지, zone-cell 과분할 금지. 열린 표면 윤곽 + 고정 구조물(난간/계단/headhouse/개구부)만 | open boundary / edge / zone count |
transition_zone | full room 아님. 문턱/계단/입구/복도=통로·경계. 독립 방 cell 금지 | passage / threshold geometry count |
site_surface | exterior와 같은 open-surface branch, 문구만 더 넓게: larger outdoor/site layout, paths/edges/zones, not enclosed architecture | open boundary / edge / zone count |
open_safe (fallback) | 공간유형 불명. enclosed apartment 금지 + 데이터 명시 구조만 보수적으로, 임의 방 분할 금지 | invent 금지(보수적) |
Codex 5단계 순서 + A/B/C 확정 반영. 구현 순서 = 의존 역순(pure helper → builder → pack → wiring → canary)로, 각 단계가 독립 green.
파일: 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 / 미지값 방어.
파일: 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)) # ★ 추가
{space_type_block} 토큰이 없다. str.replace(없는토큰, x) = 원문 그대로. 따라서 기존 selector 출력은 완전 동일. (테스트로 lock.)테스트: selector "5"/"6"/"7" 출력이 space_type 인자 유무와 무관하게 byte-identical / selector "8"은 block 삽입 확인.
신규: prompts/_base/floor_plan_prompt/8.<YYYYMMDDHHmm>/
| 파일 | 변경 |
|---|---|
schema.json | v7 byte-identical 복제 (output contract 불변 → validator 재사용) |
user_template.md | v7 + 신규 {space_type_block} 섹션 (예: ## Space type (read first) 자리) |
system.md | v7 + 공간유형 conditional 분기(§4.2 표) + Rule43 type별 표현 분기 |
space_type 값이 user prompt({space_type_block})에 들어가야 LLM이 구조적으로 인지. system.md + user_template.md 둘 다 변경 필수.| 파일 | 변경 |
|---|---|
floor_plan_prompt.py | PROMPT_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:116 | floor_plan_prompt_version: Literal["5","6","7","8"] = "5" (default 불변, "8" 추가) |
| test sync | config Literal / PROMPT_VERSION_MAP / _V6_COMPATIBLE_SELECTORS lock 테스트 갱신 |
_resolved_prompt_version()로 pack명을 바꿔 config_hash에 자동 반영 → re-run 전파. space_type은 upstream background_master_plan 데이터에서 나온 prompt input이므로 별도 hash 필드 불필요(master_plan 변경은 upstream_revision으로 잡힘).§7 참조. 코드 green과 분리 — 완성도는 여기서 판정.
| 대상 | 케이스 |
|---|---|
derive_fp_space_type | 전원동일(interior/exterior/transition/site 각각) · 불일치→priority+mix · empty→open_safe · 미지 enum 방어 |
build_fp_user_prompt | selector 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 lock | config Literal == PROMPT_VERSION_MAP keys, schema-sync 가드 |
| 회귀 | tests/pipeline 전체 green, default selector "5" 무변경 실증 |
※ 절대규칙: PASS 카운트는 deterministic 안전망일 뿐 — 프롬프트/렌더 품질 검증 아님. "테스트 통과 = 동작 검증"으로 보고 금지.
background_master_plan cp에서 직접 조회(cp 불변 유지, Codex B). 옥상이 정말 exterior_plate로 부여됐는지 등 확인.| 대상 | 기대 surface_role | 성공 기준 |
|---|---|---|
| 옥상 | exterior_plate | open plate — 가짜 둘러싼 벽·방 cell 없음, parapet=낮은 edge |
| site_surface 케이스(있으면) | site_surface | exterior와 분리 — 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 본 렌더).
floor_plan_render — 무변경(prompt만 받음)Codex 의견은 모두 검증 후 채택. 특히 Q1은 Codex의 "interior 무조건 우선"을 "이상치에만 발동"으로 좁혔고, 어선 overclaim은 내가 검증해 수정함(verify-codex-not-blindly).
vehicle 없음 → 추가 여부는 master_plan 작업과 함께 별도 결정.exterior_plate로 정확히 부여되는지 canary 1단계에서 확인. 오부여 시 본 fix만으로 불충분 → master_plan 분류 작업 선행(별도 brief).관련: brief.md(동일 폴더, 텍스트 원본) · 선행 진입점 메모리 session-20260601-w21b-w5-fp-spacetype-brief.