
    گjMk                     b   d Z ddl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m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 dd	lmZ dd
lmZ ddl m!Z!m"Z"m#Z# ddl$m%Z% ddl&m'Z'  ejP                  e)      Z*dZ+dZ,dZ- e.h d      Z/de0de1dz  fdZ2 G d de3      Z4 G d de      Z5y)u  Grok Imagine (xAI, OpenRouter 경유) 이미지 생성 클라이언트 (2026-08-13).

최종 스틸 생성 엔진 교체(#103, 사용자 확정 "grok 2 로 가자") 전용 —
GeminiImageClient 를 상속해 컨텍스트·로깅·캡처 기계를 그대로 쓰고
운반층만 OpenRouter chat/completions(modalities=["image"]) 로 바꾼다.

운영 실측 계약 (artifact/20260813_MAI이미지25_실측/calls.json):
- modalities 는 ["image"] 단독 — ["image","text"] 는 404.
- 참조는 콘텐츠 parts 로 라벨 텍스트+이미지 쌍 다중 첨부 (5장 실측 통과).
- 텍스트 총량(프롬프트+라벨) 8,000바이트 상한 — 초과는 xAI 400.
  잘라 보내는 일은 없다: 여기서는 사전 검증으로 크게 실패시키고,
  조립층(still_recipe v17 컴팩트 지도 절)이 예산을 맞춘다.
- 응답 이미지: choices[0].message.images[].image_url.url (data URL).
    N)ListOptionalTuple)settings)ImageCallBudgetExceededreserve_current_call)	GRAIN_RAW)record_send)GeminiImageClient)ensure_png_bytes)is_moderation_text)ImageSubmissionUnknownclassify_http_statusclassify_send_failure)log_llm_call)capture_generated_imagez-https://openrouter.ai/api/v1/chat/completionsi  i@  >             errreturnc                     t        | t              sy| j                  d      }t        |t              st        |t              sy|S )uI   본문 오류의 상태 코드 — 정수로 실려 올 때만 읽는다.Ncode)
isinstancedictgetboolint)r   r   s     W/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/llm/grok_image_client.py_body_error_coder"   E   s7    c4 776?D$Zc%:K    c                       e Zd ZdZy)GrokPromptOverBudgetu_   텍스트 총량이 xAI 상한을 넘는다 — 조립층에서 고칠 문제라 크게 실패.N)__name__
__module____qualname____doc__ r#   r!   r%   r%   O   s    ir#   r%   c                        e Zd ZdZddededdf fdZdefdZ	 	 	 dded	eee	      d
ee   deee
ee	f         de
e	ef   f
dZ xZS )GrokImageClientu   OpenRouter 경유 xAI Grok Imagine 클라이언트 — nb2 gen_fn 슬롯 호환.

    상속 재사용: set_context/_ctx/_trace_step/_log_ctx/_opik_meta.
    오버라이드: 키 소스(OPENROUTER_API_KEY)와 generate_image 운반층.
    Napi_keymodelr   c                 L    t         |   ||xs t        j                         y )N)r-   r.   )super__init__	_settingsgrok_image_model)selfr-   r.   	__class__s      r!   r1   zGrokImageClient.__init__Z   s     0S9S9STr#   c                     | j                   r| j                   S t        t        dd      xs dj                         }|st	        d      |S )Nopenrouter_api_key u'   OPENROUTER_API_KEY 가 설정에 없다)_fixed_api_keygetattrr2   stripRuntimeError)r4   keys     r!   _get_api_keyzGrokImageClient._get_api_key]   sH    &&&y"6;ArHHJHII
r#   promptreference_imagesaspect_ratiolabeled_referencesc                   12 d|dg}g }t        |j                  d            }|rt        |d      D ]  \  }\  }	}
|	xs d| d}|j                  d|d       |j                  ddd	t	        j
                  |
      j                  d
      z   id       |t        |j                  d            z  }|	s|j                  |	        nD|rB|D ]=  }
