
    *cIj[A                       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Zddl	m
Z
 ddlmZmZmZmZmZmZ  e
d      Zedz  Zedz  d	z  Zed
z  Zedz  Zedz  ZdZdZedz  ez  dz  dz  ez  Zedz  ez  dz  ez  dz  Z ej8                  e        ee      ej<                  vr"ej<                  j?                  d ee             ddl m!Z! ddl"m#Z# ddl$m%Z% ddl&m'Z' ddl(m)Z) ddl*m+Z+ da,d Z-d+dZ.d,dZ/d-d.dZ0d/dZ1d-d0dZ2d1dZ3d1d Z4d2d!Z5	 	 d3	 	 	 	 	 	 	 d4d"Z6d5d#Z7	 	 	 d6	 	 	 	 	 	 	 	 	 d7d$Z8	 	 d8	 	 	 	 	 	 	 d9d%Z9d:d;d&Z:d<d'Z;d=d(Z<d>d)Z=d?d*Z>y)@u  forest_exp 공용 라이브러리 — '전체 숲' 총괄 파이프라인 실험 (2026-07-04).

실험 전용(scratchpad, 커밋 금지). production 코드 차용(import) + 실데이터 읽기 전용.
- capture scope 미개방 → image_asset/체크포인트 write 0.
- 시나리오 중립: 이 파일의 어떤 템플릿에도 장소/인물 고유명사 없음.
  구체 내용은 전부 cp 데이터 문자열로 주입된다.
    )annotationsN)Path)AnyDictListOptionalSequenceTuplez*/Users/manta/Documents/Projects/TheRoad-I1backend
scratchpad
forest_expoutplanszrunlog.jsonlz$b0ad5c18-5140-4022-a2b0-805de0e5a385z$0d30302a-51c8-4ac9-98e2-6cd9180512bfprojectscheckpointsepisodesimages)settings)call_structured)call_gpt_image_bytes)get_next_key)sanitize_for_safety)_gemini_generate_contentc                     t         5ddlm}   | t        j                  t        t        j                              a t         S )Nr   OpenAI)api_keytimeout)_openai_clientopenair   r   openai_api_keyfloatllm_timeout_image_genr   s    N/Users/manta/Documents/Projects/TheRoad-I1/scratchpad/forest_exp/forest_lib.pyopenai_clientr%   /   s4    !++(889
     c                   dt        j                  d      i| } t        j                  j	                  dd       t        j                  d      5 }|j                  t        j                  | d      dz          d d d        y # 1 sw Y   y xY w)	Ntsz%Y-%m-%d %H:%M:%STparentsexist_okaFensure_ascii
)	timestrftimeRUNLOGparentmkdiropenwritejsondumps)eventfs     r$   runlogr;   <   sh    4==!45??E
MMt4	S	Q	

5u5<= 
		s   *BBc                b    t        j                  t        | z  dz  j                               d   S )Nzmanifest.jsondata)r7   loadsCP	read_text)steps    r$   	load_steprB   E   s(    ::rDy?2==?@HHr&   c           	     ,   t        d      d   }g }|D ]  }|j                  d      xs g D cg c]#  }t        |j                  d      t              r|% }}|r|j                  d      sXt	        d |D              }t	        d |D              }t        d |D              }| r|d   | k(  ri |||d	c S |s|s|j                  i |||d	|f        | rt        d
|        |st        d      |j                  d        |d   d   S c c}w )u   데이터 기준 그룹 선택: 실내+실외 멤버 공존(mixed) 그룹 중 총 shot_count 최대.

    is_indoor 가 bool 이 아닌 멤버는 제외(구조 계약 — 추측 금지).
    background_classifybuilding_groupsmembers	is_indoorgroup_idc              3  &   K   | ]	  }|d      ywrG   N .0ms     r$   	<genexpr>zpick_group.<locals>.<genexpr>U   s     5WQ{^Ws   c              3  (   K   | ]
  }|d       ywrJ   rK   rL   s     r$   rO   zpick_group.<locals>.<genexpr>V   s     :'Q!K.('s   c              3  X   K   | ]"  }t        |j                  d       xs d       $ yw)
