
    Oj                    ~   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
 ddlmZ ddlmZmZmZmZmZ ddlmZmZmZ ddlmZ dd	lmZ dd
lmZ ddlmZmZ ddl m!Z!  ejD                  e#      Z$ ejJ                  d      Z&dZ' ejJ                  d      Z(dZ)	 	 	 	 	 	 	 	 	 	 	 	 ddZ*	 d 	 	 	 	 	 	 	 	 	 	 	 	 	 d!dZ+d"dZ,	 	 	 	 	 	 d#	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d$dZ-	 	 	 	 	 	 d%dZ.	 	 	 	 	 	 d&	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d'dZ/	 	 	 	 	 	 	 	 	 	 	 d(	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d)dZ0	 	 	 	 	 	 d*dZ1	 	 	 	 	 	 	 	 d+dZ2	 	 	 	 	 	 d,dZ3	 	 	 	 	 	 	 	 d-dZ4	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d.dZ5d/d0dZ6	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d1dZ7y)2u  background_chain_render — chain plan에 따라 location별 배경 PNG를 체이닝 생성.

PR #4 (2026-04-27). background_chain_planning 결과를 입력으로 받아:
  Phase 1 (per node): gpt-5.5로 노드별 t2i prompt 생성
  Phase 2 (per node): gpt-image-2 images.edit로 PNG 생성
    - root anchor: location의 entity reference image를 ref로 사용 (없으면 text-only generate)
    - 자식 node: 부모 PNG ref + 자식 prompt → images.edit
  PromptSanitizer로 moderation 차단 시 film_previs/movie_poster/aftermath retry.

체크포인트 출력:
  data.locations[loc_id].nodes[].{...planning fields..., t2i_prompt, image_path,
                                  render_status, render_attempts, sanitize_strategies,
                                  shot_guides}  # Phase 2: list of {shot_id, guide}
  data.locations[loc_id].shot_backgrounds[].{scene_index, shot_index, shot_label,
                                              node_id, image_path}
    )annotationsN)ThreadPoolExecutoras_completed)Path)AnyDictListOptionalTuple)ImageCallBudgetExceededbind_current_budgetreserve_current_call)call_gpt_image_bytes)call_structuredcompute_dag_levels)load_promptload_schema)PromptSanitizeru,   [ㄱ-ㆎ가-힣一-鿿㐀-䶿豈-﫿぀-ヿ])
moderationsafetycontent_policy
prohibitedpolicyblockedviolates	violationz^[a-z0-9][a-z0-9_]*$u   BACKGROUND-ONLY architectural still — empty space, NO people, NO faces, NO body posture, NO action, NO weapons, NO blood. Photoreal scene without any human figures.

c                   t        dd      }|r=d|j                  dd       d|j                  dd       d|j                  d	d       d
}nd}g }|D ]9  }|j                  d|j                  dd       d|j                  d	d              ; |rdj                  |      nd}	dd}
|j	                   |
|        |
|      xs d |
|j                  dd             |
|j                  dd             |
|j                  dd             |
|j                  d	d             |
t        j                  |j                  dg       d             |
|      t        |       |
|	      
      S )Nbackground_chain_renderuser_templatezid: id z
label: labelz
description: descriptionz
[The parent's already-rendered photo will be the reference image for this node. Reuse parent's atmosphere/material/lighting language. Explicitly mention each shared_visual_anchors_with_parent entry.]u   (this is a ROOT anchor — no parent. The location reference image will be the input. Thoroughly describe wall/floor/ceiling/lighting/palette since later children inherit from this rendered photo.)z- shot_id?z: 
z
(no shots)c                N    | xs dj                  dd      j                  dd      S )Nr"   {z{{}z}})replace)ss    b/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/background_chain_render.py_escz)_build_user_prompt_for_node.<locals>._esci   s&    R  d+33C>>    z(no description)kind!shared_visual_anchors_with_parentF)ensure_ascii)
location_idlocation_descriptionnode_id	node_kind
node_labelnode_descriptionshared_anchors_jsonparent_block
shot_countshots_block)r,   strreturnr=   )r   getappendjoinformatjsondumpslen)r3   r4   nodeparentshots_in_nodetemplater:   shots_linesr,   r<   r.   s              r-   _build_user_prompt_for_noderK   I   s    4oFH6::dB'( )jj"-. /"JJ}b9: ;89 	) 	  Ky#&'r!%%r*B)CD	
  -8$))K(\K? ??%!"67M;MTXXdB'(txx+,"-.dhh}b9: HH8"="
  ,'}%%   r/   c                *   t        dd      }t        dd      }t        | ||||      }t        d||||      }	|	j	                  d      xs dj                         }
