
    Oj$u                        d 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Zddl	Zddl
mZmZmZmZmZ  ej                   e      ZdZddlmZ ddlmZ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$ ddl%m&Z&  G d de'      Z( G d d      Z)y)ug   Gemini 이미지 생성 클라이언트 — urllib 기반 구현 + 키 라운드로빈 + 자동 로깅.    N)AnyDictListOptionalTuplez]https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key})settings)ImageCallBudgetExceededreserve_current_call)get_next_key)ensure_png_bytes)ImageSubmissionUnknownclassify_http_statusclassify_send_failureunknown_send_metadata)log_llm_call)capture_generated_imagec                   0     e Zd ZdZdededef fdZ xZS )ModerationErrorzBRaised when Gemini blocks content due to safety/moderation policy.block_reasonblock_categoriesraw_responsec                 T    || _         || _        || _        t        |   d|        y )NzContent moderation blocked: )r   r   r   super__init__)selfr   r   r   	__class__s       Y/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/llm/gemini_image_client.pyr   zModerationError.__init__&   s0    ( 0(7~FG    )	__name__
__module____qualname____doc__strlistdictr   __classcell__)r   s   @r   r   r   #   s+    LHS HD HPT H Hr   r   c                   t   e Zd ZdZd dededdfdZdeeef   fdZd!dZ	e
defd	       Zdefd
Z eh d      Z eh d      Ze
defd       Ze
defd       ZdefdZdddededededeee      dededee   defdZ	 	 	 d"dedeee      dedeeeeef         deeef   f
dZededdfd       Zy)#GeminiImageClientuH   Gemini image generation client using the REST API + 키 라운드로빈.Napi_keymodelreturnc                 v    || _         |xs t        j                  | _        t	        j
                         | _        y N)_fixed_api_key	_settingsgemini_image_model_model	threadinglocal_tls)r   r)   r*   s      r   r   zGeminiImageClient.__init__0   s*    %;y;;OO%	r   c                 N    | j                   j                  | j                  ddddS )u  이 client 가 **참조 이미지를 몇 장** 받나. ★계약 한 벌.

        ★★★왜 여기인가 (Codex 2026-08-31). 상한을 canary·settings 에 **베껴
        두면** 두 벌이 되고 한쪽만 고쳐진다. **실제 client instance** 가
        내야 preflight 와 보내기 직전이 **같은 것**을 본다.

        ★`max_images=None` 은 「**모른다**」다 — 「무제한」이 아니다.
        이 client 는 `labeled_references` 를 **돌면서 다 싣고** 로컬 상한을
        **선언하지 않는다**(실측). 공식 상한을 모르면 **발명하지 않는다**.

        ★`GrokImageClient` 는 이 class 를 **상속**하므로 같은 계약을 그대로
        쓴다 — 그쪽도 로컬 상한이 없다(실측).
        Tr   N)providerr*   supports_labeled_refs
min_images
max_images)r   r   r1   r   s    r   reference_input_capabilityz,GeminiImageClient.reference_input_capability5   s+     //[[%)
 	
r   c                     t        | j                  d      si | j                  _        | j                  j                  j                  |       | S )uC   로깅 컨텍스트 설정 (thread-local — 동시 호출 안전).ctx)hasattrr4   r=   update)r   kwargss     r   set_contextzGeminiImageClient.set_contextK   s6    tyy%(DIIM		V$r   c                 0    t        | j                  di       S )u   현재 스레드의 context.r=   )getattrr4   r:   s    r   _ctxzGeminiImageClient._ctxR   s     tyy%,,r   c                     | j                   j                  d      xs | j                   j                  d      }|rt        |      S 	 ddlm}m}  |d |             S # t        $ r Y yw xY w)u  스텝 이름 — ctx > capture scope(ambient) > 'image_gen'.

        ctx 는 부르는 쪽이 채울 때만 있다. 안 채우면 전부 'image_gen' 으로
        뭉개져, 같은 실행에서 gpt 경로는 `scene_image_pipeline` 으로 남는데
        gemini 경로만 스텝을 못 갈랐다(2026-08-08 Opik 실측: 롤 86건이
        전부 image_gen). 어느 스텝이 살아 있는지가 기록의 존재 이유다.
        step	step_namer   )ambient_call_metaresolve_step_name	image_gen)rD   getr#   app.modules.llm.image_tracerrH   rI   	Exception)r   rF   rH   rI   s       r   _trace_stepzGeminiImageClient._trace_stepW   sd     yy}}V$B		k(Bt9	
 %[2C2EFF 		s   A 	A)(A)>   rG   
