
    Ojq                        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 ddlmZ ddlmZ dd	lmZ  ej.                  e      Z G d
 de      Zy)u  background_chain_render StepRunner — chain plan에 따라 배경 PNG 체이닝 생성.

PR #4 (2026-04-27). on_demand: 사용자 명시 트리거할 때만.

의존:
  - background_chain_planning (필수, 핵심 입력)
  - shot_validator (선택, shot description 컨텍스트)
  - entity_merge (선택, location 메타)

DB:
  - ImageAsset 테이블에 asset_type='background_chain_node' UPSERT
  - 위치별 entity_canon (location)에 연결
    )annotationsN)datetimetimezone)Path)AnyDictListOptional)
StepRunner)run_background_chain_render)annotate_generated_assetc                  X    e Zd ZdZddZddZddZddZdddZddZ	ddZ
dd	Zd
 Zy)BackgroundChainRenderStepuB   Step: chain plan → 노드별 배경 PNG (gpt-image) + DB 등록.c                    ddl m} t        |j                        | j                  z  dz  dz  | j
                  z  |z  dz  }|j                         r%t        j                  |j                  d            S y )Nr   settingscheckpointsepisodeszmanifest.jsonzutf-8)encoding)
app.core.configr   r   projects_dir
project_id
episode_idexistsjsonloads	read_text)selfstep_idr   cps       a/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/background_chain_render_step.py_load_prev_checkpointz/BackgroundChainRenderStep._load_prev_checkpoint%   s    ,&&'oo  oo	
   	 99;::bllGl<==    c                h   | j                  d      }|si S |j                  di       j                  dg       xs g }i }ddlm} t	        |j
                        j                  }|D ]J  }|j                  d      dk7  r|j                  dd	      }|s-||z  }|j                         sC|||d
   <   L |S )u!  location_floor_plan checkpoint에서 status='ok' + PNG 파일 존재하는 매핑.

        Phase 4: background_mode=floor_plan_anchored 시 chain_bg_render의 1순위 ref.
        체크포인트 없거나 모든 location 실패면 빈 dict (mode=off / chain_only 회귀 보장).
        location_floor_plandata	locationsr   r   statusok
image_path id)r"   getr   r   r   r   parentr   )	r   r    r'   outr   projects_rootlocrelps	            r!   _load_floor_plan_pathsz0BackgroundChainRenderStep._load_floor_plan_paths4   s     ''(=>IFF62&**;;Ar	!,X223::Cwwx D('',+C#Axxz!"CI  
r#   c                    | j                  d      }|sy|j                  di       j                  d      xs g }|syt        |      S )uC  background_planner 체크포인트에서 chain_bg_order 로드.

        Phase 5: planner cp가 있고 chain_bg_order가 비어있지 않으면 planner-driven
        path로 진입한다. 없거나 비어있으면 None → legacy `data.locations` (Phase 4)
        경로 fallback (mode=chain_only 회귀 보장).
        background_plannerNr&   chain_bg_order)r"   r-   list)r   r    orders      r!   _load_planner_chain_orderz3BackgroundChainRenderStep._load_planner_chain_orderL   sL     ''(<=vr"&&'78>BE{r#   c                    | j                  d      }|si S |j                  di       j                  d      xs g }i }|D ]-  }t        |t              s|j                  dd      }|s)|||<   / |S )u   background_planner 체크포인트에서 floor_plans 리스트 → fp_id → spec dict.

        building_group reverse lookup 용. cp 없거나 floor_plans 비어있으면 빈 dict.
        r6   r&   floor_plansr,   r+   )r"   r-   
isinstancedict)r   r    fp_listr/   fpfp_ids         r!   _load_planner_floor_plan_specsz8BackgroundChainRenderStep._load_planner_floor_plan_specs[   s|    
 ''(<=I&&$((7=2)+Bb$'FF4$EE
  
r#   c                
	   ddl m} ddlm} | j	                  d      }|s |ddd      dd	lm}  |       s |d
