
    jb                        d 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 ddlmZ ddlmZ ddlmZmZ dd	d
dddddZddddddddZdedee   fdZdedefdZ ej2                  e      ZdZddddiddd d!ddid"d#d$d d%g d%d&Zdddd'd ddid(d)d*gd&Z	 dEd+ed,ee   de
e	eef      fd-Z 	 	 	 dFd+ed.ed/e	eef   d0ed,ee   d1ee
e      de	eef   fd2Z!d3ed4ee   defd5Z"d+ed6ed3edede	eef   f
d7Z#de	eef   fd8Z$d9ed:ed6ed3ede	eef   f
d;Z%de	eef   fd<Z&	 	 	 	 	 dGd=ed6ed3eded>ed?ed@ee
eeef         dAedBee	eef      dCee   d4ee   de	eef   fdDZ'y)Hu\  요소 참조 이미지 파이프라인 — T2I 생성 + GPT LVM 검증 + 비교 선택.

흐름:
1) 의존성 순서대로 T2I 생성 (Gemini) — 변형은 기본 참조이미지 포함
2) GPT LVM 검증 → 심각도 판단
3) 심각하면 재생성 → GPT LVM이 (1)과 (2) 비교하여 더 좋은 것 선택
4) 최종 참조 이미지 확정
    N)Path)AnyDictListOptionalTuplesettingsload_prompt)GeminiImageClientModerationErrorcharacter_refcharacter_nonhuman_reflocation_refprop_refcharacter_outlook_refcharacter_composite_refcharacter_composite_derive_ref)	charactercharacter_nonhumanlocationpropoutlook	compositecomposite_derive3:4z1:1z16:9)r   r   r   r   r   r   r   entity_typereturnc                 x    t         j                  |       }|sy	 t        d|fi |S # t        t        f$ r Y yw xY w)zLoad type-specific reference image prompt template from external files.

    Returns the formatted prompt string, or None if no template is found
    (backward-compatible: caller falls back to raw t2i_prompt).
    Nref_image_prompts)_REF_PROMPT_MAPgetr   FileNotFoundErrorKeyError)r   kwargstemplate_names      ]/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/ref_image_pipeline.py_load_ref_image_promptr)   .   sI     $''4M.HHHx( s   ' 99namec                     t        d| fi |S )Nlvm_promptsr   )r*   r&   s     r(   _load_lvm_promptr-   =   s    }d5f55    z#https://api.openai.com/v1/responsesobjectFtypebooleanstringzok | minor | severe)r0   descriptionarray)r0   itemsintegerz0-100)matches_descriptionseverityissuesscore)r0   additionalProperties
propertiesrequiredzimage_1 | image_2)winnerreasonr>   r?   image_bytesimage_bytes_2c                     dt         dt        t        t        f   fd} ||       g}|r|j	                   ||             |S )ut  텍스트 + 그림 1~2장 — **조립만 한다.**

    ★`_call_gpt_lvm` 안에 있던 것을 꺼냈다. 그 함수는 「명시 모델로 한 번
     보내는」 운반층이고, 이중 호출은 `dual_vlm.ask_both` 가 소유한다 —
     둘을 한 함수에 두면 업무마다 다른 합의가 운반층으로 새어 든다
     (2026-08-27 Codex 판정).
    rawr   c                 ^    t        j                  |       j                  d      }ddd| idS )Nascii	image_urlurldata:image/png;base64,r0   rF   )base64	b64encodedecode)rC   b64s     r(   _urlz_lvm_parts.<locals>._urlp   s<    s#**73##'=cU%CDF 	Fr.   )bytesr   strr   append)r@   rA   rN   partss       r(   
_lvm_partsrS   f   sH    F% FDcN F
 $(#4"5ET-()Lr.   text_promptresponse_schemaschema_name	opik_tagsc           
         ddl m}m} d|ddddt        j                  |       j                  d       id	g}|r;|j                  dddt        j                  |      j                  d       id	       |r|n|g}	 |d
