
    j9<                    ,   d Z ddlm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 dd
lmZ ddlm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( ddl)m*Z*  ejV                  e,      Z- G d d      Z.y)u   ReferenceCompositeService — 합성(face+outlook) 이미지 생성 (W5 F24 Phase 3).

ReferenceImageService facade에서 generate_composite_image +
regenerate_composites_for_entity를 literal lift.
facade는 얇은 delegate만 유지.
    )annotationsN)datetimetimezone)Path)AnyDictListOptional)Session)settings)AppError)to_relative_image_path)t)ActivityLogger)CharacterOutlookEntityCanonEntityEpisodeLink
ImageAsset)GeminiImageClientModerationError)	key_count)annotate_generated_asset)image_to_dictc                  r    e Zd ZdZ	 	 	 	 	 	 	 	 	 	 ddZ	 	 	 	 ddZ	 	 d		 	 	 	 	 	 	 	 	 d
dZ	 d	 	 	 	 	 ddZy)ReferenceCompositeServiceu?   합성 이미지 생성 + 엔티티 기준 재생성 서비스.c                <    || _         || _        || _        || _        y N)_db_project_id	_actor_id_logger)selfdb
project_idactor_idactivity_loggers        ^/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/reference_composite_service.py__init__z"ReferenceCompositeService.__init__$   s!     %!&    c                   |r|S ddl m} | j                  j                  |j                        j                  |j                  | j                  k(  |j                  |k(        j                         D ch c]  }|d   	 }}t        |      dk(  r|j                         S |r t        j                  d|t        |             yc c}w )u  이 합성을 **어느 화 아래에** 적을 것인가.

        ★★★임의로 고르지 않는다 (Codex BLOCK 2026-09-04).

         앞 판은 `EntityEpisodeLink.canon_id == character_id` 의 **첫 행**을
         집었다 — 프로젝트 필터도 정렬도 없었다. 공유 인물 `C01` 이 1·2화에
         다 있으면 **2화 옷의 합성이 1화 폴더·DB·trace 에 기록된다.**
         화 삭제·감사·자산 계보가 통째로 어긋난다.

        규칙:
            부른 쪽이 화를 주면          → 그 화
            안 주고 링크가 **딱 하나**면 → 그 화 (종전 동작과 같다)
            0개거나 여럿이면             → `"shared"` — **안 고른다**
        r   )r      u   합성 저장 화를 못 정했다 — 인물 %s 가 %d개 화에 걸려 있는데 부른 쪽이 화를 안 줬다. `shared` 아래에 적는다 (임의로 고르면 계보가 어긋난다)shared)app.models.projectr   r   query
episode_idfilterr$   r   canon_idalllenpoploggerwarning)r"   character_idr/   r   repss         r'   _storage_episodez*ReferenceCompositeService._storage_episode0   s      8 ((..):)E)EFMM!,,0@0@@!**l: ce
QAaD  	 
 s8q=779NN?@LcRUhX 
s   4CNc                   t         j                  s$t               dk(  rt        dt	        d      d      | j
                  j                  t              j                  t        j                  |k(  t        j                  | j                  k(        j                         }| j
                  j                  t              j                  t        j                  |k(  t        j                  | j                  k(        j                         }|r|st        dt	        d      d      |j                  dk(  rt        d|j                   d	d      | j
                  j                  t              j                  t        j                   |k(  t        j"                  d
k(  t        j$                  dk(  t        j&                  j)                  d             j                         }|r#t+        |j,                        j/                         st        ddd      ddlm} |j                  xs d || j                        v }	| j
                  j                  t              j                  t        j                   |k(  t        j"                  d
k(  t        j$                  dk(        j                         }
t5        |
xr# t+        |
j,                        j/                               }|	s|st        ddd      t+        |j,                        j7                         }| j9                  ||      }t+        t         j:                        | j                  z  dz  |z  d
z  }ddlm}m } tC        t         jD                        }|jG                  | j                  |d       |	rdnd} ||      xs d}|	rdnd|fg}|	sA|jI                  d|j                   t+        |
j,                        j7                         f       	  |||j                   d|j                   d|j                   d |j                   |||||jJ                  xs d| j                  |d|d!"	      }d&| d'| d'|j                   }dd(l(m)} | j
                  jU                   |d)      | j                  d*| d'| d+d,       t        tW        tY        jZ                               | j                  d