dd      i }| j	                  d      }|r|j                  di       j                  dg       xs g D ]t  }|j                  d      }	|j                  dg       xs g D ]H  }
|
j                  dd      }|
j                  dd      xs d}d|	 d| d|	dd| fD ]
  }||d||<    J v | j                         }| j                         }| j                         }| j                         }ddlm} ddlm}  |t        |j                               } || j"                        }t%        |j&                        | j(                  z  dz  | j*                  z  dz  }|j-                  dd       t/        |j                  di       ||||t1        j2                  d d!      |t1        j2                  d"d#      t1        j2                  d$d%      d&| j5                         |||'      }|j7                  d(d      }d)|v rv|tt9        |j                  d)i             }	 | j;                  |       tG        |      }t?        d- |jA                         D              }||||j                  d      xs d)|id.S t9        |j                  d/i             }	 | jQ                  |       t?        d3 |jA                         D              }t?        d4 |jA                         D              }||||d.S # t<        $ r}t?        d* |jA                         D              }tB        jE                  d+tG        |      ||tI        jJ                                	 | jL                  jO                           # t<        $ r }tB        jE                  d,|       Y d }~ d }~ww xY wd }~ww xY w# t<        $ r}t?        d0 |jA                         D              } tB        jE                  d1tG        |      | |tI        jJ                                	 | jL                  jO                           # t<        $ r }tB        jE                  d2|       Y d }~ d }~ww xY wd }~ww xY w)5Nr   )AppErrorr   background_chain_planningzstep.no_inputuk   background_chain_planning 체크포인트 없음 — render 전에 planning step을 먼저 실행하세요.i  )codemessagestatus_code)has_openai_keyzstep.no_openai_keyu5   OPENAI_API_KEY 미설정 — gpt-image 호출 불가.shot_validatorr&   scenesscene_indexshots
shot_indexdescriptionr+   S_Shot02d)shot_idrO   )openai_client)PromptSanitizer)timeout)project_configimagesbackground_chainT)parentsexist_okBACKGROUND_CHAIN_IMAGE_MODELgpt-image-2.5-sunburstBACKGROUND_CHAIN_IMAGE_SIZE	1024x1024BACKGROUND_CHAIN_IMAGE_QUALITYhigh   )planning_data	image_dirlocation_ref_pathsfloor_plan_pathsrT   image_model	sanitizersizequalitymax_attemptsopik_metadatashot_meta_by_idplanner_chain_orderplanner_floor_plan_specs_failed_countgroupsc              3  J   K   | ]  }|j                  d       dk(  sd  ywr(   r)      Nr-   .0vs     r!   	<genexpr>z5BackgroundChainRenderStep._execute.<locals>.<genexpr>   s!     V?aaeeHoQU>U?   ##u   [CHAIN_BG_SYNC_FAIL] planner path: chain_bg ImageAsset INSERT 실패. PNG는 디스크에 있으나 DB out of sync — downstream(scene_image_pipeline)이 참조 이미지를 못 찾음. groups_total=%d ok_status=%d exc=%s
%sz-[CHAIN_BG_SYNC_FAIL] rollback also failed: %sc              3  J   K   | ]  }|j                  d       dk(  sd  ywrs   ru   rv   s     r!   ry   z5BackgroundChainRenderStep._execute.<locals>.<genexpr>   s      R!!%%/T:QArz   )completed_countapplicable_countfailed_countr&   r'   c           	   3     K   | ]B  }t        |j                  d g       D cg c]  }|j                  d      dk(  s| c}       D yc c}w w)nodesrender_statusr)   N)lenr-   )rw   rx   ns      r!   ry   z5BackgroundChainRenderStep._execute.<locals>.<genexpr>   sP      +A gr 2U 21aeeO6LPT6TQ 2UV+ Vs   AA

A