shot_countr   N)intgetrL   s     r$   rO   zpick_group.<locals>.<genexpr>W   s$     C7aCl+0q17s   (*)rF   _total_shotszgroup not found: z0no mixed indoor/outdoor group in building_groupsc                    | d    S )N   rK   )xs    r$   <lambda>zpick_group.<locals>.<lambda>b   s
    adUr&   )keyr   )	rB   rT   
isinstanceboolanysumappend
SystemExitsort)	overridegroupscandsgrN   rF   has_inhas_outtotals	            r$   
pick_groupri   I   s5   
 ,-.?@FE uuY/525 <5 {!3T: 5 <aeeJ/5W55:'::C7CC}(G!GGGgLLJQJ7EJERS  ,XJ788KLL	JJ?J#8A;%<s   (Dc                    d|  d| S )NS_ShotrK   )scene_index
shot_indexs     r$   shot_keyro   f   s    {m5--r&   c                ^  #$ t        |       }|d   D ci c]  }|d   |
 c}$t        d      }|d   }|j                         D ci c]  \  }}|j                  d      $v s|| }}}i }|j                         D ]@  \  }}|j                  d      xs g D ]#  }	|j	                  |	g       j                  |       % B t        d      d   }
i }|
D ]f  }|j                  d      xs g D ]L  }|j                  d	      |j                  d
      |j                  d      xs g d|t        |d   |d         <   N h t        d      d   D ci c]  }t        |d   |d         | }}i }t        d      d   D ]V  }t        |d   |j                  d            }|j                  d      |j                  d      |j                  d      d||<   X i }t        |j                               D ]  \  }	}t        |D ch c]
  }||   d    c}      }t        $fd|D              }|	j                  d      \  }}t        |dd       t        |      ||t        |      d|j                  |	      xs i |j                  |	      |j                  |	      d||	<    t        d      d   }i }|j                         D ]  \  }}|j                  d      xs g D ]  #|j                  #      xs i }|j                  d      d k(  s-|j                  d!      s?|j	                  |d   g       }t        #fd"|D              ri|j                  #|d!   d#         t        |j                         D ch c].  }|j                  d      xs g D ]  }t        |t              r| 0 c}}      }t               }t!               } t"        d$z  d%|d&    d'z  }!|d&   |j                  d(      |d   |||||| |!j%                         rt        |!      ndd)
}"|"S c c}w c c}}w c c}w c c}w c c}}w )*uX   그룹 → loc → bg → shot 조인 + fp/passport/베이스라인 자산 인벤토리.rF   loc_idbackground_master_planbackground_catalogapplies_to_shotsshot_extractscenesshotsscene_headingdescription
characters)rx   ry   rz   rm   rn   shot_stagingscene_detail_shot_indext2i_variationsvisible_entitiesrender_prompt_card)r~   r   r   c              3  .   K   | ]  }|   d      ywrJ   rK   )rM   llocss     r$   rO   zbuild_recon.<locals>.<genexpr>   s     >gQ,g   rl   rW   N)rm   rn   loc_idsrG   bg_ids)stagingdetailfloor_plan_renderfloor_plansdepends_on_fpstatusokpng_pathc              3  .   K   | ]  }|d    k(    yw)fp_idNrK   )rM   rX   r   s     r$   rO   zbuild_recon.<locals>.<genexpr>   s     <11W:.r   )r   r   background_chainaerial_rH   z.png
anchor_loc)
rH   r   rF   bgsrw   	fp_by_loccharacters_in_shots	passportscharacter_name_to_sidbaseline_aerial)ri   rB   itemsrT   
setdefaultr_   ro   sortedallsplitrS   r]   valuesr[   strquery_passportsquery_character_names	EP_IMAGESexists)%group_overridegrouprN   mpcatalogbide	group_bgsshot_to_bgssksedesc_by_keyscshsstaging_by_keydetail_by_keykrw   bidsbr   rG   sishi	fp_renderr   rlstcchar_idsr   name_to_sidr   reconr   r   s%                                      @@r$   build_reconr   j   s_   ~&E$))$45$4qAhKN$45D	+	,B%&G&-mmoQoFCxD9PaoIQ )+K//#Q%%*+1r1B""2r*11#6 2 $
 
