
    ki?                       U 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 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mZ  e       Zed   ZddlmZmZ  ej<                  d	      Zd
Z dZ!de"d<   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jV                   e(              yy)u  G4.5a canary — primary framing same-character-ID detection (Rule J, Task 4.3).

Detect Rule J violations (RO-8 algorithm — STRICT 0): in close-framing shots,
when a body-part close-up trigger (`focus on` / `close on` / `tight on` /
`detail on` — `_ID_BODY_PART_TRIGGERS` cross-import from G4.3) co-occurs
with a full-body description keyword (`stands` / `seated` / `leaning` /
`전신`) and BOTH refer to the SAME character ID within ±20 token distance,
that's a primary_framing rule violation.

RO-8 algorithm (per spec §5.1 + Plan Task 4.3):
- per-shot scan, build dict `{character_id: [token_pos]}` for all C##/C##O##
  occurrences.
- For each body_part_close_up_trigger token position, find nearest
  C##/C##O## within ±20 tokens — `cid_trigger`.
- For each full_body_description_keyword position, find nearest character
  ID — `cid_body`.
- If `cid_trigger == cid_body` AND BOTH nearest-character-distances ≤ 20:
  increment violation count.

RO-5 binding: close-framing shot scope filter uses
`_SPATIAL_FRAMING_CLOSE_KEYWORDS` 9-entry production-aligned (NOT the
LLM-facing 11-entry list). `손이`/`눈이`/`얼굴이` are NOT in canary scope.

RO-11 binding: `_ID_BODY_PART_TRIGGERS` G4.3 carry — direct external
import via `_g4_5a_common.render_prompt_card_imports()`. NO alias.

PR4-B7 / RO-8: STRICT 0 — exit 1 on any violation, NOT soft warning.

Trap #1 silent-fallback ban / Trap #3 CP shape / Trap #4 pinned-tuple
post-loop validation / Trap #8 module-level constants.

Override O-17 — regex/substring detector is 1차 only. baseline 비0 시 P1
follow-up: LLM-validator (gpt-5.4-mini judge) 전환.
`feedback_no_regex_postprocessing.md` carry.

Usage (config mode):
    python scripts/canary/g4_5a_primary_framing.py \
        --config canary_config.json \
        --cp-root data/projects/<pid>/checkpoints/scene_detail \
        --prompt-version 20.<timestamp> \
        --role candidate \
        --output results/g4_5a_primary_framing_candidate.json
    )annotationsN)datetime)Path)	_load_scenes_or_fail_validate_scene_preflightbuild_pinning_scene_setemit_outputload_cp_manifestload_pinning_from_argsrender_prompt_card_importsresolve_cp_rootvalidate_pinning_SPATIAL_FRAMING_CLOSE_KEYWORDS)_ID_BODY_PART_TRIGGERS%compute_render_strategy_snapshot_hashz\bC\d{2}(?:O\d{2})?\b   )standsseatedleaningu   전신ztuple[str, ...]_FULL_BODY_KEYWORDSc                 L    t        j                         j                         dz   S )NZ)r   utcnow	isoformat     'scripts/canary/g4_5a_primary_framing.pyiso8601_nowr   `   s    ??&&(3..r   c                "    | j                         S )uL  Whitespace tokenizer for RO-8 algorithm.

    NOTE: simple whitespace split is intentional — punctuation and quotes
    are treated as part of the adjacent token. The ±20 token window is
    forgiving enough that punctuation-attachment does not change RO-8
    detection results materially. canary smoke fixture covers this.
    )split)texts    r   	_tokenizer"   d   s     ::<r   c                    i }t        |       D ]L  \  }}t        j                  |      }||j                         }||v r||   j	                  |       G|g||<   N |S )z@Build dict {character_id: [token_positions]} per RO-8 algorithm.)	enumerate	_ID_REGEXsearchgroupappend)tokenscid_positionspostokmcids         r   _build_cid_positionsr/   o   sk    *,Mf%SS!='')C m#c"))#.&)Uc" & r   c                    d}t         dz   }|j                         D ].  \  }}|D ]$  }t        || z
        }|t         k  s||k  s!|}|}& 0 ||fS )u   Return (cid, distance) for nearest cid within ±20 tokens.

    If no cid is within window, returns (None, sentinel). Caller checks
    distance ≤ _WINDOW_TOKENS.
    N   )_WINDOW_TOKENSitemsabs)