Au   [CHAIN_BG_SYNC_FAIL] legacy path: background_chain_node INSERT 실패. PNG는 디스크에 있으나 DB out of sync. locations_total=%d ok_nodes=%d exc=%s
%sz4[CHAIN_BG_SYNC_FAIL] legacy rollback also failed: %sc              3  @   K   | ]  }|j                  d d        yw)total_node_countr   Nru   rv   s     r!   ry   z5BackgroundChainRenderStep._execute.<locals>.<genexpr>  s     R?Q!115?Qs   c              3  f   K   | ])  }|j                  d d      |j                  dd      z
   + yw)r   r   failed_node_countNru   rv   s     r!   ry   z5BackgroundChainRenderStep._execute.<locals>.<genexpr>  s5      
' EE$a(1551Da+HH's   /1))app.core.errorsrD   r   r   r"   app.core.openai_keysrI   r-   _load_location_ref_pathsr4   r:   rB   rT   app.modules.prompt_sanitizerrU   floatllm_timeout_image_genrW   r   r   r   r   mkdirr   osgetenvbuild_opik_metadatapopr>   _register_chain_bg_image_assets	Exceptionsumvaluesloggererrorr   	traceback
format_excdbrollback_register_image_assets)!r   moderD   r   plan_cprI   rm   shot_cpscsishshirO   sidre   rf   rn   ro   rT   rU   clientrh   rd   resultfailedrq   exc	ok_groupsrb_exc
applicable	completedr'   ok_counts!                                    r!   _executez"BackgroundChainRenderStep._executem   s   ,,,,-HI$M    	8)O  68,,-=>kk&"-11(B?E2EVVM*&&"-33B &&q1C"$&&";"ArK"#B4uSE 2a3xuSE4JK'*+60,  L 4 F "::<668 #<<>#'#F#F#H 6@
 (889
 $43F3FG	 &&'oo oo !	! 	 	t4,!++fb11- 		"@BZ[8+FII>G224+ 3%=
" OQ/
 v"5"A&**Xr23F44V<& 01JRv}}RRI#,$. &

6*@x.@	  K45		''	2& Ry?O?O?QRR
 
%%'
 
	  ) *"	
 	
y  V6==?VV	^ KC1E1E1G	GG$$&  ! LLG 
 H  	 "))+ H LLjI#y/C/C/E
  "
 	  JF  		s   L+ O +	O4AO	N$#O$	O-OOOOO	R!AQ=6QQ=	Q:Q50Q=5Q::Q==Rc           	        ddl m} ddlm}m} | j
                  j                  |      j                  |j                  | j                  k(  |j                  dk(        j                         }|si S |D ci c]&  }|j                  s|j                  |j                  ( }}|si S | j
                  j                  |      j                  |j                  | j                  k(  |j                  dk(  |j                  dk(  |j                  j!                  |j#                                     j                         }|j%                         D 	ci c]  \  }}	|	|
 }
}}	i }|D ]K  }|
j'                  |j                        }|s! ||j(                        }|s6|j+                         sG|||<   M t,        j/                  dt1        |             |S c c}w c c}	}w )u|   DB의 ImageAsset(asset_type='reference', is_primary=1)에서
        location entity → 첫 ref PNG 경로를 매핑한다.r   resolve_image_pathEntityCanon
ImageAssetlocation	referencert   z6background_chain_render: %d location ref images loaded)app.core.file_pathsr   app.models.projectr   r   r   queryfilterr   entity_typeallshort_idr,   
asset_type
is_primary	entity_idin_r   itemsr-   	file_pathr   r   infor   )r   r   r   r   
loc_canonscloc_id_by_shortassetskrx   canon_to_shortr/   ashortr3   s                  r!   r   z2BackgroundChainRenderStep._load_location_ref_paths  s    	;> GGMM+&V&&$//9'':5 SU 	 I '1+
&0AJJAJJj 	 +
 I GGMM*%V%%8%%4%%*$$(()?)?)AB	 SU 	 ,;+@+@+BC+B41a!Q$+BC!A"&&q{{3E"1;;/AQXXZE
  	LcRUhW
7+
" Ds   4GGGc                	   ddl m}m} ddlm} | j
                  j                  |      j                  |j                  | j                  k(  |j                  dk(        j                         }|D ci c]&  }|j                  s|j                  |j                  ( }}|st        j                  d       yt        j                   t"        j$                        j'                         }d}	i }