||t]        |d-         d.| d/|j                   d0|j                   |j_                  d1t         jD                        d2|j_                  d3      r'ta        jb                  |j_                  d3i       d45      nd%|j_                  d3      r!|j_                  d3i       j_                  d6      nd%dte        jf                  th        jj                        jm                         7      }| j
                  jo                  |       tq        |d8d8||	rd%n|r|
nd%fD cg c]  }||j                   c}9       | j
                  js                          tu        |      S # tL        $ r }t        d#d$|jN                   d      d%}~ww xY wc c}w ):u   합성 이미지 1장 생성 (얼굴 + 아웃룩 → 전신).

        ★``episode_id`` — **어느 화의 배정인가.** 안 주면 위 규칙으로 정하되
         임의로 고르지 않는다.
        r   zimage.gemini_key_missingi  )codemessagestatus_codezentity.not_foundi  O00zimage.null_outlookuT   은 Null Outlook(O00) — 합성 불필요 (캐릭터 참조이미지 직접 사용)	referencer+   z[composite:%zimage.face_missinguT   얼굴 참조 이미지가 없습니다. 인물 이미지를 먼저 생성하세요.)body_identity_short_ids zimage.outfit_missinguZ   아웃룩 참조 이미지가 없습니다. 아웃룩 이미지를 먼저 생성하세요.images)generate_and_validate_reference_load_ref_image_prompt)modelcomposite_single)r$   r/   operation_typecomposite_derive	compositezyFull body shot, standing pose, plain neutral background. Dress the character in the outfit shown in the reference images.u3   Reference image 1 — the character's COMPLETE BODYzReference face imagezOutfit piece: z ()z	 wearing )r$   r/   rH   	entity_id)	gemini_cliententity_nameentity_descriptionentity_type
