
    Yojq                       U d Z ddlmZ ddlZddlmZ ddlmZmZm	Z	m
Z
 ddlmZmZ ddlmZ ddlmZ  ej$                  e      Zg d	Zd
ZdZdZ	 	 	 	 d(dZ	 	 	 	 	 	 	 	 	 	 d)dZ	 	 	 	 d*dZddddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d+dZdddddiddddddd d!gd"Zded#<   d$Z	 	 	 	 	 	 	 	 d,d%Z dddd&	 	 	 	 	 	 	 	 	 	 	 	 	 d-d'Z!y).u  location_aerial — W-L (2026-07-03): 야외 location 항공뷰 배치도(야외 fp) SOT.

같은 야외 location 의 bg plate 들이 샷 관점별로 쪼개진 평면도들을 1순위 ref 로
받아(같은 loc 에 fp 가 여러 장 = fp 분열) plate 끼리 서로 다른 위상으로
갈라지는 결함의 근본 대응. 야외 location 의 위상 SOT 는 **위에서 내려다본
배치도(실내 floor plan 과 같은 도면 언어의 야외판) 1장/loc** 이며, 야외 bg
렌더는 분열된 개별 fp 대신 이것을 1순위 ref 로 받는다(실내 loc 은 기존 실내
fp 1순위 유지 — 이 모듈 무관).

★사용자 7차 정정(2026-07-03) 반영: "항공뷰"는 실사 드론 사진이 아니라
실내 fp 처럼 **위에서 그린 도면형 배치도**를 뜻한다. 또한 건물/구조물에
속한 상층 location 은 그 **구조물 레벨의 평면**으로 그려야 한다
(지면 위 단독 부지로 상상 금지 — 판단은 classify 산출 데이터가 이끈다).

- ``build_loc_aerial_context`` — background_classify ``building_groups`` 구조
  조인(순수): loc별 is_indoor/group_mixed/label/summary. 파일명/라벨 파싱 0.
- ``build_aerial_establishing_prompt`` — 도면형 site-plan T2I 프롬프트(generic
  템플릿, location label/summary 는 데이터로만 주입, 시나리오 토큰 0,
  마커/텍스트 금지).
- ``render_location_aerial`` — loc당 1장 생성(T2I; 같은 building 그룹에 실내가
  공존(mixed)하면 indoor floor plan 을 I2I ref 로 footprint 정합). 기존
  마커형 항공뷰(outdoor_site_layout_provider.generate_location_aerial_base —
  composition guide 전용 다이어그램)와 별개 producer.
- ``AERIAL_SITE_GUIDANCE`` — bg plate 렌더에 aerial 배치도를 1순위 ref 로
  첨부할 때 프롬프트에 덧붙이는 위상 계약(중립 — 도면 스타일 복제 금지).

소비자(background_render)가 flag ``outdoor_aerial_reference_enabled`` ON 일 때만
호출한다(OFF = 미호출 = 기존 경로 byte-identical).
    )annotationsN)Path)AnyDictListOptional)ImageCallBudgetExceededreserve_current_call)call_gpt_image_bytes)PromptSanitizer)	AERIAL_SITE_GUIDANCEAERIAL_FP_COHERENCE_GUIDANCEPLATE_ORDER_SCHEMAbuild_loc_aerial_context build_aerial_establishing_promptcompute_aerial_context_hashdecide_group_plate_orderrender_location_aerialvalidate_plate_orderz4.202607040120u  