t        j                  |
      rt        d|j	                  d      d	      |	j	                  d
      xs g }g }t        |t              r\|D ]W  }t        |t              s|j	                  d      xs d}|j	                  d      xs d}|sA|sD|j                  ||d       Y |
|fS )u[  노드 1개에 대한 t2i prompt + shot_guides 생성. 영어 ASCII만 검증.

    Returns:
        (t2i_prompt, shot_guides)
        - t2i_prompt: PNG 생성용 prompt (영어 한 단락)
        - shot_guides: list of {"shot_id": str, "guide": str}. v2 schema 미지원 prompt
          버전 사용 시 빈 list 반환 (legacy fallback).
    r   systemschema)stepsystem_promptuser_promptresponse_schemaopik_metadata
t2i_promptr"   znode r!   ua    t2i_prompt contains non-ASCII text (Korean/Hanja/kana detected — universal-noun rule violated)shot_guidesr%   guide)r%   rV   )r   r   rK   r   r?   strip_NON_ASCII_TEXT_REsearch
ValueError
isinstancelistdictr@   )r3   r4   rF   rG   rH   rS   rM   rN   userresultt2ishot_guides_rawrU   sgsidrV   s                   r-   generate_node_promptrd   }   s)     2H=F2H=F&)4D &#F ::l#)r
0
0
2C  %DHHTN% &L M
 	

 jj/52O(*K/4(!Bb$'&&#)rCFF7O)rEu""sU#CD " r/   c                f    t        |       j                         t        fdt        D              S )Nc              3  &   K   | ]  }|v  
 y wN ).0kmsgs     r-   	<genexpr>z/_looks_like_moderation_block.<locals>.<genexpr>   s     6!5AqCx!5s   )r=   lowerany_MODERATION_KEYWORDS)excrk   s    @r-   _looks_like_moderation_blockrq      s%    
c(..
C6!5666r/   c
                   |	||gng }	|	D 
cg c]  }
|
|
j                         s|
 }}
ddg d|sdnt        |      dk(  rdndt        |       d}|}t        d|dz         D ]  }||d	<   	 |||dd
}t        |      dk\  r/t        d       t	        | d|||dd|d   t        |      d      }nYt        |      dk(  r&t        d       t	        | d|||dd|d   dd      }n%t        d       t	        | d|d|dd|d   dd      }|st        d      |j                  |       d|d<   |c S  |S c c}
w # t        $ r  t        $ rv}t        |      rt        j                  d|t        |      dd        |||k\  rt        |      dd |d<   |cY d}~c S 	 t        |d      }|j                  |dg |      }|d    }|j                         j!                  d!      s	t"        |z   }|}|d"   j%                  |j'                  d#             n=# t        $ r1}t        j)                  d$|       d%| |d<   |cY d}~cY d}~c S d}~ww xY wY d}~t        j)                  d&|t        |      dd        ||k\  rt        |      dd |d<   |cY d}~c S t+        j,                  t        d|z  d'             Y d}~Yd}~ww xY w)(u.  단일 노드 이미지 생성. ref_paths 1+ 지원 (multi-image edit, gpt-image-2).

    ref_paths 우선. ref_path는 backward-compat (단일 ref). 둘 다 None이면 [] (text_only).

    Returns:
        {"status": "ok"|"failed", "attempts": int, "strategies": [str],
         "final_block_reason": str | None, "ref_used": str}

    ref_used 라벨:
      - "text_only" — 0 refs
      - "ref" — 1 ref (기존 호환)
      - "refs_N" — N >= 2 refs (multi-image edit)

    의미 라벨(parent/location)은 caller(render_one_location)가 결정.
    Nfailedr   	text_only   refrefs_)statusattempts
