+
    i=                      a 0 t $ R t^ RIHt ^ RIt^ RIt^ RIt^ RIt^ RIHt ^ RI	H
t
 ^ RIHtHtHtHtHt . ROtR]R&   ] U u. uF  p ]P'                  V ]P(                  4      NK!  	  up tR R	 ltRR
 R lltR R ltR R ltR R ltR R ltR R ltR R ltR R lt]R8X  d   ]P@                  ! ]! 4       4       R# R# u up i )u  G4.2 canary — camera-consistency wording presence (Rule A lift verification).

Counts shots in scope whose `t2i_prompt` (any variation) contains camera
consistency wording matching one of 4 patterns (R1-I3). Scope = NON-close
framing shots only (R2-B4 metric scope mutual exclusion).

This script is read-only — it loads a pre-computed scene_detail checkpoint
manifest.json and counts pattern matches. NO LLM calls.

Exit criteria (compared baseline vs candidate, OUT-OF-BAND analysis):
  candidate.metrics.camera_consistency_wording_present_non_close_only
    >= baseline.metrics.camera_consistency_wording_present_non_close_only
  (degradation guard — Rule A lift must not erase camera wording from
  prompts where it is structurally required.)

Usage (single config file mode — preferred):
    python scripts/canary/g4_2_camera_wording.py \
        --config canary_config.json \
        --role candidate \
        --output results/camera_wording_candidate.json

Usage (individual flags mode):
    python scripts/canary/g4_2_camera_wording.py \
        --pid <pid> --episode <ep> \
        --cp-root <path-to-scene_detail-cp-dir> \
        --prompt-version 17.202605042018 \
        --scene-index-list "[1,2,3]" \
        --shot-index-list-per-scene '{"1":[1,2],"2":[1],"3":[1,2,3]}' \
        --model-routing default \
        --prompt-source-mode file \
        --card-commit-hash <hex> \
        --chain-bg-card-snapshot-hash <hex> \
        --output results/camera_wording.json
)annotationsN)datetime)Path)AnyDictListOptionalTuplez	List[str]CAMERA_WORDING_PATTERNSc                   V ^8  d   QhRR/# )   returnstr )formats   "%scripts/canary/g4_2_camera_wording.py__annotate__r   9   s     / /S /    c                 V    \         P                  ! 4       P                  4       R ,           # )Z)r   utcnow	isoformatr   r   r   iso8601_nowr   9   s    ??&&(3..r   c               $    V ^8  d   QhRRRRRR/# )r   msgr   codeintr   Noner   )r   s   "r   r   r   =   s!      s # d r   c                n    \        R V  2\        P                  R7       \        P                  ! V4       R# )z[g4_2_camera_wording] ERROR: fileN)printsysstderrexit)r   r   s   &&r   _failr%   =   s#    	)#
/cjjAHHTNr   c                    V ^8  d   QhRRRR/# )r   argszargparse.Namespacer   Dict[str, Any]r   )r   s   "r   r   r   B   s     - -) -n -r   c                    V P                   '       d   \        V P                   4      pVP                  4       '       g   \        RV 24        VP	                  RRR7      ;_uu_ 4       p\
        P                  ! V4      pRRR4       XP                  R4      pTP                  R4      pR	TP                  R	4      RTRTR
TP                  R
4      RTP                  R4      RTP                  R4      RTP                  R4      /#  V P                  '       d!   \
        P                  ! V P                  4      MRpV P                  '       d!   \
        P                  ! V P                  4      MRpR	V P                  RXRXR
V P                  RV P                  RV P                  RV P                   /#   + '       g   i     EL9; i  \         d   p\        RT 24        Rp?EL[Rp?ii ; i  \         d   p\        RT 24        Rp?LRp?ii ; i)u{   Build the 7-field pinning block from config or individual flags.