episode_id
project_idoperation_typereference_image_ids>	   still_id	entity_id
beat_title
shot_indexentity_nameentity_typescene_indexmultiroll_tagsafety_ladderc                 j   | j                   }|j                         D ci c]  \  }}|| j                  v s|| }}}d|v rd|vr|d   |d<   d|vr| j                         }|dk7  r||d<   |j                         D ci c]  \  }}|| j                  v r||| }}}|r||d<   |S c c}}w c c}}w )u   log_llm_call 에 전달 가능한 context. 기존 column 단위 key 는 그대로
        forward, fan-out 단위 trace key 는 `metadata` dict 로 묶어 보낸다.
        rF   rG   rJ   metadata)rD   items_LOG_COLUMN_KEYSrN   _LOG_METADATA_KEYS)r   rawkvfilteredresolvedmetas          r   _log_ctxzGeminiImageClient._log_ctx   s    
 ii YY[
(TQA1F1F,FAqD[ 	 
 S=[8$'KH[!h& '')H;&(0% YY[
(TQD+++ qD[ 	 
 #'HZ %

s   B)B) B/c                     | j                   }i }| j                  | j                  z  D ]  }|j                  |      }||||<    |S )u:  Phase 4 iter 7 review I1 — Opik tracer extra_metadata 용 dict.
        DB log 의 metadata 와 동일한 fan-out 단위 추적 정보 (PID/EID 포함)
        를 Opik trace metadata 에도 병합 — 운영자가 incident triage 시
        DB grep 와 Opik 검색 결과가 일관되게 보이도록.
        )rD   r_   r`   rK   )r   ra   rf   rb   rc   s        r   
_opik_metazGeminiImageClient._opik_meta   sS     ii&&)@)@@A
AyDG	 A
 r   c                 F    | j                   r| j                   S t               S r-   )r.   r   r:   s    r   _get_api_keyzGeminiImageClient._get_api_key   s    &&&~r   )	req_bytescausedetail
attempt_nopromptref_image_ids
start_timetracerrl   c                   ddl }	t        t        j                         |z
  dz        }
|	j	                  ||n|xs dj                  d            j                         dd }t        | d|      }t        | j                        }t        ||||j                  d	      xs i 
      |d	<   t        d| j                  |dt        |      |
|d| |j                  | j!                         | j                  ||xs g dt        |      |
i | j"                  t        |||             t$        j'                  d| j                  |||       |S )um  기록을 남기고 **돌려줄 예외를 만든다** (raise 는 호출자가 한다).

        요청이 나갔을 수 있는데 결과를 못 받은 상태다. 다시 보내면 같은
        이미지에 요금이 두 번 나가므로 **재전송 없이 멈춘다.** 기록에는
        `possible_charge` 를 남겨 나중에 제공자 대시보드와 대조할 수 있게
        한다 — 이것은 durable ledger 가 아니라 best-effort 다
        (`image_send_state.unknown_send_metadata` 주석 참조).

        하위 클래스가 그대로 쓴다 — Grok·Reve 는 이 클래스를 상속한다.
        r   N   utf-8   uZ    — 보냈는지 모르므로 다시 보내지 않는다 (요금이 나갔을 수 있다))rm   r]   )rm   ro   request_shabasesubmission_unknown
model_nameuser_promptstatuserror_messageduration_msrR   )rm   ro   ry   rF   r*   rp   rq   r   errorr   extra_metadatau   %s: 보냈는지 모르는 채로 끝났다 — 다시 보내지 않는다 (사유=%s · 시도=%d · 요청지문=%s). 제공자 대시보드에서 요금 여부를 대조할 것 )hashlibinttime	monotonicsha256encode	hexdigestr   r%   rg   r   rK   r   r1   r#   logrN   ri   loggerwarning)r   rm   rn   ro   rp   rq   rr   rs   rl   r   
elapsed_msry   errr=   s                 r   _raise_submission_unknownz+GeminiImageClient._raise_submission_unknown   sg     	$..*Z74?@
nn".I,B&&w/11:SbB %h / 07<> 4==!/JK$*,J 	 	
{{'s3x" -		
 25		
 	

!!#4;;v'-26Jc(
:doo :3&+
,79:	 	 	
 	&'+{{E:{	

 
r   reference_imagesaspect_ratiolabeled_referencesc                    '( g }g (|r|j                  di       t        |d      D ]o  \  }\  }}|j                  d|xs d| di       |j                  ddt        j                  |      j	                  d      di       |s_(j                  |       q nh|rS|j                  di       |D ]:  }|j                  ddt        j                  |      j	                  d      di       < n|j                  di       d	|igd
dg|dddd}	ddlm}
  |
       ''( fd} j                         }t        j                   j                  |      }t        j                  j                  |t        j                  |	      j!                  d      ddid      }t#        j$                         }d}t&        j(                  }t&        j*                  }t-        d|dz         D ]l  }	 t/        d       t        j                  j1                  ||      5 }t        j2                  |j5                         j	                  d            }ddd        ns tI        t#        j$                         |z
  d+z        }tK        dW j                  d,tM        |      |(d- jN                    |tM        |      |       t=        d2|       tI        t#        j$                         |z
  d+z        }	  j]                         |jg                  d5g       }d}|D ]  }|jg                  d6i       }|jg                  d	g       D ]  }|jg                  d7      xs |jg                  d      }ti        |tj              r`|jg                  d8      } | rLtK        dW j                  d9d:|(d; jN                  }!'ja                   jc                          j                  (xs g  jl                  jg                  d<      |d=|i jd                  >       to        t        jp                  |        j                   d? jl                  jg                  d@      xs dA B      }dCD "ci c]9  }" jl                  jg                  |"      |" jl                  jg                  |"      ; }#}"(r(|#dD<   ts        | jl                  jg                  d@      xs dE|!|#F       ||fc c S d|v s|jg                  d      s|d   }  |r|r|d   jg                  dGdH      ndI}$tK        dW j                  d,dJ|dd)  |(d- jN                   'ja                   jc                          j                  (xs g dK|dd) | jd                  4       t_        dL|$ dMg|N      h dO}%|r|d   jg                  dG      xs dPndP}&tM        |&      ju                         |%v rtK        dW j                  d,dQ|& |(d- jN                   'ja                   jc                          j                  (xs g dRtM        |&      dd) | jd                  4       t_        dS|& tM        |&      g|N       |dT|       tK        dW j                  d,dT|(d- jN                   t=        dUt        j                  |      ddV        # 1 sw Y   gxY w# t6        $ r  t        j8                  j:                  $ r}|j5                         j	                  dd      }t=        d |j>                   d!|       }tA        |j>                  d"#      }|d$k(  r||k  r jB                  sx j                         }t        j                   j                  |      }t        j                  j                  |t        j                  |	      j!                  d      ddid      }t#        jD                  d|z         Y d}~|d%k(  r\ jG                  d&|j>                   d'|j>                   d(|dd)  |(|'t        j                  |	      j!                  d      *      |tI        t#        j$                         |z
  d+z        }tK        dW j                  d,tM        |      |(d- jN                    |tM        |      |       t=        d |j>                   d!|       |d}~wt        j8                  jP                  tR        jT                  f$ r}|}tW        |      d.k7  rU jG                  d/tY        |      jZ                   d0| |(|'t        j                  |	      j!                  d      *      |||k  r jB                  sx j                         }t        j                   j                  |      }t        j                  j                  |t        j                  |	      j!                  d      ddid      }t#        jD                  d|z         Y d}~tI        t#        j$                         |z
  d+z        }tK        dW j                  d,tM        |      |(d- jN                    |tM        |      |       t=        d1|       |d}~ww xY w# t^        $ r}}tK        dW j                  d,tM        |      |(d- jN                   'ja                   jc                          j                  (xs g d3tM        |      | jd                  4        d}~ww xY wc c}"w )XzGenerate an image from a text prompt with optional reference images.

        Returns:
            Tuple of (PNG image bytes, response_time_ms).
        text   zReference image :inline_dataz	image/pngascii)	mime_typedatapartsTEXTIMAGE1K)aspectRatio	imageSize)responseModalitiesimageConfig)contentsgenerationConfigr   )get_image_tracerc           
          j                  j                         j                  xs g d| |j                         y )Nr   r   )r   rN   r1   ri   )err_msgms_tracerrp   rq   r   s     r   _trace_errorz6GeminiImageClient.generate_image.<locals>._trace_error  s>    KK%%'t{{]-@bg2#	  r   )r*   r)   rw   zContent-Typezapplication/jsonPOST)r   headersmethodN   zgemini_image_client.urlopen)source)timeoutreplace)errorszGemini image API error z: gemini)via	retryabler{   http_zGemini image API u9    — 상류가 이미 작업을 시작했을 수 있다:    )rm   rn   ro   rp   rq   rr   rs   rl   ru   r   r|   
