
    )jmT                       d Z ddlmZ ddlZddlZddlmZmZmZm	Z	m
Z
mZmZ ddlmZ ddlmZmZ  ej$                  e      ZdZdd	d
dddZ ej.                  d      ZddZddZd dZ	 	 	 	 	 	 	 	 	 	 d!dZ	 	 	 	 	 	 	 	 d"dZdd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d#dZddddd	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 d$dZdd	 	 	 	 	 	 	 	 	 d%dZ 	 	 	 	 	 	 d&dZ!y)'u  shot_ref_classify — bgonly·prev v3·time_of_day LLM 분류 (2026-07-13 레시피 이식).

s40/s41 확정 레시피의 샷 분류 3종을 한 스텝 데이터로 산출한다:
  1) bgonly(person_visible): 샷 텍스트만으로 '살아있는 인물의 실물 몸' 등장
     판정. 신체 일부·거울 반사=인물 / 그림자·사진 속 이미지·발자국 흔적=배경
     전용. 정본: scratchpad/forest_exp/s39_threeroll_vlm.py stage_bgonly.
  2) prev v3: ★유일 기준=배경(장소) 동일성 — 이동·LOCATION 상이·앞 샷
     클로즈업(더 앞 샷 거슬러 지정 허용)·시간대 상이=null. prev 시
     usage_en(TAKE/EXCLUDE) 동시 저작. 정본: s38_lightconti_full.py stage_prev2.
  3) time_of_day: 씬별 촬영 시간대 잠금 구절 저작 — 실험의 `_time_of` 헤딩
     substring 매칭을 LLM 저작으로 대체(글자/substring 의미 판단 금지 규칙).

코드 강제 규칙(LLM 결과 후처리):
  - bgonly 샷은 prev 판정이 있어도 무효(플레이트 강제, usage_en 소거).
  - prev 는 반드시 목록 내 + 스토리 순서상 앞 샷 — 위반은 null 로 격리하고
    prev_violation 에 기록(실험은 abort, production 은 샷 격리).
  - 세 판정 모두 전 샷/전 씬 누락 시 fail-fast(AppError).

LLM 입력 계약: 씬 원문 전문(절대 자르지 않음) + 샷 목록(각 샷에
`[LOCATION: <장소 서술>]` 병기). 모델은 manifest default(gpt, 분석 계열) —
실험 정본과 동일 배정.
    )annotationsN)AnyCallableDictListOptionalSetTuple)AppError)load_promptload_schemashot_ref_classifyz1.202607132300z2.202607140310z3.202607192150z4.202608071400z5.202608271700)12345z^S(\d+)sh(\d+)$c           	     n    	 t         |    S # t        $ r  t        d| dt        t                d      w xY w)Nz2unknown shot_ref_classify prompt version selector z	 (known: ))PROMPT_VERSION_MAPKeyError
ValueErrorsorted)selectors    \/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/shot_ref_classify.pyresolve_prompt_versionr   F   sK    
!(++ 
@l)F+=$>#?qB
 	

s    )4c                    d|  d| S )uK   샷 태그 — 실험/레시피 전반의 공통 키 형식 `S{si}sh{shi}`.Ssh )scene_index
shot_indexs     r   tag_ofr#   P   s    {m2j\**    c                    t         j                  |       }|st        d|       t        |j	                  d            t        |j	                  d            fS )Nzinvalid shot tag:       )_TAG_REmatchr   intgroup)tagms     r   	parse_tagr.   U   sI    cA-cW566qwwqz?C
O++r$   c                6   ddl m} |j                         D ch c]  }|s|	 }}i }|r| j                  |      j	                  |j
                  |k(        j                         }|D ]A  }	|	j                  ||	j                  <   |	j                  s)|	j                  ||	j                  <   C i }
t        |      t        |      z  }|D ]J  }|j                  |      xs d}|j                  |      xs |}|j                  |      xs d}|r|n||
|<   L |
S c c}w )u  scene_index → 장소 서술 — scene_director primary_location 을 EntityCanon
    이름으로 풀고, 없으면 씬 헤딩으로 degrade (전부 데이터 파생, 하드코딩 0).

    shot_ref_classify / shot_conti_light 등 레시피 스텝 공용.
    r   )EntityCanon )app.models.projectr0   valuesqueryfilter
project_idallnameidshort_idsetget)dbr6   scene_primaryscene_headingsr0   vloc_ids
name_by_idrowseout
all_scenessipidr8   headings                   r   derive_location_by_scenerJ   \   s    /'..060QAq0G6!#JHH[!VK**j89SU 	
 A vvJqttzz)*