|j                  ddd	t	        j
                  |
      j                  d
      z   id       ? g }|t        kD  rddlm	} |t        |j                  d            z
  } |||t              \  }}|rU|}|}d|d|d<   t        |j                  d            |z   }t        j                  dt        |      |||t        t               |t        kD  rt        d| dt         d      | j                  d|dgdgd}|rd|i|d<   ddlm}  |       }t#        j$                  |      j                  d      }d| j'                          dd}t)        j*                         }d}d}t,        j.                  2t,        j0                  }t3        d|dz         D ]  }	 t5        d       t7        dt8        d| j                         }t:        j<                  j?                  t@        ||d!"      112fd#}tC        jD                  d$      }|jG                  |      }	 |jI                  2d%z   &      }|jK                  d'd()       tM        |tN              r|jQ                  d*      nd}|t#        j$                  |d'+      } tS        |      }!|!tU        |!d,-      nd.}"|"d/k(  rPtW        |       sE||k  r@tY        d0|        }t        j                  d1|!||       t)        jZ                  d|z         X|"d.k(  rItW        |       s>|!|!nd2}#||j]                  d.       | j_                  d3|# d4|# d5| dd6  ||||||7       n |xs tY        dG      to        t)        j*                         |z
  dAz        }'tM        |tN              r|jQ                  d*      rt#        j$                  |d*   d'+      }(tq        db| j                  |d*|(|'|dB| jt                   |jw                  | jy                         | j                  ||xs g d*|(|'| jz                  C       tW        |(      rtY        dH|(       tY        d0|(       |xs i jQ                  dI      xs g })|)r|)d   jQ                  dJ      xs i ni }*|*jQ                  dK      xs g D ]  }+tM        |+tN              r$|+jQ                  d      xs i jQ                  d      ndLxs dL},|,j                  dM      sP|,j                  dNd      d   }-tq        db| j                  |dOdP|'|dQ| jt                  }.|jw                  | jy                         | j                  ||xs g | j                  jQ                  dR      |'dSdTi| jz                  U       ||j                          t        t	        j                  |-      | j                   dV| j                  jQ                  dW      xs dX Y      }
dZD /ci c]9  }/| j                  jQ                  |/      |/| j                  jQ                  |/      ; }0}/|r||0d[<   t        |
| j                  jQ                  dW      xs d\||.|0]       |
|'fc S  d^|)r|)d   jQ                  d_      nd d`}(tq        db| j                  |d*|(|'|dB| jt                   |jw                  | jy                         | j                  ||xs g d*|(|'| jz                  C       tY        da|(       # |jK                  d'd()       w xY w# tB        j`                  $ r9}$||j]                  d.       | j_                  d8d92d%z    d:||||||7      |$d}$~$wtb        $ r  td        $ r  t:        jf                  jh                  $ rn}$|$jk                         j                  dd;<      }%tY        d=|$jl                   d>|%       }tU        |$jl                  d,-      }&|&d/k(  r#||k  rt)        jZ                  d|z         Y d}$~$J|&d.k(  rM||j]                  d.       | j_                  d?|$jl                   d@|$jl                   d5|%dd6  ||||||7      |$to        t)        j*                         |z
  dAz        }'tq        db| j                  |d*ts        |      |'|dB| jt                   |jw                  | jy                         | j                  ||xs g d*ts        |      |'| jz                  C       |d}$~$wt|        $ r}$|$}t        |$      dDk7  rF||j]                  d.       | j_                  dEt        |$      j                   dF|$ ||||||7      |$||k  rt)        jZ                  d|z         Y d}$~$to        t)        j*                         |z
  dAz        }'tq        db| j                  |d*ts        |$      |'|dB| jt                   |jw                  | jy                         | j                  ||xs g d*ts        |$      |'| jz                  C        d}$~$ww xY wc c}/w )cu  OpenRouter chat/completions 이미지 생성 — (PNG bytes, ms) 반환.

        ★aspect_ratio 는 **호출자가 줄 때만** `image_config` 로 싣는다
         (2026-09-19). 종전 문구는 「API 파라미터가 없어 전송하지 않는다 —
         참조 비율을 상속한다」였는데 둘 다 틀렸다:
          · OpenRouter 는 이 모델에 `aspect_ratio` 를 받는다
            (`/api/v1/images/models` 의 supported_parameters).
          · 참조가 여러 장이면 **어느 참조를 따를지 모른다** — 실측 S49sh14:
            가로 배경(1536×864) 한 장 + 세로 인물 참조(896×1200) 세 장을 받고
            세로 880×1184 로 나왔다(nb2 거절 → 교차 백엔드 갈래).
        안 주면 종전 그대로(안 보냄) — 바탕 지도의 비율을 따라가야 하는
        도면 지도(`marker_map_engine`)는 비율을 넘기지 않는다.
        text)typerD   utf-8   zReference image :	image_urlurlzdata:image/png;base64,ascii)rE   rI   r   )fit_grok_promptuh   grok 상한 대응 — 일반 규칙 절 %d개 덜어냄 %s (%dB → %dB, 목표 %dB, 모델 상한 %dB)u   grok 텍스트 총량 zB > ug   B(모델 상한) — 컴팩트 조립(guidance v17)·일반 규칙 절 덜어내기로도 안 맞는다user)rolecontentimage)r.   messages
