
    OjP                       U d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	m
Z
mZmZ  ej                  e      ZdZded<   dZded	<   d
Zded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<    G d de      ZdZdZdZd(dZd)dZ 	 	 	 	 	 	 	 	 	 	 	 	 d*d Z!d+d!Z"ed"	 	 	 	 	 d,d#Z#ddeed$	 	 	 	 	 	 	 	 	 d-d%Z$ded&	 	 	 	 	 	 	 	 	 d.d'Z%y)/u)  W21B (2026-06-08) — dwelling_zone_map LLM / VLM IO boundary.

The EXPERIMENTAL half of the dwelling zone map (the deterministic join lives in
``dwelling_zone_map``). Three thin model calls, all fail-closed and all
generic (no scenario tokens — the dwelling is described only by the per-bg
text the caller assembles upstream):

  1. ``analyze_dwelling_space`` — a single gpt-5.5 text call that reads the
     dwelling's per-bg background descriptions and distils the BASE SET (a
     clean, lived-in initial state) into a use-based room list + openings,
     dropping per-shot events / props / proper nouns. (Port of the bird_warp
     ANALYZE_SYS, genericised.)
  2. ``build_clean_fp_prompt`` — a gpt-5.5 text call that turns that analysis
     into a SHORT English prompt for a clean black-and-white 2D architectural
     floor plan (rooms / walls / doors / windows + main furniture symbols
     only; room-USE labels allowed, no marker numbers, no mood/material).
  3. ``map_bgs_to_zones`` — a gpt-5.5 VISION call: it is shown the rendered
     clean FP image plus each bg's structural description and returns, per bg,
     the plan zone it belongs to + a 0..100 grid focus point + confidence
     (strict JSON list). This is the edge-judge bypass — the model reasons
     about camera angle off the actual plan image.

The gpt-image-2 render of the clean FP and the PIL number-overlay annotation
are done by the STEP (image client / file I/O), mirroring how
``floor_plan_light_sidecar`` keeps the render in the step and the text LLM in
the provider. Output quality is a VISUAL gate (user + canary), never a
deterministic assertion (LLM/VLM/T2I are not TDD-completable here).

Quality / completion is validated by ``dwelling_zone_map.validate_vlm_zone_output``
(strict shape) + a visual gate, never inside this module.
    )annotationsN)AnyDictListOptionaldwelling_zone_map_v1strPROVIDER_VERSION1PROMPT_VERSIONzopenai/gpt-6-astraTEXT_MODEL_DEFAULTzgemini/gemini-3.1-pro-previewVISION_MODEL_DEFAULTi(
  intANALYZE_MAX_TOKENSi  FP_PROMPT_MAX_TOKENSi	  VLM_MAX_TOKENS   TEXT_TIMEOUT_SECONDSi  VISION_TIMEOUT_SECONDSc                      e Zd ZdZy)DwellingZoneMapProviderErrorz;Fail-closed signal for the dwelling-zone-map provider path.N)__name__