never_sentzurl_error_after_send:u*   Gemini image API 응답을 못 받았다: z/Gemini image API request failed after retries: z!Gemini image API request failed: moderation_blockedr   
candidatescontent
inlineDatar   z[image generated]success)r}   r~   output_textr   r   rR   image_idr   )rF   r*   rp   rq   output_image_idr   paramsr   /rQ   ?)context)	rP   rO   rQ   rF   rY   rV   rS   rT   r   reference_labelsgemini_image)rolerp   generation_call_idpipeline_metadatafinishReasonSTOPUNKNOWNztext_refusal: moderation_blocked_textztext_refusal:TEXT_REFUSALr   r   r   >   SPIISAFETY	BLOCKLIST
RECITATIONIMAGE_SAFETYPROHIBITED_CONTENTIMAGE_PROHIBITED_CONTENTrv   zfinish_refusal: moderation_blocked_finishzfinish_refusal:zNo image parts in responsez*Gemini image API returned no image parts: i  r   );append	enumeratebase64	b64encodedecoderL   r   rk   GEMINI_API_URL_TEMPLATEformatr1   urllibrequestRequestjsondumpsr   r   r   r/   llm_timeout_image_genllm_max_retriesranger
   urlopenloadsreadr	   r   	HTTPErrorRuntimeErrorcoder   r.   sleepr   r   r   r#   rg   URLErrorsocketr   r   typer   _check_moderation_blockr   r   rN   ri   rK   
