
    i,)                       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	 ej                  j                  d e e	e      j                         j                               ddlmZmZmZmZmZmZmZmZ  e       Zed   Zed   Z ej8                  d	      Zdd
ZddZddZ ddZ!e"dk(  r ejF                   e               yy)u  G4.3 canary — STRICT close-framing face-filling forbidden phrasing counter (Task 4.2).

Detects 6 literal forbidden substrings inside `t2i_prompt` of close-framing
shots only. Phrases are imported from
`render_prompt_card._ID_CLOSE_FACE_FORBIDDEN_PHRASES` (Override R4-B3 carry —
all 6 phrases including masculine `"his face fills the frame"`).

Note (R4-B3 carry): the v18 compact section + constraints[3] inline list
contains only 5 phrases for token cost reasons; the 6th masculine entry is
preserved via the structured field `close_framing_face_phrasing.forbidden_phrases`
which is injected to the LLM. Therefore this canary checks all 6 phrases —
the structured field is the instruction source and any leak (regardless of
which inline list dropped it) is a contract violation.

Scope: close-framing shots only (Override R3-I1).

Exit criteria (analyzer, OUT-OF-BAND):
  candidate.metrics.close_framing_face_forbidden_count == 0  (STRICT)
  baseline value is informational only.

Usage:
    python scripts/canary/g4_3_close_framing_face_forbidden.py \
        --config canary_config.json \
        --cp-root data/projects/<pid>/checkpoints/scene_detail \
        --prompt-version 17.<timestamp> \
        --role candidate \
        --output results/close_framing_face_forbidden_candidate.json
    )annotationsN)datetime)Path)_load_scenes_or_fail_validate_scene_preflightbuild_pinning_scene_setis_close_framingload_cp_manifestload_pinning_from_argsrender_prompt_card_importsvalidate_pinning _ID_CLOSE_FACE_FORBIDDEN_PHRASES_ID_REPRODUCTION_SURFACESz\bC\d{2}(?:O\d{2})?\bc                 L    t        j                         j                         dz   S )NZ)r   utcnow	isoformat     3scripts/canary/g4_3_close_framing_face_forbidden.pyiso8601_nowr   ;   s    ??&&(3..r   c                    g }| j                         }t        D ]&  }|j                         |v s|j                  |       ( |S )u   Return list of forbidden phrases that appear in `prompt` (case-insensitive
    literal substring match — NO regex needed — `feedback_no_regex_postprocessing.md` carry).)lowerr   append)promptfoundplphrases       r   find_forbidden_phrasesr   ?   s?     E	B2<<>RLL  3 Lr   c                    t        j                  d      } | j                  dt        d        | j                  dt        d        | j                  dt        d        | j                  dt        d	       | j                  d
t        dddg       | j                  dt        d        | j                  dt        d        | j                  dt        d        | j                  dt        d g d       | j                  dt        d        | j                  dt        d        | j                  dt        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                          yt        |      }|j#                         st        d| t        j                          yt%        |      }g }t'        ||      }	|	=t)               |j*                  |j,                  |g ddddi g d|d d!id"}
t/        |
|       yt1        |      }t3               }d}d}d}d}i }g }g }|	D ]H  }t5        |t6              s|j9                  d#       &|j                  d$      }|j                  d%      }||Mt;        |      t;        |      f|vrf|j=                  t;        |      t;        |      f       t?        ||||      }|tA        |      \  }}||j9                  d&| d'| d(|        d)}d)}|D ]}  }t5        |t6              s|j                  d*      }t5        |t              s6tB        jE                  |      rd}|jG                         }tH        D ]  }|jG                         |v sd} }  |j9                  |||||d+       |dz  }|s|dz  }q|dz  }d} tK        |      D ]  \  }!}t5        |t6              s|j9                  d&| d'| d,|! d-       2|j                  d*      }"t5        |"t              r|"jM                         s|j9                  d&| d'| d,|! d.       tO        |"      }#| tQ        |#      z  } |#D ]  }$|$|vs|j9                  |$         || z  }| || d/| <   K |D ]$  \  }}||f|vs|j9                  d0| d'| d1       & t)               |j*                  |j,                  ||||||||d|d d!id"}
t/        |
|       t        d2|j,                   d3|j*                   d4| d5| d6| d7| d8| d9tQ        |       d:t        j                          |ry|j,                  dk(  r|dkD  ryy# 1 sw Y   xY w);NzLG4.3 canary: close-framing face-fill forbidden phrasing counter (6 phrases).)descriptionz--config)typedefaultz--pidz	--cp-rootz--prompt-versionT)r"   requiredz--role	candidatebaseline)r"   r#   choicesz--scene-index-listz--shot-index-list-per-scenez--model-routingz--prompt-source-mode)Nfiledbz--card-commit-hashz--id-policy-card-snapshot-hashz--outputrutf-8encodingcp_rootz=[g4_3_close_framing_face_forbidden] ERROR: --cp-root required)r(      z@[g4_3_close_framing_face_forbidden] ERROR: --cp-root not found: r   )total_shotsclose_framing_shotsnon_close_framing_shots"close_framing_face_forbidden_count%close_framing_face_forbidden_per_shotforbidden_phrases_detectedclose_framing_face_forbiddenu`   scripts/canary/g4_3_close_framing_face_forbidden.py (close-framing only — all 6 phrases R4-B3))	timestampprompt_versionrolepinning	scene_setmetricsmeasurement_failuresmeasurement_scriptszscene entry is not dictscene_index_shot_indexs_shz: F
t2i_prompt)r?   
shot_indexr	   has_composite_idhas_reproduction_surface_vz: variation not a dictz': t2i_prompt missing, empty, or not str_z
pinning (sz ): not present in CP data.scenesz)[g4_3_close_framing_face_forbidden] role=z prompt_version=z	
  total=z  cf=z  ncf=z&
  close_framing_face_forbidden_count=z
  forbidden_phrases_detected=z
  measurement_failures: zR
