
    i<A                    v   U d 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mZ ddlm	Z	 ddl
mZmZmZmZmZ g dZded	<   eD  cg c]  } ej%                  | ej&                          c} Zdd
ZdddZddZddZ	 	 	 	 ddZddZddZ	 	 	 	 	 	 	 	 ddZddZedk(  r ej>                   e              yyc c} w )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Tuple)z+match.*reference.*(camera|framing|position)z!same.*(angle|position).*referencez-(push in|pull out|deviation).*from.*referencezreference.*camera_position	List[str]CAMERA_WORDING_PATTERNSc                 L    t        j                         j                         dz   S )NZ)r   utcnow	isoformat     %scripts/canary/g4_2_camera_wording.pyiso8601_nowr   9   s    ??&&(3..r   c                j    t        d|  t        j                         t        j                  |       y )Nz[g4_2_camera_wording] ERROR: file)printsysstderrexit)msgcodes     r   _failr   =   s#    	)#
/cjjAHHTNr   c           	        | j                   rt        | j                         }|j                         st        d|        	 |j	                  dd      5 }t        j                  |      }ddd       j                  d      }|j                  d      }|j                  d	      |||j                  d
      |j                  d      |j                  d      |j                  d      dS 	 | j                  rt        j                  | j                        nd}| j                  rt        j                  | j                        nd}| j                  | j                  | j                  | j                  | j                   dS # 1 sw Y   xY w# t        $ r}t        d|        Y d}~8d}~ww xY w# t        $ r}t        d|        Y d}~d}~ww xY w)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_hashr&   r$   r%   r'   r(   r)   r*   zGfailed to parse --scene-index-list / --shot-index-list-per-scene JSON: )configr   existsr   openjsonload	Exceptiongetr$   loadsr%   r&   r'   r(   r)   r*   )argscfg_pathfpcfgexcr$   r%   s          r   load_pinningr9   B   s   
 {{$ -hZ89	2sW5iim 6 77#56$'GG,G$H!775> 0)B WW_5"%''*>"? #(: ;+.773P+Q
 	

_ $$ JJt,,-*. 	 -- JJt55637 	" xx,%>++"55 11'+'G'G 7 65 	2*3%011	2.  _WX[W\]^^_sI   F F *F *AF2  F
F 	F/F**F/2	G;GGc                    g d}|D cg c]  }| j                  |      dv s| }}|rt        d| d       t        | d   t              st        d       t        | d   t              st        d       y y c c}w )	Nr+   )N z#pinning missing required field(s): zF. Provide via --config <json> or individual flags. NO silent defaults.r$   z*pinning.scene_index_list must be list[int]r%   z>pinning.shot_index_list_per_scene must be dict[str, list[int]])r2   r   
isinstancelistdict)pinningrequiredkmissings       r   validate_pinningrC   r   s    H
 #C(Qgkk!n
&Bq(GC1' ;" #	

 g0148:;g9:DANO B Ds
   A7A7c           	        g }| d   }| d   D ]  }t        |      }|j                  t        |            }||j                  |      }|t        d| d       t	        |t
              s%t        d| dt        |      j                   d       |st        d| d       |D ]  }|j                  |t        |      f          |S )	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.)	intr2   strr   r<   r=   type__name__append)r?   outshot_mapsisi_int	raw_shotsshis          r   build_pinning_scene_setrP      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                   | dz  }|j                         st        d|        	 |j                  dd      5 }t        j                  |      cd d d        S # 1 sw Y   y xY w# t
        $ r}t        d|        Y d }~y d }~ww xY w)Nzmanifest.jsonzmanifest.json not found at r    r!   r"   zmanifest.json parse error: )r-   r   r.   r/   r0   r1   )cp_rootmanifest_pathr6   r8   s       r   load_cp_manifestrT      s{    o-M!+M?;<3g6"99R= 766 3+C51223s4   A# A	A# A A#  A# #	B,A??Bc                    | j                  d      }t        |t              sy|j                  d      }t        |t              sy|j                  d      dk(  ry|j                  d      d	u ryy
)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)Fz&render_prompt_card missing or not dictbackground_binding)Fz9render_prompt_card.background_binding missing or not dictmodeskipped_close_framing)TN"close_framing_skips_background_refT)FN)r2   r<   r>   )scenerpcbbs      r   is_close_framingr^      sh     ))(
)Cc4 @	%	&Bb$S	vvf~00	vv23t;r   c                  g }t        |       D ]  \  }}t        |t              s|j                  | d| d       .|j	                  d      }t        |t
              r|j                         s|j                  | d| d       wt        t        t              D ]-  \  }}|j                  |      s||vs|j                  |       /  t        |      |fS )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 차단).

    Per-variation validation (Codex iter3 BLOCKING — `var.get("t2i_prompt") or
    ""` silent absorb 제거):
      - variation 이 dict 아니면 measurement_failures 적재 + skip.
      - t2i_prompt 누락 / non-str / empty 면 measurement_failures 적재 + skip.
    detail_schema.json variation.required 가 t2i_prompt 명시 — 누락은 contract
    위반.
    z/varz: variation not a dict