target_posr*   best_cid	best_distr.   	positionscpds           r   _nearest_cidr;      sg      H"I'--/YBBO$AN"q9}		  0 Yr   c                   t        |       }t        |      }|syd}t               }g }t        |      }t	        |      D ]z  }t
        D ]o  }t        |j                               }	||	z   |kD  r%dj                  ||||	z          j                         }
|
|j                         k(  s^|j                  |        z | g }t        |      D ]?  \  }}|j                         t        fdt        D              s/|j                  |       A |r|sy|D ]f  }t        ||      \  }}||t        kD  r|D ]C  }t        ||      \  }}||t        kD  r||k(  s%||f}||v r.|j                  |       |dz  }E h |S )u  RO-8 same-character-ID detection algorithm (PR4-B7 STRICT 0).

    Build per-shot {cid: [pos]} dict. For each body-part close-up trigger
    token position, find nearest cid (`cid_trigger`). For each full-body
    keyword position, find nearest cid (`cid_body`). If `cid_trigger ==
    cid_body` AND both distances ≤ 20: violation.

    Returns count of violations in this prompt.
    r    c              3  B   K   | ]  }|j                         v   y wNlower).0kw	tok_lowers     r   	<genexpr>z7_detect_same_character_id_violations.<locals>.<genexpr>   s     E1D2rxxzY&1Ds   r1   )r"   r/   setlenranger   r    joinrA   r(   r$   anyr   r;   r2   add)promptr)   r*   
violations
seen_pairstrigger_positionsnr+   triggertlenwindow_textfull_body_positionsr,   tposcid_triggerdist_triggerfposcid_body	dist_bodypair_keyrD   s                       @r   $_detect_same_character_id_violationsr\      s    vF(0MJ'*uJ $&FAQx-Gw}}'DTzA~((6#cDj#9:@@BKgmmo-!((- .  &(f%SIIK	E1DEE&&s+ &
 $7!$0}$E!\.('D".t]"CHi>)h& $<z)x(a
 ( "$ r   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        |       t        |      }t        |      }g }t        ||      }|t        |||      }t        ||       yt        |      }t               }	d}
d}i }g }t        d t        D              }|D ]  }t!        |t"              s|j%                  d       &d|vsd|vr|j%                  d       @|d   }|d   }||Ot'        |      }t'        |      }||f|vrl|	j)                  ||f       t+        ||||      }|d}d}t-        |      D ]  \  }}t!        |t"              s|j%                  d| d| d | d!       2d"|vr|j%                  d| d| d | d#       R|d"   }t!        |t              r|j/                         s|j%                  d| d| d | d$       |j1                         t3        fd%|D              }|sd}|t5        |      z  } |j%                  |||d&       |r|
dz  }
|dkD  s||z  }| d'| }||v r||xx   |z  cc<   |||<    |D ]$  \  }}||f|	vs|j%                  d(| d| d)       & t7               |j8                  |j:                  |||
||t=        t>              t=        t@              t=        t              d*|d| xr |dk(  d+d,id-
}t        ||       tC        d.|j:                   d/|j8                   d0|
 d1| d2tE        |       d3tF        jH                  4       |ry|j:                  dk(  r|dkD  ryy)5Nu   G4.5a canary: primary framing same-character-ID detection (close-framing shots only, RO-8 algorithm ±20 tokens, Rule J, STRICT 0).)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$--render-strategy-card-snapshot-hashz--outputr1   r   c              3  <   K   | ]  }|j                           y wr?   r@   )rB   rC   s     r   rE   zmain.<locals>.<genexpr>  s     P0O"288:0Os   zscene entry is not dictscene_index_shot_indexz%scene missing scene_index/_shot_indexFs_sh_vz: variation not a dict
