
    aj91                    *   d Z ddlmZ ddlZddlmZ ddlmZmZm	Z	m
Z
  ej                  e      ZddZddZddZdd		 	 	 	 	 	 	 	 	 dd
ZdZddddd	 	 	 	 	 	 	 	 	 	 	 	 	 ddZdddd	 	 	 	 	 	 	 	 	 	 	 	 	 ddZdd	 	 	 	 	 	 	 	 	 	 	 	 	 ddZddZy)u  마커 맵 엔진 — 누가 그리는지 호출부가 몰라도 되게 (2026-08-25).

왜 있나 (사용자 지시 "코드 상수를 설정으로, 문제가 생기면 후보 모델로") —
마커를 그리는 두 자리가 `gpt-image-2` 를 **소스 상수로** 박고 있었다
(`outdoor_place_canon_step.py:37` · `shot_conti_light.py:77`). 설정으로 못
바꾸니 실패해도 다른 모델을 시켜 볼 길이 없었다.

08-25 실측 S2sh1 — 「카메라 시야각(FOV) 부채꼴이 피사체가 있는 방향(남동)이
아닌 반대 방향(북서)으로 열려 있다」로 검사 3회 소진, 스텝이 partial 로 멈췄다.
★세 번 다 **같은 방향으로** 틀렸다. 우연이 아니라 그 모델의 계통적 편향이고,
 같은 모델로 되풀이하는 것은 돈만 쓴다(3장 사서 3장 버렸다). 메모리에도 같은
 결함이 남아 있다 — "geometry 는 정확한데 모델이 쐐기를 반대로 그린다".
 그래서 되풀이가 아니라 **모델을 바꾼다**.

★두 경로는 API 모양이 다르다 — 모델 문자열만 갈아 끼우면 안 된다.

    gpt-image-2 계열   OpenAI `images.edit`     참조를 **파일**로 올린다
    그 밖(슬러그)      OpenRouter chat          참조를 **대화 속 이미지**로

 그래서 여기서 갈라 부르고, 호출부는 「이 모델로 그려라」만 안다.

★후보가 비면 후퇴하지 않는다 = 종전 동작 그대로. 설정에 슬러그를 넣는
 순간 켜진다.
    )annotationsN)Path)AnyDictListOptionalc                 B   ddl m}  t        t        | dd      xs d      j	                         }t        t        | dd      xs d      }g }|g|j                  d      D cg c]  }|j	                          c}z   D ]  }|s||vs|j                  |        |S c c}w )u  이 순서로 물러난다 — [기본, 후보1, 후보2, …].

    ★중복은 걷는다. 기본과 후보에 같은 슬러그가 들어 있으면 같은 모델을
     두 번 시키는 셈이라 값이 없다(그 되풀이를 없애려고 만든 장치다).
    r   settingsmarker_map_image_model marker_map_fallback_models,)app.core.configr   strgetattrstripsplitappend)r   baserawoutxms         \/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/marker_map_engine.pymarker_map_modelsr   "   s     )wx!92>D"EKKMD
gh <bAGR
HCCV#))C.9.Qqwwy.99#JJqM : J :s    Bc                >    | xs d}|dk(  xs |j                  d      S )uP  이 저장소가 Gemini 로 직접 부르는 이름인가.

    `nb2` 는 이 저장소가 쓰는 별명이고 물리 모델은 설정이 준다. Gemini 는
    이미 키 풀·재시도 기계를 갖고 있으므로 OpenRouter 로 우회하지 않는다 —
    같은 모델을 두 길로 부르면 과금도 기록도 갈린다.
    r   nb2gemini)
startswith)modelr   s     r   is_gemini_modelr"   3   s%     	A:/h//    c                .    d| xs dv xr t        |        S )u   OpenRouter 슬러그인가 — `provider/model` 꼴이면 그렇다.

    `gpt-image-2` 처럼 provider 접두가 없는 것은 OpenAI 직행이다. 이
    가름은 **문자열의 뜻을 읽는 것이 아니라 형식을 보는 것**이라
    substring 의미 판단이 아니다.
    /r   )r"   r!   s    r   is_openrouter_modelr'   >   s      5;B>(>$>>r#   )capture_contextc           
     >   ddl m} ddlm} | dk(  r|j                  n| } ||      }|r |j
                  d
i | t        |d      D 	cg c]-  \  }}	d| dt        t        |	            j                         f/ }
}}	|j                  ||
	      \  }}|S c c}	}w )u   nb2(또는 다른 Gemini 이미지 모델)로 그린다 — PNG bytes.

    2026-08-25 사용자 판정: **공간·방향은 nb2 가 훨씬 낫다.** 그래서 카메라
    관련 도해는 이 갈래가 기본이 된다.
    r   r
   )GeminiImageClientr   r&      