No silent defaults. Missing → exit 1 with clear error.
z--config file not found: rutf-8encodingNz--config parse error: scene_index_listshot_index_list_per_scenepidmodel_routingprompt_source_modecard_commit_hashchain_bg_card_snapshot_hashzGfailed to parse --scene-index-list / --shot-index-list-per-scene JSON: )configr   existsr%   openjsonload	Exceptiongetr.   loadsr/   r0   r1   r2   r3   r4   )r'   cfg_pathfpcfgexcr.   r/   s   &      r   load_pinningrA   B   s   
 {{{$  -hZ89	2sW55iim 6 77#56$'GG,G$H!3775> 0')BSWW_5 #''*>"?(: ;)3773P+Q
 	

_ $$$ JJt,,-*. 	 --- JJt55637 	" 	txx,#%>++d55D11%t'G'G 7 655 	2*3%011	2.  _WX[W\]^^_sZ   G (F/?G G* 3G* "G* /G 	:G  G G'G""G'*H5HHc                    V ^8  d   QhRRRR/# )r   pinningr(   r   r   r   )r   s   "r   r   r   r   s     P Pn P Pr   c                :   . ROpV Uu. uF  q P                  V4      R	9   g   K  VNK  	  ppV'       d   \        RV R24       \        V R,          \        4      '       g   \        R4       \        V R,          \        4      '       g   \        R4       R# R# u upi )
r0   r.   r/   Nz#pinning missing required field(s): zF. Provide via --config <json> or individual flags. NO silent defaults.z*pinning.scene_index_list must be list[int]z>pinning.shot_index_list_per_scene must be dict[str, list[int]])r0   r.   r/   r1   r2   r3   r4   )N )r;   r%   
isinstancelistdict)rC   requiredkmissings   &   r   validate_pinningrL   r   s    H
 #C(Qkk!n
&Bqq(GC1' ;" #	

 g01488:;g9:DAANO B Ds
   BBc                    V ^8  d   QhRRRR/# )r   rC   r(   r   zList[Tuple[int, int]]r   )r   s   "r   r   r      s     ! !!!r   c           	        . pV R,          pV R,           F  p\        V4      pVP                  \        V4      4      pVf   VP                  V4      pVf   \        RV R24       \	        V\
        4      '       g&   \        RV R\        V4      P                   R24       V'       g   \        RV R24       V F  pVP                  V\        V4      34       K!  	  K  	  V# )	u  Cartesian (scene_index, shot_index) tuples from pinning block.

Strict — pinning enumerates EXACT (si, shi) tuples. A scene_index without
a corresponding shot_index_list entry (or with a non-list / empty list)
is a contract violation, not a silent default. fail-fast via _fail.
r/   r.   z(pinning.scene_index_list contains scene zF but shot_index_list_per_scene has no entry for it. NO silent default.z"pinning.shot_index_list_per_scene[z] must be list[int], got z. NO silent default.uR   ] is empty — a pinned scene must enumerate at least one shot. NO silent default.)	r   r;   r   r%   rF   rG   type__name__append)rC   outshot_mapsisi_int	raw_shotsshis   &      r   build_pinning_scene_setrX      s     "$C23H())RLLV-	 V,I:6( CT U )T**4VH =I//00DF 4VH =V W CJJC)* ) *, Jr   c                    V ^8  d   QhRRRR/# )r   cp_rootr   r   r(   r   )r   s   "r   r   r      s     3 3d 3~ 3r   c                L   V R ,          pVP                  4       '       g   \        RV 24        VP                  RRR7      ;_uu_ 4       p\        P                  ! V4      uuRRR4       #   + '       g   i     R# ; i  \
         d   p\        RT 24        Rp?R# Rp?ii ; i)zmanifest.jsonzmanifest.json not found at r*   r+   r,   Nzmanifest.json parse error: )r6   r%   r7   r8   r9   r:   )rZ   manifest_pathr>   r@   s   &   r   load_cp_manifestr]      s    o-M!!+M?;<3g66"99R= 7666 3+C51223s5   A? 
A+ 
A? +A<	6A? <A? ?B#
BB#c                    V ^8  d   QhRRRR/# )r   scener(   r   zTuple[bool, Optional[str]]r   )r   s   "r   r   r      s      N /I r   c                   V P                  R4      p\        V\        4      '       g   R# VP                  R4      p\        V\        4      '       g   R# VP                  R4      R8X  d   R	# VP                  R4      RJ d   R	# R
# )u  Return (cf_flag, error_message).

Reads from `scene["render_prompt_card"]["background_binding"]` — the actual
CP shape produced by detail_steps.py (scene["background_binding"] does NOT
exist as a flat field). When the scene record is missing the
`render_prompt_card` envelope or its `background_binding` field, returns
(False, reason) so the caller can append to measurement_failures
(NO silent absorb — Codex C1 fix).

error_message is None on success.
render_prompt_cardbackground_bindingmodeskipped_close_framingT"close_framing_skips_background_ref)Fz&render_prompt_card missing or not dict)Fz9render_prompt_card.background_binding missing or not dict)TN)FN)r;   rF   rH   )r_   rpcbbs   &  r   is_close_framingrh      s}     ))(
)Cc4  @@	%	&Bb$SS	vvf~00	vv23t;r   c                    V ^8  d   QhRRRR/# )r   
variationszList[Dict[str, Any]]r   zTuple[bool, List[str]]r   )r   s   "r   r   r      s     6 6$66r   c                6   . pV  F  pVP                  R4      ;'       g    Rp\        V\        4      '       g   K6  \        \        \
        4       F7  w  rEVP                  V4      '       g   K  WA9  g   K&  VP                  V4       K9  	  K  	  \        V4      V3# )u   Return (any_match, list_of_matched_pattern_strs).

Caller is responsible for validating `variations` is a non-empty list
(Codex iter2 BLOCKING 2 — silent absorb 차단).

t2i_promptrE   )	r;   rF   r   zipr
   _COMPILED_PATTERNSsearchrQ   bool)rj   matched_patternsvarpromptrawcompileds   &     r   
