
    jY                       d Z ddlmZ ddlZddlZddlZddlm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  ej(                  e      Zd	Zd
ZdZdZdZdZdZdZdZdZ dZ! G d de"      Z#	 	 	 	 	 	 d%dZ$d&dZ%ed	 	 	 	 	 	 	 d'dZ&ed	 	 	 	 	 	 	 d(dZ'd)dZ(	 	 	 	 	 	 	 	 	 	 d*dZ)d+dZ*dddddddd 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d,d!Z+d"d#	 	 	 	 	 	 	 	 	 d-d$Z,y).un  W21B-W7 W-C2 (2026-06-12) — zoom continuity crop/i2i-fill 렌더 경로.

zoom_continuity_anchor(21.73) manifest 의 zoom 멤버 still 을 독립 T2I 대신
**source_wide still 의 실제 생성 이미지에서 crop + i2i fill** 로 만든다
(스파이크1 ③ 포팅, Codex 판정 ②: scene_image 정식 generation path 안의
분기 — post-pass 금지, 단건/배치 공용 helper).

절차 (스파이크 검증 그대로 — gpt-5.5 bbox + gemini i2i fill 유지, Codex ③):
  1. source_wide still 의 primary scene 이미지 로드 (부재 → fallback)
  2. VLM bbox (litellm vision, normalized 좌표) — focal 묘사는 anchor 의
     locked_elements 에서 조립 (generic — 시나리오 토큰 0)
  3. 16:9 확장 crop (중심 유지 + 클램프 + ×1.15 margin) → LANCZOS upscale
  4. gemini i2i fill — "모든 요소 위치 그대로, 선명도/디테일만 보강"
     (+ caller 가 전달한 prop refs 동반, moderation 시 PromptSanitizer 재시도)

Codex W_C1_REVIEW WC2 guards:
  - **artifacts 의무**: bbox JSON / crop_box(전·후) / crop_upscaled.png /
    viability.json / fallback reason — zoom_continuity_anchor checkpoint dir
    하위 ``wc2_artifacts/S{si}sh{shi}/`` 에 저장.
  - **source_wide_crop_viability 진단**: bbox found + 면적 비율(과대/과소) +
    zoom target 실재 — fail-fast gate 아님, canary acceptance 기준 (기록만).
  - fallback 은 비차단 (caller 가 기존 T2I 경로로 진행) 그러나 canary
    acceptance 에서는 실패로 간주 — "fallback green ≠ W-C 성공".

flag OFF / anchor 부재 시 이 모듈은 호출되지 않는다 (caller gate).
    )annotationsN)BytesIO)Path)AnyDictListOptionalTuple)generation_context)capture_artifactwc2_artifactszgemini/gemini-3.1-pro-preview   iX  g?g~jth?)i   i`  gffffff?a  You locate a region in an image. Return JSON {"bbox": [x0, y0, x1, y1], "found": true|false, "note": "..."} with normalized coordinates (0.0-1.0, x0<x1, y0<y1) of the TIGHT region containing: %s. Include a small margin so the whole subject is inside. Return ONLY the JSON object.u  Photorealistic cinematic close-up. Refine the FIRST reference image into a sharp, detailed close-up. It is a zoomed-in crop of a film frame: keep EVERY element exactly where it is — same poses, same object positions and angles, same surfaces, same lighting. Do NOT move, add, remove or restage anything. Only enhance sharpness, texture and detail lost in the enlargement.
Locked visual facts (must remain exactly as in the crop): %su  
Object references: the additional labeled reference images show the true appearance of specific objects present in the crop. For each such object, keep its position, orientation, scale and physical condition exactly as in the crop, but its printed or displayed content (picture, text, screen) must match the object reference image — the enlargement made that content illegible, and the object reference is the source of truth for it. Do not invent different printed or displayed content.c                      e Zd ZdZy)ZoomContinuityRenderErroruO   fallback 신호 — caller 는 기존 T2I 경로로 진행 + diagnostic 기록.N)__name__
__module____qualname____doc__     a/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/zoom_continuity_render_service.pyr   r   h   s    Yr   r   c                   dj                  d | j                  d      xs g D              xs d}t        |z  }|r	|t        z  }|S )u  i2i fill 프롬프트 조립 (순수) — locked facts + object ref SOT 절(조건부).

    SOT 절은 printed_prop anchor 계약이 적용된 object ref 가 있을 때만 붙는다
    (Codex narrow: 일반 prop ref 만 있는 still 의 프롬프트는 byte-identical).
    ; c              3  b   K   | ]'  }|j                  d       r|j                  d d       ) yw)description N)get).0les     r   	<genexpr>z$build_fill_prompt.<locals>.<genexpr>t   s/      ,Tb66-  	}b!,Ts   -/locked_elementszthe composition of the crop)joinr   _FILL_PROMPT_FILL_OBJECT_REF_CLAUSE)zoom_targethas_object_content_sot_reflocked_textprompts       r   build_fill_promptr)   l   s^     )) ,7OO<M,N,TRT,T  ' 
'  K'F!))Mr   c                   | j                  d      xs g D cg c]F  }|j                  d      dv r1|j                  d      r |j                  dd      j                         H }}|sQ| j                  d      xs g D cg c]3  }|j                  d      r |j                  dd      j                         5 }}dj                  d |D              xs dS c c}w c c}w )	uF   anchor locked_elements → VLM bbox 의 focal 묘사 (generic 조립).r!   kind)	held_propsubject_pose
prop_stater   r   r   c              3  &   K   | ]	  }|s|  y w)Nr   )r   ps     r   r    z*build_focal_description.<locals>.<genexpr>   s     +1Qs   z"the focal subject of the zoom shot)r   stripr"   )r%   r   partss      r   build_focal_descriptionr3   ~   s     //"34:::B66&>HHFF=! 	}b!''): 
   "oo&78>B>
>vvm$ FF="%++-> 	 

 99+++S/SS
s   AC>8C)modelc          
        ddl m} i }|j                  d      rddlm}  |       }|r||d<   t        j                  |       j                         } |dd|i|dt        |z  dd	d
dd| idgdgt        t        dddid}|j                  r#|j                  d   j                  j                  nd xs d}	t        j                  |	      S )Nr   )llm_completionzgemini/)get_next_keyapi_keyr4   system)rolecontentuser	image_urlurlzdata:image/png;base64,)typer=      r?   json_object)messagestimeoutmax_completion_tokensnum_retriesresponse_formatr   r   )app.core.openai_keysr6   
startswithapp.modules.llm.gemini_key_poolr7   base64	b64encodedecode_BBOX_SYSTEMVISION_TIMEOUT_SECONDSBBOX_MAX_TOKENSchoicesmessager;   jsonloads)
image_bytestarget_descriptionr4   _llm_completionextrar7   keyb64respr;   s
             r   vlm_locate_bboxr[      s     E	"@n"E)


;
'
.
.
0C 
 ,9K*KL$$(>se&DEG) 
 '-/D 37,,t||A&&..DORG::gr   )marginc          	        | \  }}|\  }}}}||z   dz  |z  ||z   dz  |z  }
}	||z
  |z  ||z
  |z  }}|dk  s|dk  rt        d|       ||z  dk  r	|dz  dz  }n|dz  dz  }t        ||z  |      t        ||z  |      }}t        dt        ||z
  |	|dz  z
              }t        dt        ||z
  |
|dz  z
              }t        |      t        |      t        ||z         t        ||z         fS )ul   normalized bbox → 16:9 확장 crop 박스(px, 중심 유지+프레임 클램프) — 스파이크1 포팅.   r   zinvalid bbox gqq?   	   )r   minmaxint)
image_size	bbox_normr\   WHx0y0x1y1cxcybwbhlefttops                  r   expand_crop_16x9rr      s    DAqNBB2g]QbA 1B2g]R"WMB	Qw"''-	{(CDD	Bw"Wq[!Vb[fa #b6k1"5Bq#a"fb26k*+D
aQVR"q&[)
*CIs3xTBYS2X??r   c                0   t        | j                  d            }d}d}|sd}nA	 | d   \  }}}}t        d||z
        t        d||z
        z  }|t        kD  rd}n|t        k  rd}|||| j                  d
d      dS # t
        t        t        f$ r d	}Y /w xY w)uL   source_wide_crop_viability (Codex WC2 guard 3) — 기록만, 차단 아님.foundokNbbox_not_foundbboxg        bbox_too_large_weak_zoombbox_too_small_low_confidencebbox_malformednoter   )rt   
area_ratioverdictr{   )boolr   rb   VIABILITY_MAX_AREA_RATIOVIABILITY_MIN_AREA_RATIOKeyError	TypeError
ValueError)rw   rt   r}   r|   rh   ri   rj   rk   s           r   assess_viabilityr      s    '"#EG"&J"	'!&\NBBS27,s3b/BBJ444669  $	  )Z0 	'&G	's   A A< <BBc                   ddl m} ddlm}m} | j                  |      j                  |j                  |k(  |j                  |k(  |j                  |d   k(  |j                  |d   k(        j                         }|st        d|d    d|d    d      | j                  |      j                  |j                  |k(  |j                  |j                  k(  |j                  dk(  |j                   dk(        j#                  |j$                  j'                               j                         }|st        d|d    d|d    d	      t)        |j*                        }	|	j-                         s,t)        |j.                        j0                  |j*                  z  }	|	j3                         st        d
|j*                         |	j5                         |j                  fS )Nr   settings)
ImageAsset
SceneStillr@   zsource still Sshz
 not foundscenez has no primary scene imagezsource image file missing: )app.core.configr   app.models.projectr   r   queryfilter
project_id
episode_idscene_index
shot_indexfirstr   still_idid
asset_type
is_primaryorder_by
created_atdescr   	file_pathis_absoluteprojects_dirparentexists
read_bytes)
dbr   r   sourcer   r   r   	src_stillassetpaths
             r   _load_source_primary_bytesr      s    )9 		!!Z/!!Z/""fQi/!!VAY.	

 
  'VAYKr&)J?A 	A 		!!Z/9<</!!W,!!Q&	

 
*'',,.	/	 
 'VAYKr&)4OPR 	R DH))*11EOOC;;='*EeooEV(WXX ??ehh&&r   c                    ddl m} t        |j                        | z  dz  dz  |z  dz  t        z  d| d| z  }|j                  dd	       |S )
Nr   r   checkpointsepisodeszoom_continuity_anchorSr   T)parentsexist_ok)r   r   r   r   WC2_ARTIFACTS_DIRNAMEmkdir)r   r   r   r   r   ds         r   _artifacts_dirr     sl    (X""#j0=@:M
	/	02G	Hk]"ZL
)	* 
 GGD4G(Hr   F)r   	prop_refsprop_refs_have_anchor_contract
trace_metasource_bytesr   source_asset_idc                  &' ddl m} t        ||||      &|j                  d      t	        |j                  d      xs d      d'd5&'fd}|
,|  |d	      	 t        | ||t        |d               \  }
}||}t        |      }|'d<   t        'd<   	 t        |
|      }&dz  j                  t        j                   |t        d|d      d       t#        |      }|'d<   &dz  j                  t        j                   |dd      d       |d   s |d      |j%                  t'        |
            }	 t)        |j*                  |d         }&dz  j                  t        j                   |d   t	        |      t	        |j*                        d      d       |j/                  |      j1                  t2        |j4                        }t'               }|j7                  |d       |j9                         }&dz  j;                  |       t=        ||d |||!      5  t?        |d"|r|gnd|j                  d      |j                  d      t	        |j                  d      xs d      d#$       ddd       tA        |tC        |      xr |%      }d&|fg}|xs g D ]  \  }}|jE                  ||f        dd'l#m$} dd(l%m&} d)d)||||d*}|	r|jO                  |	        |jP                  di | |} d}!d+D ]:  }"	 |jS                  | |,      \  }#}$| 'd-<   &d.z  j                  | d       |#| 'fc S   |d4|!       # t        $ r} |d
|       |d}~ww xY w# t        $ r'} |dt        |      j                   d|       |d}~ww xY w# t        $ r} |t-        |            |d}~ww xY w# 1 sw Y   ExY w# |$ rT}|}! |       jU                  | d/t	        tW        |d0g       xs g       |"dz   1      }%|%j                  d2|       } Y d}~d}~wt        $ r'} |d3t        |      j                   d|       |d}~ww xY w)6u  zoom still 의 crop/i2i-fill PNG 생성 — (png_bytes, fill_prompt, diagnostics).

    실패는 ZoomContinuityRenderError — caller 가 기존 T2I 로 fallback 하고
    artifacts 의 fallback reason 으로 남긴다 (비차단, canary 에선 실패 간주).

    ★동시성 규칙 (2026-06-12 S23sh4 실측 — SQLAlchemy "concurrent operations
    are not permitted"): batch ThreadPool worker 에서는 db 세션을 절대 전달하지
    말 것 (코드베이스 규칙 "worker 안 self.db query 금지" 와 동일). batch 는
    ``source_bytes`` 를 직접 전달한다 — scene_paths_by_index_by_id (resume
    초기화 + 메인 스레드 batch 경계 갱신, worker 읽기 전용) 에서 읽은 bytes.
    db 조회 경로는 단건(메인 스레드) regen 전용.
    r   )Imagegroup_idr   r   )r   r   c                |    | d<   dz  j                  t        j                  dd      d       t        |       S )Nfallback_reasonzfallback.jsonFr@   ensure_asciiindentutf-8encoding)
write_textrR   dumpsr   )reasonart_dirdiagnosticss    r   _failz+generate_continuity_crop_png.<locals>._failF  sD    )/%&	?	"..JJ{qAG 	/ 	U(00r   Nz5source_unavailable: no source_bytes and no db sessionzsource_unavailable: focal_description
bbox_modelzbbox_vlm_error: z: z	bbox.json)rU   r4   F)r   r   r   source_wide_crop_viabilityzviability.jsonr@   r   rt   rv   rw   zcrop_box.json)re   crop_box_pxrd   PNGzcrop_upscaled.pngzoom_continuity)stager   r   r   zoom_continuity_crop)rw   r   r   )r:   input_image_idspipeline_metadata)r&   uT   Reference image 1: the zoomed crop to refine — keep composition and poses EXACTLY.)ModerationError)PromptSanitizerscene_image_gen)stepoperation_typer   r   r   r   )r   r@   r^   )labeled_referencesfill_promptzfill_prompt.txtSAFETYblock_categories)attemptsanitized_promptzi2i_error: z&i2i_moderation_blocked_after_retries: )r   strreturnr   ),PILr   r   r   listr   tupler   r3   VISION_MODEL_DEFAULTr[   	Exceptionr?   r   r   rR   r   r   openr   rr   sizer   cropresizeUPSCALE_SIZELANCZOSsavegetvaluewrite_bytesr   r   r)   r~   append#app.modules.llm.gemini_image_clientr   app.modules.prompt_sanitizerr   updateset_contextgenerate_imagesanitizegetattr)(r   r   r   r   r   r%   gemini_clientr   r   r   r   r   r   r   r   _resolved_source_asset_idexcfocalrw   	viabilityimcrop_boxcrop_upbuf
crop_bytesr   labeledlabelimgr   r   _log_ctxr(   last_excr   png_msresr   r   s(                                         @@r   generate_continuity_crop_pngr  "  s   8 Z[*MGOOJ/{x06B7#K
1 :OPP	?6PJ
E+h2G,H7J3L3
 "7O $K0E',K#$ !5KM|U3 {&&

%1;59; %	' 	 '  !&I09K,-++

95;g , OW$%% 
GL)	*B'#BGGT&\: **

fd8n"&rww-1 	2 +  ggh&&|U]]CG
)CLLeJ""//
; 
J&7{z
 	'2Ao.t('OOJ7{x8>B?			
	
  $#'	?#U7UK
 
`		(G  o2o
ss|$ & D< "+  "  H 
#M))F$(H	L$33Fw3WHC)/K&((44Vg4N++   8
C
DDm ) 	?.se453>	?  M&tCy'9'9&:"SEBCLM& % 'CHo3&'"
 