strategiesfinal_block_reasonref_usedry   )modelsizequalityn   z"background_chain_render.edit_multi)sourceeditbackground_chain_noder|   )budget_sourcer|   	ref_count)modeprompt	ref_pathscall_kwargscapture_rolecapture_metadataz#background_chain_render.edit_singlez background_chain_render.generategeneratezempty b64 responseokrx   z4render_node_image: moderation block (attempt %d): %s      r{      SAFETY)original_promptblock_reasonblock_categoriesattemptsanitized_promptzBACKGROUND-ONLYrz   strategyz&render_node_image: sanitize failed: %szsanitize_failed: z6render_node_image: non-moderation error attempt %d: %s   )existsrE   ranger   r   RuntimeErrorwrite_bytesr   	Exceptionrq   loggerwarningr=   minsanitizelstrip
startswith_BACKGROUND_ONLY_REINFORCEMENTr@   r?   errortimesleep)openai_clientimage_modelr   out_pathref_path	sanitizerr~   r   max_attemptsr   p
valid_refsinfocurrent_promptr   r   pngrp   	s_attemptsanitize_result	sanitizedses                         r-   render_node_imager      sD   8 "*"6XJB	 'GY!-AHHJ!YJG ")Kj/Q.Z)*
D NL1,-"Zb	,
 %"	K :!#$,PQ*!)( +!8)M$($4%(_& ZA%$,QR*!)( +!8)N$($4%&& %,NO*!#)" +!8)K$($4%&& "#788  %!DNK{ .L Kk HZ ' 	 $	,+C0JSXds^ $<(?14S$3D-.K  #GQI&/&8&8(6%-)+ )	 '9 'O !00B CI %++-889JK$BY$N	%.N&--o.A.A*.MN   LL!I2N3DRD1ID-.KK  LLQ #c(4C.2,&-0Xds^)*JJs1w;*++I$	,sx   D6D6D6/CD;;KAJ?KA3HJ?	II:I;J??KIJ?:J?K"J??Kc                N    |j                  |       }|r|j                         r|S y rg   )r?   r   )r3   location_ref_pathsr   s      r-   _resolve_location_ref_pathr   U  s'     	{+AQXXZr/   c                	   || z  }|j                  dd       t        |j                  dg       xs g       }|D ci c]  }|d   |
 }}t        |j                  dg       xs g       }|j                  d      xs |j                  d      xs d}i }i }d	}|D ]  }|j                  |      }|st        j	                  d
| |       /t
        j                  |xs d      s)t        j	                  d| |       i |ddd||<   |dz  }q|j                  d      xs dj                         }|r|j                  |      nd}g }|j                  dg       xs g D ]N  }|r6||v r2t        ||         }|j                  d|       |j                  |       ;|j                  |dd       P 	 t        | |||||
      \  }}|| dz  } 	 | j                         j                  |j                                g }!d}"d}#|!|j%                         r|!j                  |       |r,||v r(||   }$|$j%                         r|!j                  |$       d}"|"s!t'        | |      }$|$|!j                  |$       d}#|duxr |j%                         }%|!sd}&n|%r|"rd }&n|%r|#rd!}&n|%rd"}&n|"rd#}&n|#rd$}&nd}&t)        |||| |!||||	%	      }'|&|'d&<   |'d'   d(k(  r'| ||<   i ||t        |       d(|'d)   |'d*   |&|d+||<   Yi ||dd,|'d)   |'d*   |'j                  d-      xs d|&|d.||<   |dz  } g }(t+               })|D ]  }*|j                  |*      }|r|j                  d/      s(|j                  dg       xs g D ]  }||)v r|)j-                  |       t/        j                  d0|      }+|+s2t1        |+j3                  d            },t1        |+j3                  d1            }-|(j                  |,|-||*|d/   d2         | |j                  d3|       |j                  dd      |D *cg c]  }*|*|v s||*    c}*|(|t5        |      d4S c c}w # t        $ rE}t        j	                  d| ||       i |dddd	g t        |      dd g d||<   |dz  }Y d}~d}~ww xY w# t         t"        f$ r4}t        j	                  d| ||       i |ddd||<   |dz  }Y d}~9d}~ww xY wc c}*w )5u   단일 location chain plan을 받아 노드별 이미지를 순차 생성.

    location 안에서는 부모-자식 종속이 있어 sequential. location 간은 caller가 병렬화.
    Tparentsexist_oknodesr!   execution_orderr4   rationale_summaryr"   r   uF   render_one_location: %s execution_order entry %r not in nodes — skipu@   render_one_location: %s node_id %r not snake_case ASCII — skiprejected_node_idz'node_id must match ^[a-z0-9][a-z0-9_]*$)render_statusrender_errorru   	parent_idNshot_idsr%   r%   r$   r3   r4   rF   rG   rH   rS   z0render_one_location: %s/%s prompt gen failed: %sprompt_failedr   )rT   