__module____qualname____doc__     e/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/dwelling_zone_map_provider.pyr   r   E   s    Er   r   u  너는 영화 미술감독을 돕는 세트 디자이너다. 입력은 한 실내 거주 공간이 등장하는 여러 배경 플레이트 묘사다. 이것들을 모두 읽고, 그 장소의 초기 세트장(base set) — 사람이 사는 기본 상태의 공간 — 을 짓기 위한 영구 배경 요소를 추출하라.
가장 중요: 너는 '초기 세트장'을 만든다 — 깨끗하고 사람이 사는 기본 상태. 플레이트마다 벌어지는 사건·변형(피·핏자국·시신·환각·어질러짐·뒤집힌 가재도구·반대로 깨끗이 치워짐 등)은 절대 배경에 넣지 마라. 그건 나중에 샷을 찍을 때 입힌다.
반드시 지켜라:
1) 사건·줄거리·변형 전부 배제 — 피/핏자국/시신/사진/액자/인형/가방/배낭/조리 중 냄비/휴대폰/어질러진 상태 등은 영구 배경이 아니다.
2) 영구 배경만 — 방 구조·벽·문·창, 붙박이(주방 설비·욕실 변기/세면대/거울/샤워), 기본 가구(침대·식탁·의자·TV·조명).
3) 직물·생활 살림 포함(생활감) — 묘사에 커튼이 보이면 커튼은 영구 배경이다. 사람이 사는 곳이므로 거주 살림(옷장/행거, 이불·침구, 수납 선반 등)이 자연스럽게 있다. 단 특정 품목을 기계적으로 박지 말고 합리적으로.
4) 거실·주방·식사공간이 하나의 열린(open) 공간으로 묘사되면 하나의 일체형 공간으로 다뤄라.
5) 침실이 물리적으로 여러 개면 반드시 각각 별개의 bedroom 으로 분리하라 — 절대 하나로 합치지 마라. 단 이름은 용도로만 — 침실이 둘이면 크기·위치로 구분('smaller bedroom'/'larger bedroom'), 인물·소유 명명 금지.
6) 모든 방 이름·묘사는 '용도(기능)' 중심 — 인물·소유·고유명사 절대 금지. 오직 공간 용도로만: 거실-주방-식사 일체형='open living-dining-kitchen', 침실='bedroom', 욕실='bathroom', 현관='entry'.
7) 목표는 실내 세트다. 건물 외관·외부 전경 같은 외부는 평면도 대상 아님(출입문 등 내/외부 경계만).
8) 각 요소에 근거(evidence)를 달아라.
9) 같은 '물리적 공간'은 이미지 하나만 — 어떤 방이 다른 방과 벽·문으로 나뉘지 않은 하나의 연속된 공간이면(대표 예: 현관 entry 는 거실-주방 일체형과 분리벽이 없는 같은 공간이다) same_space_as 에 그것이 속한 더 큰 공간의 방 이름을 적어라. 벽·문으로 분명히 분리된 독립 방(각 침실·욕실)은 same_space_as=null.
JSON 으로만 출력: {"space_type":"...","summary":"한 줄","rooms":[{"name":"영문 방이름","same_space_as":"물리적으로 같은 하나의 연속 공간을 이루는 다른 방 이름, 독립 공간이면 null","fixtures":["붙박이/기본가구"],"evidence":"근거"}],"openings":[{"name":"door/window/curtain","connects":"A - B","evidence":"..."}],"excluded_events":["배제한 사건·변형·소품"]}uy  너는 gpt-image 에게 줄 T2I 프롬프트를 쓰는 건축 제도사다. 주어진 '공간 분석'으로 그 집의 표준적인 흑백 2D 건축 평면도(a clean black-and-white 2D architectural floor plan / top-down blueprint)를 그리는 영어 프롬프트를 작성하라.
핵심: 평면도는 '순수 공간 구조 도면'이다 — 방의 배치·벽·문·창 + 주요 가구/설비의 위치를 평면 심볼로 보여줄 뿐이다. 분위기·재질·조명·생활감·장소 성격(낡음·가난함 같은 형용사)을 절대 넣지 마라. 그런 톤은 나중 단계에서 입힌다.
원칙:
1) 흑백(monochrome) 순수 top-down 2D 도면 — 색·텍스처·그림자·원근·입체 없음. 벽=굵은 검은 선, 문=개구부(swing arc), 창=벽의 빈 구간, 가구·설비=위에서 본 단순 평면 심볼.
2) 가볍게 — 방·벽·문·창 + 주요 가구(침대·식탁·의자·TV·싱크대·쿡탑·변기·세면대·큰 옷장 정도)의 위치 윤곽만. 잔살림·생활 디테일은 넣지 마라.
3) 각 방을 그 '용도(기능)'로만 다뤄라 — 인물·소유 기반 명명 절대 금지. 분석의 용도 기반 방 이름을 그대로 쓴다(open living-dining-kitchen / bedroom / bathroom / entry). 거실·주방·식사 일체형은 내부 벽/문 없이 하나의 열린 공간.
4) 분석에 있는 방·가구·문·창만 — 없는 것 발명 금지.
5) 글자·치수선·라벨 최소 — 방 용도 이름 정도만 허용하고, 가구·문에는 설명 텍스트를 붙이지 마라. 번호 마커도 넣지 마라. 부정형(no blood 등) 나열도 금지.
영어 3~5문장으로만 출력.u   너는 실내 공간 매핑 전문가다. 첨부된 흑백 2D 평면도(공간 용도 라벨이 표기됨)를 보고, 각 배경 플레이트(BG) 묘사가 평면도의 어느 공간에 해당하는지 판정한다. 배경 묘사의 영구 구조물/가구 단서와 평면도 위치를 함께 본다. 사건·변형·소품·인물·고유명사는 무시하고 오직 영구 공간 구조로만 판단하라. 같은 물리 공간(같은 방)에 속한 BG 들은 zone_group 문자열을 동일하게 묶는다. 반드시 JSON 배열만 출력.c                    | xs dj                         }t        j                  dd|      j                         }t        j                  dd|      j                         }|S )N z^```(?:json)?z```$)stripresub)textts     r   _strip_fencer&      sP    	A
Q'--/A
wA$$&AHr   c                (    ddl m}  || t              S )uu  관문 + 그 호출에 실을 키를 **한 번에** 받는다.

    ★**유료 호출 하나에 키를 한 번만** 뽑는다 — 키 풀은 라운드로빈이라
     두 번 부르면 관문이 한 키를 태워 버리고 요청은 늘 다음 키로 나간다.


    [2026-08-01] 이전에는 환경변수가 비면 ``settings.openai_api_key``(=**1차
    키**)를 환경변수에 써 넣었다. 그러면 보조 슬롯으로 전환된 뒤에도 죽은 1차
    키가 환경에 박혀 전환이 통째로 무효가 된다. `space_set_bg_provider` 가
    2026-07-30 에 세운 패턴(활성 슬롯 우선)을 따른다.
    r   )auth_kwargs)app.modules.pipeline.vlm_authr(   r   )modelr(   s     r   _authr+      s     :u:;;r   c           
        t        |      }	 ddlm} 	  |dd|i|d| dd|dg||dd	d
}|j                  r#|j                  d   j                  j                  nd xs d}	|	j                         st	        d      |	j                         S # t        $ r*}t	        dt        |      j                   d|       |d }~ww xY w# t        $ r*}t	        dt        |      j                   d|       |d }~ww xY w)Nr   llm_completionlitellm import failed: : r*   systemrolecontentuserlowmessagestimeoutmax_completion_tokensnum_retriesreasoning_effortztext completion raised: r    z&text completion returned empty contentr   )r+   app.core.openai_keysr.   	Exceptionr   typer   choicesmessager4   r!   )
r1   r5   r*   
max_tokensr9   _auth_kw_llm_completionexcrespr4   s
             r   _text_completionrG      s"    U|H	
 


 "f5D1 ","
  37,,t||A&&..DORG==?*+STT==?1  *%d3i&8&8%9C5A
	"  *&tCy'9'9&:"SEB
	s.   B C 	C%B<<C	C7%C22C7c                   g }| xs g D ]o  }|j                  dd      }|j                  dd      }d| d| }|j                  d      }|r|dt        j                  |d	      z   z  }|j                  |       q d