t2i_prompt
output_dirextra_referencesoutlook_description
trace_metazimage.generation_blockedzImage generation blocked: Nz
composite::)textzjUPDATE image_asset SET is_primary = 0 WHERE project_id = :pid AND prompt_used LIKE :key AND is_primary = 1z[composite:%)pidkey	file_path[z] +generation_model	generated
validationF)ensure_asciiscore)idr$   
asset_typerL   r/   r[   prompt_usedr^   statusreview_notesvalidation_score
is_primary
created_atreference_composite)pipeline_rolestageinput_image_ids);r   gemini_api_keygemini_key_countr   r   r   r.   r   r0   rc   r$   r   firstshort_idnamer   rL   rd   ri   re   liker   r[   existsapp.core.body_identityrA   bool
read_bytesr:   projects_dir'app.modules.pipeline.ref_image_pipelinerD   rE   r   gemini_image_modelset_contextappenddescriptionr   block_reason
sqlalchemyrW   executestruuiduuid4r   getjsondumpsr   nowr   utc	isoformataddr   commitr   )r"   r7   
outlook_idipr/   char_entoutlook_ent
face_assetrA   _deriveoutfit_asset
_outfit_ok
face_bytesreference_dirrD   rE   rM   _ctypecomposite_promptlabeled_refspipe_resultexccomposite_keysql_textassetas                             r'   generate_composite_imagez2ReferenceCompositeService.generate_composite_imageS   s0    &&+;+=+B :AF`Daorss88>>+.55knn6TVaVlVlpt  qA  qA  WA  B  H  H  Jhhnn[188:9UWbWmWmqu  rB  rB  XB  C  I  I  K{ 2A>P<Q_bcc5()#==/)}~  HHNN:&V$$4%%4%%*'',,^<<	 UW 	 j&:&:!;!B!B!D 4  ?U  cf  g  g 	C$$*/F0  xx~~j188  J.
0E0E0TV`VkVkopVp

%' 	 BT,"8"89@@BD
z 6  A]  kn  o  o*../::<
**<D
X223d6F6FFQT^^alls)0K0KL!!T-=-=*ew!x (/#K1&9 H H 	 HOC)J8
 !+"2"2!34l,,-88:<=	F9+'}}oR0@0@/AC&.mm_Ik>N>N=O#P"+(!- &1%<%<%B #'"2"2",&8!-	!K8 %\N!J<qP/S
 ## !a
|1=?	@ 4::< ''""!,[-EFM?"X]]O1[=M=M<NO(__-?A\A\]^i^m^mnz^{KOOL"$ETYZ  BFOZ_kOl[__\2>BB7Krv||HLL1;;=
 	U /' #D*,$$ ]  $	
 	U##_  	F :F`adaqaq`rDs  BE  F  F	FP$s   -A!X/ -Y/	Y8YYc                   | j                   j                  t              j                  t        j                  |k(  t        j
                  | j                  k(        j                         }|sg S g }|j                  dk(  r| j                   j                  t              j                  t        j                  |k(  t        j
                  | j                  k(        j                         }|D ]  }| j                   j                  t              j                  t        j                  |j                  k(        j                         }|r|j                  dk(  rl	 | j                  ||j                  ||j                        }|j!                  |        |S |j                  dk(  r|j                  dk(  r|S | j                   j                  t              j                  t        j                  |k(  t        j
                  | j                  k(        j                         }|D ]=  }	 | j                  |j                  |||j                        }|j!                  |       ? |S # t"        $ r-}	t$        j'                  d||j                  |	       Y d}	~	d}	~	ww xY w# t"        $ r,}	t$        j'                  d|j                  ||	       Y d}	~	d}	~	ww xY w)un   엔티티(인물 또는 아웃룩)의 이미지가 변경될 때, 관련 합성 이미지를 모두 재생성.	characterr?   )r   r/   z$Composite regen failed for %s+%s: %sNoutlook)r   r.   r   r0   rc   r$   r   rq   rP   r   r7   r2   r   rr   r   r/   r}   	Exceptionr5   r6   )
r"   rL   r   entityresultscomboscoo_entresultr   s
             r'    regenerate_composites_for_entityz:ReferenceCompositeService.regenerate_composites_for_entity   sh    ,33KNNi4OQ\QgQgkok{k{Q{|  C  C  EI )+,XX^^$45<< --:<L<W<W[_[k[k<kce  {3::;>>R]];Z[aacU^^u4j "::!2==R#%== ; 2F NN6* :  9,%'XX^^$45<< ++y8:J:U:UY]YiYi:ice  l!::r#%== ; 2F NN6*  % ! jNN#I9VXVcVcehiij ! lNN#I2??\egjkkls0   :I#%:J#	J,"JJ	K%"KK)
r#   
OrmSessionr$   r   r%   r   r&   r   returnNone)r7   r   r/   Optional[str]r   r   )NN)
r7   r   r   r   r   r   r/   r   r   zDict[str, Any]r   )rL   r   r   r   r   zList[Dict[str, Any]])__name__
__module____qualname____doc__r(   r:   r   r    r)   r'   r   r   !   s    I
'
' 
' 	
'
 (
' 

'!%2!7:!N !$(i$i$ i$ 	i$
 "i$ 
i$\ !00 0 
	0r)   r   )/r   
__future__r   r   loggingr   r   r   pathlibr   typingr   r   r	   r
   sqlalchemy.ormr   r   app.core.configr   app.core.errorsr   app.core.file_pathsr   app.i18n.loaderr   app.logging.activity_loggerr   r-   r   r   r   r   #app.modules.llm.gemini_image_clientr   r   app.modules.llm.gemini_key_poolr   rp   #app.services.image_capture.annotater   "app.services.image_service_helpersr   	getLoggerr   r5   r   r   r)   r'   <module>r      sf    #    '  , , 0 $ $ 6  6 [ [ R I H <			8	$M Mr)   