| j
                  j                  |      j                  |j                  | j                  k(  |j(                  dk(        j+                  |j,                  j/                         |j0                  j/                               j                         D ]7  }|j2                  s|j2                  |
vs|j                  |
|j2                  <   9 |j5                         D ]  \  }}|j7                  d      d	k7  r|j7                  d
d      xs d}|s5ddlm}  ||      }|j7                  dd      xs d}|j7                  |      }|st        j                  d||       t=        |j7                  d      xs d      }|j7                  dd      xs d|d}|j7                  dg       xs g }g }|D ]m  }t?        |t@              s|j7                  dd      xs d}|j7                  dd      xs d}|sC|r|jC                  d| d|        ]|jC                  |       o |rdjE                  |      nd}| j
                  j                  |      jG                  | j                  d||| jH                        jK                         }|rO||_&        |j7                  dd      xs d|_'        ||_(        ||_)        ||_*        d|_+        d|_        d|_,        |}nu |t[        t]        j^                               | j                  d|| jH                  |||||j7                  dd      xs dddd||      }| j
                  ja                  |       |
j7                  |      }|tc        |d|g        ntc        |dd!d"t[        |      z   gi#       |	d$z  }	 |	r1| j
                  je                          t        jg                  d%|	       yyc c}w )&um  Phase 5 — group PNG를 ImageAsset(asset_type='chain_bg') UPSERT.

        match key (application-level): (project_id, asset_type, entity_id,
        variant_index, episode_id). variant_index는 location 단위로 1+ 단조 증가
        (floor_plan은 v00=0이 primary, chain_bg는 v01부터).

        file_path는 projects_root 기준 RELATIVE (Phase 3 컨벤션 + T5 review I1 fix).
        t2i_guide는 group의 shot_guides[]를 newline join한 텍스트 — Phase 6
        scene_detail consumer가 이를 prepend하여 chain_bg PNG의 가구 위치 가이드를
        scene t2i 작성에 주입한다.
        r   r   r   r   uT   background_chain_render (planner): no location EntityCanon — skip chain_bg DB syncN
floor_planr(   r)   png_pathr+   to_relative_image_pathlocation_iduI   background_chain_render (planner): location %s no canon — skip group %svariant_indexvariant_labelrx   rR   shot_guidesrS   guide[z] 
chain_bg)r   r   r   r   r   
t2i_prompt	generatedr]   )r,   r   r   r   r   r   r   	t2i_guider   prompt_usedgeneration_modelr(   r   variant_type
created_atrY   pipeline_roleinput_image_idsunresolved_inputsfloor_plan:r   pipeline_metadatart   zEbackground_chain_render (planner): registered %d chain_bg ImageAssets)4r   r   r   r   r   r   r   r   r   r   r   r   r,   r   warningr   nowr   utc	isoformatr   order_byr   descr   r   r   r-   r   r   intr=   r>   appendjoin	filter_byr   firstr   r   r   r   r   r(   r   struuiduuid4addr   commitr   )r   rq   r   r   r   canonsr   canon_id_by_shortr   
registered_fp_id_by_canon_fpgroup_idgrespng_path_strr   rel_png_path	loc_shortcanon_idr   r   r   guide_linessgr   r   t2i_guide_textexisting
_chain_row_fp_id_for_chains                                 r!   r   z9BackgroundChainRenderStep._register_chain_bg_image_assets<  sj    	?, GGMM+&V&&$//9'':5 SU 	 HN,\v!QRQ[Q[QZZ-=v,\ NNf ll8<<(224
 +-GGMM*%V%%8%%5 Xj++002J4I4I4N4N4PQSU  }}o!E14. %llnNHdxx!T)88J39rL C1,?L39rI(,,Y7H_x  9 >Q?M HH_b9Tqs@S=TM ((="5;K%'K!!"d+ffY+1rw+1r#**Qse2eW+=>#**51 " 8CTYY{3N j)#)&"/#     %1"'+xxb'A'GR$)6&%3"(0%"-&'#,D)%
'4::<(#)&#"/"/,* $r : @b%=& !)"
" J'.228<+(.@%5$6
 ).@':]SQY]=Z<[&\ !OJw -z GGNNKKW m -]s   0R?R?c                   ddl m} ddlm}m} | j
                  j                  |      j                  |j                  | j                  k(  |j                  dk(        j                         }|D ci c]&  }|j                  s|j                  |j                  ( }}|st        j                  d       yt        j                   t"        j$                        j'                         }d}	i }