The FIRST attached image is a flat TOP-DOWN SITE PLAN of this location (an outdoor floor plan) — the location's spatial MASTER reference. It fixes the real layout: each structure's footprint, size and position, the entrances and approach paths, level changes, and where the permanently installed elements sit. Keep this render consistent with that layout — anything shown here must exist where the plan puts it, and the scene must read as standing somewhere INSIDE that plan. Do NOT copy the plan's drawing style: no plan lines, flat colour fills, symbols or top-down map view in the output — render a fully photoreal scene at the camera position and angle this prompt describes. The plan marks doorways, windows and other openings with small hollow-circle symbols — those circles are drafting notation ONLY: never draw circles, rings or marker overlays in this render, and never reproduce any circular or ring-shaped marking from the plan as a painted pattern, decal or decoration on any surface; at each marked position render the real door, window, gate or opening itself, on the wall the plan puts it on. Where the plan marks blank lettering panels, this render shows the place's own lettering in those positions — the same characters and mark wherever it appears. Lighting, weather and time of day follow THIS prompt. If any earlier instruction calls a floor plan the base visual reference, that means THIS attached site plan.u  

The ATTACHED image is the TOP-DOWN INTERIOR FLOOR PLAN of the building this location belongs to — a STRUCTURAL REFERENCE ONLY, never content to copy. Draw the site plan described above as a fresh drawing in the same drafting style. Keep the building's footprint size, shape, proportions and the placement of its doors, windows and level access consistent with that interior plan, so the two plans read as the same single building. Place the site plan's hollow-circle opening markers so they COINCIDE with the interior plan's door, window and stair positions on the shared walls — the two plans' openings must line up as the same physical openings. Do NOT copy the interior plan's room contents, furniture symbols, text or numbers into the site plan.c                   i }| xs g D ]  }t        |t              s|j                  d      xs g D cg c]3  }t        |t              r!t        |j                  d      t              r|5 }}|sit	        |j                  d      xs d      }|st        d |D              }t        d |D              }|xr |}|D cg c]  }t	        |j                  d      xs d      rgt	        |j                  d      xs d      t	        |j                  d      xs d      t	        |j                  d	      xs d      t        |d         d
 }	}t	        |j                  d      xs d      }
|D ]x  }t	        |j                  d      xs d      }|r||v r(|t        |d         |t	        |j                  d      xs d      t	        |j                  d	      xs d      |
|	d||<   z  |S c c}w c c}w )uw  background_classify ``building_groups`` → loc별 aerial 컨텍스트 (순수).

    반환: ``{loc_sid: {"group_id", "is_indoor", "group_mixed", "label",
    "summary", "anchor_loc", "group_members"}}``.

    ★8차 정정: aerial 산출 단위는 loc 가 아니라 **같은 장소 그룹**
    (building_groups 그룹 = 하나의 건물/구조물/부지) — 같은 그룹의 야외
    loc 들은 통합 배치도 1장을 공유한다. ``group_members`` 는 그룹 전
    멤버(실내 포함)의 {loc_id, label, summary, is_indoor} 리스트로,
    통합 배치도 프롬프트의 place 데이터가 된다.

    building_fp_link.build_group_membership_by_loc 와 같은 구조 조인 규칙
    (``is_indoor`` 가 bool 아닌 멤버 제외 — 추측 금지, group_id 없는 그룹 제외,
    같은 loc 여러 그룹이면 first-wins = classify 산출 순서 결정론).
    members	is_indoorgroup_id c              3  &   K   | ]	  }|d      ywr   N .0ms     Z/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/location_aerial.py	<genexpr>z+build_loc_aerial_context.<locals>.<genexpr>   s     9A;s   c              3  (   K   | ]
  }|d       ywr   r   r   s     r!   r"   z+build_loc_aerial_context.<locals>.<genexpr>   s     >gan,gs   loc_idlabelsummaryr$   r%   r&   r   