1::&  C]#c.&99J#)r~~c")c $$R(.B$GB	 
 J' 7s
   DDc                    | xs g D ci c]  }|j                  d      | }}|D cg c]	  }||vs| }}|rt        dt         | d| d      |S c c}w c c}w )Nshotstep.contract_violation.u    판정 누락 샷:   codemessagestatus_code)r<   r   _MODULE)itemstagswhatitgottmissings          r   _index_items_by_shotr[      s     ).
4"266&>2C
4/$Q!3,q$G/+G95f0	:
 	

 J 5/s   A	AAr'   )max_attemptsc        
           |}
d}t        d|	dz         D ]/  } | |||
||||      }	 t        |j                  d      ||      c S  |# t        $ r}|}|j                  d      xs g D ch c]  }|j                  d       nc c}w }}|D cg c]	  }||vs| nc c}w }}t        j                  dt        ||t        |             |dz   dj                  |      z   }
Y d}~d}~ww xY w)	u   번들 판정 콜 + 누락 태그 힌트 재시도 (E2E 실측: 대형 입력에서 tier
    fallback 응답이 태그 누락/표기 이탈할 수 있음 — 1회 교정 재시도).Nr&   project_configschema_nameopik_metadatarT   rL   u+   %s %s attempt=%d 누락 %d샷 — 재시도u   

[재시도 지시] 직전 응답에서 다음 샷들이 누락되었거나 태그 표기가 달랐다. 목록의 모든 샷을, 입력에 적힌 태그 문자열 그대로(shot 필드) 빠짐없이 포함해 다시 판정하라: z, )	ranger[   r<   r   loggerwarningrS   lenjoin)call_structured_fnstep_tagsystemuserschemarU   rV   r_   ra   r\   currentlast_errattemptresexcrW   rX   rY   rZ   s                      r   _call_items_with_retryrq      s    G$(HL1,- ) '
	'(8$EE .6 N  	H,/GGG,<,B,BC,Bb266&>,BCCC"&7$Q!3,q$7G7NN=wW
 aa ))G$	% 	s6   A	C+C&*BC&	BBAC&&C+r   )prompt_versionrg   r_   ra   c                
  1 |ddl m} |}t        |      }	t        | d       }
|
D cg c]&  }t	        t        |d         t        |d               ( }}|si i dS t        ||
      D ci c]  \  }}||j                  d	      xs d
 c}}1t        |
D ch c]  }t        |d          c}      }i |xs i t         dddit         dddit         dddit         dddit         dddii}t        t        d|	      }t        t        dd|	      }dj                  1fd|D              }t        |t         d|d| ||d||	      }t        t        d|	      }t        t        dd|	      }dj                  fd|D              }dj                  1fd|D              }t        |t         d|d| d | ||d!||	      }t        t        d"|	      }t        t        d#d|	      } |t         d|d$| ||t         d|%      }|j                  d&      xs g D ci c]  }t        |d         | }}|D cg c]	  }||vs| }}|rt        d't         d(| d)*      i } 	 t        t        d+|	      }!t        t        d,d|	      }"|!!t        |t         d|!d| d | "|d-||	      } t        |      D #ci c]  \  }#}||#
 }$}#}i }%|D ]a  }t!        ||   d.         }&||   }'|'j                  d!      xs d}(|'j                  d/      xs d
})d}*|(:|(|$vrd0|(d1}*n|$|(   |$|   k\  rd0|(d2}*|*rt"        j%                  d3||*       d4\  }(})|&s|(d4\  }(})|&||   j                  d5      xs d
j'                         ||   j                  d6      xs d
|(|'j                  d6      xs d
|(|)nd
|*d7|%|<   | s| |   }+|+j                  d8      xs d
|%|   d8<   |+j                  d9      xs d
|%|   d9<   t!        |+j                  d:            |%|   d:<   |+j                  d;      xs d
|%|   d<<   d |D ci c]T  }t)        |      ||   j                  d=      xs d
||   j                  d8      xs d
||   j                  d;      xs d
d>V },}d
}-	 t        t        d?|	      }.t        t        d@d|	      }/|.9 |t         d|.dA| /|t         d|%      }0|0xs i j                  dB      xs d
}-|%|,|-dCS c c}w c c}}w c c}w c c}w c c}w # t        $ r d}!Y w xY wc c}}#w c c}w # t        $ r d}.Y w xY w)DuO  분류 3종 실행 → 체크포인트 payload.

    Args:
        shots: [{scene_index, shot_index, description}] — 선택(selected) 샷만.
        scene_texts: scene_index → 씬 원문 전문 (자르지 않음).
        scene_headings: scene_index → 씬 헤딩.
        location_by_scene: scene_index → 장소 서술(데이터 파생 — 하드코딩 금지).

    Returns:
        {"shots": {tag: {person_visible, bgonly_reason_ko, prev, prev_reason_ko,
                          usage_en, prev_violation}},
         "scenes": {str(scene_index): {time_of_day_en, basis_ko}}}
    Nr   )call_structuredc                :    t        | d         t        | d         fS )Nr!   r"   )r*   )ss    r   <lambda>z'run_shot_ref_classify.<locals>.<lambda>   s    c!M"23S<5IJr$   )keyr!   r"   )shotsscenesdescriptionr1   _bgonlymodelgpt_prev_time_of_day_world_anchor_shot_placebgonly_system)versionbgonly_schema