image_pathr   render_attemptssanitize_strategiesr   rU   .pnguF   render_one_location: %s/%s out_path escapes loc_image_dir: %s — skiprejected_pathz&out_path resolved outside location dirFrt   zfloor_plan+parentfloor_plan+locationfloor_plan_onlyrG   location	r   r   r   r   r   r   r~   r   r   r|   rx   r   ry   rz   )rT   r   r   r   r   r|   rU   rs   r{   )rT   r   r   r   r   r   r|   rU   r   zS(\d+)_Shot(\d+)r   )scene_index
shot_index
shot_labelr5   r   location_name)r3   r   r   r   shot_backgroundsfailed_node_counttotal_node_count)mkdirr\   r?   r   r   _SAFE_NODE_ID_REmatchrW   r]   
setdefaultr@   rd   r   r=   resolverelative_torZ   OSErrorr   r   r   setaddreintgrouprE   ).r3   location_data	image_dirr   r   r   r   r~   r   r   rS   shot_meta_by_idfloor_plan_pathloc_image_dirnodes_inr   nodes_by_idr   r4   rendered_pathsenriched_nodesfailed_nodesr5   rF   r   rG   rH   rc   metarT   rU   rp   r   r   parent_usedlocation_used	candidatefloor_plan_presentr|   r_   r   
seen_shotsnidmr   r   s.                                                 r-   render_one_locationr   _  s   & +Mt4%)-*;*;GR*H*NB%OHBJ-K(Qagqj(K-K!%m&7&78I2&N&TRT!UO 	01 	01	  ')N02NL"w'LLXW   %%gm4LLRW''!3 I'N7#
 ALXXk*0b779	/8+d.088J+1r1C3/#9OC01	3/$$T*$$R%HI 2	&:'%9++'#J< !gYd#33	**=+@+@+BC" !#	&?+A+A+C_-n4&y1I!  +"2;@RSI$  + $ 4'DO,B,B,D 	 "HK*HM,H(HH!H"H"'#%

 &z(t#&.N7#	'	'(!(m!%#)*#5'-l';$*	'N7#
'
'( !)#)*#5'-l'; &

+? @ FB$*
'N7# ALI #N .0eJs#l+55R(.B.Cj NN3,c2Aaggaj/KQWWQZJ##*(!o%  /	 , #&**?KH*../BBG1@Z#C>DY.%Z,)0 W .Lv  	LLBWc	'	'  !0#$') #C#!	'N7# AL!	4 G$ 	LLXWc''!0 H'N7#
 AL	t [sA   P?-Q
-R	S
'S
	R:RRS$)SSc                  	
 
t               xs i |0t        || j                  d      xs i |xs i 	
      S | j                  d      xs i }|st        j	                  d       i ddS i }d}d	
fd}t        |t        |            xs d	}t        |
      5 }t        |      }|j                         D ci c])  \  }}t        |t              r|j                  |||      |+ }}}t        |      D ]/  }|j                         \  }}|||<   ||j                  dd      z  }1 	 ddd       t        j	                  dt        |      |       ||dS c c}}w # 1 sw Y   4xY w)uw  전체 chain 렌더 진입점.

    두 경로:
      - planner_chain_order=None (LEGACY, Phase 4): location 단위 ThreadPool 병렬
        (max_workers=2). 각 location 내부는 부모-자식 sequential. mode=off / chain_only
        회귀 보장.
      - planner_chain_order=List[str] (PLANNER-DRIVEN, Phase 5): chain_bg_order에
        따라 group_id별 SEQUENTIAL 처리. variant_index/variant_label은 location_id
        단위로 v01부터 단조 증가. floor_plan PNG 1순위 ref, parent_group PNG 2순위,
        location ref 3순위. 결과는 data.groups[group_id]에 저장.

    floor_plan_paths: location_id → floor_plan PNG path. legacy/planner 양쪽 모두
    1순위 ref로 사용. 빈 dict이면 기존 flow 유지.

    planner_floor_plan_specs: fp_id → {primary_location_id, building_group,
    location_ids, ...}. building_group reverse lookup용.
    Ngroups)planner_chain_orderplanning_groupsfloor_plan_specsfloor_plan_pathsr   r   r   r   r   r~   r   r   rS   r   	locationsz6background_chain_render: no locations in planning datar   )r   _failed_countc                   |j                  d      dk7  r-|j                  d      s| | |j                  d|       dg g dddfS 	 | t        | |
	j                  |             fS # t        $ ra}t        j	                  d	| |d
       | | |j                  d|       t        |      g g dt        |j                  dg             dfcY d }~S d }~ww xY w)Nrx   r   r   r   zno nodes in planning datar   )r3   r   skipped_reasonr   r   r   r   )r3   r   r   r   r   r   r   r~   r   r   rS   r   r   z)background_chain_render: %s exception: %sT)exc_inforu   )r3   r   r   r   r   r   r   )r?   r   r   r   r   r=   rE   )loc_iddatarp   r   r   r   r   r   r   rS   r   r   r   r~   s      r-   _processz-run_background_chain_render.<locals>._process  s   88H%dhhw.?%!%/6!B"=$&%&$%  	.""##5+'#)+ / 0 4 4V <    
	LLDfc\`La%!%/6!BS$&%&$'"(=$>  
	s   (A- -	C6ACCCru   max_workersr   uE   background_chain_render: done — %d locations, %d failed nodes total)r  r=   r  Dict[str, Any]r>   zTuple[str, Dict[str, Any]])r   _run_planner_driven_renderr?   r   r   r   rE   r   r   itemsr[   r]   submitr   r_   )planning_datar   r   r   r   r   r~   r   r   rS   r   r	  r   r   planner_floor_plan_specslocations_inlocations_outtotal_failed_nodesr  workerspool_submit_processlidr  futuresfutr_   s    `````````` `              r-   run_background_chain_renderr  L  s   D #%	'-2 &) 3)--h7=25;-1'#%'+
 	