anchor_loc)r   r   group_mixedr%   r&   r(   group_members)
isinstancedictgetboolstrany)building_groupsoutgroupr    r   r   
has_indoorhas_outdoormixedmember_datar(   locs               r!   r   r   y   s   $ &(C &B&%&		),22
2!!T"z!%%2Dd'K 2 	 
 uyy,23999
>g>>*{ 	
 155?(b) aeeHo34QUU7^1r2quuY/526!!K.1	  	 	
 <06B7
AaeeHo+,C#*$!!K.1$QUU7^1r2quuY/526(!,CH	 5 'N JI
	
s   8G/BG4c                   ddl }ddl}t        | xs d      t        |xs d      t        |xs d      |xs g D cg c]u  }t        |j                  d      xs d      t        |j                  d      xs d      t        |j                  d      xs d      t	        |j                  d            dw c}d	}|j                  |j                  |d
d      j                  d            j                         dd S c c}w )u  그룹 통합 배치도의 **입력 구조 컨텍스트** 해시 (순수, sha256 16자).

    Codex 8차 NARROW: 캐시 재사용을 prompt_version 만으로 판정하면 같은
    group_id 가 유지된 채 building_groups 멤버/label/summary/is_indoor/
    anchor_loc 또는 mixed fp 링크가 바뀌는 resume 에서 낡은 배치도가
    부활한다. 이 해시는 구조 데이터만 접는다: group_id + 멤버 리스트
    (loc_id/label/summary/is_indoor, classify 산출 순서 보존) + anchor_loc
    + mixed fp SOT(fp_id — 링크 유무·정체 변화 감지). 소비자는
    prompt_version AND context_hash 둘 다 일치할 때만 캐시를 재사용한다.
    r   Nr   r$   r%   r&   r   r'   )r   r(   fp_sotr   TF)	sort_keysensure_asciizutf-8   )	hashlibjsonr/   r-   r.   sha256dumpsencode	hexdigest)r   r   r(   r:   r>   _jsonr    payloads           r!   r   r      s    "  B'**+fl# mm
 $ aeeHo34QUU7^1r2quuY/526!!%%"45	 $
	G >>Gt%@	 ik#2 
s   A:C=
c                   g }| xs g D ]  }|j                  d      rdnd}dj                  d t        |j                  d      xs d      j                         t        |j                  d      xs d      j                         fD              }|s|j	                  d	| d
|         |rdj                  |      nd}dj                  ddd|ddddddddddg      S )u  같은 장소 그룹의 통합 배치도(site plan) T2I 프롬프트 (generic 템플릿).

    실내 floor plan 과 같은 도면 언어의 야외판 — 위에서 수직으로 내려다본
    flat 컬러 배치도(실사 사진/3D 아님). ★8차 정정: 그룹(하나의 건물/
    구조물/부지) 전 멤버를 **한 장에 통합** — 건물은 지붕뷰(roof plane,
    상층 요소가 그 위에 보임)+주변 지상 공간, 탈것/구조물은 전체 형태.
    상업/공공 시설의 간판 판 위치도 표기(도면이므로 빈 판 — 문자 금지).

    ``members`` 는 classify 산출 데이터({label, summary, is_indoor})로만
    주입 — 템플릿 자체는 어떤 시나리오에도 범용(특정 장소 명사 0,
    마커/라벨/텍스트 0 강제).
    r   zindoor partzoutdoor part c              3  $   K   | ]  }|r| 
 y wNr   )r   xs     r!   r"   z3build_aerial_establishing_prompt.<locals>.<genexpr>   s!      
