
    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e!d<   dZ"de!d<   dZ#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jZ                   e*              yy) ui  G4.5a canary — view-mixing extension (G4.4 carry + Rule J extension, Task 4.4).

Detect:
1. **G4.4 view-mixing carry** — same-character-ID full-body verb (`stands`/
   `seated`/`leaning`/`walking`/`running`/`kneeling`/`lying`) co-occurrence
   with body-part close-up trigger (`focus on`/`close on`/`tight on`/
   `detail on`) within ±20 token nearest-character distance. baseline
   regression 0 verification — G4.4 v19 baseline must remain at 0.
2. **G4.5a Rule J extension** — close-framing shot scope, **2 distinct
   character IDs** both having face/head close-up keyword (`face` / `눈` /
   `얼굴`) within their respective ±20 token windows. close-framing scope
   uses `_SPATIAL_FRAMING_CLOSE_KEYWORDS` 9-entry production-aligned (RO-5).

Total `view_mixing_extension_count = g4_4_view_mixing_count + two_face_close_up_count`.

Scope: ALL pinned shots — both detectors are intent-agnostic (G4.4 carry
+ G4.5a extension).

PR4-B7 binding: STRICT 0 — exit 1 on any violation. baseline regression
gate enforces G4.4 v19 == 0 condition (carry verification).

Trap #1 silent-fallback ban / Trap #3 CP shape / Trap #4 pinned-tuple
post-loop / 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) 전환.