REFERENCE :labeled_references )r   r   #app.modules.llm.gemini_image_clientr*   gemini_image_modelset_context	enumerater   r   
read_bytesgenerate_image)r!   prompt	ref_pathsr(   r   r*   physicalclientiplabeledpng_elapseds                r   render_via_geminir@   H   s     )E/4~++5HX.F-_-$Y24241a QCq!4A<#:#:#<=2  4))&W)MMCJ4s   2Bz#https://openrouter.ai/api/v1/images
marker_map)roler(   openai_renderaspect_ratioc                    t        |       rt        | |||      S t        |       rt        | |||||      S |t	        | d       ||       S )u  마커 맵 한 장 — **호출부는 모델 종류를 몰라도 된다.**

    ``aspect_ratio`` — OpenRouter 전용 이미지 엔드포인트 갈래에만 싣는다
    (2026-09-19). 안 실으면 seedream 이 기본 정사각형(2048×2048)을 낸다 —
    16:9 원본을 변환한 최종본이 정사각형으로 나왔다(S72sh43 실측). 주지
    않으면 요청은 예전과 byte 동일하다(마커 맵 호출부는 안 준다).

    세 갈래가 API 모양이 다르다:
        nb2·gemini    Gemini 직접 (키 풀·재시도를 이미 갖고 있다)
        provider/…    OpenRouter `/api/v1/images`
        그 밖         OpenAI `images.edit`

    ★OpenAI 갈래만 호출부가 넘긴다(`openai_render`) — 그쪽은 client·
     sanitizer·size 같은 자리별 사정이 붙어 있어 여기로 끌어오면 자리마다
     다른 것을 하나로 뭉개게 된다.
    )r!   r7   r8   r(   r!   r7   r8   rB   r(   rD   u:    은 OpenAI 갈래인데 그리는 함수를 못 받았다)r"   r@   r'   render_via_openrouter
ValueError)r!   r7   r8   rB   r(   rC   rD   s          r   draw_marker_map_via_modelrI   m   sw    4 u )+- 	- 5!$)$+,H 	H iQRT 	Tr#   )rB   r(   rD   c           
     P   |st        d      d| v rddlm}  ||       }|r |j                  di | t	        |d      D 	cg c]-  \  }}	d| dt        t        |	            j                         f/ }
}}	|j                  ||
	      \  }}|S t        | |||||
      S c c}	}w )u>  OpenRouter 경유로 마커 맵을 그린다 — PNG bytes.

    grok-imagine 계열은 이 저장소가 이미 쓰던 chat 경로를 그대로 타고
    (`GrokImageClient` 재사용 — 같은 운반 코드를 두 벌 두지 않는다),
    그 밖의 이미지 모델은 **전용 `/api/v1/images`** 로 간다.
    uF   마커 맵은 바탕 맵을 참조로 받아야 한다 — 참조 0장zgrok-imaginer   )GrokImageClientr&   r+   r,   r-   r.   rF   r0   )
rH   !app.modules.llm.grok_image_clientrK   r3   r4   r   r   r5   r6   _render_via_images_api)r!   r7   r8   rB   r(   rD   rK   r:   r;   r<   r=   r>   r?   s                r   rG   rG      s     abbE u-F11(A686tq! !1%tCF|'>'>'@A6 	 8--w . 0X
!Fid'lD D8s   2B")rD   c                   ddl }ddl}ddl}ddl}	ddlm}
 ddlm} ddlm	} ddl
m} t        |
dd      xs dj                         }|st        d	      g }|D ][  }|j                  t!        t#        |            j%                               j'                  d
      }|j)                  ddd| id       ] | ||d}|r||d<   |	j*                  j-                  t.        |j1                  |      j3                  d      d| ddd      }t5        |xs i       }|j7                         } |d       	 |	j*                  j9                  ||