| j
                  j                  |      j                  |j                  | j                  k(  |j(                  dk(        j+                  |j,                  j/                         |j0                  j/                               j                         D ]7  }|j2                  s|j2                  |
vs|j                  |
|j2                  <   9 |j5                         D ]  \  }}|j7                  |      }|st        j                  d|       1|j7                  d	g       xs g D ]  }|j7                  d
      dk7  r|j7                  dd      }|s. ||      }|j7                  dd      }| j
                  j                  |      j                  |j                  | j                  k(  |j2                  |k(  |j(                  dk(  |j8                  |k(  |j:                  | j:                  k(        j=                         }|r(||_        |j7                  dd      |_         d|_!        |}nn |tE        tG        jH                               | j                  d|| j:                  ||j7                  dd      dd|d|      }| j
                  jK                  |       |
j7                  |      }|tM        |d|g       ntM        |dddtE        |      z   gi       |	dz  }	  |	r1| j
                  jO                          t        jQ                  d|	       yyc c}w )u$  노드 PNG를 ImageAsset 테이블에 UPSERT (asset_type='background_chain_node').

        producer 측 명시적 ``to_relative_image_path()`` 호출 (Claude I2). ImagePathType이
        자동 normalize 하지만 ``_register_chain_bg_image_assets`` 와 일관성을 위해 명시.
        r   r   r   r   uA   background_chain_render: no location EntityCanon — skip DB syncNr   uC   background_chain_render: location %s has no EntityCanon — skip DBr   r   r)   r*   r+   r,   background_chain_noder   r   r]   )r,   r   r   r   r   r   r   r   r(   r   r   r   rY   r   r   r   r   rt   z2background_chain_render: registered %d ImageAssets))r   r   r   r   r   r   r   r   r   r   r   r   r,   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r-   r   r   r   r   r   r(   r   r   r   r   r   r   r   )r   r'   r   r   r   r   r   r   r   r   r   r   loc_idloc_datar  noder@   node_idr  	_node_row_fp_id_for_nodes                        r!   r   z0BackgroundChainRenderStep._register_image_assets  s    	?> GGMM+&V&&$//9'':5 SU 	 HN,\v!QRQ[Q[QZZ-=v,\ NN^_ll8<<(224