!   s   r%   r   r&   z- (z) 
z&- an ordinary real-world outdoor site.uW  ONE flat TOP-DOWN SITE PLAN of a single real-world place, drawn in the exact drafting style of a clean architectural floor plan: flat colour fills, thin dark outlines, simple fixture symbols, straight-down orthographic view — NOT a photograph, NOT a 3D render, NOT an oblique aerial photo; no perspective, no photo textures, no cast shadows.u   This ONE place consists of these connected parts — draw them TOGETHER as one coherent whole (one building/structure and its immediate surroundings), never as separate lots:u  Draw the WHOLE place in one plan: the full outline of the main building or structure seen from directly above — its roof plane, with whatever stands on that top level drawn in place — plus every open ground or floor area around and on it, the entrances, stairs and approach paths that connect the parts, and every permanently installed element as a simple plan symbol in its true position. Indoor parts stay under the roof outline — do NOT expose their interior rooms; the roof plane and its top-level features represent the building. If the place is a vehicle or vessel, draw the ENTIRE craft from above as one complete shape. Where the place carries lettering, include those lettering panels at their true positions, drawn as blank panels. Keep the geometry simple, unambiguous and physically plausible — this plan is the place's master spatial reference that later renders must stay consistent with.u,  Frame the plan a little WIDER than the place itself: include a modest margin of the immediate surroundings — the adjacent street, ground, water or neighbouring structures the place actually touches — so the place reads in its real context. The place itself stays the dominant subject of the plan.u  Mark EVERY opening of the place — each doorway, gate, window and level-access point (the top of a stair, a ladder, a hatch) — with a small hollow CIRCLE symbol placed exactly ON the wall, parapet, railing or hull line that opening belongs to. An opening symbol must sit on a real physical boundary line — never floating in open space — and openings between two parts must be shared by both. Doors that lead into an enclosed part sit on that part's outline at the side where the entrance really is. These hollow opening circles must be the ONLY circular marks anywhere on the plan — do not add any other round motifs, ring patterns or circular decorations to roofs, decks, floors or walls.u  If the place's main level sits ON TOP of a building or structure (an upper level above the ground), the plan must make that unmistakable: draw the supporting building's full roof outline as that level's boundary, keep a clearly visible roof-edge/parapet line around it, place the level-access opening symbol where one arrives onto that level, and let the surrounding ground or street read as OUTSIDE and BELOW that outline — never draw an upper level as if it were its own ground-level lot with a yard of its own.zSTRICT: NO people, NO animals, NO text, letters, numbers, labels, arrows, logos or watermarks anywhere in the image. The ONLY marker symbols allowed are the hollow-circle opening markers and the standard flat plan fixture symbols described above.)r-   joinr/   stripappend)r   lines_partsr    tagbodyplace_blocks         r!   r   r      s      K]] uu[1m~xx 
AEE'N(b)//1AEE)$*+113
 
 SED623  -8$))K(0  99	% 		; 	
	 		?
 		" 		3 		EG G G    1536x864high   )building_fp_path	sanitizersizequalitymax_attemptscapture_input_image_idsDict[str, Any]c                (   t        |      }|duxr |j                         }|r	|t        z   }ddg ||t        dd}|}|
t	               }t        d|	dz         D ]  }||d<   	 |||dd}|r%t        d	
       t        | d||g|dd	|dd|
      }n#t        d
       t        | d|d|dd|dd|
      }|st        d      |j                  |       d|d<   t        |      |d<   ||d<   |c S  |S # t        $ r  t        $ r}t        |      j                         t        fddD              }|r||	k  r	 |j                  |dg t!        |d            }|j#                  dd      }|r+|}|d   j%                  |j#                  d              Y d}~=n*# t        $ r}d!| dd" |d#<   |cY d}~cY d}~c S d}~ww xY wt&        j)                  d$|||	t        |      dd"        ||	k\  rt        |      dd" |d#<   |cY d}~c S Y d}~d}~ww xY w)%u  같은 장소 그룹 1곳의 통합 항공뷰 배치도(site plan) PNG 생성.

    - ``place_id`` = building_groups 의 group_id(로그/캡처 메타 전용 —
      프롬프트에는 넣지 않는다). ``members`` = 그룹 전 멤버의
      {label, summary, is_indoor} 데이터.
    - ``building_fp_path`` 가 존재하면(mixed 그룹) I2I edit(fp ref) +
      AERIAL_FP_COHERENCE_GUIDANCE — 건물 footprint/개구부를 실내 fp 와 정합
      (같은 도면 언어). 없으면 순수 T2I.
    - moderation 류 차단은 PromptSanitizer 로 bounded retry (render_one_
      background 미러). 실패는 raise 하지 않고 status='failed' dict 반환 —
      호출자(background_render)는 해당 그룹의 aerial 없이 기존 경로로
      fail-safe 진행한다.

    Returns dict: ``{status, attempts, strategies, building_fp_used,
    prompt_used, prompt_version, final_block_reason, png_path?}``.
    )r   Nfailedr   )statusattempts