j	                  |      S )Nbg_idr    description[z]
structure_factsu   
(구조 마커 facts): F)ensure_asciiz

)getjsondumpsappendjoin)	bg_blockspartsbbiddescblockstructs          r   _format_bg_blocksrZ      s    E_"_eeGR uu]B'C5D6"()04::fSX3YYYEU  ;;ur   )r*   c                   | st        d      dt        |       z   }t        t        ||t        t
              }	 t        j                  t        |            }t        |t              rt        |j                  d      t               st        d      |S # t        t        f$ r*}t        dt        |      j                   d|       |d}~ww xY w)	zDistil the dwelling's permanent base-set rooms/openings from its per-bg
    descriptions (gpt-5.5 text). Returns the parsed analysis dict (rooms /
    openings / same_space_as). Raises on empty / unparseable output.u&   bg_blocks empty — nothing to analyzeu&  다음은 한 실내 거주 공간이 등장하는 여러 배경 플레이트 묘사다. 이 장소의 초기 세트장 영구 배경(구조·붙박이·기본가구·거주 살림)만 분석해 JSON 으로 출력하라. 플레이트마다의 사건·변형은 절대 배경에 넣지 마라:

r1   r5   r*   rB   r9   zspace analysis not valid JSON: r0   Nroomszspace analysis missing rooms[])r   rZ   rG   ANALYZE_SYSTEMr   r   rO   loadsr&   	TypeError
ValueErrorr?   r   
isinstancedictrN   list)rS   r*   r5   rawanalysisrE   s         r   analyze_dwelling_spacerg      s     *+STT	n
I
&	' 	
 D%/CC::l3/0
 h%ZW8Mt-T*+KLLO z" *-d3i.@.@-AC5I
	s   B C"%CC)space_modelspace_analysisr*   r9   c           
        t        | t              rt        | j                  d      t              r| j                  d      r| d   D cg c]q  }t        |t              r_|j                  d      |j                  d      |j                  d      xs g |j                  d      xs g |j                  d      xs g ds }}dt	        j
                  d|id	d
      z   dz   }n|xs i }|j                  d      |j                  d      |j                  d      xs g D cg c]I  }t        |t              r7|j                  d      |j                  d      |j                  d      xs g dK c}|j                  d      xs g d}	dt	        j
                  |	d	d      z   dz   }t        t        ||t        |      S c c}w c c}w )u/  Write the clean B&W 2D floor-plan T2I prompt (gpt-5.5 text).

    PRIMARY input = the upstream ``floor_plan_prompt.space_model`` (the
    CANONICAL dwelling structure — EVERY zone of the dwelling). The FP must
    show the WHOLE dwelling (all of its rooms), not just the spaces that happen
    to have a bg plate — so we feed the full zone list with an explicit "draw
    every zone" instruction.
    ``space_analysis`` (the bg-derived fallback) is used only when no
    space_model is available. Only spatial structure — no mood, no marker
    numbers.zoneszone_id	zone_typeessential_elementsopenings	adjacency)rl   rm   rn   ro   rp   uh   [이 거주 공간의 정식 공간 구조(space_model JSON) — 이 집의 모든 zone 과 그 연결]