d|dgd||dddt        j                   |||	            }
|
j                  d   j                  j                  }|st        d|       t        j                  |      S )uF   GPT LVM (Vision) 호출 — LiteLLM Router 경유, Opik 자동 추적.r   )build_call_metadatarouter_completiontext)r0   r[   rF   rG   rH   rE   rI   gptuser)rolecontentjson_schemaT)r*   schemastrict)r0   r`   )modelmessagesresponse_formattimeoutmetadataz+GPT LVM returned empty response for schema=)app.modules.llm.llm_clientrY   rZ   rJ   rK   rL   rQ   r
   llm_timeout_validationchoicesmessager_   RuntimeErrorjsonloads)r@   rT   rU   rV   rA   rW   rY   rZ   r_   tagsresponserC   s               r(   _call_gpt_lvmrq   {   s&   ? -E5KFL\L\]hLiLpLpqxLyKz3{+|}G  9OPVP`P`anPoPvPvw~P  PA  8B  0C  D	
 "9}D !!g67!$/?VZ[
 // %[$7H 

1

%
%
-
-CHVWW::c?r.   entity_descriptionoutlook_descriptionc                 r    | xs dj                         }|xs dj                         }|r||k(  r|S | d| S )u  검사에 넘길 설명 — **옷 상세를 싣는다** (2026-09-20 Codex 감사).

    합성 갈래에서 `entity_description` 은 `<인물> wearing <옷 이름>`
    한 줄이다. 생성 문안은 `outlook_description`(옷의 실제 상세)을 받는데
    **검사는 그 한 줄만** 받았다. 그래서 「커다란 밀짚모자와 거대한 장화」가
    빠진 그림도 검사를 통과했다 — 검사가 그런 요구가 있는 줄 몰랐다.

    ★새 유료 검사를 더하는 것이 아니다. **같은 호출에 요구사항을 싣는다.**
    ★그래도 「검사 통과」가 「모자·장화·비율·층 순서까지 충족」의 증거는
     아니다. 그 판단은 다음 판 그림을 사람이 보는 자리에 남는다.
     u   
입은 것의 상세: )strip)rr   rs   baseextras       r(   _validation_descriptionry      sJ     $"++-D &B--/EETMV+E733r.   entity_namec                 v    ddl m} t        d|||      } |d|t        |       t        d      }t        |      S )u[  참조 이미지 검증 — **두 모델이 각각 한 번**, 둘 다 severe 여야 재생성.

    Returns:
        `{"matches_description", "severity", "issues", "score", "_dual"}`.
        모양은 종전 그대로다 — 호출부가 `severity` 를 읽는 계약이 살아 있다.

    ## 합의 규칙 — **이 호출자가 소유한다**

    `severity == "severe"` 면 호출부가 **그림을 하나 더 산다**(`:324`).
    그래서 이 자리의 합의는 **돈을 쓰는 문턱**이다:

        둘 다 severe    → severe   (재생성한다)
        엇갈림          → split    (**안 산다**)
        한쪽 실패       → incomplete (**안 산다**)

    ★엇갈릴 때 안 사는 쪽으로 간다. 한 모델이 「괜찮다」고 하면 그 그림을
     버릴 근거가 약하고, 틀렸을 때 잃는 것은 참조 한 장의 품질인데
     맞았을 때 아끼는 것은 **유료 이미지 한 장**이다.
    ★`issues` 는 **합치지 않고 이어 붙인다** — 서로 다른 뜻의 자유문장을
     글자로 거르면 안 된다(이 저장소 금지 규칙).
    r   ask_bothref_validation)rz   r   rr   rV   )app.modules.llm.dual_vlmr}   r-   rS   _VALIDATION_SCHEMAcombine_validation)r@   rz   rr   r   r}   promptduals          r(   validate_reference_imager      sG    6 2.[M_aF$f{+-? 02D d##r.   c                 N   | j                   D cg c]:  }|j                  r,t        |j                  xs i j	                  d      xs d      < }}| j
                  sd}nHt        d |D              rd}n3t        d |D              rd}nt        t        |            dk(  r|d	   nd}g }| j                   D ]K  }|j                  xs i j	                  d
      xs g D ]#  }|j                  d|j                   d|        % M | j                   D cg c]:  }|j                  r,t        |j                  xs i j	                  d      xs d	      < }}| j
                  rt        d | j                   D              nd|||rt        |      nd	| j                         dS c c}w c c}w )uW   모델별 검증 → 재생성 문턱. **판단은 여기 있고 실행부엔 없다.**r8   ru   
incompletec              3   &   K   | ]	  }|d k(    ywsevereN .0xs     r(   	<genexpr>z%combine_validation.<locals>.<genexpr>        (CqQ(]C   r   c              3   &   K   | ]	  }|d k(    ywr   r   r   s     r(   r   z%combine_validation.<locals>.<genexpr>   r   r   split   r   r9   [] r:   c              3      K   | ]8  }|j                   r*t        |j                  xs i j                  d              : yw)r7   N)okboolpayloadr#   )r   cs     r(   r   z%combine_validation.<locals>.<genexpr>   s7      #)144 !))/r&&'<=>s   >A F)r7   r8   r9   r:   _dual)callsr   rP   r   r#   completeallanylensetrQ   aliasintmin
provenance)r   r   sevagreedr9   itscoress          r(   r   r      s   AE Add 		R$$Z06B7C ==	(C(	(	(C(	(s3x=A-Q7FZZIIO((28b8BMMAaggYb-. 9  AE

 
1 199?''05A6
F 
 -1MM  # #)ZZ#)  )?D &VA"	 	!s   ?F?F"image_1image_2c                 v    ddl m} t        d||      } |d|t        | |      t        d      }t        |      S )u  두 이미지 비교 — **두 모델이 각각 한 번**, 둘 다 바꾸자고 해야 바꾼다.

    Returns: `{"winner": "image_1"|"image_2", "reason", "_dual"}`

    ## 합의 규칙 — **위 검증과 다르다**

    여기서는 **이미 산 두 장** 중 하나를 고른다. 돈이 더 나가지 않으므로
    문턱의 뜻이 다르다:

        둘 다 image_2  → image_2  (바꾼다)
        그 밖          → image_1  (원래 것을 지킨다)

    ★이진 선택이라 **점수 합산 같은 것을 넣지 않는다.** 엇갈리면 새로
     만든 것으로 바꿀 근거가 약하니 원래 것을 지킨다.
    r   r|   ref_comparison)rz   rr   r   )r   r}   r-   rS   _COMPARISON_SCHEMAcombine_comparison)r   r   rz   rr   r}   r   r   s          r(   compare_two_imagesr     sG    * 2.4FHF$fw02D 02D d##r.   c           	         | j                   D cg c]:  }|j                  r,t        |j                  xs i j	                  d      xs d      < }}| j
                  r|rt        d |D              rdnd}| j                   D cg c]A  }|j                  r3d|j                   d|j                  xs i j	                  d      xs d C }}|d	j                  |      | j                         d
S c c}w c c}w )uM   모델별 승자 → 최종 승자. 엇갈리면 **원래 것을 지킨다.**r>   ru   c              3   &   K   | ]	  }|d k(    yw)r   Nr   )r   ps     r(   r   z%combine_comparison.<locals>.<genexpr>'  s     4ea9ner   r   r   r   r   r?   z / )r>   r?   r   )
r   r   rP   r   r#   r   r   r   joinr   )r   r   picksr>   reasonss        r(   r   r   "  s    AE A !))/r&&x06B7E ==U4e44 :C  

,"1add 177)2qyyB33H=CDE"  ,

7(;__&( (,s   ?C-AC2gemini_client
t2i_prompt
output_dirextra_referencesstyle_context
trace_meta
state_hintc           
        () |j                  dd       ddl}d|d}|r|j                  |        | j                  d?i | t        j                  d|       ddlm} dd	lm	} t        |||
xs |
      }|r|)t        j                  d|       n|)|r| d) )t        j                  |d      (d}d}d}dt        t           f()fd}t        d      D ]  }	 | j!                  )(      \  }} n |t/        d!       ||j1                          d"z  }|j3                  |       dd#lm} dd$lm}m}  ||)|t<        j>                  |jA                  |jB                        jE                         d%       tG        ||
      } 	 tI        || |      }!|!j                  d2      d3k(  rt        j                  d4       	 | j!                  )(      \  }#}$||j1                          d"z  }%|%j3                  |#       tQ        ||#|       }&|&j                  d5d6      }'t        j                  d7|'|&j                  d8d             |&|!d9<   |'d:k(  r#tS        |%      |#)|!|&dt<        j>                  ||d;	S 	 tS        |      |)|!d'|rt<        j>                   d=| nt<        j>                  ||d>S # t"        $ r}t        j%                  d|dz   |j&                         |dk(  r |       }|r|\  }}Y d}~ |dk\  r 	  | |             }dddd}|	r|	i|d<   d|d<   d|d<   |j)                  )|j&                  |j*                  |dz   |      }|j                  d)      )|}t        j                  d|j                  dd      )dd        n)# t,        $ r}t        j%                  d |       |d}~ww xY wY d}~d}~ww xY w# t,        $ rT}t<        jJ                  r!t        j%                  d&|       d'd(d)gddd*}!ndd+l&m'}"  |"d,d-d.|d/d01      Y d}~d}~ww xY w# t,        $ r"}t        j%                  d<|       Y d}~d}~ww xY w)@u  참조 이미지 생성 + GPT LVM 검증 + 필요 시 재생성 비교.

    Args:
        trace_meta (Phase 4 iter 7 W3): observability metadata —
            {project_id, episode_id, entity_id, scene_index, shot_index}.
            llm_call_log 의 PID/EID NULL 사고 차단용.

    Returns:
        {
            "file_path": str,
            "image_bytes": bytes,
            "validation": {...},
            "was_regenerated": bool,
            "generation_model": str,
        }
    T)parentsexist_okr   Nref_image_gen)steprz   r   z,Generating reference image for: %s (type=%s))PromptSanitizer)OpenAIClient)rr   rs   z3Using type-specific ref prompt template for %s (%s)z

r   r   c                     ddl m}  ddlm} t	        t        | dd            j                  d      D cg c]"  }|j                         r|j                         $ }}|D ]A  }	  ||      }|j                  
	      \  }}|rt        j                  d|       ||fc S C y c c}w # t        $ r!}t        j                  d	||       Y d }~od }~ww xY w)
Nr   r	   )build_cine_client(still_cine_moderation_fallback_providersru   ,r   labeled_referencesaspect_ratiouX   Reference T2I: %s 가 검열로 막혀 **%s 로 만들었다** (글은 원문 그대로)u"   대체 제공자 %s 도 실패: %s)app.core.configr
   "app.modules.pipeline.cine_providerr   rP   getattrr   rv   generate_imageloggerwarning	Exception)_str   r   namesr*   clientout_excaspectcurrent_promptrz   r   s            r(   _try_other_providersz=generate_and_validate_reference.<locals>._try_other_providers  s    3H$';R)A %BBG%*%M  %Mqwwy  %M  DP*40..)'7!' / Q
 NN45@$H 9$	   %   PCT3OOPs   'B%$;B**	C3CC   r   z-Reference T2I blocked (attempt %d) for %s: %sr      reference_identity)semantic_modesingle_subject_onlyoverride_strategy_prefixsubjectsource_statesforbid_unharmed_rewritepreserve_subject_state)attemptsemantic_constraintssanitized_promptz*Sanitized prompt for %s (strategy: %s): %sstrategyru   P   zSanitization failed: %sz'Failed to generate reference image for z.png)embed_png_metadata)datetimetimezone)r   rz   r   rc   
created_atu   Reference LVM validation unavailable for %s: %s — explicit override (ALLOW_LVM_VALIDATION_UNAVAILABLE=true). Proceeding with _validation_unavailable marker.Funavailableu7   LVM validation unavailable — explicit override active)r7   r8   r9   r:   _validation_unavailable)AppErrorz-step.ref_image_pipeline.lvm_validation_failedz$Reference LVM validation failed for z: u   . To proceed without LVM (e.g. transient outage) set ALLOW_LVM_VALIDATION_UNAVAILABLE=true env. Silent absorb 금지 (feedback_no_silent_fallback).i  )coderk   status_coder8   r   u$   Severe issue for %s — regeneratingr>   r   z&Comparison for %s: winner=%s reason=%sr?   
comparisonr   )		file_pathr@   prompt_used
validationr   was_regeneratedgeneration_modelsanitization_infofallback_providerzRegeneration failed for %s: %su   →)r   r@   r   r   r   r   r   r   r   )*mkdiruuidupdateset_contextr   infoapp.modules.prompt_sanitizerr   app.modules.llm.openai_clientr   r)   _ASPECT_MAPr#   r   rO   ranger   r   r   block_reasonsanitizeblock_categoriesr   rl   uuid4write_bytesapp.modules.png_metadatar   r   r   r
   gemini_image_modelnowutc	isoformatry   r    allow_lvm_validation_unavailableapp.core.errorsr   r   rP   )*r   rz   rr   r   r   r   r   r   r   r   rs   r   
ctx_kwargsr   r   
ref_promptimg_bytes_1r   _fallback_usedr   r   elapsedr   _alt	sanitizer_constraintssanitize_resultsan_excpath_1r   r   r   _checked_descriptionr   r   img_bytes_2r   path_2r   r>   r   r   s*    `    `                                 @@r(   generate_and_validate_referencer   .  s4   : TD1  """J
 *%M+
+ KK>[Y<: (-0F4FJ
 #I;Xcd# )?$~.>?__[%0FK %)N(5/  0 83	#0#?#?%#3# $@ $ K
  l D[MRSSTZZ\N$//F
{# <+v "",,ll8<<0::<   3/1 -&:K

D ~~j!X-:KH,	O*99%#3# : NK
  TZZ\N$"77F{+ ,[+7KJ  ^^Hi8FKK@#VZ^^Hb-IK (2J|$"!$V#.#1",",'+(0(C(C):)7
 
 #  ["%    **+3~.>?#+#>#>.+ y  ,	NNJ"Q;S5E5EG !|+-26/K!|+LN;	 &:+/04*	0 5@*4ML1>BL!:;=AL!9:"+"4"4"C$4$4c6J6J#AIL #5 #J "1!4!45G!X$3!H')<)<Z)Ln]`^`Nac 8'B	cQ,	Z  44NN7 S	 (-)TU+/J 1D:;/g CD
  	 	 X  	ONN;[#NN	Osu   $J:O B2P5 :
O8OO
BNO	O'N??OOO	P2A	P--P25	Q >QQ )N)
lvm_resultNN)Nru   NNN)(__doc__rJ   rm   loggingtimepathlibr   typingr   r   r   r   r   r   r
   app.modules.prompt_loaderr   #app.modules.llm.gemini_image_clientr   r   r"   r  rP   r)   r-   	getLogger__name__r   OPENAI_API_URLr   r   rO   rS   rq   ry   r   r   r   r   r   r   r.   r(   <module>r,     sU        3 3 $ 1 R !2&* 9
  (3- 63 6S 6 
		8	$6
 ! &	20

 h'
 $G< G & ! .
 8$ 8$  :>'/	$sCx.2 $%)%)--- #s(^- 	-
 E?- S	"- 
#s(^-`442:3-44*"$"$"$ "$ 	"$
 
#s(^"$JS#X <$$$ $ 	$
 
#s(^$>	(S#X 	(& ;?+/ $)-[$[[ [ 	[
 [ [ tE#u*$567[ [ c3h([ [ "#[ 
#s(^[r.   