& 	+&,"  LM!44/1M% %N +s<016QG		0D-h7 *//1
1	T$% KKd3S81 	 

  (C**,KC!'M#&**-@!"DD ) 
1 KKOM. '9KLL
 
1	0s   :E;.E5A E;5E;;Fc                    |sy|j                         D ]I  }t        |t              s|j                  d      xs g }| |v s.|j                  d      xs d}|xs dc S  y)u*  building_group reverse lookup.

    loc_id가 어떤 floor_plan의 location_ids에 포함되면 그 fp의 primary_location_id를
    반환. 단독 location도 자체 floor_plan(primary=loc_id)에 매핑되므로 동일 로직으로
    처리된다. fp_specs 미제공/매칭 실패 시 None.
    Nlocation_idsprimary_location_idr"   )valuesr[   r]   r?   )r  fp_specsfpidsprimarys        r-   _find_primary_loc_for_chain_bgr"    se     oo"d#ff^$*S=ff239rG?d"   r/   c                2    | s|s|sy| r|ry| r|ry| ry|ryy)u:   Phase 5 ref_used 라벨 분류 — 디버그 traceability.rt   zfloor_plan+parent_groupr   r   parent_groupr   rh   )floor_plan_usedr   r   s      r-   _classify_planner_ref_usedr&    s-     {m;(=$ r/   c                v   t        |j                  d      xs g       }d}|D ]  }|j                  d      dk(  s|} n |9|j                  d      xs g }|r"|D ]  }|j                  d      |d   k(  s|} n ||r|d   }|nt        |      }d|d	<   g }t               }|D ]D  }|j                  d
g       xs g D ])  }	|	|v r|j	                  |	       |j                  |	       + F |r||d
<   |S | d|j                  d      xs | |j                  dd      xs dg dd|j                  dd      xs dg d	S )u  T6 group result에서 LLM 호출용 synthetic node spec 빌드.

    T6의 _run_planner_driven은 그룹당 LLM 1회 호출로 nodes[]를 산출한다 (보통
    1개 root anchor). T7은 그룹당 PNG 1개를 생성하므로, 그룹의 root node를
    선택하여 generate_node_prompt에 그대로 전달한다. nodes가 비었거나 root가
    없으면 group_id 자체를 임시 anchor_root로 합성한다.
    r   Nr0   anchor_rootr   r!   r   r"   r   r   r   r   )	r!   r0   r#   r$   r   r   depth	rationaler1   )r\   r?   r]   r   r   r@   )
group_idgroup_resultr   chosenr   ordersynth	all_shotsseenrc   s
             r-   _build_synthetic_node_for_groupr2    s}    #'|'7'7'@'FB"GE'+F55=M)F  ~  !239r55;%(*F  ~%qVk!	EAuuZ,22$;  %	 3   )E* !!/2>h#''(;R@FB!%%&92>D"-/
 