>	"8	,B-/K&&/'R'B!#!8!vvm4 ff\28bJKM!2B|4DEF (  >*733A 	=!1\?3Q63   02M'1R&}(=> ff%56 "'9 :"$&&)=">
a 2 (*E;,,./D$?$Q)A,x0$?@>g>>	((7#Cr!"v;c("Tl	
 r"(b	
 &))"-#''+	
b		 0  -.}=I13I//#QUU?+1r1Ee$*AuuX$&155+<**1X;;<<<JJAjMJK 2 $ %,,. ..Q155;N;TRT;TQ$Q, ;Tq. . /H!I')K"44zAR@SSW7XXO *%ii-#'!,3B3I3I3K3/QUE Le 6 R$ @4.s#   PP$PP;P$
33P)
c                    dt          d} i t        j                  ddi}t        j                  ddddd	d
d	d| g	dd|d      j
                  }i }|j                         j                         D ]m  }d|vr|j                  dd      \  }}t        |      }|j                         s	t        |z  }|j                         sRt        |      ||j                         <   o |S )uI   캐릭터 short_id → primary reference PNG 절대경로 (DB read-only).zSELECT ec.short_id, ia.file_path FROM image_asset ia JOIN entity_canon ec ON ec.id::text=ia.entity_id::text WHERE ia.project_id='zl' AND ia.asset_type='reference' AND ec.entity_type='character' AND ia.is_primary=1 AND ia.status='generated'
PGPASSWORDtheroad_dev_2026psql-h	localhost-Utheroad-d-tAcTcapture_outputtextenvcheck|rW   )
PROJECT_IDosenviron
subprocessrunstdoutstrip
splitlinesr   r   is_absoluteROOTr   r   )sqlr   r   resultlinesidpathps           r$   r   r      s    	  *| ,W	W  ;RZZ
:'9
:C
..	{D)T9fcR$Ct f   F		&&(d?JJsA&	TJ}}tA88:"%a&F399; ) Mr&   c                 `   dt          d} i t        j                  ddi}t        j                  ddddd	d
d	d| g	dd|d      j
                  }i }|j                         j                         D ]=  }d|v s|j                  dd      \  }}|j                         ||j                         <   ? |S )u-   캐릭터 이름 → short_id (DB read-only).z:SELECT name, short_id FROM entity_canon WHERE project_id='z' AND entity_type='character'r   r   r   r   r   r   r   r   r   Tr   r   rW   )	r   r   r   r   r   r   r   r   rsplit)r   r   r   r   r   namer   s          r$   r   r      s    	'L(E	G  ;RZZ
:'9
:C
..	{D)T9fcR$Ct f   F		&&($;C+ID##&99;F4::<  ) Mr&   c                   dt          d|  d| d}i t        j                  ddi}t        j                  ddd	d
dddd|g	dd|d      j
                  }g }|j                         j                         D ]m  }t        |j                               }|j                         st        |j                         z  }|j                         sT|j                  t        |             o |S )u^   해당 (scene, shot)의 production 최종 스틸 경로들 (베이스라인 육안 비교용).zrSELECT ia.file_path FROM image_asset ia JOIN scene_still ss ON ss.id::text=ia.still_id::text WHERE ia.project_id='z/' AND ia.asset_type='scene' AND ss.scene_index=z AND ss.shot_index=z ORDER BY ia.created_at DESCr   r   r   r   r   r   r   r   r   Tr   )r   r   r   r   r   r   r   r   r   r   r   r   r_   r   )rm   rn   r   r   r   pathsr   r   s           r$   query_production_stillsr      s    	  *| ,)]*=j\ J&	&  ;RZZ
:'9
:C
..	{D)T9fcR$Ct f  E		&&(}}tzz|#A88:LLQ  ) Lr&   c           	     ^   t        j                         }t        | |||| d|ii| |      }t        |t              r|n9t        j                  |D cg c]  }|j                  d      dk(  s| c}d      }	t        d| |t        t        j                         |z
  d      ||	d	       |S c c}w )