*,GGMM*%V%%8%%5 Xj++002J4I4I4N4N4PQSU  }}o!E14. !* 1FH(,,V4HY[a  Wb17R788O,4XXlB/+B/((4,GGMM*-V"--@",,8"--1HH"//7:"--@ UW  )+H&+/88L"+EH(&1HO (I *tzz|,#'??#:"*#'??"$$(HH\2$>)A*%,#$#&!I GGKK	*"1"5"5h"?".,!1C)8(9
 -!1C+>QTU]Q^A^@_*` a
i 8 !2z GGNNKKLjY c -]s   0PPc           	        ddl m} ddlm} ddlm} | j                         }|rut        |      }t        |      }| j                  j                  |      j                  |j                  | j                  k(  |j                  | j                  k(  |j                  dk(  |j                  j!                  |            j#                         }t%               }|D ]C  }	 ||	j&                        }
|
s|
j)                         s)|j+                  |	j                         E |t%        |      z  }t        |      }t-        t%        |      |z
        }||k(  rd}n
|dk(  rd}nd}g }|r%|j/                  t        |       d	| d
| d|         ||dk(  ||||dd      S | j1                  d      }|s |dg ddddd      S |j3                  d      xs i j3                  d      xs i }g }|j5                         D ]T  }|j3                  d      xs g D ]:  }|j3                  d      xs |j3                  d      }|s*|j/                  |       < V |s |dg ddddd      S t        |      }| j                  j                  |      j                  |j                  | j                  k(  |j                  | j                  k(  |j                  dk(  |j                  j!                  |            j#                         }t%               }|D ]C  }	 ||	j&                        }
|
s|
j)                         s)|j+                  |	j                         E |t%        |      z  }t        |      }t-        t%        |      |z
        }||k(  rd}n
|dk(  rd}nd}g }|r%|j/                  t        |       d| d
| d|         ||dk(  ||||dd      S )ud  chain_bg/background_chain_node 산출물 무결성 검증 (planner+legacy dual path).

        production 분기 (Phase 5+):
          - planner-driven: `background_planner.data.chain_bg_order` 가 비어있지 않으면
            ImageAsset.asset_type='chain_bg' 행 + variant_type=group_id + 파일 stat 검증.
          - legacy: planner cp 없거나 chain_bg_order 비어있으면 background_chain_planning
            체크포인트의 `data.locations[loc_id].nodes[]` 노드 id를 기대치로 사용해
            asset_type='background_chain_node' + variant_type=node_id + 파일 stat 검증.

        plan.md(2026-05-01) §4.4 Task 23 가정과 production 차이:
          - asset_type은 'reference'가 아니라 'chain_bg' / 'background_chain_node'
          - traceability는 prompt_used regex가 아니라 variant_type 컬럼
          - planner 입력은 background_chain_planning이 아니라 background_planner
          - legacy 입력 키는 chain_plan.nodes가 아니라 nodes (locations[loc_id].nodes)

        cleanup_artifacts override 안 함 → default noop (사용자 caveat 따름).
        r   r   )CompletionReport)r   r   cleanmissingpartialz# chain_bg groups missing (expected=z, found=z): planner)chain_bg_expectedchain_bg_foundpath)is_completer  severitymetadatarE   Tnoner&   r'   r   r,   r  legacyr  z( chain_bg_node images missing (expected=)r   r   app.core.integrity_reportr  r   r   r:   r8   r   r   r   r   r   r   r   r   r   r   setr   r   r   sortedr   r"   r-   r   )r   r   r  r   chain_orderexpected_groupsexpectedrowsgroups_with_filerr3   	found_setfoundmissing_groupsr  missing_msgsr   r'   node_idsr1   r  nidnodes_with_filemissing_nodess                           r!   verify_completionz+BackgroundChainRenderStep.verify_completion6  s   $ 	;>1 446";/O?+H77==,33%%8%%8%%3''++O<	
 ce   #u&q{{3$((8  )3+??I	NE#C$89$DEN "!$$&(L##>*+ ,!!)
(5'^<LN $%0$!)1&+%		 	 ,,-HI# "w/0AvV  [[(.B33K@FB	 ##%C(.B.hhtn;(;OOC( / &
 # "w/0AxX 
 x=ww}}Z(//!!T__4!!T__4!!%<<##''1	

 #% 	 %A"1;;/AQXXZ##ANN3  $c(m3	Is8}y89HHaZ H H}%& '%JhugSI  !W, %-"' 		
 		
r#   N)r   r   returnzOptional[Dict[str, Any]])r2  zDict[str, Path])r2  zOptional[List[str]])r2  zDict[str, Dict[str, Any]])resume)r   r   r2  Dict[str, Any])rq   r4  r2  None)r'   r4  r2  r5  )__name__
__module____qualname____doc__r"   r4   r:   rB   r   r   r   r   r1   r#   r!   r   r   "   s;    L0$_
B,\RhdZLx
r#   r   )r9  
__future__r   r   loggingr   r   r   r   r   pathlibr   typingr   r   r	   r
   app.core.step_runnerr   ,app.modules.pipeline.background_chain_renderr   #app.services.image_capture.annotater   	getLoggerr6  r   r   r:  r#   r!   <module>rC     sX    #   	   '  , , + I			8	$L


 L

r#   