
    jY                       d Z ddlmZ ddlZddlmZmZmZmZm	Z	m
Z
mZmZmZ ddlmZmZ dZdZdZd	Zd
ZdZdZdZdZdZ ej6                  ddj9                  e      z   dz   dj9                  e      z   dz         ZdZ ej6                  dez   dz   dj9                  e      z   dz         Z ej6                  ddj9                  e      z   dz   ej@                        Z! ej6                  ddj9                  e      z   dz         Z"d#dZ#d$dZ$	 	 	 	 	 	 d%dZ%	 	 	 	 	 	 d&dZ&	 	 	 	 	 	 	 	 	 	 d'dZ'	 d(	 	 	 	 	 	 	 	 	 d)dZ(	 	 	 	 	 	 	 	 	 	 	 	 d*dZ)	 	 	 	 	 	 	 	 d+d Z*	 	 	 	 d,d!Z+g d"Z,y)-u  shot_director ↔ shot_staging visible/off-camera reconciliation helpers.

2 종류의 deterministic detector:

1. **Producer-side (description-only)** — `detect_gaze_pattern_exclusions`:
   shot_director 가 shot_staging 결과를 보지 못한 채 LLM emit 한
   visible_entity_ids 와 비교하기 위한 lexicon-based diagnostic
   candidate 추출. Area #3 v1: production decision 권한 없음 (audit
   field 만 emit, `shot_director.py:182-201` mutation 폐기).

2. **Consumer-side hybrid (Path 1, structured + OFFSCREEN_RE gate)** —
   `detect_offscreen_drift_structured`: shot_staging v13 의 character_angles
   [].gaze_direction_kind=="looks_at_character" + gaze_target_id 와
   camera_direction 의 OFFSCREEN_RE coarse gate 를 조합. blocking-eligible
   (`VisibleStagingDriftError` raise).

3. **Consumer-side proximity diagnostic (Path 2, NL fallback)** —
   `detect_offscreen_drift_proximity_diagnostic`: camera_direction NL
   proximity 윈도우 안 character name → drift candidate. Diagnostic only
   (caller-side `logger.warning`). Path 1 (structured) miss 또는
   character_angles 부재 시 보조.

Area #3 v1: 이전 merged drift helper 단일 함수 폐기 + Path 1/2 분리.
Path 2 production blocking 권한 제거. mode= param 금지 (silent coupling
회피).
    )annotationsN)	AnyDictIterableListMappingOptionalSequenceSetTuple) resolve_visible_character_targetvalidate_pairing)	u   올려다보u   내려다보u	   응시하u	   바라보u	   쳐다보u	   마주보u	   노려보u   뒤돌아보u	   돌아보)u   다u   는u   며u   면서u   고u   다가u   면)