t2i_promptz: t2i_prompt key missingz': t2i_prompt missing, empty, or not strc              3  &   K   | ]  }|v  
 y wr?   r   )rB   rC   prompt_lowers     r   rE   zmain.<locals>.<genexpr>>  s     H2B,.s   )rh   
shot_indexhas_close_framing_z
pinning (sz ): not present in CP data.scenestotal_shots_with_close_framingprimary_framing_violation_count"primary_framing_violation_per_shottrigger_phrases_usedfull_body_keywords_usedclose_framing_keywords_usedprimary_framingu   scripts/canary/g4_5a_primary_framing.py (close-framing shots only, RO-5 9-entry, RO-8 ±20 tokens, RO-11 G4.3 _ID_BODY_PART_TRIGGERS carry, Rule J, Override O-17 regex 1차 only)
	timestampprompt_versionrolepinning	scene_setmetricsmeasurement_failuresexit_criteria_thresholdexit_criteria_passmeasurement_scriptsz[g4_5a_primary_framing] role=z prompt_version=z"
  total_shots_with_close_framing=z"  primary_framing_violation_count=z
  measurement_failures: zQ
Exit criteria (STRICT 0): candidate.metrics.primary_framing_violation_count == 0)re   )%argparseArgumentParseradd_argumentstr
parse_argsr   r   r   r
   r   _empty_outputr	   r   rF   tupler   
isinstancedictr(   intrK   r   r$   striprA   rJ   r\   r   r}   r~   listr   r   printrG   sysstderr)parserargsr   cp_rootr9   r   scenesoutpinning_set
seen_in_cprt   ru   rv   scene_set_outclose_kw_lowerscenesishisi_intshi_int
variationsshot_has_close_framingshot_violation_countvivarrL   	has_closeshot_keyro   s                               @r   mainr      s~   $$!F 
d;
c48
#t<
*tD
[)	   ,3E
%C   )TB
$	   ,3E
.S$   
t<D$T*GWd#G	'	"B&(!"&:;F~D'+?@C)'2K'*uJ%&"&'#9;& "M P0OPPN%& ''(AB%e)C ''7 =!M":Rc(GK/().67$8

  "'  ,GBc4($++xs7)2bT1GH 3&$++xs7)2bT1IJ &Ffc*&,,.$++xs7)2bT 2( ) !<<>LHHHI%)" $H%  1 -8 	!!!7
 	
 "*a/*!#+/CC+ 7),H==28<(<
 ) 38<S \ !	S9J& ''RDC5(HI ! !]--		".L/ 3$()?$@'+,?'@+/0O+P
 !5#$$$ 5/14 1 
1 CD T	
'		{ 3--. /,,J+K L+*
+ ,##&';#<"= >A	B ZZ
 yyK$Ca$Gr   c                    t               | j                  | j                  |g ddi t        t              t        t
              t        t              d|ddddid
S )Nr   rs   Frz   zJscripts/canary/g4_5a_primary_framing.py (close-framing shots only, Rule J)r{   )r   r}   r~   r   r   r   r   )r   r   r   s      r   r   r     sj     !]--		.//024$()?$@'+,?'@+/0O+P
 !5#$#5 
# r   __main__)returnr   )r!   r   r   	list[str])r)   r   r   dict[str, list[int]])r5   r   r*   r   r   ztuple[str | None, int])rL   r   r   r   )r   r   )r   zargparse.Namespacer   r   r   r   r   r   ),__doc__
__future__r   r   rer   r   pathlibr   pathinsertr   __file__resolveparent_g4_5a_commonr   r   r   r	   r
   r   r   r   r   _CONSTSr   !app.core.steps.render_prompt_cardr   r   compiler%   r2   r   __annotations__r   r"   r/   r;   r\   r   r   __name__exitr   r   r   <module>r      s!  *V #  	 
   3tH~--/667 8
 
 
 %
& #**K"L 

 BJJ/0	 
( _ /"' (<~CL
  
	> zCHHTV r   