r/   c                     t        | ||d      S )u   planner_chain_order를 level-based parallelizable batches로 분할.

    Phase 5.3 wrapper — Phase 7에서 추출한 generic compute_dag_levels로 위임.
    동일한 시그니처/동작 유지 (회귀 0).
    r   )r.  r  renderable_setparent_fieldr   )r   r   r4  s      r-   _compute_chain_bg_levelsr6  7  s     !% 	 r/   c                   |j                  dd      }|j                  d      xs dj                         }|j                  dd      }g }d}d}d}t        ||      }|r.||v r*||   }|#|j                         r|j	                  |       d}|s.||v r*||   }|#|j                         r|j	                  |       d}|r,||v r(||   }|j                         r|j	                  |       d}|s8|s6|j                  |      }|#|j                         r|j	                  |       d}t        |||      }t        | |      }g }|j                  dg       xs g D ]N  }|r6||v r2t        ||         } | j                  d	|       |j	                  |        ;|j	                  |dd
       P d}!|j                  d      xs |xs d}"	 t        ||"||!||      \  }#}$|| d| dz  }&	 |&j                         j                  |j                                t%        |	|
|#|&|||||	      }'||'d<   |'j                  d      dk(  r|&j                         ri d| d|d|ddd|d|d|dt        |&      d|#d|$d|d |'j                  d!d"      d#|'j                  d$g       d%t'        |j                  d%      xs g       dt'        |j                  d      xs g       d&|j                  d&d      xs dd'|S | ||d(|'j                  d)      xs d|||d|#|$||'j                  d!d"      |'j                  d$g       d*S # t        $ r<}%t        j                  d| |%       | ||dt        |%      dd |||ddg |dcY d}%~%S d}%~%ww xY w# t         t"        f$ r0}%t        j                  d| |%       | ||dd|||d|#|$|dcY d}%~%S d}%~%ww xY w)+u  단일 renderable group 1개에 대한 prompt 생성 + 이미지 render. Pure function.

    호출자가 미리 status=='ok' + loc_id 비어있지 않음을 확인한 group만 들어옴.
    rendered_paths_snapshot은 이번 level 시작 시점의 부모 PNG dict (read-only).
    공유 상태(rendered_paths/results/total_failed)는 호출자가 결과 수신 후 갱신.

    Returns: 결과 dict (status: ok | prompt_failed | rejected_path | failed)
    r3   r"   r   r   FNTr   r%   r   r   r   zAbackground_chain_render (planner): group %s prompt gen failed: %sr   r   r+  r3   r   rx   r   variant_labelvariant_indexr|   png_pathrT   rU   r   _r   uS   background_chain_render (planner): group %s out_path escapes image_dir: %s — skipr   zout_path escapes image_dirr   r|   rx   r   r+  r9  r:  r;  rT   rU   r   ry   r   r   rz   scenesr   r%  rs   r{   )r+  r3   r   rx   r   r9  r:  r|   r;  rT   rU   r   r   r   )r?   rW   r"  r   r@   r&  r2  r]   r   rd   r   r   r   r=   r   r   rZ   r   r   r\   )(r+  r,  r:  r9  rendered_paths_snapshotr   r   r   r   r   r   r   r~   r   r   rS   r   r  parent_group_idr   r   r%  r   r   primary_for_groupfp_path
parent_pngloc_refr|   
synth_noderH   rc   r   parent_for_llmr4   rT   rU   rp   r;  r   s(                                           r-   _render_one_group_plannerrF  I  s   8 mR0F#''4:AACO $$_b9M IOKM 7v?OP.2BB"#457>>#3W%"Ov)99"6*7>>#3W%"O ?.EE,_=
Z(K ;$((07>>#3W% M)/;VH 1<HJ*,M~~j"-33so5,-DOOIs+  &  S!DE 4 N 	,- 		 
"6!5!''#

K: fXQ}oT::H
&&y'8'8':;, #!
D  DxxT!hoo&7

6
 ]
 d	

 ]
 ]
 
 H
 *
 ;
 
 txx
A6
 "488L"#=
 d<++H5;<
 Z^^J7=2>
  L$$VR06B!
" #
 	
( &!56<"&& "$88J2#xxb9 a  
Oc	

 !!*%HTcN** (
 	

4   
ac	

 !!*%8** $&(
 	

s<   L: "-N :	M?1M:4M?:M?O%N<6O<Oc                    t        j                  dd      j                         }|s| S 	 t        |      }|dk  ry||kD  r|S |S # t        $ r t
        j                  d||        | cY S w xY w)uT   BACKGROUND_CHAIN_RENDER_WORKERS env → clamp to [1, cap]. 잘못된 값은 default.BACKGROUND_CHAIN_RENDER_WORKERSr"   u?   BACKGROUND_CHAIN_RENDER_WORKERS=%r not int — using default %dru   )osgetenvrW   r   rZ   r   r   )defaultcaprawvs       r-   _resolve_workersrO    s{    
))5r
:
@
@
BCH 	1u3w
H  M	
 s   A "A)(A)c                :   | st         j                  d       di ii ddS |j                  dd       i }i }i }| D ].  }|j                  |      }|st         j	                  d|       .|j                  dd	      }|d
k7  r`t         j                  d||       ||j                  dd	      |j                  dd	      dd| d	dd	d	g |j                  dd	      xs d	d||<   |j                  dd	      xs d	}|sMt         j	                  d|       |d	|j                  dd	      ddd	dd	d	g |j                  dd	      xs d	d||<   
|j                  |d      }|d|df||<   |dz   ||<   1 t        |j                               }t        | ||      }t        |      }i }d}t               }h d}|r*t         j                  dt        |      t        |      |       t        |      D ]$  \  }}|s
t        |      }t        dt        |t        |                  } t        |       5 }!t        t               }"|D #ci c]5  }#|!j#                  |"|#||#   ||#   d   ||#   d   |||||||||	|
|||      |#7 }$}#t%        |$      D ]P  }%|$|%   }#	 |%j'                         }&|&||#<   |&j                  dd	      }*|*d
k(  rt1        |&d#         ||#<   G|*|v sL|dz  }R 	 d d d        t         j                  d$|dz   t        |      t        |      |        ' t         j                  d%t        |      |       | D #ci c]  }#|#|v s|#||#    }+}#d|+i|+|dS c c}#w # t(        $ r}'t         j+                  d|#|'       |j                  |#      xs i }(|j                  |#d      })|#|(j                  dd	      |(j                  dd	      dt-        |'      d d! |)d   |)d   dd	d	g |(j                  d      xs d	j/                         d"}&Y d }'~'hd }'~'ww xY w# 1 sw Y   7xY wc c}#w )&uM  Phase 5 planner-driven render path (Phase 5.3 level-based parallel).

    Phase 5: chain_bg_order대로 SEQUENTIAL 처리 (1 group/turn).
    Phase 5.3: parent_id로 DAG level batch 구성 → 같은 level 내 ThreadPool 병렬.
    각 group은 여전히 별도 LLM call (HARD constraint per CLAUDE.md). variant_index/
    variant_label은 chain_bg_order대로 deterministic 사전 할당 — 병렬화에도 같은 결과.

    ref 우선순위 (변경 없음):
      1. building_group reverse lookup으로 찾은 floor_plan PNG
      2. group의 parent_id에 해당하는 chain_bg PNG (rendered_paths)
      3. location ref

    workers: BACKGROUND_CHAIN_RENDER_WORKERS env (default 4, capped at 8).

    Returns: {
        "data": {"groups": {group_id: result}},
        "groups": {group_id: result},
        "_failed_count": int,
    }
    z>background_chain_render (planner-driven): empty chain_bg_orderr   r   )r  r   r  Tr   uN   background_chain_render (planner): group_id %s not in planning groups — skiprx   r"   r   uH   background_chain_render (planner): group %s status=%s — render skippedr3   r   skipped_planningzplanning status=noner   )r+  r3   r   rx   r  r9  r|   r;  rT   rU   r   uJ   background_chain_render (planner): group %s has empty location_id — skipskipped_no_loczempty location_idru   rN  02d>   rs   r   r   z\background_chain_render (planner-driven): %d renderable groups across %d levels (workers=%d)r  )r+  r,  r:  r9  r>  r   r   r   r   r   r   r   r~   r   r   rS   r   z=background_chain_render (planner): group %s render raised: %s)r   r"   rs   Nr   r8  r;  zKbackground_chain_render (planner): level %d/%d done (%d groups, workers=%d)uH   background_chain_render (planner-driven): done — %d groups (%d failed))r   r   r   r?   r   r   keysr6  r]   rO  rE   	enumeratemaxr   r   r   rF  r  r   r_   r   r   r=   rW   r   ),r   r   r   r   r   r   r   r   r   r~   r   r   rS   r   pre_resultsvariantslocation_variant_counterr+  r,  rx   r  next_idxr4  levelsresultsr   total_failedr  failure_statuses	level_idxlevelrendered_snapshotlevel_workersr  _submit_planner_rendergidr  futureresrp   gr_metavar
res_statusordered_resultss,                                               r-   r  r  "  s   J TUrN
 	
 OOD4O0 .0K+-H/1'&**84NN` !!(B/T>KKZ&
 %+//rB!-!1!1/2!F,$4VH"=!#" !)--k2>D"%K! !!-4:NN\ %!!-!1!1/2!F*"5!#" !)--k2>D"%K! +//:&!HS>(:;+3a< (o (r )N &&9?N[F *.k):G&(NL GCjVg	

 &f-	5 0As7CJ78M:d &99R%S", !+* !C) * !0!5"*3-"2"*3-"2,=%5%5'9'"/ +'#!-"/$3%  & '( !+  . 'w/fo --/C,  # WWXr2
%*.s:*?N3'#33 A%LC 07 ;z 	YM3v;E
M	
G .P KKRGl &92%8cC7NWS\%8  2 ?+!% S< ! LLWS .11#6<"G",,sG4C$''.{{="'E)0_b)I"*(+C#),Q),Q$*$&&(')&-kk+&>&D"%K%K%MCE ;:T2s[   'P;:M5PM2PP3	P=PP	P	B P	=PP	PP	)r3   r=   r4   r=   rF   r
  rG   Optional[Dict[str, Any]]rH   List[Dict[str, Any]]r>   r=   rg   )r3   r=   r4   r=   rF   r
  rG   rl  rH   rm  rS   rl  r>   z Tuple[str, List[Dict[str, Any]]])rp   r   r>   bool)NN	1024x1024high   N)r   r   r   r=   r   r=   r   r   r   Optional[Path]r   Optional[PromptSanitizer]r~   r=   r   r=   r   r   r   zOptional[List[Path]]r>   r
  )r3   r=   r   Dict[str, Path]r>   rr  )ro  rp  rq  NNN)r3   r=   r   r
  r   r   r   rt  r   r   r   r=   r   rs  r~   r=   r   r=   r   r   rS   rl  r   #Optional[Dict[str, Dict[str, Any]]]r   rr  r>   r
  )zgpt-image-2.5-sunburstNro  rp  rq  NNr   NNN) r  r
  r   r   r   rt  r   r   r   r=   r   rs  r~   r=   r   r=   r   r   rS   rl  r   ru  r	  r   r   zOptional[Dict[str, Path]]r   zOptional[List[str]]r  ru  r>   r
  )r  r=   r  Dict[str, Dict[str, Any]]r>   zOptional[str])r%  rn  r   rn  r   rn  r>   r=   )r+  r=   r,  r
  r>   r
  )r   	List[str]r   rv  r4  r   r>   zList[List[str]])$r+  r=   r,  r
  r:  r   r9  r=   r>  rt  r   rv  r   rt  r   rt  r   r   r   r   r   r=   r   rs  r~   r=   r   r=   r   r   rS   rl  r   ru  r>   r
  )rq     )rK  r   rL  r   r>   r   )r   rw  r   rv  r   rv  r   rt  r   r   r   rt  r   r   r   r=   r   rs  r~   r=   r   r=   r   r   rS   rl  r   ru  r>   r
  )8__doc__