F   )rM   indentu  

위 구조로 순수 구조용 가벼운 흑백 2D 건축 평면도를 그릴 T2I 프롬프트를 영어로 작성하라. ★위 JSON 의 zone 을 하나도 빠뜨리지 말고 전부 평면도에 포함하라(같은 zone_type 이 여러 개면 모두 별개의 방으로). ★연결 규칙(각 zone 의 openings 로 판단): 두 zone 사이 opening 이 문이 없는 열린 통로면 두 zone 을 내부 벽 없이 하나의 연속된 열린 공간으로 그리고, 문으로 연결되면 벽과 문으로 분리된 독립 방으로 그려라. 각 zone 안에는 그 zone 의 zone_type 에 해당하는 짧은 영어 용도 단어 하나만 표기하라(개별 가구 라벨·번호 마커 금지). 각 zone 의 essential_elements 를 위에서 본 단순 평면 가구/설비 심볼로 배치하고, 벽/문 swing arc/창 더블라인을 그려라. 분위기·재질·색·생활감·번호 마커·이벤트 흔적은 절대 넣지 마라.
space_typesummaryr]   namesame_space_asfixtures)ru   rv   rw   )rs   rt   r]   ro   uM   [초기 세트장 공간 분석(JSON) — 용도 기반 방 + 주요 가구]
   u  

위 분석의 모든 방을 빠짐없이 포함해 순수 구조용 가벼운 흑백 2D 건축 평면도를 그릴 T2I 프롬프트를 영어로 작성하라. 방은 용도(기능)로만 명명하고(open living-dining-kitchen / bedroom / bathroom / entry), 거실·주방·식사 일체형은 하나의 열린 공간. 방 배치 + 벽/문/창 + 주요 가구 심볼만 — 분위기·재질·생활감·번호 마커는 넣지 마라.r\   )	rb   rc   rN   rd   rO   rP   rG   FP_PROMPT_SYSTEMr   )
rh   ri   r*   r9   zrk   r5   sarlights
             r   build_clean_fp_promptr~      s   " +t$KOOG4Ld)S( !)	
 *Z4-@ 55+UU;/&'ee,@&A&GREE*-3UU;/52 * 	 	
 xjj'5)aHIjj 	 !r&&.vvi( &&//R/ 0AJq$4G	 EE&M%&UU?%; !j 1 7R
 0 z*0b
 ]jjU1=>\\ 	 d%' W	