Exit criteria (STRICT): candidate.metrics.close_framing_face_forbidden_count == 0))argparseArgumentParseradd_argumentstr
parse_argsr   r   configr   openjsonloadgetr.   printsysstderrexistsr
   r   r   r8   r9   _emitr   set
isinstancedictr   intaddr   r	   _COMPOSITE_ID_REsearchr   r   	enumeratestripr   len)%parserargsr:   cp_root_strfpcfgr.   cpr=   scenesoutpinning_set
seen_in_cpr0   cf_count	ncf_countforbidden_totalforbidden_per_shotforbidden_phrases_unionscene_set_outscenesishi
variationscf_flagcf_errrE   rF   varpr   surf
shot_countvir   phrases_in_promptphs%                                        r   mainr   J   sd   $$bF 
d;
c48
#t<
*tD
sK!+[ 9  ;
,3E
5CN
)TB
.S$ 4  6
,3E
8sDQ

t<D$T*GW"K{{$++##C'#:b))B-C ;ggi(llM::	;G>>PQXPYZ::		'	"B&(!"&:;F~$"11II '(+,679;.0 %9.C$
, 	c4)'2K'*uJKHIO)+)+ "M%& ''(ABYY}%ii&:GSXk1BS*+.ub#?ST
*51 ''!B4s3%r&(BC !#( Cc4(%A!S!#**1-'+$WWY5Dzz|r)370 6  	 ' 0(@
 	 	qNIA
 ,GBc4($++t3se2bT)?@ WW\*Ffc*&,,.$++t3se2bT)PQ  6v >#/00J'44+2226 ( -" 	:%,6bT3%=)K N !	S9J& ''RDC5(HI ! !]--		"&#+'02A5G*A
 !5*? 
C. 
#t	
3DII;>NtObObNc d-uXJfYK @00?/@ A((?'@ A##&';#<"= >\		] ZZ yyKOa$7Y ;:s   WWc                   |j                   dk(  rHt        j                  | t        j                  ddd       t        j                  j                  d       y t        |j                         }|j                  j                  dd       |j                  dd	
      5 }t        j                  | |ddd       d d d        y # 1 sw Y   y xY w)N-F   T)ensure_asciiindent	sort_keys
)parentsexist_okwr+   r,   )
outputrP   dumprT   stdoutwriter   parentmkdirrO   )ri   rc   out_pathre   s       r   rW   rW     s    {{c		#szza4P

$dT:]]3]1RIIc2E!tL 211s   CC
__main__)returnrL   )r   rL   r   z	list[str])r   r[   )ri   rZ   rc   zargparse.Namespacer   None)$__doc__
__future__r   rI   rP   rerT   r   pathlibr   pathinsertrL   __file__resolver   _g4_3_commonr   r   r   r	   r
   r   r   r   _CONSTSr   r   compiler]   r   r   r   rW   __name__exitr   r   r   <module>r      s   8 #   	 
   3tH~--/667 8	 	 	 %
&#*+M#N  #$?@ 2::67 /EPM zCHHTV r   