scan_scenerv      s     #%&,,"&#&& !8:LMMCv&&.$++C0 N	  !"$455r   c                   V ^8  d   QhRR/# )r   r   r   r   )r   s   "r   r   r      s     c cc cr   c                 ,   \         P                  ! R R7      p V P                  R\        RRR7       V P                  R\        RR7       V P                  R\        RR	R7       V P                  R
\        RRR7       V P                  R\        RRR7       V P                  R\        RRR.RR7       V P                  R\        RRR7       V P                  R\        RRR7       V P                  R\        RR7       V P                  R\        R. RTOR7       V P                  R\        RR7       V P                  R\        RR7       V P                  R\        RRR7       V P	                  4       p\        V4      p\        V4       RpVP                  '       d_   \        VP                  4      P                  R R!R"7      ;_uu_ 4       p\        P                  ! V4      pRRR4       XP                  R#4      pV'       g   VP                  pV'       g   \        R$4       \        V4      pVP                  4       '       g   \        R%V 24       \!        V4      pVP                  R&4      p\#        V\$        4      '       g   \        R'4       VP                  R(4      p	\#        V	\&        4      '       g   \        R)4       \)        \+        V4      4      p
\)        4       p^ p^ p^ p^ p. p. p. pV	 EFp  p\#        V\$        4      '       g   K  VP                  R*4      pVP                  R+4      pVe   Vf   KH  VV3V
9  d   KS  VP-                  \/        V4      \/        V4      34       V^,          p\1        V4      w  ppV'       d   VP3                  R,V R-V R.V 24       VP3                  R*VR/VR0V/4       V'       d   V^,          pK  V^,          pVP                  R14      p\#        V\&        4      '       d	   V'       g   VP3                  R,V R-V R224       EK.  \5        V4      w  ppV'       d
   V^,          pV F  pVV9  g   K  VP3                  V4       K  	  EKs  	  V
 F)  w  ppVV3V9  g   K  VP3                  R3V R-V R424       K+  	  R5\7        4       R6VP8                  R7VP:                  R8VR9VR:R;VR<VR=VR>VR?V/R@VRARBRC//pVP<                  RD8X  dJ   \        P>                  ! V\@        PB                  RE^RRF7       \@        PB                  PE                  RG4       Mq\        VP<                  4      pVPF                  PI                  RRRH7       VP                  RIR!R"7      ;_uu_ 4       p\        P>                  ! VVRE^RRF7       RRR4       \K        RJVP:                   RKVP8                   RLV RMV RNV ROV RPV RQ\M        V4       RR2\@        PN                  RS7       V'       d   ^# ^ #   + '       g   i     EL; i  + '       g   i     L; i)Uz<G4.2 canary: camera-consistency wording presence (NCF-only).)descriptionz--configNzCJSON file with all 7 pinning fields + cp_root + episode (preferred))rO   defaulthelpz--pid)rO   rz   z	--episodez=Episode key (used in CP path resolution if cp_root not given)z	--cp-rootzEDirectory containing scene_detail manifest.json (offline canary mode)z--prompt-versionTz=Prompt version label, e.g. 16.202605041200 or 17.202605042018)rO   rI   r{   z--role	candidatebaselinez,Reporting role label (baseline or candidate))rO   rz   choicesr{   z--scene-index-listzJSON list[int] e.g. '[1,2,3]'z--shot-index-list-per-scenez%JSON dict, e.g. '{"1":[1,2],"2":[1]}'z--model-routingz--prompt-source-mode)rO   rz   r~   z--card-commit-hashz--chain-bg-card-snapshot-hashz--outputz"Output JSON path or '-' for stdoutr*   r+   r,   rZ   z)--cp-root (or config.cp_root) is requiredz--cp-root path does not exist: dataz!cp['data'] is missing or not dictscenesz+cp['data']['scenes'] is missing or not listscene_index_shot_indexs_shz: 