isinstancer%   rD   r   	b64decoder   upper))r   rp   r   r   r   r   ilabel	img_bytesbodyr   r   r)   urlreqrr   
last_errorr   max_retriesattemptresponsepayloadexc
error_text_verdictr   r   text_refusal	candidater   partr   b64call_idrb   _capture_metafinish_reason_refusal_reasons_finishr   rq   s)   ``                                     @@r   generate_imagez GeminiImageClient.generate_image   s'     LL&&)*)23Eq)I%%E9fe&F1A!A/FGH!%0 & 0 0 ; B B7 K$  !((/ *J LL&&)*-	!%0 & 0 0 ; B B7 K$  . LL&&)* "5)*'-w&7#/!% !	
 	B"$	 ##%%,,4;;,Pnn$$D!((1#%78	 % 
 ^^%
'+
11//Qa0GTe$,IJ^^++C+AX"jj)?)?)HIG B 1n dnn.;tCDJ ;;F7!*o:$1 6:]]
 Z*5!B:,OPP$..*Z74?@
	((1* [[r2
&*#ImmIr2GGR0"hh|4O8Ok40%//&1C". #'+{{(;#,*0=	# BF	#  !%!1!1!3"&++f*7*=2,0IIMM*,E(2$2L#A+/?? $   %5",,S1'+{{m1'+yy}}5E'F'M#&N%P%	&)&
  $yy}}Q/; tyy}}Q//& & ) )@MM*<=/%!%/?!@!RN#)/6.;  )*44T>dhhv&6#'<L{ 1 $F =G