r  	9H!#,,WS"4b9?R@! - C
 WW/8F 	L+d3i&8&8%9C5ABK	Lsz   M M :N AN5>5O	MMM	N("N

N	N2N--N25N?Q	A	PQ	""QQ	r   )current_modelc               <   ddl m} |xs t        }g }|xs i j                         D ]  \  }}|s	| j	                  |      j                  |j                  |k(  |j                  |k(  |j                  dk(  |j                  dk(        j                         }	|	qt        |	dd      dk7  r	 t        j                  t        |	dd      xs d	      }
|
j                  d
      xs i j                  d      }||k7  s|j!                  |        |S # t        $ r d}Y (w xY w)u  지금 남아 있는 결과가 **다른 모델로 만들어진** zoom 대상 키들.

    ## 왜 필요한가 — 지문만 움직여선 아무 일도 안 난다

    ★bbox 모델을 Sol → gemini 로 바꾸고 스텝 지문에 접어도, 재개는
     `scan_completed_scene_stills` 가 **primary 파일 존재만** 보고
     done 을 만들고(`scene_persistence_service.py:462-507`) legacy loop 가
     그 done 을 제외한 뒤에야 zoom 호출로 간다
     (`scene_image_service.py:702-704`). **완주한 Sol zoom 은 gemini
     bbox 를 한 번도 안 부르고 그대로 남는다** (2026-08-27 Codex BLOCK).

    ★그래서 **낡은 대상만** done 에서 뺀다. 전체 force 는 필요 없다 —
     그건 zoom 과 무관한 샷까지 다시 산다.

    ## 신원은 최종 primary 다 — 중간 파일이 아니다

    ★`bbox.json` 은 crop 을 뜨는 **도중에** 쓴다(:356). i2i fill 이
     거절당하거나 프로세스가 그 사이에 죽으면 파일만 새 모델 이름을
     달고 남고 primary 는 옛 Sol 그림 그대로다 — 그러면 이 함수가
     「gemini 로 만든 것」이라 읽어 **낡은 것을 안 뺀다**
     (2026-08-27 Codex BLOCK 3회차). 완료 신원은 done 을 만드는 것과
     같은 자리, 곧 **최종 primary ImageAsset** 에서 읽는다.

    판별:
      - primary 가 없다 → 낡지 않았다 (아직 안 만들었고 done 에도 없다)
      - primary 가 `zoom_continuity_crop` 이 아니다 → 낡지 않았다
        (zoom 이 fallback 으로 빠져 T2I 로 나온 것 — bbox 가 안 실렸다)
      - `zoom_continuity_crop` 인데 `review_notes.zoom_continuity.bbox_model`
        이 없거나 다르다 → **낡았다**
    r   )r   r   r@   Nvariant_typer   review_notesz{}r   r   )r   r   r   itemsr   r   r   r   r   r   r   r   rR   rS   r   r   r   )r   r   still_id_by_keyr	  r   wantstalerX   r   primarynotesgots               r   stale_zoom_targetsr    s'   J .00DE)/R668XHHZ V%%3##x/%%0%%*	 UW 	 ?7ND15KK	JJwwEMNE99./52::<HC $;LL/ 90 L	  	C	s   *ADDD)r%   Dict[str, Any]r&   r~   r   r   )r%   r  r   r   )rT   bytesrU   r   r4   r   r   r  )rd   Tuple[int, int]re   zList[float]r\   floatr   zTuple[int, int, int, int])rw   r  r   r  )
r   r   r   r   r   r   r   r  r   zTuple[bytes, str])
r   r   r   r   r   rc   r   rc   r   r   )r   r   r   r   r   r   r   rc   r   rc   r%   r  r   r   r   z!Optional[List[Tuple[str, bytes]]]r   r~   r   zOptional[Dict[str, Any]]r   zOptional[bytes]r   Optional[str]r   r  r   z!Tuple[bytes, str, Dict[str, Any]])
r   r   r   r   r  zDict[Any, Any]r	  r   r   z	List[Any])-r   
__future__r   rJ   rR   loggingior   pathlibr   typingr   r   r   r	   r
   "app.services.image_capture.contextr   app.services.image_capture.sinkr   	getLoggerr   loggerr   r   rN   rO   r   r   r   CROP_MARGINrM   r#   r$   r   r   r)   r3   r[   rr   r   r   r   r  r  r   r   r   <module>r$     s]  4 #      3 3 A <			8	$' & 7       # C T Z	 Z@D$T* &	*** 	*
 *b  	@@@ 	@
 @.2('('('*-('7F('('V  48+0+/$("%)gEgE gE 	gE
 gE gE  gE gE 1gE %)gE )gE "gE gE #gE 'gE^ AAA $A
 A Ar   