u   얼굴   눈   눈빛u   표정u   상체u	   뒷모습   시선u	   옆얼굴u	   옆모습   정면).u   어깨u   팔u   팔뚝u	   팔꿈치u   손u   손목u	   손바닥u   손등u	   손가락u   다리u	   허벅지u   무릎u	   정강이u   발u   발목u	   발바닥u	   발가락u   등u   허리u	   엉덩이u	   옆구리u   가슴u   배u   골반u   머리u   머리카락u   이마u   턱u   뺨u   볼u   광대u   관자놀이u   목u	   목덜미u	   후두부u	   뒤통수u   입u   입술u   혀u   잇몸u   이빨u   귀u   코u   콧등u   피부u   살갗)u	   쪽으로u   쪽을u   향해u   향한u	   향하여u   쪽에u   방향으로)u   고개r   u   눈길r   r   )
u   클로즈업zClose-upzclose-upzClose upzclose upCUr   u   구도u   익스트림 클로즈업ECU)zoff[\-\s]?camerazoff[\-\s]?screen	offscreenzout[\-\s]?of[\-\s]?framezout[\-\s]?of[\-\s]?shotu   화면\s*밖u   프레임\s*밖u   카메라\s*밖<      z(?:|z)(?:)u&   [가-힣A-Za-z][가-힣A-Za-z\s]{0,20}z(?P<who>u   )\s*[의가]\s*(?:u	   의\s*(?:z)$c                    t        t        j                  d|             }|syt        |      D ]:  }|j	                  d      j                         }t        j                  |      r8|c S  y)u  gaze verb 직전 텍스트에서 마지막 [를을] 앞 명사구 추출.

    "굳은 표정으로 남자 직원을 " → "남자 직원" (마지막 [을] 앞).
    매칭 없으면 "".

    Body-part possession 차단: 매치된 명사구가 "X의 [BODY_PART]" 형태이면
    SUBJECT 의 부분 묘사이지 gaze TARGET 이 아니므로 skip 하고 그 이전
    매치로 fallback. 모두 body-part 면 "" 반환 (TARGET 미달).
    u3   ([가-힣A-Za-z][가-힣A-Za-z\s]{0,20})\s*[를을]    )listrefinditerreversedgroupstrip_BODY_PART_POSSESSION_REsearch)text_before_verbmatchesmspans       Z/Users/manta/Documents/Projects/TheRoad-I1/backend/app/modules/pipeline/shot_visibility.py_last_object_targetr*      sg     2;;> G gwwqz!#**40	 
     c                p    t         j                  |       }|sy|j                  d      j                         S )u   gaze verb 다음 텍스트에서 첫 Y[의] FRAMING 의 Y 추출.

    "눈망울이 붉게 젖은 수리영의 얼굴 클로즈업" → "수리영".
    r   who)KOREAN_FRAMING_REr$   r!   r"   )text_after_verbr'   s     r)   _first_framing_subjectr0      s1    
 	  1A775>!!r+   c                8    t         fdt        D              }|si t               fS t         fdt        D              }|si t               fS t        j                         }|si t               fS |j                  d      j                         }|r|j                         d   nd}t               }|r@|j                         D ]-  \  }}	|s	t        |      dk\  s||v s|j                  |	       / i }
t        D ]  }d}	  j                  ||      }|dk(  rt        d|dz
        } || }d	\  }}}|j                         D ]7  \  }}	|rt        |      dk  s|	|v r|j                  |      }||kD  s2||	|}}}9 |	||
vr||
|<   |t        |      z   } |
|fS )
u  gaze verb 가 명시 안 된 close-up 패턴 fallback.

    4-token 게이트 — 모두 매치되어야 발동:
      1. close-up marker (`클로즈업` / `정면` / `구도` / CU 등)
      2. gaze noun (`고개` / `시선` / `눈길` / `눈빛`)
      3. framing pattern (`SUBJECT의 [얼굴/눈/...]`)
      4. directional phrase (`TARGET 쪽으로/향해/...`) — TARGET 직전 30 chars
         이내 character name.

    예: "혜수 쪽으로 고개를 든 수리영의 얼굴 클로즈업"
        → close-up 마커 클로즈업, gaze noun 고개, SUBJECT 수리영, TARGET 혜수.

    Returns: (excluded_dict, subject_id_set). 게이트 미달 시 ({}, set()).
    c              3  &   K   | ]  }|v  
 y wN ).0r'   descriptions     r)   	<genexpr>z._detect_directional_pattern.<locals>.<genexpr>   s     C1BAqK'1B   c              3  &   K   | ]  }|v  
 y wr3   r4   )r5   gr6   s     r)   r7   z._detect_directional_pattern.<locals>.<genexpr>   s     >+Q[(+r8   r-   r      r   r   )r;   Nr   )any_CLOSE_UP_MARKERSset_GAZE_NOUNSr.   r$   r!   r"   splititemslenadd_DIRECTIONAL_TAILSfindmaxrfind)r6   