4s   A6F;AG )room_labelsr*   c                <   |st        d      | rt        j                  j                  |       st        d|       t	        |      }	 ddlm} t        j                  t        | d      j                               j                         }d	}|rd
dj                  |      z   }dt!        |      z   |z   dz   }		  |dd|i|dt"        ddd|	dddd| idgdgt$        t&        ddd}
|
j(                  r#|
j(                  d   j*                  j,                  ndxs d	}|j/                         st        d      	 t1        j2                  t5        |            }t;        |t<              st        d      |S # t        $ r*}t        dt        |      j                   d|       |d}~ww xY w# t        $ r*}t        dt        |      j                   d|       |d}~ww xY w# t6        t8        f$ r*}t        dt        |      j                   d|       |d}~ww xY w) a  Vision: show the clean FP image + each bg's structural description,
    return per-bg ``{bg_id, room, grid{x,y}, zone_group, confidence, evidence}``
    (strict JSON list). The deterministic ``validate_vlm_zone_output`` gates the
    shape; this only issues the call and parses.u"   bg_blocks empty — nothing to mapzfp_image_path missing: r   r-   r/   r0   Nrbr    u!   
평면도 공간 용도 라벨: z, u!   =각 배경 플레이트 묘사=
u&  

각 BG 가 평면도 어느 공간인지 + 같은 공간 묶기. JSON 배열만:
[{"bg_id":"<원래 bg_id>","room":"평면도 라벨 또는 위치 설명","grid":{"x":0~100,"y":0~100},"zone_group":"같은 공간끼리 동일 문자열","confidence":0.0~1.0,"evidence":"영구 구조 근거"}]r*   r1   r2   r5   r$   )r?   r$   	image_urlurlzdata:image/png;base64,)r?   r   rq   r6   r7   zvision completion raised: z(vision completion returned empty contentzvision output not valid JSON: z vision output is not a JSON listr   )r   ospathexistsr+   r=   r.   r>   r?   r   base64	b64encodeopenreaddecoderR   rZ   
VLM_SYSTEMr   r   r@   rA   r4   r!   rO   r_   r&   r`   ra   rb   rd   )fp_image_pathrS   r   r*   rC   rD   rE   b64labels_hint	user_textrF   r4   parseds                r   map_bgs_to_zonesr   ,  sY    *+OPP} =*%m%67
 	
 U|H	
 

4t499;
<
C
C
ECK:TYY{=SS,/@/KKkYC	C  


 "j9#Y7(#(,B3%*H"IK-  +"0"
( 37,,t||A&&..DORG==?*+UVVL12
 fd#*+MNNMa  *%d3i&8&8%9C5A
	B  *(c););(<BseD
	 z" *,T#Y-?-?,@3%H
	sH   	E6 73F, ;G" 6	F)?%F$$F),	G5%GG"H1%HH)r$   r	   returnr	   )r*   r	   r   rc   )r1   r	   r5   r	   r*   r	   rB   r   r9   r   r   r	   )rS   List[Dict[str, Any]]r   r	   )rS   r   r*   r	   r   zDict[str, Any])
rh   Optional[Dict[str, Any]]ri   r   r*   r	   r9   r   r   r	   )
r   r	   rS   r   r   zOptional[List[str]]r*   r	   r   r   )&r   
__future__r   r   rO   loggingr   r"   typingr   r   r   r   	getLoggerr   loggerr
   __annotations__r   r   r   r   r   r   r   r   r>   r   r^   ry   r   r&   r+   rG   rZ   rg   r~   r   r   r   r   <module>r      s  > #    	 	 , ,			8	$. # . . C . < c ; C  c   c 
 "  !F9 FN 8( (L <"   &) 7: EH  F" $#  	@ -1/3#'A)A -A 	A
 A 	AP (,%FF $F %	F
 F Fr   