modalitiesrA   image_config)get_image_tracerzBearer zapplication/json)AuthorizationzContent-TypeN   zgrok_image_client.urlopen)source)kindgranularityrW   r.   POST)dataheadersmethodc                      t         j                  j                        5 } t        j                  | j                         j                  d            cd d d        S # 1 sw Y   y xY w)NtimeoutrF   )urllibrequesturlopenjsonloadsreaddecode)responsereqr`   s    r!   
_roundtripz2GrokImageClient.generate_image.<locals>._roundtrip   sO    // 0 25=#zz(--/*@*@*IJ2 2 2s   2AA()max_workers<   r_   FT)waitcancel_futureserror)ensure_ascii
openrouter)viasubmission_unknown	retryablezGrok image API error: u2   grok 본문 오류 %s — 다시 보낸다 (%d/%d)u   코드없음body_u   Grok image API 본문 오류 u9    — 상류가 이미 작업을 시작했을 수 있다:    )causedetail
attempt_no	req_bytesr?   ref_image_ids
start_timetracerwall_timeoutu$   grok 왕복 총 마감시간 초과(u=   s) — 서버가 연결을 붙잡은 채 응답을 안 끝냄replace)errorszGrok image API error z: http_zGrok image API i  )
model_nameuser_promptstatuserror_messageduration_msreference_image_ids)stepr.   r?   r{   r   ro   r   extra_metadata
never_sentznetwork_error_after_send:u(   Grok image API 응답을 못 받았다: unreachablezmoderation blocked: choicesmessageimagesr8   zdata:,z[image generated]success)r   r   output_textr   r   r   image_idbackendgrok2)r   r.   r?   r{   output_image_idr   paramsr   /operation_type?)context)	
project_id
episode_idr   r   scene_index
shot_indexstill_id	entity_idr   reference_labels
grok_image)rN   r?   generation_call_idpipeline_metadatazno image in response (finish=finish_reason)zGrok image API: r*   )Ilenencode	enumerateappendbase64	b64encoderg   GROK_TEXT_BYTE_LIMIT!app.modules.pipeline.still_reciperL   loggerwarningGROK_TEXT_BYTE_HARD_LIMITr%   _modelapp.modules.llm.image_tracerrT   rd   dumpsr>   time	monotonicr2   llm_timeout_image_genllm_max_retriesranger   _record_send
_GRAIN_RAWra   rb   RequestOPENROUTER_CHAT_URL_futuresThreadPoolExecutorsubmitresultshutdownr   r   r   r"   r   r   r<   sleepunknown_raise_submission_unknownTimeoutErrorr   r   ro   	HTTPErrorrf   r   r    r   str_log_ctxlog_trace_step
_opik_meta	Exceptionr   rE   r&   
startswithsplit_ctxokr   	b64decoder   )3r4   r?   r@   rA   rB   rO   r{   
text_bytesilabel	img_byteslblshedrL   label_bytesfittedbefore_bytesbodyrT   _tracerrz   r\   r|   
last_error_sendmax_retriesattemptrj   _ex_futpayload	_body_err_text_code_bv_shownexc
error_text_verdict
elapsed_msr   r   msgimrJ   b64call_idk_capture_metari   r`   s3                                                    @@r!   generate_imagezGrokImageClient.generate_imagee   s   ( #)&9:w/0
)23Eq)I%%E96!1!A6<='7!'!1!1)!<!C!CG!L M"   c#**W"566
!((/ *J -	'7!'!1!1)!<!C!CG!L M"   . ,, J$s6==+A'BBK*%9;LFD)&,f=
 w!78;F

 CIt\:(*C	 11&(D,- .UU  [["(W=>")

 $2L#AD A"$JJt$++G4	&t'8'8':&;<.
 ^^%
'+
11//Qa0GJ$,GH % j6dkk nn,,'i! - K 11a@zz*-B"kk'B,k?GLLeDLA -7w,EGKK(4 ( JJyuEE,Y7E" $/ 0<H5I {*$6u$= '; 6%14UG<&>
P!7K9 

1w;/ 22;M!<# +0*; ,!MM*>?"<<$)&"2&CF8 LQQVW[X[Q\P]%_'.)#)'1' =   } 1Z ;] ;;$..*Z74?@
gt$W)=**WW-EBC ;;F7!z$1 6:]]
 KK%%'t{{6+1r'&t  
 "#&"%9##?@@!7u=>>=b%%i06B3:wqz~~i(.B778$**B T* FF;'-2225902:79 >>'*))C#A&C" ;;F/ j$1	 6:]]	G KK%%'t{{6+1r $		j 9&!7+#    
 )  %;;-q99==)9:AcBDIA
 99==#/ 499==##   4A01#YY]]#34D#*"/ j((e +j .QXgajnn_.M^b-ccde 	
{{w: -	
 26	

 	!!#4;;v'-2gS"4?? 	 	

 -cU344s LLeDLAl ((  $MM"6744(B7R<. QU U&)!)' 5   + )  <<)) '! XXZ..wy.I
)+CHH:R
|DF
 0lK{*w+/EJJq7{+33 (&:;88 %chhZ0"1#(( <FFPQURUFVEW!Y#*i%]#-g 9   !$.."2Z"?4!GH
 #{{w"%j/z(5 :>
 ))+4;;!1D""#j/ *4??	   !  & 

 )-= (&:;88 9$s):L:L9MN!I#O#*i%]#-g 9   k)JJq7{+ $.."2Z"?4!GH
 #{{w"%c(
(5 :>
 ))+4;;!1D""#c( *4??	   M&zsg   3A=_1^+B>_A_!>k+_  _k4`

3k=A5f&8C.f&&k3A3k,Bkk)NN)NNN)r&   r'   r(   r)   r   r1   r>   r   r   bytesr   r    r   __classcell__)r5   s   @r!   r,   r,   S   s    U U3 U$ Uc  37&*@DG5G5 #4;/G5 sm	G5
 %T%U
*;%<=G5 
ucz	G5r#   r,   )6r)   r   concurrent.futuresfuturesr   rd   loggingr   urllib.errorra   urllib.requesttypingr   r   r   app.core.configr   r2   app.core.image_call_budgetr   r   app.core.send_ledgerr	   r   r
   r   #app.modules.llm.gemini_image_clientr   app.modules.llm.image_formatr    app.modules.llm.image_moderationr    app.modules.llm.image_send_stater   r   r   app.modules.llm.llm_loggerr   app.services.image_capture.sinkr   	getLoggerr&   r   r   r   r   	frozensetGROK_RETRYABLE_BODY_CODESobjectr    r"   r<   r%   r,   r*   r#   r!   <module>r
     s     %      ( ( 1 9 < A 9 ? 
 4 C			8	$E      &&?@ & S4Z j< jY5' Y5r#   