shot_indexrh   t2i_variationsz,: t2i_variations missing, empty, or not listz
pinning (sz ): not present in CP data.scenes	timestampprompt_versionrolerC   	scene_setmetricstotal_shotsnon_close_framing_shotsclose_framing_shots1camera_consistency_wording_present_non_close_only+camera_consistency_wording_patterns_matchedmeasurement_failuresmeasurement_scriptscamera_wordingzMscripts/canary/g4_2_camera_wording.py (R1-I3 4 pattern, NCF-only scope R2-B4)-F)ensure_asciiindent	sort_keys
)parentsexist_okwz[g4_2_camera_wording] role=z prompt_version=z
  total_shots=z  ncf=z  cf=z)
  camera_wording_present_non_close_only=z
  matched_patterns=z
  measurement_failures: z
Exit criteria (degradation guard, manual cross-compare):
  candidate.metrics.camera_consistency_wording_present_non_close_only
    >= baseline.metrics.camera_consistency_wording_present_non_close_onlyr   )Nr    db)(argparseArgumentParseradd_argumentr   
parse_argsrA   rL   r5   r   r7   r8   r9   r;   rZ   r%   r6   r]   rF   rH   rG   setrX   addr   rh   rQ   rv   r   r   r   outputdumpr"   stdoutwriteparentmkdirr!   lenr#   )parserr'   rC   cp_root_strr>   r?   rZ   cpr   r   pinning_set
seen_in_cptotal	ncf_countcf_countncf_with_wordingmatched_patterns_unionscene_set_outr   r_   rT   rW   cfcf_errrj   	any_matchmatchedprR   out_paths                                 r   mainr      s   $$RF 
db  d c48
#t\  ^
#td  f
*t\  ^
sK!+[ 9K  M ,3<  >
5CF  H
)TB
.S$ 4  6
,3E
7c4P

tA  CD4 GW #K{{{$++##C'#::b))B-C ;ggi(ll9:;G>>/y9:	'	"B 66&>DdD!!12XXhFfd##;<-g67K'*uJEIH(**,M&(%&&YY}%ii&:9K'BS*+
%e,
F ''B4s3%r&* 	2|S
 	 MHQ	 YY/0
*d++: ''B4s3%KL '
3	7!A..&--a0 G T !	S9J& ''RDC5(HI ! 	[]$--		7]5%y!8?AQ9;Q
 	 4m 
C& {{c		#szza4P

$dT:]]3]11RIIc2E!tL 2 

%dii[0@ATAT@U Vvi[hZ @33C2D E45 6##&';#<"= >T		U ZZ
   ;::T 21s   W/.X/X 	X	__main__)z+match.*reference.*(camera|framing|position)z!same.*(angle|position).*referencez-(push in|pull out|deviation).*from.*referencezreference.*camera_position)   )!__conditional_annotations____doc__
__future__r   r   r8   rer"   r   pathlibr   typingr   r   r   r   r	   r
   __annotations__compile
IGNORECASErn   r   r%   rA   rL   rX   r]   rh   rv   r   rP   r$   )r   r   s   0@r   <module>r      s   !D #   	 
   3 3&   =TT<SqbjjBMM2<ST /
-`P&!H326(cL zHHTV c
 Us   %C