j:                        5 }|j=                  |j?                         j'                  d            }ddd       tK              }|%t        d"|  d#|j1                  |d$%      dd&        tG        |j7                         |z
  dz        } || |d'd(||jI                  d      |jI                  d      |jI                  d       xs |)        |||  d*| +      S # 1 sw Y   xY w# t@        $ r} || |dtC        |      jD                   d| dd tG        |j7                         |z
  dz        |jI                  d      |jI                  d      |jI                  d       xs |!        d}~ww xY w),uT  OpenRouter 전용 이미지 엔드포인트 — 참조는 `input_references`.

    ★예산은 **보내기 직전 한 번**만 예약한다. 재시도를 여기 두지 않는 것도
     같은 이유다 — 후퇴는 이미 「다른 모델」이 맡고, 같은 자리에 두 겹을
     쌓으면 한 지출이 두 번 세어진다.
    r   Nr
   )reserve_current_call)ensure_png_bytes)log_llm_callopenrouter_api_keyr   u'   OPENROUTER_API_KEY 가 설정에 없다ascii	image_urlurlzdata:image/png;base64,)typerT   )r!   r7   input_referencesrD   zutf-8zBearer zapplication/json)AuthorizationzContent-TypePOST)dataheadersmethodz#marker_map_engine.openrouter_images)sourcetimeouterrorz: i  i  
project_id
episode_idoperation_type)
model_nameuser_promptstatuserror_messageduration_msra   rb   rc   u.   OpenRouter images 응답에 그림이 없다 (z): F)ensure_asciii,  z[image generated]success)rd   re   output_textrf   rh   ra   rb   rc   r%   )context)&base64jsontimeurllib.requestr   r   app.core.image_call_budgetrO   app.modules.llm.image_formatrP   app.modules.llm.llm_loggerrQ   r   r   RuntimeError	b64encoder   r   r5   decoder   requestRequestOPENROUTER_IMAGES_URLdumpsencodedict	monotonicurlopenllm_timeout_image_genloadsread	ExceptionrV   __name__intget_extract_image_bytes)r!   r7   r8   rB   r(   rD   rm   _jsonro   urllibr   rO   rP   rQ   keyrefsr<   rZ   bodyreqctxt0resppayloadexcr   elapseds                              r   rM   rM      s    (?=78126<"
C
C
ECDEEDSV 7 7 9:AA'J[#(,B4&*I"JL 	M  f$GD  ,^
..
 
 EKK$5$<$<W$E$+C5/!35=C ! EC $"
%C		B EF^^##X;; $ =@Dkk$))+"4"4W"=>G= w
'C
{<UG3{{7{7=>@A 	A 4>>#b(D01GEv0$GGL1GGL1 #(8 9 ATC CE7!D6):;;/= =  6'&*3i&8&8%9C5#A$3#G!$dnn&6&;t%C!D # 5 # 5$'GG,<$=$E	G 	s1   4'H? /H3
H? 3H<8H? ?	KA:KKc                   ddl ddldfd}| g}|r|j                         }t        |t              rdD ]  }|j                  |      }t        |t              rt        |      dkD  r ||      }|r|c S t        |t              sQt        |j                  d      t              sq ||d         }|s|c S  |j                  |j                                n!t        |t              r|j                  |       |ry)u   응답에서 그림 bytes 를 꺼낸다 — data URL·b64·URL 어느 모양이든.

    ★모양을 하나로 단정하지 않는다. 제공자마다 다르고, 틀리면 「그림이
     없다」로 읽혀 산 것을 버리게 된다.
    r   Nc                X   | j                  d      r$j                  | j                  dd      d         S | j                  d      r7j                  j	                  | d      5 }|j                         cd d d        S 	 j                  |       S # 1 sw Y   xY w# t        $ r Y y w xY w)Nzdata:r   r+   httpx   r^   )r    	b64decoder   rw   r~   r   r   )sdlrm   r   s     r   	_from_strz'_extract_image_bytes.<locals>._from_str  s    << ##AGGCOA$677<<''3'72wwy 87	##A&& 87  		s   %B B B	B)(B))b64_jsonimage_base64rU   rT       rU   )r   r   )rm   rp   pop
isinstancer|   r   r   lenextendvalueslist)	r   r   stacknodekvgotrm   r   s	          @@r   r   r      s     	 IE
yy{dD!EHHQKa%#a&2+#A,C"
a&:aeeElC+H#AeH-C"
 F LL'd#LL   r#   )returnz	List[str])r!   r   r   bool)
r!   r   r7   r   r8   	List[Any]r(   Optional[Dict[str, Any]]r   bytes)r!   r   r7   r   r8   r   rB   r   r(   r   rD   zOptional[str]r   r   )r   r   )__doc__
__future__r   loggingpathlibr   typingr   r   r   r   	getLoggerr   loggerr   r"   r'   r@   ry   rI   rG   rM   r   r0   r#   r   <module>r      s  0 #   , ,			8	$"0? 15  	
 . D >  04"&% %  %  	% 
 %  .%   %  % Z 04"&DD D 	D
 D .D  D DP #'F<F< F< 	F<
 F< .F<  F< F<R%r#   