u`   call_structured 차용 — model 강제(project_config), 전체 원문 전달(자르기 금지).model)project_configschema_nametemperaturetyper   Fr-   llmrW   )kindrA   r   dur_ssystem_promptuser_prompt)
r0   	monotonicr   r[   r   r7   r8   rT   r;   round)
step_tagr   r   response_schemar   r   t0r   r   	user_texts
             r$   r   r   	  s     
	B-o 7E"23+F
  *+s;;Kq155=F#:K;%BQI
E8e4>>+b0!4*9F G M	 	<s   B**B*c                    t        j                  t        |       j                               j	                  d      }ddd| idS )Nascii	image_urlurlzdata:image/png;base64,)r   r   )base64	b64encoder   
read_bytesdecode)r   b64s     r$   png_data_urlr    sB    


4:002
3
:
:7
CCu8Nse6T.UVVr&   c                   |J |j                   j                  dd       |j                         rt        d| dt	        |      d       |S |rdnd}|}t        j                         }	d}
t        d	|d	z         D ]  }	 t        t               |||r|D cg c]  }t	        |       c}ndd
||d	dd      }|j                  |       t        dd
|| |xs g D cg c]  }t	        |       c}||t        t        j                         |	z
  d	      t        |      t	        |      |d       |c S  t        d|  d|
       c c}w c c}w # t        $ r4}|}
t        d| |t	        |      dd d       t        |      }Y d}~	d}~ww xY w)u   gpt-image-2 generate/edit (production call_gpt_image_bytes 차용).

    capture scope 미개방 상태로 호출 → DB 기록 없음. moderation 류 실패 시
    sanitize_for_safety 로 1회 재시도(bounded).
    NTr)   imgr   r   tagskipr   editgeneraterW   zgpt-image-2)r   sizequalityn)modeprompt	ref_pathscall_kwargsedit_image_as_list)r   enginer  r  refsr
  attemptr   bytesr   r  	img_error  r   r  r  errorzimg_gpt failed for : )r3   r4   r   r;   r   r0   r   ranger   r%   write_bytesr   len	Exceptionr   RuntimeError)r  r  r  r
  r  out_pathmax_attemptsr  currentr   lastr  r   pngexcs                  r$   img_gptr&     s    OO$6c8CMRS6zDG		B $DL1,-	3&d748404a3q640d&3T(/a9#'C   %E]DQT.2jbj:jSVj:D&t~~7G"7La1P Xc(mwP Q O .( ,SED6:
;;! 1 ;  	3DK Xds^- .)'2G		3s7   >EE&3EE+AE
E	F)FFc                v   |J |j                   j                  dd       |j                         rt        d| dt	        |      d       |S |D cg c]   \  }}|t        |      j                         f" }}}|}	t        j                         }
d}t        d|dz         D ]  }	 t        t               t        j                  |	||      }|j                  |       t        dd	d
| |D cg c]  \  }}t	        |       c}}|D cg c]  \  }}|	 c}}|t        t        j                         |
z
  d      t!        |      t	        |      |	d       |c S  t'        d|  d|       c c}}w c c}}w c c}}w # t"        $ r4}|}t        d| |t	        |      dd d       t%        |	      }	Y d}~d}~ww xY w)uR   nb2(gemini image) i2i 차용 — 라벨드 ref 멀티 입력, 최종/임시샷용.NTr)   r  r   r  rW   )aspect_rationb2i2i)r   r  r  r  r  
ref_labelsr  r   r  r   r  r  r  r  zimg_nb2 failed for r  )r3   r4   r   r;   r   r   r   r0   r   r  r   r   r   gemini_image_modelr  r   r  r  r   r  )r  r  labeled_refsr(  r   r!  labelr   inputsr"  r   r#  r  r$  _r   r%  s                    r$   img_nb2r1  H  s    OO$6c8CMRS<HILqud1g((*+LFIG		B $DL1,-	3* ; ;Wf)C   %EUE#0<=1SV=1=">A1">&t~~7G"7La1P Xc(mw	P Q
 O .$ ,SED6:
;;- J >">  	3DK Xds^- .)'2G		3s>   %E)%AE;*E/?E;E5AE;/E;;	F8)F33F8c           
        d|  d|d   rdnd ddj                  |d          d	|j                  d
       d	g}|j                  d|j                  d              |j                  d      r&|j                  ddj                  |d                 |j                  d      xs i }|r|r|j                  d|j                  d              |j                  d|j                  d              |j                  d      xs g }|r)|j                  dt        j                  |d      z          |j                  d      xs i }|r)|j                  dt        j                  |d      z          dj                  |      S )uG   한 샷의 내용을 LLM 입력 텍스트 블록으로 (전체 원문).[z] (rG   indooroutdoorz; loc=,r   z; scene_heading=rx   )zdescription: ry   rz   zcharacters: , r   zframing_scale: framing_scalezcamera_direction: camera_directionkey_bg_elementszkey_bg_elements: Fr-   frame_spatial_contractzframe_spatial_contract: r/   )joinrT   r_   r7   r8   )r   r   include_staginglinesstkbefscs          r$   
shot_blockrC  l  sn   CAkN	B CHHQy\*++;AEE/<R;SSTV WE	LL=}!5 678uu\|DIIao$>#?@A	
y		RB2rvvo'>&?@A)"&&1C*D)EFGff&'-2LL,tzz#E/RRSff-.4"LL3::c>? @99Ur&   c                    g }| D ]a  }|j                  d      rdnd}|j                  d| d|j                  d       d|j                  d       d	|j                  d
              c dj                  |      S )NrG   r4  r5  z- (r8  rq   z) r.  r  summaryr/   )rT   r_   r=  )rF   r?  rN   r  s       r$   members_blockrF    su    E%%,h)s3%r!%%/!2"QUU7^4DBquuYGWFXYZ  99Ur&   c                 V    t        j                  t        dz  j                               S )Nz
recon.jsonr7   r>   PLANSr@   rK   r&   r$   
load_reconrJ    s    ::u|+66899r&   c                \    t        j                  t        |  dz  j                               S )N.jsonrH  )r   s    r$   	load_planrM    s%    ::u$u~-88:;;r&   c                    t         j                  dd       t         |  dz  }|j                  t        j                  |dd             |S )NTr)   rL  FrW   )r.   indent)rI  r4   
write_textr7   r8   )r   r=   r   s      r$   	save_planrQ    s@    	KKtK,4&ALLDuQ?@Hr&   )r9   Dict[str, Any]returnNone)rA   r   rS  rR  )N)rb   Optional[str]rS  rR  )rm   rS   rn   rS   rS  r   )r   rU  rS  rR  )rS  zDict[str, str])rm   rS   rn   rS   rS  z	List[str])gptg?)r   r   r   r   r   r   r   rR  r   r   r   r"   rS  rR  )r   z
str | PathrS  rR  )N1536x864highN   )r  r   r  r   r  zOptional[Sequence[str | Path]]r
  r   r  r   r   Optional[Path]r!  rS   rS  r   )z16:9NrY  )r  r   r  r   r-  z Sequence[Tuple[str, str | Path]]r(  r   r   rZ  r!  rS   rS  r   )T)r   r   r   rR  r>  r\   rS  r   )rF   zList[Dict[str, Any]]rS  r   )rS  rR  )r   r   rS  rR  )r   r   r=   rR  rS  r   )?__doc__
__future__r   r   r7   r   r   sysr0   pathlibr   typingr   r   r   r   r	   r
   r   BACKENDEXPOUTrI  r2   r   
EPISODE_IDr?   r   chdirr   r   insertapp.core.configr   app.modules.llm.llm_clientr   #app.modules.llm.gpt_image_primitiver   app.modules.llm.gemini_key_poolr   app.modules.llm.safetyr   app.modules.gemini_i2i_editorr   r   r%   r;   rB   ri   ro   r   r   r   r   r   r  r&  r1  rC  rF  rJ  rM  rQ  rK   r&   r$   <module>rl     s   #   	  
   = =89


\L(	Ekg	~	 4
3
	J#m3j@:M:
*Z7*DxO	 	 w<sxxHHOOAs7|$ $ 6 D 8 6 B>I:.Up4&6 7< '03%3$W
 KO39AB%<%<-0%<$%<;>%<GK%<R DH !<<2@<<&*<H*:<r&   