name_to_idhas_close_uphas_gaze_nounframing_matchwho_fullsubject_tokensubjectsnamesidexcludedd_phrasestartidxwindow_startwindowbest_idxbest_sid	best_nameposs   `                   r)   _detect_directional_patternr\      s   $ C1BCCL35y>+>>M35y%,,[9M35y
 ""5)//1H,4HNN$R("MH#))+ID#D	Q4=+@S! ,  "H&""8U3Cbyq#(+L c2F,8)Hh	'--/	cs4y1}xll4(>47d	hH 0 #(@%."#h-'E!  '( Xr+   c                   | r|si S i }t               }t        j                  |       D ]  }| d|j                          }| |j	                         d }t        |      }t        |      }|r|sD|j                         D ]-  \  }	}
|	s	t        |	      dk\  s|	|v s|j                  |
       / |j                         D ]%  \  }	}
|	rt        |	      dk  r|	|v s|
|vs!|	||
<   '  |s6t        | |      \  }}||z  }|j                         D ]  \  }
}|
|vs|||
<    |j                         D 
ci c]  \  }
}|
|vs|
| c}}
S c c}}
w )u  description 에서 close-up gaze 패턴이 가리키는 gaze TARGET (offscreen)
    을 추출.

    Two paths (둘 다 고신뢰 게이트):

    1. **gaze verb path** — `KOREAN_GAZE_VERB_RE` 매치 + `_KOREAN_FRAMING_NOUNS`
       매치 둘 다 있어야 함. body-part possession (`X의 어깨/손/팔/...`) 은
       `_last_object_target` 단계에서 skip 되어 SUBJECT 부분 묘사 false-trigger
       차단 (S12_Shot13).
    2. **directional path** — gaze verb 가 명시 안 된 close-up 케이스. close-up
       marker + gaze noun + framing + directional phrase 4-token 모두 매치
       의무. TARGET 은 directional phrase 직전 30 chars 윈도우 안 character
       name (S12_Shot4: "혜수 쪽으로 고개를 든 수리영의 얼굴 클로즈업").

    Args:
        description: shot 묘사 한국어 텍스트.
        name_to_id: {entity_name → short_id} (scene_ve 기준).

    Returns:
        {short_id → matched_name} — visible 에서 제외할 entity. 빈 dict =
        패턴 미매치 / 안전.
    Nr<   )r?   KOREAN_GAZE_VERB_REr   rT   endr*   r0   rB   rC   rD   r\   )r6   rI   rR   rO   gv_matchbeforeaftertarget_spansubject_spanrP   rQ   
d_excluded
d_subjectsnms                 r)   detect_gaze_pattern_exclusionsrh      sl   4 j	!HH (00=.hnn./HLLNO,)&1-e4, $))+ID#D	Q4<+?S! , $))+ID#3t9q={"s(': $	 , >, !<"

J 	J!'')GC(" " *
 $,>>#3K#3Rs(7JCG#3KKKs   1E>Ec                8   |si S t        t        j                  |            }|si S i }t        |       }i }| D ];  }|j	                  |      xs dj                         }	|	rt        |	      dk  r7|||	<   = t               }
|rq|D ]l  }t        |t              s|j	                  d      xs dj                         }|rt        |      dk  rH|j	                  |      }|s\|
j                  |       n |r|D ch c](  }|j                  d      r|dd j                         r|* }}|D ]h  }t        |t              s|d   }|dk7  r|j	                  d	      }t        ||d
       t        |||d
      }|P|\  }}||v sZ||vs_||
vsd|||<   j |S c c}w )u  Path 1 — hybrid gate (camera_direction OFFSCREEN_RE + structured gaze_target_id).

    Gate 1: camera_direction MUST contain explicit offscreen phrase
            (OFFSCREEN_RE match). Frame-internal mutual gaze 정상 (drift 아님).
    Gate 2: character_angles[].gaze_direction_kind == "looks_at_character"
            with valid gaze_target_id resolved against visible_set
            (Q7 dispatch helper, structural validation only — name matching X).

    In-frame guarantee: character_angles[].character == visible canonical_name 인
    sid 는 drift 후보에서 영구 제외.

    Both gates required. Returns blocking-eligible drift entities.
    r   r<   	characterCr   Ngaze_direction_kindlooks_at_charactergaze_target_idzshot_visibility.path1)where)r   OFFSCREEN_REr   r?   getr"   rC   
isinstancedictrD   
startswithisdigitr   r   )visible_idscamera_directioncharacter_angles
id_to_nameoff_matchesdriftvisible_setname_to_visible_sidrQ   rg   in_frame_sidscach_sidvisible_character_idskind	target_idresolvedrP   s                      r)   !detect_offscreen_drift_structuredr   <  s   & 	|,,-=>?K	Ek"K +-nnS!'R..0SWq["%B	  "eM"Bb$'&&%+224BR1%))"-C!!#& # (!
(Ts#QR(8(8(: [ 	 !
 #Bb$'+,D++/0IT94KL7%-	H  IC{"u$},!c
- #0 L9!
s   9-Fc                \   |si S t        t        j                  |            }|si S i }t        |       }i }| D ];  }|j	                  |      xs dj                         }	|	rt        |	      dk  r7|||	<   = t               }
|rq|D ]l  }t        |t              s|j	                  d      xs dj                         }|rt        |      dk  rH|j	                  |      }|s\|
j                  |       n |D ]3  }t        d|j                         t        z
        }|||j                          }d}d}d}| D ]\  }||v s||
v r|j	                  |      xs dj                         }|rt        |      dk  r@|j                  |      }||kD  sW|}|}|}^ ||||<   t        t        |      |j                         t         z         }||j                         | }| D ]K  }||v s||
v r|j	                  |      xs dj                         }|rt        |      dk  r@||v sE|||<    3 6 |S )u   Path 2 — camera_direction OFFSCREEN_RE + proximity NL fallback.

    Diagnostic only (caller MUST log/warning, MUST NOT raise).
    character_angles (optional) provides in_frame_guarantee against
    false-positive.
    r   r<   rj   r   r;   N)r   rp   r   r?   rq   r"   rC   rr   rs   rD   rG   rT   _PROXIMITY_PRErH   minr_   _PROXIMITY_POST)rv   rw   ry   rx   rz   r{   r|   r}   rQ   rg   r~   r   r   omwspre_textrX   rY   rZ   rP   rU   we	post_texts                          r)   +detect_offscreen_drift_proximity_diagnosticr     sG    	|,,-=>?K	Ek"K +-nnS!'R..0SWq["%B	  "eM"Bb$'&&%+224BR1%))"-C!!#& # BHHJ/0#Brxxz2	Ce|sm3NN3'-2446D3t9q=..&CX~ 	  'E(O%&?(BC$RVVXb1	Ce|sm3NN3'-2446D3t9q=y !c
 / @ Lr+   c                   i }|r|s|S t         j                  |      s|S t               }|D ]J  }t        |t              s|j                  d      xs dj                         }|s:|j                  |       L |xs dj                         }	| D ]k  }
t        |
t              r|
j                  d      s%|j                  |
      xs dj                         }|sK|
|	k(  s||	k(  rV||v r[||v rdnd}d| d||
<   m |S )u  C10 Phase 1 — visible 후보 인물 중 후행 staging 이 화면 밖/부재로
    연출한 인물을 결정론적으로 식별.

    Hard gate (전부 필요):
      1. C## 가 visible character 후보.
      2. character_angles 가 non-empty (producer 가 필드를 채웠음).
      3. 그 인물의 canonical name 이 character_angles[].character 에 없음.
      4. 그 인물이 POV character 가 아님.
      5. camera_direction 에 OFFSCREEN_RE 매치 존재.

    `gaze_direction_kind == "off_screen"` 은 의도적으로 사용하지 않음 — schema
    상 시선 방향이지 인물 가시성 결정이 아니다 (in-frame 인물도 off_screen
    gaze 가능).

    Returns:
        {subject_id: reason}. reason 에 confidence 태그("named" = canonical
        name 이 camera_direction 에 등장 / "structural" = 구조 신호만) 를
        provenance 로 포함. 다운그레이드 동작에는 영향 없음.
    rj   r   rk   named
structuralzscreen_presence_reconciliation: visible candidate absent from character_angles, non-POV, camera_direction off-screen reference (confidence=r   )
rp   r$   r?   rr   rs   rq   r"   rD   strrt   )rv   rw   rx   ry   pov_characterresultangle_namesr   r   povrQ   rP   
confidences                r)   $detect_offscreen_referenced_subjectsr     s   4  F#3/0EKb$&&%+224B#	  B
%
%
'C3$)<s#)r002#:; $(8 8Wl
%,a) 	s " Mr+   c                   |xs dj                         }g }| xs g D ]h  }t        |t              s|j                  d      xs dj                         }|r||k(  r?|j                  |      }|sS||vsX|j	                  |       j |S )u  촬영 계획(shot_staging)이 **화면 안에 세운** 등록 인물의 entity id —
    `detect_offscreen_referenced_subjects` 의 반대 방향.

    실측(컨트리로드 2판, 2026-09-19): shot_director VE 에는 없는데
    character_angles 에는 있는 등록 인물이 선택 샷 239개 중 35쌍이었다.
    조립 문안(카메라 지시)은 그 인물을 화면에 세우는데 참조가 안 실려,
    로봇 찰리가 **사람 뒷모습**으로 그려졌다(S60sh4·S64sh7).

    - 이름 → id 는 정확한 이름 조인이다(구조 필드 — 뜻 판단 아님). 호출자는
      같은 이름이 둘 이상인 인물을 `id_by_name` 에서 **빼고** 넘긴다.
    - POV 인물(카메라가 그 사람의 눈)은 화면 밖이라 뺀다.
    - 정본에 없는 이름(단역·군중)은 건너뛴다.

    반환 순서는 character_angles 순서, 중복 없음.
    r   rj   )r"   rr   rs   rq   append)rx   
id_by_namer   r   outr   rP   eids           r)   staged_in_frame_character_idsr     s    ( B
%
%
'CC$"$"d#{#)r002ts{nnT"3c>JJsO % Jr+   c           
     6   i }| D ]H  \  }}|xs dj                         }|s|s |j                  |t                     j                  |       J |j	                         D ci c])  \  }}t        |      dk(  s|t        t        |            + c}}S c c}}w )u   (entity id, 이름) → {이름: id}. **같은 이름이 둘 이상이면 뺀다** —
    어느 쪽인지 모르는 이름을 한쪽에 붙이면 남의 참조가 실린다.r   r   )r"   
setdefaultr?   rD   rB   rC   nextiter)
charactersseenr   rP   rg   idss         r)   unique_character_ids_by_namer   ;  s    
 !#D	Tjb!2OOB&**3/   04zz|M|GBs3x1}BT#Y|MMMs   #B:B)	rp   r   r   r   r^   r.   rh   r   r   )r%   r   returnr   )r/   r   r   r   )r6   r   rI   Mapping[str, str]r   zTuple[Dict[str, str], set])r6   r   rI   r   r   Dict[str, str])
rv   Sequence[str]rw   r   rx   Sequence[Dict[str, Any]]ry   r   r   r   r3   )
rv   r   rw   r   ry   r   rx   z"Optional[Sequence[Dict[str, Any]]]r   r   )rv   r   rw   r   rx   r   ry   r   r   Optional[str]r   r   )rx   r   r   r   r   r   r   z	List[str])r   zIterable[Tuple[str, str]]r   r   )-__doc__
__future__r   r   typingr   r   r   r   r   r	   r
   r   r   app.core.gaze_directionr   r   _KOREAN_GAZE_STEMS_KOREAN_GAZE_TAILS_KOREAN_FRAMING_NOUNS_BODY_PART_NOUNSrE   r@   r>   _OFFSCREEN_PHRASESr   r   compilejoinr^   _NAME_FRAGMENTr.   
IGNORECASErp   r#   r*   r0   r\   rh   r   r   r   r   r   __all__r4   r+   r)   <module>r      s  4 # 	 U U U  M   " h  > 	   !bjj
SXX()) - XX()*,01  ;BJJ.  $ XX+,-/34 
 rzz
SXX())D0MM &2::388,--5 
0"==!=  =@BLBL!BL BLTOOO /O "	O
 Ol <@	KKK "K 9	K
 K\::: /: "	:
 !: :z.! ! 	D
N)
N
N
r+   