Usage (config mode):
    python scripts/canary/g4_5a_view_mixing_extension.py \
        --config canary_config.json \
        --cp-root data/projects/<pid>/checkpoints/scene_detail \
        --prompt-version 20.<timestamp> \
        --role candidate \
        --output results/g4_5a_view_mixing_ext_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leaningwalkingrunningkneelinglyingztuple[str, ...]_VIEW_MIXING_FULL_BODY_VERBS)faceu   눈u   얼굴_FACE_CLOSE_UP_KEYWORDS   c                 L    t        j                         j                         dz   S )NZ)r   utcnow	isoformat     -scripts/canary/g4_5a_view_mixing_extension.pyiso8601_nowr$   `   s    ??&&(3..r"   c                "    | j                         S )z)Whitespace tokenizer (G4.4 carry parity).)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 {cid: [token_pos]}.)	enumerate	_ID_REGEXsearchgroupappend)tokenscid_positionspostokmcids         r#   _build_cid_positionsr5   i   si    *,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.N   )_WINDOW_TOKENSitemsabs)
target_posr0   best_cid	best_distr4   	positionscpds           r#   _nearest_cidrA   w   sg    
  H"I'--/YBBO$AN"q9}		  0 Yr"   c                   t        |       }t        |      }|syt        |      }g }t        |      D ]?  \  }}|j	                         t        fdt        D              s/|j                  |       A g }t        |      D ]z  }t        D ]o  }t        |j                               }	||	z   |kD  r%dj                  ||||	z          j	                         }
|
|j	                         k(  s^|j                  |        z | |r|syd}t               }|D ]d  }t        ||      \  }}|	|t        kD  r|D ]B  }t        ||      \  }}|	|t        kD  r||k(  s$||f}||v r-|j                  |       |dz  }D f |S )u   G4.4 carry — same-character-ID full-body verb + body-part trigger
    co-occurrence within ±20 token nearest-character distance.

    Returns count of view-mixing pairs in this prompt.
    r   c              3  B   K   | ]  }|j                         v   y wNlower).0v	tok_lowers     r#   	<genexpr>z+_detect_g4_4_view_mixing.<locals>.<genexpr>   s     L/K!qwwyI%/K    r7   )r(   r5   lenr*   rF   anyr   r.   ranger   r&   joinsetrA   r8   add)promptr/   r0   nfb_positionsr1   r2   trig_positionstriggertlenwindow_text
violations
seen_pairstposcid_triggerdist_triggerfposcid_body	dist_bodypair_keyrI   s                       @r#   _detect_g4_4_view_mixingrc      s    vF(0MFA Lf%SIIK	L/KLL$ & !#NQx-Gw}}'DTzA~((6#cDj#9:@@BKgmmo-%%c* .  ~J'*uJ$0}$E!\,"? D".t]"CHi9~#=h& $<z)x(a
 !	  r"   c                  
 | j                         
t        
fdt        D              syt        |       }t	        |      }t        |      dk  ryg }t        |      D ]?  \  }}|j                         t        fdt        D              s/|j                  |       A t        |      dk  ryt               }|D ]/  }t        ||      \  }}	||	t        k  s|j                  |       1 t        |      dk\  rdS dS )u0  G4.5a Rule J extension — close-framing scope, 2 distinct cids each with
    face/head close-up keyword within ±20 token nearest-character distance.

    Returns 1 if 2+ distinct character IDs each have a face/head close-up
    keyword within their respective ±20 windows; else 0. Per-shot scope.
    c              3  B   K   | ]  }|j                         v   y wrD   rE   )rG   kwprompt_lowers     r#   rJ   z,_detect_two_face_close_up.<locals>.<genexpr>   s!      -Lr
l"-LrK   r      c              3  B   K   | ]  }|j                         v   y wrD   rE   )rG   rf   rI   s     r#   rJ   z,_detect_two_face_close_up.<locals>.<genexpr>   s     I1H2rxxzY&1HrK   r7   )rF   rN   r   r(   r5   rM   r*   r   r.   rQ   rA   r8   rR   )rS   r/   r0   face_positionsr1   r2   cids_with_facer_   r4   distrg   rI   s             @@r#   _detect_two_face_close_uprm      s     <<>L -L  vF(0M
=A !#Nf%SIIK	I1HII!!#& &
 >Q  #uN }5	T?t~5s# 
 N#q(1/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}d}i }g }|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'        ||||      }||j!                  ||d       |
dz  }
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#       |t-        |      z  }|t/        |      z  } ||z  }||z  }|dkD  s|dkD  s||d$|| d%| <    |D ]$  \  }}||f|	vs|j!                  d&| d| d'       & ||z   }t1               |j2                  |j4                  |||
||||t7        t8              t7        t:              t7        t<              d(|d| xr |dk(  d)d*id+
}t        ||       t?        d,|j4                   d-|j2                   d.|
 d/| d0| d1| d2tA        |       d3tB        jD                  4       |ry|j4                  dk(  r|dkD  ryy)5Nur   G4.5a canary: view-mixing extension (G4.4 carry baseline 회귀 0 + Rule J extension, all pinned shots, STRICT 0).)descriptionz--config)typedefaultz--pidz	--cp-rootz--prompt-versionT)rp   requiredz--role	candidatebaseline)rp   rq   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--outputr7   r   zscene entry is not dictscene_index_shot_indexz%scene missing scene_index/_shot_index)rx   
shot_indexs_sh_vz: variation not a dict
t2i_promptz: t2i_prompt key missingz': t2i_prompt missing, empty, or not str)g4_4_view_mixingtwo_face_close_up_z
pinning (sz ): not present in CP data.scenestotal_shotsg4_4_view_mixing_counttwo_face_close_up_countview_mixing_extension_countview_mixing_per_shottrigger_phrases_usedfull_body_verbs_usedface_close_up_keywords_usedview_mixing_extensionu   scripts/canary/g4_5a_view_mixing_extension.py (all pinned shots, G4.4 carry baseline 회귀 0 + Rule J extension ±20 tokens, 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_view_mixing_extension] role=z prompt_version=z
  total_shots=z
  g4_4_view_mixing_count=z  two_face_close_up_count=z
  view_mixing_extension_count=z
  measurement_failures: zM
Exit criteria (STRICT 0): candidate.metrics.view_mixing_extension_count == 0)rv   )#argparseArgumentParseradd_argumentstr
parse_argsr   r   r   r
   r   _empty_outputr	   r   rQ   
isinstancedictr.   intrR   r   r*   striprc   rm   r$   r   r   listr   r   r   printrM   sysstderr)parserargsr   cp_rootr?   r   scenesoutpinning_set
seen_in_cpr   r   r   r   scene_set_outscenesishisi_intshi_int
variations	shot_g4_4shot_two_facevivarrS   r   s                              r#   mainr      sb   $$A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K68 "M%& ''(AB%e)C ''7 =!M":Rc(GK/().67$8

 !!
 	 	q	 ,GBc4($++xs7)2bT1GH 3&$++xs7)2bT1IJ &Ffc*&,,.$++xs7)2bT 2( ) 1&99I6v>>M' -* 	)+=0q=MA-$-%2; F81WI!67s ~ !	S9J& ''RDC5(HI ! 	!88  
 !]--		"&&<'>+F$8$()?$@$()E$F+/0G+H	
 !5#$$$ 1+q0 $H 
-C> T	
-dii[ 9--. /$ &$$:#; <##:"; <))D(E F##&';#<"= >=	> ZZ yyK$?!$Cr"   c                    t               | j                  | j                  |g ddddi t        t              t        t
              t        t              d|ddddid
S )Nr   r   Fr   z_scripts/canary/g4_5a_view_mixing_extension.py (all pinned shots, G4.4 carry + Rule J extension)r   )r$   r   r   r   r   r   r   )r   r   r   s      r#   r   r     sq     !]--		&''(+,$&$()?$@$()E$F+/0G+H	
 !5#$##D 
' r"   __main__)returnr   )r'   r   r   	list[str])r/   r   r   dict[str, list[int]])r;   r   r0   r   r   ztuple[str | None, int])rS   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+   r   __annotations__r   r8   r$   r(   r5   rA   rc   rm   r   r   __name__exitr!   r"   r#   <module>r      s5  "F #  	 
   3tH~--/667 8
 
 
 %
&")*K"L 

 BJJ/0	1 o ,   /
'  2j%0Psl
  
	B zCHHTV r"   