c              3  2   K   | ]  }| d |      yw)z: Nr    ).0rY   desc_by_tags     r   	<genexpr>z(run_shot_ref_classify.<locals>.<genexpr>   s"     ADqQCr+a.!12Ds   u   샷 목록:
bgonly)	rg   rh   ri   rj   rk   rU   rV   r_   ra   prev_systemprev_schemaz

c           
   3  t   K   | ]/  }d | dj                  |d       dj                  |d        1 yw)u   [씬 z] r1   r   N)r<   )r   rG   r?   scene_textss     r   r   z(run_shot_ref_classify.<locals>.<genexpr>  sF      B t2n((R01KOOB4K3LMs   58c              3  p   K   | ]-  }| d j                  t        |      d   d       d|     / yw)z [LOCATION: r   r1   z]
  N)r<   r.   )r   rY   r   location_by_scenes     r   r   z(run_shot_ref_classify.<locals>.<genexpr>  sO       A #\+//	!QDE F1~	 s   36u   씬 원문 전문:
u   

샷 목록(시간순):
prevtime_of_day_systemtime_of_day_schemau   씬 목록:

r^   rT   rM   u   time_of_day 판정 누락 씬: rN   rO   shot_place_systemshot_place_schema
shot_placeperson_visibleusage_enzprev=u    목록 밖 — null 격리u    앞 샷 아님 — null 격리zshot_ref_classify %s: %s)Nr1   
handled_by	reason_ko)r   r   bgonly_reason_kor   prev_reason_kor   prev_violationplace_enenvironmentconfined_structurebasis_koplace_basis_kotime_of_day_en)r   r   r   world_anchor_systemworld_anchor_schemau   씬 원문 전문:

world_anchor_en)ry   rz   r   )app.modules.llm.llm_clientrt   r   r   r#   r*   zipr<   rS   r   r   rf   rq   r   FileNotFoundError	enumerateboolrc   rd   stripstr)2ry   r   r?   r   rr   rg   r_   ra   rt   resolvedorderedrv   rU   rY   	scene_idspcbg_sys	bg_schema	shots_txtbg_gotprev_sysr   
scenes_txtshots_loc_txtprev_gottod_sys
tod_schematod_resrW   tod_gotrG   tod_missing	place_gotsp_sys	sp_schemai	tag_order	out_shotsr   	prev_itemr   r   	violationsp
out_scenesr   wa_sys	wa_schemawa_resr   s2    ```                                             @r   run_shot_ref_classifyr      s   0 !>,%n5HJG JQQAF3q'(#ao*>?DQr**47g4F4FDAqAEE- &B&4FK w?w!Am,-w?@I

R
)7gu-)5GU+)< 7E"2)=!GU#3);'5!1
B /8DFG_dHMI		ADAAI#-9G$YK(#
F 7M8DHg}dHMK  J II   M
 &-9E"#J</KM?[#
H '#7JG%tXJ !)< 
*&i|,#G .5[[-A-GR-G-GrB}"-G   !*?	"Rw->2	K?+G955k]C
 	
 ,.IW&98L($
	
 *1y,&zl2N /# '
	  #,D/2/$!QA/I2+-IfQi(89:QK	'mmF3;t==,2#'	9$#D8+FG	4IaL0#D8+JK	91iH!)h$"2%ND(,!!9==6<"CCE &q	k : @b'mmK8>B$($4"'	
	!  1B')vvj'9'?RIaL$*,&&*?*E2IaL' 26+,2.IaL-.-/VVJ-?-E2IaL)*K \  B 	B%bkoo.>?E2
39r
39r	
 	
    OW&;XN*D(
	
 #i}%$ZL1")=1'
 "<R,,->?E2 * u R @H @$  & 3R"  sT   +T/ TT.T
	T#T#7%T( T:
AU (%U (T76T7UUF)selected_onlyc          	        ddl }ddlm}m} | j	                  |      j                  |j                  |k(  |j                  dk(        j                         D ci c]%  }|j                  r|j                  |j                  ' }}i }	| j	                  |      j                  |j                  |k(  |j                  |k(        j                         }
|
D ]  }|j                  |j                  }}|| |r|j                  s/	 |j                  |j                   xs d      }t%               }|D ]o  }t'        |t(              r$|j+                  d      xs% |j+                  d      nt'        |t,              r|nd}|sN||v sS|j/                  t-        ||                q ||	t1        t3        |      t3        |            <    |	S c c}w # |j"                  $ r Y 	w xY w)ut  샷별로 VE(`scene_still.visible_entities_json`)에 있는 **캐릭터
    엔티티**의 short_id — 참조 이미지 유무와 묶지 않는다(참조가 없어도 그
    캐릭터는 화면에 있다).

    ``selected_only`` — 이미지가 실제로 만들어지는 **선택된 샷만**
    (`is_selected` 가 참인 행). 참조 생성 보호가 이것을 쓴다.r   N)r0   
SceneStill	characterz[]r9   	entity_id)jsonr2   r0   r   r4   r5   r6   entity_typer7   r:   r9   
episode_idr!   r"   is_selectedloadsvisible_entities_jsonJSONDecodeErrorr;   
isinstancedictr<   r   addr#   r*   )r=   r6   r   r   _jsonr0   r   rD   char_sid_by_idrE   rC   rowrG   shivesidsr@   eids                     r   load_character_sids_by_tagr     s    : +&	""j0##{2

 
	A :: 	
ajj  	  "C
	!!Z/!!Z/

 
 	 //3>>C:	S66>$?B A a& t2k 2%a-a4 
 sn,^C012  *.F3r7CH%&' ( JO	6 $$ 		s   *GGGGc                    i }| xs i j                         D ]_  \  }}t        |xs i       }t        |j                  |      xs d      }|r&|j                  dd      s|j	                  dd|       |||<   a |S )uY  캐릭터가 보이는 샷은 **무조건** `person_visible=True` — 새 dict 를
    돌려준다(받은 것은 안 바꾼다).

    덮은 샷에는 두 칸을 남긴다 — `person_visible_llm`(모델 판정값)과
    `person_visible_forced_by`(덮게 만든 캐릭터 short_id). 판정과 규칙을
    기록에서 가를 수 있게.
    r    r   TF)r   person_visible_llmperson_visible_forced_by)rT   r   r   r<   update)classify_shotscharacter_sids_by_tagrE   r,   rW   charss         r   #force_person_visible_for_charactersr     s     &(C"(b//1R"(^,005;< 0$7II##().  
 C 2 Jr$   )r   r   returnr   )r!   r*   r"   r*   r   r   )r,   r   r   zTuple[int, int])
r=   r   r6   r   r>   Dict[int, str]r?   r   r   r   )rT   List[Dict[str, Any]]rU   	List[str]rV   r   r   Dict[str, Dict[str, Any]])rg   zCallable[..., Dict[str, Any]]rh   r   ri   r   rj   r   rk   Dict[str, Any]rU   r   rV   r   r_   Optional[Dict[str, Any]]ra   r   r\   r*   r   r   )ry   r   r   r   r?   r   r   r   rr   r   rg   z'Optional[Callable[..., Dict[str, Any]]]r_   r   ra   r   r   r   )
r=   r   r6   r   r   r   r   r   r   Dict[str, Set[str]])r   r   r   r   r   r   )"__doc__
__future__r   loggingretypingr   r   r   r   r   r	   r
   app.core.errorsr   app.modules.prompt_loaderr   r   	getLogger__name__rc   rS   r   compiler(   r   r#   r.   rJ   r[   rq   r   r   r   r    r$   r   <module>r      s  , #  	 B B B $ >			8	$
 
 
 
 
 
9 > "**'
(
+
,    "  #	 
  F'08;2 ,5, , 	,
 , , , , -, ,, , ,j BF/3.2__  _ #	_
 &_ _ @_ -_ ,_ _l #555*-55 5p-. r$   