__future__r   rC   loggingrI  r   r   concurrent.futuresr   r   pathlibr   typingr   r   r	   r
   r   app.core.image_call_budgetr   r   r   #app.modules.llm.gpt_image_primitiver   app.modules.llm.llm_clientr    app.modules.pipeline._dag_levelsr   app.modules.prompt_loaderr   r   app.modules.prompt_sanitizerr   	getLogger__name__r   compilerX   ro   r   r   rK   rd   rq   r   r   r   r  r"  r&  r2  r6  rF  rO  r  rh   r/   r-   <module>r     sx    #   	 	  ?  3 3 
 E 6 ? > 8			8	$  RZZ   2::56 5 111 1 %	1
 (1 	1t /3... . %	.
 (. ,. &.l7  $+/&*UUU U 	U
 U )U U U U $U Uz' $ .2;?&*jj!j j (	j
 j j )j j j j ,j 9j $j jd 0+/.2;?26/3DH~M!~M~M (~M 	~M
 ~M )~M ~M ~M ~M ,~M 9~M ~M 0~M -~M B~M  !~ML' ,  		&99 9 9x".  	$BB !B 	B
 B -B 0B &B (B B B B )B B B  !B" ,#B$ 9%B& 'BJ(V"V /V 0	V
 &V V (V V V )V V V V ,V 9V  !Vr/   