t2i_promptz': t2i_prompt missing, empty, or not str)	enumerater<   r>   rI   r2   rF   stripzipr   _COMPILED_PATTERNSsearchbool)	
variationsscene_labelmeasurement_failuresmatched_patternsidxvarpromptrawcompileds	            r   
scan_scenerp      s    $ #%j)S#t$ ''-tC5(>? &&#&flln ''-tC5(OP  !8:LMMCv&..$++C0 N *  !"$455r   c                 
   t        j                  d      } | j                  dt        d d       | j                  dt        d        | j                  dt        d d	       | j                  d
t        d d       | j                  dt        dd       | j                  dt        dddgd       | j                  dt        d d       | j                  dt        d d       | j                  dt        d        | j                  dt        d g d       | j                  dt        d        | j                  dt        d        | j                  dt        dd        | j	                         }t        |      }t        |       d }|j                  rUt        |j                        j                  d!d"#      5 }t        j                  |      }d d d        j                  d$      }|s|j                  }|st        d%       t        |      }|j                         st        d&|        t!        |      }|j                  d'      }t#        |t$              st        d(       |j                  d)      }	t#        |	t&              st        d*       t)        t+        |            }
t)               }d+}d+}d+}d+}g }g }g }|	D ]/  }t#        |t$              s|j                  d,      }|j                  d-      }||<||f|
vrC|j-                  t/        |      t/        |      f       |d.z  }t1        |      \  }}|r|j3                  d/| d0| d1|        |j3                  |||d2       |r|d.z  }|d.z  }|j                  d3      }t#        |t&              r|s|j3                  d/| d0| d4       t5        |d/| d0| |5      \  }}|r|d.z  }|D ]  }||vs|j3                  |        2 |
D ]$  \  }}||f|vs|j3                  d6| d0| d7       & t7               |j8                  |j:                  |||||||d8|d9d:id;}|j<                  d<k(  rHt        j>                  |t@        jB                  d=d>d?       t@        jB                  jE                  d@       nht        |j<                        }|jF                  jI                  ddA       |j                  dBd"#      5 }t        j>                  ||d=d>d?       d d d        tK        dC|j:                   dD|j8                   dE| dF| dG| dH| dI| dJtM        |       dKt@        jN                  L       |ry.y+# 1 sw Y   xY w# 1 sw Y   kxY w)MNz<G4.2 canary: camera-consistency wording presence (NCF-only).)descriptionz--configzCJSON file with all 7 pinning fields + cp_root + episode (preferred))rG   defaulthelpz--pid)rG   rs   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)rG   r@   rt   z--role	candidatebaselinez,Reporting role label (baseline or candidate))rG   rs   choicesrt   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)Nr   db)rG   rs   rw   z--card-commit-hashz--chain-bg-card-snapshot-hashz--outputz"Output JSON path or '-' for stdoutr    r!   r"   rR   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 listr   scene_index_shot_index   s_shz: )r{   
shot_indexr^   t2i_variationsz,: t2i_variations missing, empty, or not list)rh   ri   z
pinning (sz ): not present in CP data.scenes)total_shotsnon_close_framing_shotsclose_framing_shots1camera_consistency_wording_present_non_close_only+camera_consistency_wording_patterns_matchedcamera_wordingzMscripts/canary/g4_2_camera_wording.py (R1-I3 4 pattern, NCF-only scope R2-B4))	timestampprompt_versionroler?   	scene_setmetricsri   measurement_scripts-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   )(argparseArgumentParseradd_argumentrF   
parse_argsr9   rC   r,   r   r.   r/   r0   r2   rR   r   r-   rT   r<   r>   r=   setrP   addrE   r^   rI   rp   r   r   r   outputdumpr   stdoutwriteparentmkdirr   lenr   )parserr4   r?   cp_root_strr6   r7   rR   cpry   rz   pinning_set
seen_in_cptotal	ncf_countcf_countncf_with_wordingmatched_patterns_unionscene_set_outri   r[   rL   rO   cfcf_errrg   	any_matchmatchedprJ   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&* 	S "
 	 MHQ	 YY/0
*d+: ''B4s3%KL 'B4s3%(!5
	7
 !A..&--a0 O \ !	S9J& ''RDC5(HI ! !]--		" '0#+AQ;Q
 !5m 
C& {{c		#szza4P

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

%dii[0@ATAT@U Vvi[hZ @33C2D E45 6##&';#<"= >T		U ZZ
  G ;:\ 21s   7U"U'U$'U0__main__)returnrF   )r}   )r   rF   r   rE   r   None)r4   zargparse.Namespacer   Dict[str, Any])r?   r   r   r   )r?   r   r   zList[Tuple[int, int]])rR   r   r   r   )r[   r   r   zTuple[bool, Optional[str]])rg   zList[Dict[str, Any]]rh   rF   ri   r   r   zTuple[bool, List[str]])r   rE   ) __doc__
__future__r   r   r/   rer   r   pathlibr   typingr   r   r   r	   r
   r   __annotations__compile
IGNORECASErd   r   r   r9   rC   rP   rT   r^   rp   r   rH   r   )r   s   0r   <module>r      s   !D #   	 
   3 3&   =TT<SqbjjBMM2<ST /
-`P&!!!H32#6$#6 #6 $	#6
 #6LgT zCHHTV O Us   #B6