strategiesbuilding_fp_usedprompt_usedprompt_versionfinal_block_reason   ra   )modelrY   rZ   nz&background_render.location_aerial_edit)sourceeditlocation_aerialT)budget_sourceplace_idrc   )modeprompt	ref_pathscall_kwargscapture_rolecapture_metadatar\   z*background_render.location_aerial_generategenerateFzempty b64 responseokr`   png_pathrd   c              3  &   K   | ]  }|v  
 y wrI   r   )r   kmsgs     r!   r"   z)render_location_aerial.<locals>.<genexpr>  s!       	A S	s   )
moderationsafetycontent_policy
prohibitedpolicyblockedviolates	violationSAFETY   )original_promptblock_reasonblock_categoriesattemptsanitized_promptr   rb   strategyzsanitize_failed:    rf   z$location_aerial %s attempt %d/%d: %s)r   existsr   AERIAL_PROMPT_VERSIONr   ranger
   r   RuntimeErrorwrite_bytesr/   r	   	Exceptionlowerr0   sanitizeminr-   rN   loggererror)openai_clientimage_modelrn   r   out_pathrW   rX   rY   rZ   r[   r\   rp   fp_attachedinfocurrent_promptr   rr   pngexcis_moderationsr	sanitizedserz   s                          @r!   r   r   F  s   < .g>F"$.L3C3J3J3LK66 '/"D N#%	L1,-"ZZ	$"	K $CE*!)/0 +!2 E$,,0	& -D  %GI*!#)" +!2 I$,,1	& -D "#788  %!DN"8}D"0DKg .z KS ' 	 &	c(.."C  	  M <!7 "++(6%-)+ #GQ	 , B !#'92 >I )2\*11"&&2DE  ! !  +B40#6 -.KK  LL6'<S$3 ,&-0Xds^)* 'I&	sV   #BC11H4H9AFH	F?!F:-F?.H2H:F???H>HHobjectFarraytypestringu:   bg_id generation order — a permutation of the input list)r   itemsdescriptionzBone sentence: why the first bg_id best establishes the whole place)r   r   )orderfirst_reasonr   r   )r   additionalProperties
propertiesrequiredr   u  You order the outdoor background renders of ONE real-world place for sequential image generation.

The renders are generated one at a time. The FIRST render is generated with only the place's top-down site plan as reference, so it must be the view that best ESTABLISHES the whole place — the widest, most representative view of the main structure and its surroundings. Each LATER render is generated with the PREVIOUS successful render as its visual reference, so order the rest to maximize visual continuity: views that overlap or sit next to each other should be adjacent in the order, and wider views should generally come before tighter partial views of the same area.

Rules:
- Return EVERY bg_id from the input exactly once (a permutation).
- If a render lists depends_on entries, it must come AFTER all of them (it depicts a later state of that render).
- Judge only from the given data; do not invent anything about the place.c                   t        |      }t        |       t        |      k7  rdt        |        dt        |       S t        |       D ci c]  \  }}||
 }}}t        |      }|xs i j	                         D ]>  \  }}||vr|xs g D ]+  }	|	|v s|j                  |	d      ||   kD  s!d|d|	c c S  @ yc c}}w )u1  LLM 이 낸 순서의 결정론 검증 (순수). 위반 사유 문자열 / 정상 None.

    - 순열: candidate_ids 와 정확히 같은 집합, 중복/누락/외부 id 금지.
    - 의존: ``depends_on_by_bg[bid]`` 의 각 dep(후보 집합 내의 것만)가
      bid 보다 먼저 와야 한다.
    znot a permutation: got=z
 expected=zdependency violated: z before its dep N)listsorted	enumeratesetr   r-   )
r   candidate_idsdepends_on_by_bgcandibidposcand_setdepsdeps
             r!   r   r     s     De}t$%fUm_ 5t~'	
 !*% 0
1 0fa36 0C
14yH&,"335	Tc>:2:Ch3773#3c#h#>.sg5EcWMM  6  2s   B<)project_configopik_metadatamax_retriesc                   ddl }|D cg c]   }t        |j                  d      xs d      " }}|D 	ci c]K  }t        |j                  d      xs d      |j                  d      xs g D 	cg c]  }	t        |	       c}	M }
}}	t        |      ddddd}t	        |      dk  rd	|d
<   |S |j                  |dddd      }d}t        d|dz         D ]  }||d<   	  |dt        |t        |d|      }|j                  d      xs g D cg c]  }t        |       }}t        |||
      }|t        |      ||d<   d|d
<   t        |j                  d      xs d      |d<   d|d<   |c S  ||d<   t        j                  d|        |S c c}w c c}	w c c}	}w c c}w # t        $ r2}t        |      dd }t        j                  d| |||       Y d}~d}~ww xY w)uy  같은 장소 그룹 1곳의 plate 생성 순서를 LLM 으로 결정 (비차단).

    ``candidates`` 항목: ``{bg_id, location_label, location_summary,
    surface_role, state_label, depends_on, t2i_prompt}`` — t2i_prompt 는
    **전문**(자르기 금지 계약). ``depends_on`` 은 후보 집합 내 bg_id 만.

    반환: ``{"order", "order_source"('llm'|'fallback_structural'),
    "first_reason", "attempts", "error"}``. LLM 실패/검증 실패가
    ``max_retries`` 를 소진하면 입력 순서 그대로 fallback (구조 순서 =
    기존 lane 순서 — 결정론). 예외를 밖으로 던지지 않는다.
    r   Nbg_idr   
depends_onfallback_structural)r   order_sourcer   ra   r   rV   trivialr   zWOrder these renders for sequential chained generation. Return every bg_id exactly once.)place_rendersinstructionF)r<   indentrg   ra   background_plate_order)stepsystem_promptuser_promptresponse_schemar   schema_namer   r   llmr   r   r   z-plate_order group=%s attempt %d/%d failed: %su9   plate_order group=%s exhausted — 구조 순서 fallback)r?   r/   r-   r   lenrA   r   PLATE_ORDER_SYSTEMr   r   
ValueErrorr   r   warning)r   
candidatescall_structured_fnr   r   r   rD   cr   dr   r2   r   last_errr   resultrJ   r   r   r   s                       r!   r   r   '  sH   ( 8BC
1Sw-2.
MC
 	 A 	AEE'N b!UU<06B6$
6qCF6$
 	
 	   m$-C :'N
++'3	
   
K HK!O,!J	'-0' 2-4+F '-jj&9&?R&?A&?SV&?EA,}&68I$ ++ CL"'C"%fjj&@&FB"GCCLJ+ -8 CL
NNCXOJ} D$
T B  	3x~HNN?'; 	sG   %F9F.F F.F"FAF"FF""	G+'GG)r1   List[Dict[str, Any]]returnzDict[str, Dict[str, Any]])
r   r/   r   r   r(   r/   r:   r/   r   r/   )r   r   r   r/   )r   r   r   r/   rn   r/   r   r   r   r   rW   zOptional[Path]rX   zOptional[PromptSanitizer]rY   r/   rZ   r/   r[   intr\   zOptional[List[str]]r   r]   )r   	List[str]r   r   r   zDict[str, List[str]]r   zOptional[str])r   r/   r   r   r   r   r   Optional[Dict[str, Any]]r   r   r   r   r   r]   )"__doc__
__future__r   loggingpathlibr   typingr   r   r   r   app.core.image_call_budgetr	   r
   #app.modules.llm.gpt_image_primitiver   app.modules.prompt_sanitizerr   	getLogger__name__r   __all__r   r   r   r   r   r   r   r   __annotations__r   r   r   r   rS   r!   <module>r      s>  : #   , , E 8			8	$
* ) * @
G :)::z%% "% 	%
 % 	%Pe!e 	e^ (,+/37NN N 	N
 "N N %N )N N N N 1N Nv ! h'L	
 
  .)'& N . * + 	B 04.2TT %T 	T
 -T ,T T TrS   