1!!.&9Y   ;;F7 .|DS/A.BC&$1	 6:]]	 KK%%'kk&+1r0"4C(j#   ",]O<"0!1$ $
 @J:a=$$^4:rw<#33 ;;F7 0	:&$1	 6:]]	 KK%%'kk&+1r2'l4C(j#   ".wi8"%g,$  	1:> 	
{{w6J -	
 26	

 G

SZH[\`]`HaGbcdd BA + <<)) (` XXZ..wy.I
),CCHH:RPZ|*\]
 0hG{*w+/E.."&"3"3"55<<4;;X_<`$nn44djj&6&=&=g&F%35G$HQW 5  JJq7{+33 88 %chhZ0"3CHH: >FFPQURUFVEW!Y#*%]#-g"&**T"2"9"9'"B 9   !$.."2Z"?4!GH
 #{{w"%j/z(5 :>
 S_j9"%<SXXJb#UV\__LL))6>>: $e 
 )-=88 5d3i6H6H5IJ!KC5Q#*%]#-g"&**T"2"9"9'"B 9   k).."&"3"3"55<<4;;X_<`$nn44djj&6&=&=g&F%35G$HQW 5  JJq7{+ $.."2Z"?4!GH
 #{{w"%j/z(5 :>
 S_j9"%TUXTY#Z[addI$eh  	 ;;F7!#hJ$1 6:]]
 KK%%'kk&+1r+3s8&#   	x)sk   +-Z=3Z0Z=,i< >l0Z:	5Z=='i9$C9c#C!c1i95Di4A3i44i9<	lA8k==lr  c                    | j                  di       }|j                  d      }|rQ|j                  dg       D cg c](  }|j                  dd      dvr|j                  dd      * }}t        ||| 	      | j                  d
g       }|ro|d   j                  dd      }|dv rV|d   j                  dg       }|D cg c](  }|j                  dd      dvr|j                  dd      * }}t        d|| 	      |sV|j                  dg       }|D cg c](  }|j                  dd      dvr|j                  dd      * }}|rt        d|| 	      yyc c}w c c}w c c}w )z0Detect moderation blocks in the Gemini response.promptFeedbackblockReasonsafetyRatingsprobability
NEGLIGIBLE)r  LOWcategoryr   r   r   r   r   rv   )r   r   IMAGE_OTHERr   N)rK   r   )	r  prompt_feedbackr   r
categoriesr   r  safety_ratings	high_risks	            r   r   z)GeminiImageClient._check_moderation_block4  s    "++&6;&**=9 ),,_bAAA555=RR j),A  
 ")!+$  [[r2
&qM--nbAM II!+A!2!2?B!G ,+uu]L9AVV EE*i0+  
 &!)%/!(  ,00"EN ('A555=RR j),'  
 %!)%.!(   7 s   -E=-E-E)NN)r+   r(   )Nz16:9N)r   r    r!   r"   r#   r   r   r   r;   rA   propertyrD   rN   	frozensetr_   r`   rg   ri   rk   r   r   r   floatbytesr   r   r   r  staticmethodr%   r   r   r   r   r(   r(   -   s   R& &3 &$ &

DcN 
, -d - -S . ! "  # $  $  2 D   c  &*	11%(16911$,T#Y$71 1 $'1 E?	1
 
 1l 37"@DMeMe #4;/Me 	Me
 %T%U
*;%<=Me 
ucz	Me^
 , ,$ , ,r   r(   )*r"   r   r   loggingr2   r   r   urllib.errorr   urllib.requesttypingr   r   r   r   r   	getLoggerr   r   r   app.core.configr   r/   app.core.image_call_budgetr	   r
   app.modules.llm.gemini_key_poolr   app.modules.llm.image_formatr    app.modules.llm.image_send_stater   r   r   r   app.modules.llm.llm_loggerr   app.services.image_capture.sinkr   rM   r   r(   r   r   r   <module>r/     s    m         3 3			8	$ d  2 9 9  4 CHi Ht tr   