
    f7j             	       J   d Z ddlZddlZddlZddlZddlmZmZ ddlm	Z	 ddl
mZmZmZmZ ddlmZ ddlmZ ddlmZ dd	lmZ dd
lmZ ddlmZ  ej8                  e      Zdeeef   dee   fdZ deeeeef         dee   fdZ!deeeeef         de"eeeef      eeeef      f   fdZ#deeeeef         de"eeeeef      f   fdZ$deeeeef         de"eeeeef      f   fdZ%ddlm&Z' d Z(dZ)dKdedededefdZ*dedefdZ+dededefdZ,de-fdZ.deeef   fd Z/d!eeef   deeef   fd"Z0 G d# d$      Z1d%edefd&Z2dLd%ed'efd(Z3d)ee   d%edee   fd*Z4 G d+ d,e1e      Z5 G d- d.e1e      Z6 G d/ d0e1e      Z7 G d1 d2e1e      Z8d)ee   d3eeef   dee   fd4Z9 G d5 d6e1e      Z: G d7 d8e1e      Z; G d9 d:e1e      Z< G d; d<e1e      Z= G d= d>e1e      Z> G d? d@e1e      Z?dAee   dBee   dCe@de@fdDZAdEZBdFZC G dG dHe1e      ZD G dI dJe1e      ZEy)MuU   요소 Phase StepRunner — entity_extract, entity_review, entity_detail, entity_t2i.    N)ThreadPoolExecutoras_completed)Path)AnyDictListOptional)assemble_user_prompt)
StepRunnergrounding_binding)call_structured)load_prompt)episode_carrysrcreturnc                     | j                  d      }t        |t              rt        |      ng }t        | j                  d            D ]  }|s||vs|j	                  |        |S )uI   `visual_traits` + 채택한 크기 견줌 — 중복은 넣지 않는다.visual_traitsbuild_scale)get
isinstancelistscale_trait_sentencesappend)r   baseoutlines       Q/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/entity_steps.py_traits_with_scaler      sX    77?#D"4.$t*BC%cggm&<=DOJJt > J    r   c           	          t        |       \  }}|D cg c])  }|j                  dd       d|j                  dd       + c}S c c}w )u8  채택한 견줌을 **결정적 문장**으로 — `visual_traits` 에 실을 형태.

    ★왜 `visual_traits` 인가 (2026-09-20 Codex ㉢): 샷 문안은
     `stable_traits` 를 읽고, 그것은 `visual_traits` 에서 온다
     (`checkpoint_sync/entity_sync_service`). T2I context 에만 넣으면
     **참조 그림 한 장까지만** 닿고 샷마다 크기가 유지되지 않는다.
     새 DB 칼럼을 만들지 않고 **기존 배선**을 쓰는 최소안이다.

    ★의미를 더하지 않는다 — 모델이 낸 `compared_to`·`relation` 을 정해진
     꼴로 잇기만 한다. `metadata_json` 은 쓸 수 없다(`entity_metadata` 가
     character 를 닫힌 모양으로 접어 버린다).
    ★원자료(`build_scale`)는 따로 보존한다 — 근거와 렌더 문장은 다르다.
    compared_to u   과(와) 견주어 relation)_scale_rowsr   )r   woven_ds       r   r   r      s]      ;'HE1 A 55#
$$7j"8M7NO  s   .Ac                 (   g }g }| xs g D ]  }t        |t              st        |j                  d      xs dj	                               xr, t        |j                  d      xs dj	                               }|r|n|j                  |        ||fS )u	  근거 있는 견줌만 고른다 — `distinctive` 와 **같은 구조 게이트**.

    ★`source_quote` 가 비어 있지 않은지만 본다. 「원문에 그 말이 있는지」를
     대조한 것이 아니다 — 그 표현으로 넓히지 않는다.
    source_quoter#   r$   )r   dictboolr   stripr   )r   r&   excludedr(   oks        r   r%   r%   6   s     #%E%'HR!T"AEE.)/R6689 :j)/R6689 	(**1-   (?r    c                 h    t        |       \  }}|sd|fS dj                  d |D              }d|z   |fS )u}  크기(키·덩치) **견줌** 블록 — 2026-09-20 사용자 지시.

    > "대략적으로 비교될만한 형태로 묘사하는 정도로… 표준 크기 및 덩치
    >  등으로 비교하기 쉽게… 또한 프롬프트에서 비교적인 부분은 필요할듯해"

    ## 왜 있나

    정본에 **인물 크기가 아예 없었다.** 그래서 키가 성인 남자를 넘지 않는
    로봇이 화면에서 3m 병기로 그려졌고, 사람이 정본을 손으로 고쳐야 했다.

    ★막고 있던 것은 추출 팩의 「**바디 묘사 절대 금지**」였다. 그 금지는
     몸매·체형 묘사를 막으려던 것인데 크기까지 같이 지웠다. 새 팩(v19)은
     `build_scale` 을 **구조 칸**으로 받고 그 금지와 다른 축임을 명시한다.

    ## 게이트 — `distinctive` 와 **같은 구조 신호**

    `source_quote` 가 비어 있지 않은 항목만 채택한다(본문·기획서 직접
    근거). 글자로 뜻을 재지 않는다 — 이 저장소 금지 규칙.

    ★**수치가 아니라 견줌**을 싣는다. T2I 는 센티미터를 안 받는다(실측)
     — 「보통 성인보다 머리 하나 작다」는 그릴 수 있다.

    Returns:
        (block, excluded) — `block` 은 t2i context 에 붙일 문자열(채택
        항목이 없으면 빈 문자열), `excluded` 는 근거 없이 들어온 항목.
    r#   
c           
   3      K   | ]@  }d |j                  dd       d|j                  dd       d|j                  dd       d B yw)	- r"   r#   u    과(와) 견주어: r$       (원문 인용: r*   )Nr   .0r(   s     r   	<genexpr>z*build_build_scale_block.<locals>.<genexpr>j   sW       A QUU="%&&;AEE*b<Q;R
AEE."56a	9s   AAu   

[크기 — 견줌으로]
아래는 이 요소의 크기를 다른 것과 견준 것이다. 수치가 아니라 견줌이므로 **그 견줌이 한눈에 보이게** 그린다.
)r%   join)r   r&   r.   liness       r   build_build_scale_blockr<   I   sZ    : "+.OE88|II   E	MOT	U 	 r    distinctive_visual_traitsc                 >   | xs g }g }g }|D ]j  }t        |t              st        |j                  d      xs dj	                               }|j                  d      dk(  }|r|r|n|j                  |       l |sd|fS dj                  d |D              }d| |fS )u  ② P0a generic fix (2026-06-18) — evidence-gated distinctive 외형 블록.

    entity_detail 의 ``distinctive_visual_traits`` 중 **렌더용으로 채택할 항목만**
    골라 entity_t2i context 로 넘길 별도 블록 문자열을 만든다. 채택 조건은 두 개의
    **구조적 신호**다 (trait 텍스트를 글자/regex/substring 으로 의미 판정하지 않음
    — feedback-no-literal-substring-meaning. literal/metaphor 판단은 LLM 이
    ``interpretation_kind`` 로 내려준 구조화 결과를 코드가 그대로 신뢰한다):

      1. ``source_quote`` 가 비어 있지 않다 (본문 직접 근거 존재).
      2. ``interpretation_kind == "literal_visual"`` (신체 부위의 색/형태/표식을
         평이하게 사실로 진술한 경우. metaphorical_impression / ambiguous 는 제외).

    이로써 (a) 본문 근거 없는 invention(빈 quote), (b) 근거는 있으나 시적·인상적
    비유(예: '금빛 물결이 일렁이는 눈동자')는 둘 다 t2i_prompt(ref/scene SOT)로
    승격되지 않는다. 반대로 평이한 색 진술(예: '붉은 눈동자')은 보존된다.

    Returns:
        (block, excluded): ``block`` 은 t2i context 에 append 할 문자열(채택 항목이
        없으면 빈 문자열), ``excluded`` 는 채택되지 않은 distinctive 항목 dict 목록
        (각 trait/interpretation_kind/interpretation_reason 포함 — '근거 있는데 왜
        빠졌나' 추적용 diagnostic).
    r*   r#   interpretation_kindliteral_visualr1   c              3      K   | ]]  }d |j                  dd       d|j                  dd       |j                  d      xs dj                         rd|d    ndz   dz    _ yw)	r3   traitr#   r4   r*   attribution_noteu   ; 연결근거: r5   N)r   r-   r7   s     r   r9   z0build_distinctive_trait_block.<locals>.<genexpr>   s      	 A QUU7B  1!%%2K1LM ()/R668 q!3456		
 	 s   A#A%uO   

[원문 인용으로 확인된 문자적 특이 외형 — 반드시 반영]
)r   r+   r,   r   r-   r   r:   )r=   dvtsr&   r.   r(   	has_quote
is_literalr;   s           r   build_distinctive_trait_blockrG   v   s    2 %*D"$E%'H!T"!%%/52<<>?	UU015EE
(BB1E  8|II 	 	 	E ``e_fgiqqqr    grounding_entity_sync_extc                      ddl m}  | S )uU   갈래 표 — **계약 한 곳**에서 온다. ★여기서 다시 적지 않는다.r   ENTITY_KEY_TO_OWNER)$app.modules.pipeline.grounding_carryrL   rK   s    r   _ENTITY_KEYSrN      s    Hr    z2.202609030640nameetypesidc                 4    |rt        |      S t        | |      S )uQ   인스턴스 열쇠 — short_id 가 있으면 그것, 없으면(legacy) `_qkey`.)str_qkey)rO   rP   rQ   s      r   _ikeyrU      s    3s82dE 22r    r   c                 x    dd l }|j                  |  dt         j                  d            j	                         S )Nr   |utf-8)hashlibsha256)ENTITY_INSTANCE_IDENTITY_CONTRACT_VERSIONencode	hexdigest)r   rY   s     r   _identity_hashr^      s6    >>TF!$M#NOVVW^_`jjllr    c                     | d|  S )uP  이 스텝의 **열쇠**. ★이름 하나로는 갈래가 갈리지 않는다.

    같은 원문 낱말이 `location` 과 `location_part` 로 **둘 다** 잡힐 수 있다 —
    「이발소」와 「이발소」(그 안의 고정 설비). 이름만으로 열쇠를 쓰면 하나가
    다른 하나를 덮거나 건너뛴다.
      )rO   rP   s     r   rT   rT      s     WF4&!!r    c                      ddl m}   |        S )u  산출 모양을 **검증하는** 갈래. ★검증기가 아는 것에서 파생한다.

    ★★`location_part` 가 빠져 있었다 — 그 갈래만 검증 없이 sync 까지 갔다
    (Codex 재현 2026-09-01). 손으로 적으면 갈래가 늘 때 또 빠진다.
    r   validated_entity_types)app.core.entity_metadatard   rc   s    r   _VALIDATED_TYPESrf      s     @!##r    c           
         ddl m}m} ddlm}  | |t        | dd                  }	 | j                  d      xs i }|j                  |j                        }|r>||j                  k7  r/ddlm	}  |dd|j                   d|d|j                  dd      |r|j                  |iS i S # t        $ r$}|rddlm	}  |dd	| d
d      |i cY d}~S d}~ww xY w)u6  앞 CP 의 **새 모양 표식**을 그대로 나른다. ★없으면 안 만든다.

    ★sync(`entity_sync_service`)가 읽는 것은 `entity_t2i` CP 다. 표식이
    거기 없으면 `owner_keys` 가 옛 셋만 내고 **`location_part` 행이 조용히
    버려진다** (Codex 재현 2026-09-01).
    r   resolve_grounding_modeuses_chunk_producerrH   project_configNentity_mergeAppErrorzentity_t2i.marker_unreadableu)   C(c) 판인데 앞 CP 를 못 읽었다 (r5     codemessagestatus_codezentity_t2i.marker_missingu   C(c) 판인데 앞 CP 의 `u   ` 가 u    다 — 아는 판은 uS    뿐이다. 이것을 legacy 로 접으면 location_part 가 조용히 사라진다)app.core.grounding_moderi   rj   app.modules.pipelinerI   getattr_load_prev_checkpoint	Exceptionapp.core.errorsrn   r   CHUNK_SCHEMA_MARKERCHUNK_SCHEMA_VERSION)	runnerri   rj   _xchunkprevexcrn   gots	            r   _carry_chunk_markerr      s   >D 6($/!1 2E	++N;Ar ((2))
*C/// 	-,223I3I2J&w5//2 3JJ  	 -0B""C(7R7+  03CC5J! '** 	s   B+ +	C4CCCdonec                     h d}i }t               D ]C  \  }}| j                         D cg c]  }|j                  d      |k(  s| }}|s||v s?|||<   E |S c c}w )uy  `entity_type` 별로 나눈다. ★**있는 갈래만** 칸을 만든다.

    ★★빈 칸을 늘 만들면 legacy 체크포인트의 **모양이 바뀐다**. 지금 갈래가
    셋뿐인 주행은 앞과 **한 글자도 안 달라야** 한다 (Codex D-inert · 09-01).
    ★기존 세 칸은 **비어도 만든다** — 옛 소비자가 그 칸을 그냥 읽는다.
    >   props	locations
charactersentity_type)rN   valuesr   )r   alwaysr   keyownervr   s          r   _by_laner      sg     2FC"n
U++-I-Q155+?5+Hq-I#-CH % J Js   AAc                   P    e Zd ZdZdefdZd ZdefdZdefdZdede	e
   fdZy	)
_EntityStepMixinuF   요소 단계 공통 — fulltext 로드, 이전 단계 결과 로드.r   c                 L   ddl m} ddlm} | j                  j                  |      j                   ||j                              j                  |j                  | j                  k(        j                         }|r|j                  sddlm}  |ddd      |j                  S )	Nr   )Episode)undeferrm   zstep.no_fulltextu'   시나리오 텍스트가 없습니다.ro   rp   )app.models.projectr   sqlalchemy.ormr   dbqueryoptionsfulltextfilterid
episode_idfirstry   rn   )selfr   r   eprn   s        r   _load_fulltextz_EntityStepMixin._load_fulltext  s    .* GGMM'"WWW--./VGJJ$//12UW	 	 0'A 
 {{r    c                     ddl m}m}  | || j                              sy| j	                  d      }|xs i j                  d      xs i j                  d      xs g }|xs dS )u  A0 가 건진 후보. ★없으면 ``None`` — legacy 가 한 글자도 안 달라진다.

        ★``shadow_plan`` 에서도 ``None`` 이다. A0 는 돌지만 **나가는 프롬프트에
        안 실린다** — 관측이 하류를 바꾸면 shadow 가 아니다(계약 §12).
        후보를 프롬프트에 넣으면 추출 산출이 달라지고, 그러면 「shadow 를 켠
        것만으로 하류가 stale 되지 않는다」가 거짓이 된다.
        r   buys_v2_researchri   Ngrounding_a0data
candidates)rt   r   ri   rk   rw   r   )r   r   ri   cpcandss        r   _a0_candidatesz_EntityStepMixin._a0_candidates&  s`     	U 6t7J7J KL''7('-222<@FB}r    c                 V   ddl m}  || j                        }i }| j                         r&ddlm} |j                   |j                                ddddj                  t        | dd	            }|r6t        j                  | j                  | j                  || j                        nd	}|r(|j                  t        j                                ||d
<   |s|S ddl}ddl} |j"                  d|i|dd      }	|j%                  |	j'                  d            j)                         dd S )u  ★★결속 계약·팩 bytes 를 지문에 접는다 (Codex BLOCK-2).

        안 접으면 이미 완료된 `entity_all`/`entity_extract`/`entity_merge`
        체크포인트가 그대로 SKIP 되어 **새 ID 칸이 영영 안 생긴다** — 배포해도
        고친 것이 아무 데도 안 닿는 부류다.

        ★후보가 없는 판(legacy 포함)은 **옛 값 그대로** 돌려준다. 그래야
        legacy 체크포인트가 안 깨진다.

        ★저장과 비교가 **이 함수 하나**를 봐야 한다. `_execute` 가 이 값을
        `data["config_hash"]` 로 안 돌려주면, 저장은 project_config 만 보고
        비교는 이것을 봐서 **매번 어긋난다**.
        r   )compute_config_hashr   	characterlocationprop)entity_all_characterentity_all_locationentity_all_propstep_idr#   carry_rosterNr   TF)	sort_keysensure_asciirX      )app.core.step_runnerr   rk   r   ru   r   updatepack_fingerprintr   rv   _ecroster_digestr   
project_idr   rY   jsondumpsrZ   r\   r]   )
r   r   r   extra_gb_owner_digestrY   r   raws
             r   _config_hashz_EntityStepMixin._config_hash6  s    	="4#6#67 ELL---/0 +6)3%+--0SdIr2.4 	
 :@ $$TWWdoov%)__6EG 	LL--/0$+E.!Kdjj&$0%0#'e=~~cjj12<<>sCCr    c                     | j                  d      }|r)|j                  di       j                  d      r|d   d   S | j                         S )Ntext_cleanupr   cleaned_text)rw   r   r   )r   r   s     r   _load_cleaned_textz#_EntityStepMixin._load_cleaned_texth  sK    ''7"&&$((8f:n--""$$r    r   c                     ddl m} t        |j                        | j                  z  dz  dz  | j
                  z  |z  dz  }|j                         r%t        j                  |j                  d            S y )Nr   )settingscheckpointsepisodeszmanifest.jsonrX   )encoding)
app.core.configr   r   projects_dirr   r   existsr   loads	read_text)r   r   r   r   s       r   rw   z&_EntityStepMixin._load_prev_checkpointn  s    , &&'oo  oo	
   	 99;::bllGl<==r    N)__name__
__module____qualname____doc__rS   r   r   r   r   r	   r   rw   ra   r    r   r   r     sH    P & 0Dc 0Dd%C %S Xd^ r    r   r   c                     ddl m} ||    S )u   갈래 → `short_id` 접두. ★**계약 모듈 한 곳**에서 온다.

    `"C"`·`"L"`·`"P"` 를 호출부에 박아 두면 갈래가 늘어도 여기는 모른다.
    r   )OWNER_PREFIX).app.modules.pipeline.grounding_entity_contractr   )r   r   s     r   _prefix_forr     s    
 Lr    modec                 j    ddl m}  || j                  | j                  || j                  |dk(        S )u6  앞 화 명부 `(블록, 허용 ID)`. ★첫 화는 `("", [])` 라 바이트 불변.

    ★세 갈래가 **같은 함수**를 쓴다 — 손으로 세 번 적으면 한 곳이 빠진다.
     실제로 그 부류의 결함이 있었다(`binding_out` 이 한 갈래에만 빠져
     `NameError` 였다).
    r   )build_roster_blockforce)refresh)"app.modules.pipeline.episode_carryr   r   r   r   )r|   r   r   r   s       r   _prior_rosterr     s8     F fii):):E$//$'/L Lr    entitiesc                 L    ddl m}  || j                  | j                  ||      S )u  엔티티 리스트에 `short_id` 를 부여한다 — **프로젝트 장부에서**.

    ★★★종전에는 `f"{prefix}{i:02d}"` 로 **이 목록 안 위치**를 번호로 썼다.
     그래서 화가 달라도 늘 `C01` 부터 시작했고, `entity_canon` 의 유일성이
     `(project_id, short_id)` 라 2화의 `C01` 이 **1화의 행을 찾아 덮었다.**
     실측(골목 끝 `da049582`): 1화 C01=민수 였는데 지금 DB 의 C01 은 2·3화의
     정임이고, 민수는 C04 로 밀려 있다.

    ★이미 `short_id` 가 있는 것은 **안 건드린다** — 앞 화에서 물려받은
     신원이거나 producer 가 준 것이다.
    r   )assign_short_ids)app.core.entity_identityr   r   r   )r|   r   r   r   s       r   _assign_short_idsr     s"     :FIIv'8'8%JJr    c                   (    e Zd ZdZddeeef   fdZy)EntityAllCharacterStepuG   인물 리스팅 — shot 기반 1회 호출 (fallback: 씬 체이닝).r   c                    | j                  d      }|r't        j                  |j                  di       d      nd}| j                  d      }|rddlm}  ||| j                  d	
       |r"|j                  di       j                  dg       ng }t        d |D              }i }t        | d|      }	i }
i }|dkD  r>ddl	m
}  |||| j                  | j                         | j                         ||	|
|	      }nt        j                  d       | j                  d      }|r"|j                  di       j                  dg       ng }ddl	m}  |dd|| j                  | j                         || j                         ||	|

      }t!        | |d       |j                  d      r[|D ci c]#  }|j                  d      |j                  d      % }}|d   D ]%  }|j                  |j                  d            |d<   ' t#        |      t#        |      d| j%                         d|i|r%t&        j(                  |j                  d      xs i ini |
rt*        j(                  |
ini |rd|idS i dS c c}w )Nvisual_world_rulesr   Fr   r#   shot_validatorr   )assert_no_failed_scenesr   )consumer_stepscenesc              3   R   K   | ]  }t        |j                  d g              ! ywshotsNlenr   r8   ss     r   r9   z2EntityAllCharacterStep._execute.<locals>.<genexpr>  !     H<a#aeeGR01<   %'r   )list_characters_from_shots)a0_candidatesbinding_outprior_roster	carry_outprovenance_outzDentity_all_character: no shots found, falling back to scene chaining
scene_savesegmentslist_entities_by_typer   r   r   r   r   
unverifiedrO   short_idr   ledgername_provenancecompleted_countapplicable_countfailed_countconfig_hashr   )rw   r   r   r   "app.core.steps.shot_validator_stepr   rk   sumr   "app.modules.pipeline.entity_listerr   build_opik_metadatar   loggerwarningr  r   r   r   r   
LEDGER_KEYr   )r   r   rules_cpvisual_rulesshot_cpr   shots_scenestotal_shots_binding_roster_carry_provr   r   save_cpr   r  c_by_name_us                       r   _executezEntityAllCharacterStep._execute  s   --.BCS[tzz(,,vr":Oac ,,-=>R#GT-@-@PfgDKw{{62.228R@QSH<HH
 $&  k48!# !#?U3l##T%=%=%?"113$$$J NNab00>GDKW[[,00R@QSFP.Kt/B/B((*6 #113$$8J 	$
K8
 99\"BLM*QfquuZ'88*HML)!)bffVn!=: *#&z?J !  $002%z J  ( !^^X\\(-C-IrJ-/	J 9?S^^V4BJ ;@.6JK 	K$ FHJK 	K Ns   %(I0Nresumer   r   r   r   r   rS   r   r!  ra   r    r   r   r     s    QKKc3h KKr    r   c                   (    e Zd ZdZddeeef   fdZy)EntityAllLocationStepu-   배경 리스팅 — shot 기반 1회 호출.r   c                    | j                  d      }|r't        j                  |j                  di       d      nd}| j                  d      }|r"|j                  di       j                  dg       ng }t	        d |D              }i }t        | d	|      }i }	|d
kD  r>d
dlm}
  |
|d	|| j                  | j                         | j                         |||		      }nt        j                  d       | j                  d      }|r"|j                  di       j                  dg       ng }d
dlm}  |dd	|| j                  | j                         || j                         |||	
      }t        | |d	       t        |      t        |      d
| j!                         d|i|r%t"        j$                  |j                  d      xs i ini |	rt&        j$                  |	idS i dS )Nr   r   Fr   r#   r   r   c              3   R   K   | ]  }t        |j                  d g              ! ywr   r   r   s     r   r9   z1EntityAllLocationStep._execute.<locals>.<genexpr>  r   r   r   r   list_entities_from_shotsr   r   r   r   z=entity_all_location: no shots, falling back to scene chainingr   r   r  r  r   r  r  rw   r   r   r   r  r   r  r*  rk   r  r   r  r  r  r   r   r   r   r  r   )r   r   r  r  r  r  r  r  r  r  r*  r   r  r   r  s                  r   r!  zEntityAllLocationStep._execute  s   --.BCS[tzz(,,vr":Oac,,-=>DKw{{62.228R@QSH<HH $&  j$7!#?S0j,8K8K((*$:M:M:O$$	8I NNZ[00>GDKW[[,00R@QSFP-Jd.A.A((*6 #113$$8I 	$	:6#&y>s9~ !  $002$i I  ( !^^X\\(-C-IrJ-/	I 9?S^^V4IJ 	J EGIJ 	Jr    Nr"  r$  ra   r    r   r&  r&        72Jc3h 2Jr    r&  c                   (    e Zd ZdZddeeef   fdZy)EntityAllPropStepu-   소품 리스팅 — shot 기반 1회 호출.r   c                    | j                  d      }|r't        j                  |j                  di       d      nd}| j                  d      }|r"|j                  di       j                  dg       ng }t	        d |D              }i }t        | d	|      }i }	|d
kD  r>d
dlm}
  |
|d	|| j                  | j                         | j                         |||		      }nt        j                  d       | j                  d      }|r"|j                  di       j                  dg       ng }d
dlm}  |dd	|| j                  | j                         || j                         |||	
      }t        | |d	       t        |      t        |      d
| j!                         d|i|r%t"        j$                  |j                  d      xs i ini |	rt&        j$                  |	idS i dS )Nr   r   Fr   r#   r   r   c              3   R   K   | ]  }t        |j                  d g              ! ywr   r   r   s     r   r9   z-EntityAllPropStep._execute.<locals>.<genexpr>@  r   r   r   r   r)  r+  z9entity_all_prop: no shots, falling back to scene chainingr   r   r  r  r   r  r  r,  )r   r   r  r  r  r  r  r  r  r  r*  r   r  r   r  s                  r   r!  zEntityAllPropStep._execute:  s   --.BCS[tzz(,,vr":Oac,,-=>DKw{{62.228R@QSH<HH $&  fd3!#?S,flD4G4G((*$:M:M:O$$	8E NNVW00>GDKW[[,00R@QSFP)FL$*=*=((*6 #113$$8E 	$v.#&u:3u: !  $002 % I  ( !^^X\\(-C-IrJ-/	I 9?S^^V4IJ 	J EGIJ 	Jr    Nr"  r$  ra   r    r   r/  r/  7  r-  r    r/  c                   (    e Zd ZdZddeeef   fdZy)EntityExtractStepuB   Step 8 (legacy): 요소 추출 (3턴: 인물 -> 배경 -> 소품).r   c                 :   | j                         }| j                  d      }|r't        j                  |j	                  di       d      nd}| j                  d      }|r7t        j                  |j	                  di       j	                  dg       d      nd}dd	lm}  ||||| j                  | j                               }t        |j	                  d
g             t        |j	                  dg             z   t        |j	                  dg             z   }	|	|	d|dS )Nr   r   Fr   r#   r   r   r   )extract_all_entitiesr   r   r   r	  r
  r  r   )
r   rw   r   r   r   (app.modules.pipeline.entity_extractor_v4r5  rk   r  r   )
r   r   r   r  r  r  segments_jsonr5  resulttotals
             r   r!  zEntityExtractStep._executer  s%   **,--.BC  JJx||FB/eD 	 ,,\:
  JJFB'++J;%  	 	R%$$&
 

<,-&**["-./&**Wb)*+ 	  % %	
 	
r    Nr"  r$  ra   r    r   r3  r3  o  s    L'
c3h '
r    r3  sid_mapc                     | D ]9  }|j                  d      r|j                  |j                  dd      d      |d<   ; | S )uA   entity_all의 name→short_id 매핑을 extract 결과에 주입.r  rO   r#   r6   )r   r;  es      r   _inject_short_idsr>    s>    uuZ #KKfb(92>AjM  Or    c                   (    e Zd ZdZddeeef   fdZy)EntityExtractCharacterStepu[   Step 8: 인물 추출 — entity_all_character 리스트 기반으로 상세 설명 추가.r   c                    | j                         }| j                  d      }|r't        j                  |j	                  di       d      nd}| j                  d      }|xs i j	                  di       j	                  t
        j                        xs i }i }|r|j	                  di       j	                  d      rt|d   d   }|D 	ci c]  }	|	d   |	j	                  d	d       }
}	d
dlm}  ||d||| j                  | j                         | j                         |      }t        ||
       n+d
dlm}  ||d|d| j                  | j                               }t        |      t        |      d
| j                         d|t
        j                  t        j                   d|i|j	                  d      xs g g       idS c c}	w )Nr   r   Fr   r#   r   r   rO   r  r   "extract_entities_by_type_with_listr   r   r   extract_entities_by_typer  lostrG  	contestedr  r   rw   r   r   r   r   r  r7  rC  rk   r  r   r>  rF  r   r   merge_ledger)r   r   r   r  r  all_cp_prev_ledgerr  	name_listr=  r;  rC  r   rF  s                 r   r!  z#EntityExtractCharacterStep._execute  s   **,--.BCS[tzz(,,vr":Oac++,BC"))&"599#..IOR#%fjj,00>v|4IAJKAqy!%%
B"77GKc;+y,@S@S((*$:M:M:O$&J j'2Y1(KWY[_[n[npt  qI  qI  qK  LJ#&z?J !  $002%z )9)9&5"*,,v"6"<"')*+	,- 	- L   F>Nr"  r$  ra   r    r   r@  r@    s    e"-c3h "-r    r@  c                   (    e Zd ZdZddeeef   fdZy)EntityExtractLocationStepuZ   Step 9: 배경 추출 — entity_all_location 리스트 기반으로 상세 설명 추가.r   c                    | j                         }| j                  d      }|r't        j                  |j	                  di       d      nd}| j                  d      }|xs i j	                  di       j	                  t
        j                        xs i }i }|r|j	                  di       j	                  d      rt|d   d   }|D 	ci c]  }	|	d   |	j	                  d	d       }
}	d
dlm}  ||d||| j                  | j                         | j                         |      }t        ||
       n+d
dlm}  ||d|d| j                  | j                               }t        |      t        |      d
| j                         d|t
        j                  t        j                   d|i|j	                  d      xs g g       idS c c}	w )Nr   r   Fr   r#   r   r   rO   r  r   rB  r   rD  rE  r  rG  rH  r  rJ  )r   r   r   r  r  rL  rM  r  rN  r=  r;  rC  r   rF  s                 r   r!  z"EntityExtractLocationStep._execute  s   **,--.BCS[tzz(,,vr":Oac++,AB"))&"599#..IOR#%fjj,00=v{3IAJKAqy!%%
B"77GKc:*it?R?R((*$:M:M:O$&I i1Y0:|UWY]YlYlnr  oG  oG  oI  JI#&y>s9~ !  $002$i )9)9&5"*,,v"6"<"')*+	,- 	- LrO  Nr"  r$  ra   r    r   rQ  rQ    s    d"-c3h "-r    rQ  c                   (    e Zd ZdZddeeef   fdZy)EntityExtractPropStepuW   Step 10: 소품 추출 — entity_all_prop 리스트 기반으로 상세 설명 추가.r   c                    | j                         }| j                  d      }|r't        j                  |j	                  di       d      nd}| j                  d      }|xs i j	                  di       j	                  t
        j                        xs i }i }|r|j	                  di       j	                  d      rt|d   d   }|D 	ci c]  }	|	d   |	j	                  d	d       }
}	d
dlm}  ||d||| j                  | j                         | j                         |      }t        ||
       n+d
dlm}  ||d|d| j                  | j                               }t        |      t        |      d
| j                         d|t
        j                  t        j                   d|i|j	                  d      xs g g       idS c c}	w )Nr   r   Fr   r#   r   r   rO   r  r   rB  r   rD  rE  r  rG  rH  r  rJ  )r   r   r   r  r  rL  rM  r  rN  r=  r;  rC  r   rF  s                 r   r!  zEntityExtractPropStep._execute  s   **,--.BCS[tzz(,,vr":Oac++,=>"))&"599#..IOR#%fjj,009vw/IAJKAqy!%%
B"77GKc6&)\4;N;N((*$:M:M:O$&E eW-Y,Xv|RQUQdQdfjf~f~  gA  BE#&u:3u: !  $002 % )9)9&5"*,,v"6"<"')*+	,- 	- LrO  Nr"  r$  ra   r    r   rT  rT    s    a"-c3h "-r    rT  c                   >    e Zd ZdZdeeef   fdZddeeef   fdZy)EntityFilterStepu@   Step 11: 저빈도 요소 필터링 (3씬 이하 -> LLM 판단).r   c           
         ddl m} ddlm} | j	                  d      }|xs i j                  d      xs i }|s |ddd	      |D ci c]9  \  }}|j                  |      s|d
v r|t        |j                  |      xs g       ; }}}t        d |j                         D              }||d| j                         |g g ddddS c c}}w )uI  C(c) 판 — **호출 0 투영**. ★저빈도 필터를 다시 안 돌린다.

        ★★★producer 가 한 판독에서 **등록 문턱**(반복 또는 두 축 예외)을
        이미 걸었다. 여기서 저빈도 LLM 을 또 돌리면 —

            ①같은 뜻을 두 번 판단하고(그 판정이 갈리면 정본이 없다)
            ②`location_parts` 를 아예 못 보고 **네 번째 갈래가 사라진다**
              (앞 판이 실제로 그랬다 — Codex 재현 2026-09-01)

        그래서 등록된 줄을 **그대로** 통과시킨다.
        r   rm   rK   rl   r   step.no_inputu_   entity_merge 산출 없음 — C(c) 판에서 필터는 그 투영을 그대로 통과시킨다ro   rp   r   r   r   c              3   2   K   | ]  }t        |        y wNr   r8   r   s     r   r9   z5EntityFilterStep._chunk_projection.<locals>.<genexpr>;       3NqCFN   uc   C(c) producer 가 등록 문턱을 이미 걸었다 — 같은 뜻을 다시 판단하지 않는다)filtered_entitiesremoved_entitiesremovedprojected_fromprojection_reasonr  )
ry   rn   rM   rL   rw   r   r   r  r   r   	r   rn   rL   r   r   k_olanesr:  s	            r   _chunk_projectionz"EntityFilterStep._chunk_projection"  s     	-L''7b~~f%+$?	! !
 :M N9L2HHQK1(L#L D!*++9L N3ELLN33$%d.?.?.A%*$&2"0B
 	
Ns   	>Cc           
      x	   ddl m}m}  | || j                              r| j	                         S | j                         }| j                  d      }|rQ|j                  d      r@|d   j                  dg       |d   j                  dg       |d   j                  dg       d}n| j                  d	      }| j                  d
      }| j                  d      }	|xs i j                  di       j                  dg       |xs i j                  di       j                  dg       |	xs i j                  di       j                  dg       d}| j                  d      }
|
r"|
j                  di       j                  dg       ng }dD ]  \  }}| j                  |      }|s|j                  d      s,|}ddlm	} |d   j                  |g       D ci c]  }|d    ||       }}|j                  |g       D ]  }d|vs|j                  |d   d      |d<   !  t               }| j                  d      }|r|j                  di       j                  d      rr|d   d   D ]V  }|j                  d      s|j                  |j                  dd             |j                  |j                  dd             X |j                  d       ddl m}m}  || j                        }| j                  d      }|xs i j                  d      xs i } ||      r?ddlm} |s |dd| dd      |j                  d       }||k7  r |d!d"|d#|d$d      |j                  d%      xs g }|rdd&lm}m} | j                  d'      xs i j                  d      xs i j                  d(      xs g }|r ||||      } | j'                         D ]A  \  }!}"|j)                  |!g        ||!   j+                  |"       ||"D #ch c]  }#|#d)   	 c}#z  }C | r4t,        j/                  d*t1        d+ | j3                         D                      |||      }$|$rt,        j/                  d,t5        |$             ||$z  }dd-lm}% dd.lm}&  |%||||&|r|nd | j                  | j=                         /      }'d0d0d|'d1S c c}w c c}#w )2Nr   rh   rl   r   r   r   r   rZ  entity_extract_characterentity_extract_locationentity_extract_propr   r   ))r   r   )r   r   )r   r   )_appearance_countrO   
shot_countentity_relation	relationsvisual_similaritybase_short_idr#   variant_short_idr   grounding_planrm   z$entity_filter.grounding_plan_missingzgrounding_mode=up    인데 고증 계획 체크포인트가 없다 — 보호 목록 없이 거르면 조사 대상이 지워진다ro   rp   r   z"entity_filter.grounding_plan_staleu   고증 계획이 mode=u    로 돌았는데 지금은 u     다 — 다시 돌려야 한다decided)materialize_missing_entitiesprotected_short_idsr   r   r  uE   grounding: 어려운 단발 대상 %d개를 엔티티로 등록했다c              3   2   K   | ]  }t        |        y wr\  r]  r^  s     r   r9   z,EntityFilterStep._execute.<locals>.<genexpr>  s     :MqCFMr`  uO   grounding: 저빈도 필터 보호에 %d개 추가 (research/design/unresolved))filter_low_frequency_entities)ENTITY_MIN_OCCURRENCES)r   r   r   
max_scenesry  rk   opik_metadata   r6  )rt   ri   rj   rk   rj  r   rw   r   "app.modules.pipeline.entity_filterro  setadddiscardr   ry   rn   &app.modules.pipeline.grounding_overlayrx  ry  items
setdefaultextendr  infor  r   r   r{  r   r|  r  )(r   r   ri   rj   r   merge_cpr   char_cploc_cpprop_cpr  r   rP   prefixrL  r   ro  r=  	count_mapprotected_sidsrel_cprelr   _g_mode_g_cp_g_datarn   _cp_mode
_g_decidedrx  ry  _a0_new_key_rowsr_g_protectedr{  r|  r9  s(                                           r   r!  zEntityFilterStep._executeJ  s   	B 5d6I6IJK))++**, --n=V,&v.22<D%f-11+rB!&)--gr:H 001KLG//0IJF001FGG&}"11&"=AA,PRS$l//;??RP!-R,,VR8<<WbIH ,,\:BI7;;vr*..z2>r JME6//7F&**V,P&,Vn&8&8b&AC&A vY(9!(<<&A  C!eR0A#1,*3--&	1*E, 1 J "e++,=>fjj,00=f~k277./"&&sww'CD"&&sww/A2'FG 3 ""2& 	U()<)<=**+;<;B##F+1rG$0?.wi 8g h #	  {{6*H7"=5h\A] '{*JL #	  [[+1r
C  //?E2S['$&L(9@=? 3CXN#'::<KD% ''b1TN))%0 #e&Deq}e&DDN $0 KK_:DKKM::< /z8DLe%' l*NT	$ /-2@d..224
  ! !	
 	
SCX 'Es   R2<R7Nr"  	r   r   r   r   r   rS   r   rj  r!  ra   r    r   rW  rW    s0    J&
4S> &
PU
c3h U
r    rW  c                   (    e Zd ZdZddeeef   fdZy)EntityReviewStepuO   Step 12: 요소 교차 검증 — 다른 모델로 추출 결과 교차 확인.r   c                    | j                         }| j                  d      }|r*|j                  di       j                  d      r	|d   d   }n| j                  d      }| j                  d      }| j                  d      }|s|s|rs|xs i j                  di       j                  dg       |xs i j                  di       j                  dg       |xs i j                  di       j                  d	g       d
}n:| j                  d      }|r|j                  d      sddlm}	  |	ddd      |d   }ddlm}
  |
||| j                  | j                               }t        |j                  dg             t        |j                  dg             t        |j                  d	g             |d}t        |d         t        |d         z   t        |d	         z   }ddd|dS )Nentity_filterr   ra  rl  rm  rn  r   r   r   rZ  entity_extractr   rm   rY     entity_extract 결과 없음ro   rp   )review_entities)r   r   rk   r~  )r   r   r   reviewr  r6  )r   rw   r   ry   rn   $app.modules.pipeline.entity_reviewerr  rk   r  r   r   )r   r   r   	filter_cpextract_datar  r  r  r   rn   r  review_resultfilteredr:  s                 r   r!  zEntityReviewStep._execute  s   **, ..?	vr2667JK$V,-@AL 001KLG//0IJF001FGG&G#*=b"5"5fb"A"E"ElTV"W"(,B!3!3FB!?!C!CKQS!T%m00<@@"M  112BC488F#38", >$' 
  $F|H'!..224	
 |//bABl..{B?@,**7B78#	
 &'(;'()(7#$% 	  ! !	
 	
r    Nr"  r$  ra   r    r   r  r    s    Y:
c3h :
r    r  c                   >    e Zd ZdZdeeef   fdZddeeef   fdZy)EntityMergeStepuZ   요소 중복 병합 — 타입 간 중복/유사 요소를 LLM으로 판별하여 제거.r   c                 D   ddl m} ddlm} | j	                  d      }|xs i j                  d      xs i }|s |ddd	      |D ci c]9  \  }}|j                  |      s|d
v r|t        |j                  |      xs g       ; }}}t        d |j                         D              }d|d|ddd| j                         t        j                  t        j                  di |g g dg dt        |j                  d      xs i j                  d      xs d      diS c c}}w )u  C(c) 판 — 새 producer 산출을 **그대로 투영**한다. ★호출 0.

        ★★앞 판은 옛 추출 여섯을 끄고 **대체 산출을 안 이었다** — 그러면
        `total=0` 이라 빈 배열을 정상 completed 로 내고, 하류(`entity_detail`·
        `entity_t2i`·DB sync)에서 **엔티티가 통째로 사라진다** (Codex 재현
        2026-09-01). 「중복 호출을 막았다」가 아니라 **전부 잃은 것**이다.

        ★여기서 판단하지 않는다 — producer 가 낸 갈래 줄을 옮기고 병합 칸만
        빈 값으로 맞춘다. 모델을 **한 번도 안 부른다**.
        r   rm   rK   grounding_chunkr   rY  uf   grounding_chunk 산출 없음 — C(c) 판에서 엔티티는 그 producer 가 낸 줄이 정본이다ro   rp   rZ  c              3   2   K   | ]  }t        |        y wr\  r]  r^  s     r   r9   z4EntityMergeStep._chunk_projection.<locals>.<genexpr>C  r_  r`  r	  r
  r  r  F	contractsadapterr#   )rc  mergesmerge_provenance_askedmerge_refusedrd  projection_contract)ry   rn   rM   rL   rw   r   r   r  r   r   	_sync_extrz   r{   rS   rf  s	            r   rj  z!EntityMergeStep._chunk_projection+  sA    	-L''(9:b~~f%+$D	! !
 :M N9L2HHQK1(L#L D!*++9L N3ELLN33u&8%A}d.?.?.A ))9+I+I 		 */""3'*XXk*0b55i@FB(H	
 	
Ns   	>Dc                 
  & ddl m}m}  | || j                              r| j	                         S | j                  d      }| j                  d      }| j                  d      }|xs i j                  di       j                  dg       }|xs i j                  di       j                  dg       }|xs i j                  di       j                  d	g       }	t        |      t        |      z   t        |	      z   }
|
dk(  rddd| j                         g g g g d
dS | j                  d      }|r|j                  di       ni }|j                  dd      }|j                  dd      }| j                  d      }|r"|j                  di       j                  dg       ng }dj                  d |D              }g }|D ]>  }|j                  |j                  dd       d|d    d|j                  dd              @ |D ]>  }|j                  |j                  dd       d|d    d|j                  dd              @ |	D ]>  }|j                  |j                  dd       d|d    d|j                  dd              @ d| d| d| ddj                  |      z   dz   }d d!d"d#d$id%d&id!gd'd(}dd)lm} dd*lm& t        &fd+||z   |	z   D              }|rFd"d,d d#d$id#d$id-d.d/gd'd(d0|d1   d2<   d!d2g|d3<   |d4 |j                          j#                         z   z  }g }g }t%               }	 t'        d5d6||| j                  d5| j)                         7      }|j                  d!g       }|j                  d2g       xs g }|r||z   |	z   }|D  ch c]  } | j                  dd       }!} |D "cg c]	  }"|"|!vs|" }#}"|#rt,        j/                  d9|#       t%        |      |!z  }$t1        |||$      }|$|z  }$|D cg c]  }|j                  dd      |$vs| }}|D cg c]  }|j                  dd      |$vs| }}|	D cg c]  }|j                  dd      |$vs| }	}t,        j3                  d:t5        |$             t        |      t        |      z   t        |	      z   }%|%|
d| j                         |||	|||t7        |      d;dS # t*        $ r!}t,        j/                  d8|       Y d }~od }~ww xY wc c} w c c}"w c c}w c c}w c c}w )<Nr   rh   rl  rm  rn  r   r   r   r   )r   r   r   rc  r  r   erar#   regionscene_summary	summariesr1   c              3   l   K   | ],  }d |j                  dd       d|j                  dd        . yw)u   씬scene_index?z: r  r#   Nr6   r   s     r   r9   z+EntityMergeStep._execute.<locals>.<genexpr>w  s=      !
 !%%s+,Bquu_b/I.JKs   24r   rO   z (character): descriptionz (location): z	 (prop): u   [세계관] 시대: u
   , 지역: u   

[씬별 요약]
u   

[전체 요소 목록]
u  

위 요소 목록에서 타입이 다르지만 실질적으로 같은 대상인 항목을 찾으세요.
예: 같은 물체가 배경(location)과 소품(prop)에 동시 등록된 경우.
중복이 있으면 제거할 short_id를 선택하세요. 없으면 빈 배열을 반환하세요.
각 타입 쌍을 고르게 검토하되, 실질적으로 같은 대상이 서로 다른 타입으로 등록된 경우를 찾으세요.objectremovearraytypestringu   제거할 short_id 목록)r  r  r  F)r  
propertiesrequiredadditionalPropertiesr   )FIELDc              3   @   K   | ]  }|j                          y wr\  r6   )r8   r=  	_GB_FIELDs     r   r9   z+EntityMergeStep._execute.<locals>.<genexpr>  s       F&DAEE)&Ds   u4   제거하는 것마다 어느 것으로 합치는지)remove_short_idkeep_short_idr  r  )r  r  r  r  r  r  z

rl   uu   당신은 시나리오 요소 분석 전문가입니다. 중복 요소를 찾아 제거할 항목을 판별합니다.stepsystem_promptuser_promptresponse_schemark   schema_namer~  z Entity merge LLM call failed: %sz0Entity merge: LLM returned unknown short_ids: %szEntity merge: removed %s)r   r   r   rc  r  r  r  )rt   ri   rj   rk   rj  rw   r   r   r   r:   r   ru   r   &app.modules.pipeline.grounding_bindingr  anymerge_instructionr-   r  r   r  rx   r  r  _carry_merge_provenancer  r   sorted)'r   r   ri   rj   r  r  r  r   r   r   r:  vwr_cpvwr_datar  r  sum_cpr  summary_textentity_linesr  lpr  remove_schemar   _has_provenancerc  r  _refusedr9  r   all_rowsr=  all_sidsrQ   unknown
remove_set	new_totalr  s'                                         @r   r!  zEntityMergeStep._executeW  s   	B 5d6I6IJK))++ ,,-GH++,EF,,-BCm((488rJ
\r&&vr266{BG	B##FB/33GR@J#i.03u:=A:'(aQR#'#4#4#6+-BXZ[] ]
 ++,@A-36::fb)ll5"%h+ ++O<CIFJJvr*..{B?r	yy !
!
 
 A155B#7"8!F)NSTSXSXYfgiSjRk lm A155B#7"8!F)MRSRWRWXefhRiQj kl A155B#7"8!F)IaeeTabdNeMf gh  #3%z& :  ,~ .'(*.))L*ABEVV 	 #$h/#> "
$)
" 	BM F&09&<u&DF FU$,2H+=*0();# "3O D,15M,'1 *28(<M*%6$9C$9$9$;$A$A$CCCK	D$# V' -#22*"668F jj2.GZZ"-3F
 !I-5H7?@x!j"-xH@&-EgsH1DsgGEQSZ[W0J /xLH("J%/[Z155R3HPZ3Z!ZJ[$-YIqz21Fj1XIIY %Q1z2)>j)PQEQKK2D4DE
Oc)n4s5zA	( %,,.(&" *9 "(!1

 	
+  	DNN=sCC	D AE \YQsO   -AS	 S6,	S;6S;8T T T6T T
T
		S3S..S3Nr"  r  ra   r    r   r  r  (  s0    d*
4S> *
XT
c3h T
r    r  r  r  r  c                 D   ddl m}m} | D ci c]!  }t        |j	                  d      xs d      |# }}|D ch c](  }|j	                  |      xs i j	                  |      r|* }}|s
t               S i }	t               }
t               }|xs dD ]  }t        |xs i j	                  d      xs d      }t        |xs i j	                  d      xs d      }||vrL||v r$|
j                  |       |	j                  |d       t|j                  |       |r||k(  s||vs||v r|
j                  |       ||	|<    |
|t        |	      z
  z  }
|	j                         D ]  \  }} |||   ||           |
rt        j                  d	t        |
             |
S c c}w c c}w )
u  ★★지우는 행의 **고증 후보 ID 를 남는 행에 옮긴다** (3b 계약 6).

    안 옮기면 병합 한 번에 그 대상의 원문 근거가 사라지고, 뒤에서
    「조사 대상이었는데 아무 엔티티에도 없다」가 된다.

    ★★**대응이 성립하지 않으면 그 행을 안 지운다** (Codex BLOCK-4).
    schema 가 허용하는 반례들 —

        remove=[P01], merges=[]           근거를 든 행이 그냥 사라진다
        keep 이 목록에 없다                 옮길 데가 없다
        keep 도 remove 중이다               옮겨 봐야 같이 사라진다
        keep == remove                     자기 자신으로 합친다
        한 remove 가 두 keeper 로            같은 후보가 두 행에 생긴다

    「지우고 나서 경고」는 이미 사라진 뒤라 아무 소용이 없다.

    Returns:
        ★**지우면 안 되는 short_id 들.** 호출부가 `remove_set` 에서 뺀다.
    r   )r  
carry_intor  r#   ra   r  r  NuK   Entity merge: 근거를 옮길 데가 없어 **삭제를 거부한** 행 %s)r  r  r  rS   r   r  r  popr  r  r  r  )r  r  r  r  r  r=  by_sidrQ   bearingplanrefused	seen_frommr   dsts                  r   r  r    s   * I7?@x!c!%%
#)r*A-xF@( 6js::c?(b--e4 jG 6uD5GUI\r\17-- 128b917--06B7g)KKHHS$ccSjCv$5
9JKKS	 $ #d)#$GJJLS6#;s, !Y7O	 NG A6s
   &F-F2      c                   @     e Zd ZdZddeeef   fdZdef fdZ xZ	S )EntityDetailStepuE   Step 10: 요소 상세 — 시각적 상세 정보 + short_id 확정.r   c                    ./0123456789:;  j                         9 j                  d      }|r+|j                  di       j                  d      r
|d   d   }n j                  d      }|rLd|j                  di       v r8t               D ci c]"  \  }}|d   j                  |      r	||d   |   $ }}}n j                  d      } j                  d      } j                  d      }	|xs i j                  di       j                  d	g       |xs i j                  di       j                  d
g       |	xs i j                  di       j                  dg       d}t	        |j                               sddlm}
  |
ddd      ddlm	}m
}  | | j                              }g }t               }t               D ]  \  }}|j                  |g       D ]  }|d   }t        |j                  d      xs d      }|r|sddlm}
  |
ddd|d| d      |r|n||f}||v r5|rddlm}
  |
ddd|d|d| d       t        j                  d!||       |j!                  |       |j#                  |||f         |D ch c]%  }t%        |      d"k\  s|d#   st        |d#         ' c}4dd$lm}  |d% |D              }|D ci c]?  }t%        |      d"k\  s|d#   s||d   |d&   f   d&k(  s)|d   |d&   ft        |d#         A c}6d't        d(t        d)t        d*t        f46fd+/dd,lm0m}  j                  d-      }|r|j                  di       ni }d;|j                  d.      s|j                  d/      rld0|j                  d.d       d1|j                  d/d       ;|j                  d2g       D ]/  }|j                  d3      d4k(  s;d5|j                  d6d       z  ;1 dd7lm}  | j4                   j6                   j8                        }|j;                  d8d9      ::|j;                  d:d;      z  : |d<      7d=j=                  d> d? |D        D              } 0d@|9A      ;z   :z   } i 8t?         dB      r j-                  dCdD      ntA        dCdD      5dEtB        t        tD        f   d*tB        t        tD        f   fdF3dGtB        t        tB        f   d*tF        tH           fdH1dIt        dJtF        t           d*tB        t        tB        f   f/03579: ;f	dK.dLtJ        d*tL        fdM2dNtN        dOtN        dPtF        tH           d*tB        t        tB        f   f.12fdQ}!tQ        dt%        |      tR              D "cg c]  }"||"|"tR        z     }#}"t        jU                  dRt%        |      t%        |#      tR               t%        |#      d&k  r7tW        |#d&      D ]'  \  }$}%8jY                   |!|$t%        |#      |%             ) nddSl-m.}& ddTl/m0}' ddUl1m2}(  |( |'|!            }) |&tg        th        t%        |#            V      5 }*tW        |#d&      D $%cg c]"  \  }$}%|*jk                  |)|$t%        |#      |%      $ }+}$}%	 |+D ]!  },8jY                  |,jm                                # 	 d d d        tq        8fdW|D              }-tO        |-      t%        |      ts        dt%        |      tO        |-      z
         ju                         |8tv        dXdYS c c}}w c c}w c c}w c c}"w c c}%}$w # tJ        $ r |+D ]  },|,jo                            w xY w# 1 sw Y   xY w)ZNr  r   ra  rl   rc  rl  rm  rn  r   r   r   rZ  r   rm   rY  r  ro   rp   rh   rO   r  r#   zentity_detail.identity_missingi  u,   producer 가 short_id 를 주지 않았다:  (u$   ) — 신원 없이 사지 않는다)rq   rs   rr   z entity_detail.identity_duplicateu   short_id 가 겹친다: , u   ) — 사지 않고 선다uB   Entity queue: 중복 제거(legacy · short_id 없음) — %s (%s)      Counterc              3   0   K   | ]  }|d    |d   f  yw)r   r  Nra   r8   qs     r   r9   z,EntityDetailStep._execute.<locals>.<genexpr>}  s     >AAaD!A$<s   r  enamerP   eidr   c                 @    |v r|S | |fv r| |f   S |r|  d| S | S )N:ra   )r   rP   r  _sid_set_unique_sids      r   _detail_keyz.EntityDetailStep._execute.<locals>._detail_key  sB    h
u~,"E5>22).eWAeW%9E9r    )_load_prompt_load_schemar   r  r  u   
[세계관]
시대: 	   
지역: rules	rule_typecostume   
의상규칙: r  )get_planning_contextcharacters_textu!   ## 기획서 인물 참고 정보locations_textu!   ## 기획서 장소 참고 정보z turn1_7_detail_batch_schema.jsonr1   c              3   R   K   | ]  \  }}}|rd | d| d| dnd| d| d ! ywz- [z] r  r5   r3   Nra   )r8   rO   rP   rQ   s       r   r9   z,EntityDetailStep._execute.<locals>.<genexpr>  sJ      %
$g eS /2s3%r$r%*D6E7!7LL$gr   c              3   X   K   | ]"  }|d    |d   t        |      dk\  r|d   ndf $ ywr   r  r  r  r#   Nr]  r  s     r   r9   z,EntityDetailStep._execute.<locals>.<genexpr>  s3     $gZfUVadAaD3q6Q;1Q4B%PZf   (*turn1_7_detail_batchentity_listr   r  entity_extractor_v2systementc                     | j                  dd      | j                  dg       | j                  dg       | j                  dg       dS )Nr  r#   r   r=   r   )r  r   r=   r   r6   )r  s    r   _recordz*EntityDetailStep._execute.<locals>._record  sL      #ww}b9!$"!=
 .1WW5PRT-U
  #ww}b9 r    havec           	          | D cg c]3  ^}}}|r|d   r|d   |vs|r|d   s| d| |vr||vr|||r|d   ndf5 c}}}S c c}}}w )Nr   r  r#   ra   )rowsr  ntr  s        r   _missingz+EntityDetailStep._execute.<locals>._missing  sr     =A<@1q1Q4QqT%5qtA3as4)?ATM A!r+D  s   8Ar  tagsc                 B  	 dj                  d d | D        D              } d|      z   z   }}t        | D ch c]%  }t        |      dk\  s|d   st        |d         ' c}      }j	                  di       j	                  d	i       j	                  d
i       j	                  di       j	                  d      }|rC|At        j                        }|t        d | D              rdgng z   |d   d	   d
   d   d   d<   t        d||j                  |j                  |            }	i }
|	j	                  d	g       D ]T  } |d   |j	                  dd      t        |j	                  d      xs d      j                               } |      |
|<   V |
S c c}w )Nr1   c              3   R   K   | ]  \  }}}|rd | d| d| dnd| d| d ! ywr  ra   )r8   r!  r"  rQ   s       r   r9   z:EntityDetailStep._execute.<locals>._ask.<locals>.<genexpr>  sH       !\IAq# ,/3se2aS1#Q'b2aSNB!\r   c              3   X   K   | ]"  }|d    |d   t        |      dk\  r|d   ndf $ ywr  r]  r  s     r   r9   z:EntityDetailStep._execute.<locals>._ask.<locals>.<genexpr>  s3     !\W[RS1Q41A!!"MW[r  r  r  r  r  r  r   r  	entity_idc              3   H   K   | ]  }t        |      d k\  xr |d      yw)r  r  Nr]  r  s     r   r9   z:EntityDetailStep._execute.<locals>._ask.<locals>.<genexpr>  s%     'QDqSVq[-AQqT(BDs    "r#   enumentity_detail_batchr  rO   r   )r:   r  r   rS   r   copydeepcopyr  r   rk   r  r-   )r   r  r$  listingpromptschemar  sidsid_propr9  r   r  r   r  r  r  _systemdetail_schemar   planning_blockr   world_blocks                r   _askz'EntityDetailStep._execute.<locals>._ask  s    ii  !\W[!\  G "&#! 	 -	-F #FdKdc!fkad3qt9dKLD$((r:>>z2NGR(\2)>ss;?O +}5C'QD'Q$QRDWYZ |$Z09,GTU[\$*%" &#22'"66t<F $&Czz*b1!#f+sww}b/I"%cggk&:&@b"A"G"G"IK"3<C 2 J+ Ls   FFFr   c                 v    ddl m} ddlm}  ||       xs$ t	        | |      xs t        t        | dd            S )Nr   )ResearchCallBudgetExceeded)is_abortis_run_deadlineF)app.core.research_call_budgetr9  app.core.run_controlr:  r   r,   rv   )r   r9  r:  s      r   
_must_stopz-EntityDetailStep._execute.<locals>._must_stop  s@     Q5SM DZ5O%P DGC):EBCEr    cir:  r   c                    i }	 |j                   |dd|  d| g              ||      }|r"	 |j                   |ddd|  d| g             t	         ||            }|r"t        j                  d| ||t	        |             |S # t        $ r+} 	|      r t        j                  d| ||       Y d }~d }~ww xY w# t        $ r+} 	|      r t        j                  d| ||       Y d }~d }~ww xY w)	Nr+  zchunk:/z$Entity detail chunk %d/%d failed: %sentity_detail_retryretryz*Entity detail chunk %d/%d retry failed: %su;   Entity detail chunk %d/%d: %d/%d 요소가 끝내 비었다)r   rx   r  r  r   )
r?  r:  r   r   r   missleftr7  r#  r>  s
          r   
_run_chunkz-EntityDetailStep._execute.<locals>._run_chunk  s   #%CW

4&;rd!E7>S=TUV
 D#&DaJJtD*?%,rd!E7.C$D F G xc*+D\!5$D	;J%  Wc?Er5RUVVW ! a!#NN#OQSUZ\_``as.    B
 !C 
	B>!B99B>	C5
!C00C5u5   Entity detail: %d요소 → %d묶음(최대 %d개씩))r   )bind_current_research_budget)bind_current_tracemax_workersc              3   l   K   | ]+  ^}}}|r|d    r|d    v s|r|d    s| d| v s|v rd - yw)r   r  r  Nra   )r8   r!  r"  r  entity_detailss       r   r9   z,EntityDetailStep._execute.<locals>.<genexpr>0  sT      ]*(!Qqt1!71Q41QCjN&Ba>FY ls   14)entity_queuerL  identity_contractr  )<r   rw   r   rN   r  r   ry   rn   rt   ri   rj   rk   r  rS   r  r  r  r   r   collectionsr  (app.modules.pipeline.entity_extractor_v3r  r  app.core.planning_doc_contextr  r   r   r   inject_if_availabler:   hasattrr   r   r   r   tupleBaseExceptionr,   intrange_DETAIL_CHUNK_SIZEr  	enumerater   concurrent.futuresr   r<  rG  app.modules.llm.opik_tracerH  min_DETAIL_CHUNK_WORKERSsubmitr9  cancelr  maxr   r[   )<r   r   r  r  r  rg  rh  r  r  r  rn   ri   rj   _strictrM  	seen_keys	etype_keyetype_labelr=  rO   rQ   r   r  _Counter_pair_nr  r  r  r  r  pctxentity_list_textdetail_promptrF  ichunksr?  r   r   rG  rH  sendpoolfutsf_haver7  r  r  r#  r>  r  r  r3  r  r4  rL  r   r5  r6  s<   `                                             @@@@@@@@@@@@@@r   r!  zEntityDetailStep._execute=  s   **, ..?	vr2667JK ()<=H11.AHIfb)AA *68)72'/33A6 x/22)7  8 445OP334MN445JK#*=b"5"5fb"A"E"ElTV"W"(,B!3!3FB!?!C!CKQS!T%m00<@@"M 8??$%0$6  	X%&<T=P=P&QR$&	&2n"I{\\)R0y!%%
+1r238"(HVY-YZ^Yaacdocp  qU  ,VW W ct[&9)#<&,N\_1I#PRSWRZZ\]h\i  jD  0EF FNN#gimoz{c"##T;$<=! 1 '5$ (4L|!s1v{qtC!I|L3>>>8D|1ARSXYZ[X\ahjklmjnpqrsptiuavz{a{!ad|S1Y.|	:s 	:3 	:S 	:S 	: 	X ++,@A-36::fb)<<(,,x"83HLL4K3LJW_WcWcdlnpWqVrsK\\'2.55%2%5aeeM26N5O#PPK /
 	G#DOOT__dggN112CEhi 	$223CEhii$%GH99 %
$gZf$g%
 
 %"(
 	 )	) +-dN3 $$%:HE9DEZ\d9e 		c3h 	DcN 	$	c4i 	T%[ 	(	C (	tCy (	T#t)_ (	 (	T	EM 	Ed 	E	3 	s 	$u+ 	$sDy/ 	2 !C$57IJLJa q%7!78J 	 LK%s6{4F	Hv;!%fa0D%%jS[$&GH 1 >RE
 &&B:&NOD#4I3v;0WX\`(1&!(<>(<HB D"c&k4@(<  >!&--ahhj9 " Y  ]*] ]
  #5z #L 13|#4s5z#AB,,.%1^*SU
 	
Q8\ M }fL$> % !
 "	 YXsf   '\%
\+\+$\+\0$\0*\0;\0\5]%('\:]%&] :]% "]""]%%].c                    ddl m}m}  | || j                              st        
|          S t        t        
|                }ddlm} g }dD ]5  \  }} |d||      }|j                  | d|j                  d              7 dd	l}	|	j                  |d
z   d
j                  |      z   j                  d            j                         S )u^  ★인스턴스 신원 계약 + **실제로 고른 팩**(prompt·schema 의 버전과 원본 bytes 해시)을 지문에 싣는다 (Codex 06:50 NON-BLOCK ·
        이번 live 뒤). 실측: 제가 만든 팩이 loader 에 안 실렸는데 지문은 그대로라 알 길이 없었다 — 다음 팩 변경 때 옛 CP 가 조용히 되쓰이지 않게.r   rh   )resolve_effective))r  r/  )turn1_7_detail_batch_schemar0  r  )kindr  raw_content_hashNrW   rX   )rt   ri   rj   rk   superr   r^   app.modules.prompt_loaderrr  r   r   rY   rZ   r:   r\   r]   )r   ri   rj   r   rr  digestsstemrt  r  rY   	__class__s             r   r   zEntityDetailStep._config_hash=  s     	X ##9$:M:M#NO7'))eg245?iJD$!"7DIANNdV1QUU+=%>$?@A j 	~~tczCHHW,==EEgNOYY[[r    r"  )
r   r   r   r   r   rS   r   r!  r   __classcell__rz  s   @r   r  r  :  s1    O~
c3h ~
@\c \ \r    r  c                   @     e Zd ZdZdef fdZddeeef   fdZ xZ	S )EntityT2iStepu(   Step 11: T2I 프롬프트 병렬 생성.r   c                     ddl m}m}  | || j                              st        |          S t        t        |                S )u*  ★인스턴스 신원 계약을 지문에 싣는다 — 옛 (name, type) 열쇠로 구운 CP 는 어긋나 다시 굽는다(entity_detail 과 같은 상수).
        ★legacy/v2 는 옛 값 그대로(byte-identical) — 기존 에피소드의 재개를 막지 않는다(PR #82 리뷰 2026-09-03).r   rh   )rt   ri   rj   rk   rv  r   r^   )r   ri   rj   rz  s      r   r   zEntityT2iStep._config_hashW  s>     	X"#9$:M:M#NO7'))eg2455r    c                    9:;<=>?@ABCDEFGH  j                  d      }|r|j                  d      sddlm}  |ddd      |d   d	   }|d   j                  d
      xs |d   j                  di       C j                  d      xs i }i =i <t	               D ]s  \  }}|j                  d      xs i j                  |      xs g D ]A  }t        |t              st        |j                  d      xs d      j                         }	|	sD|j                  d      xs g }
t        |
t              r	 t        j                  |
      }
|j                  d      t        |
t              r|
ng dd}t        |j                  d      xs d      j                         }t        |j                  d      xs d      j                         xs |}|r|=|<   |	 d| }<j                  |d      }|dk(  r	||f<|<   0|4|d   |us=d <|<   D v d;<=fd	;d;;Cfd	Ai D|D ]G  }t        |      dk\  s|d   s|d   Dt        |d   |d         <   Dj                  |d   |d          I  j!                         }|r'|dk(  r"|j                  di       j                  di       ni B|D ch c]  ^}}}t        ||       c}}}G|D ch c]  ^}}}|s	|d   st        |d          c}}}@ddlm}  |d |D              >|D ch c]	  ^}}}||f c}}}Fd ?d t&        f>?@FGfd!}BrBj)                         D cg c]  \  }} |||      r| }}}|rQt*        j-                  d"t        |      |d d#        Bj)                         D ci c]  \  }} |||      s|| c}}BBr{=s<rvg }t        Bj)                               D ]2  \  }}t        |t              st        |j                  d      xs d      j                         rEt        |j                  d      xs d      }	t        |j                  d      xs d      }t        |j                  d      xs d      } ;|	||      } t        | j                  d      xs d      j                         s|j                  d      xs g }!i || j                  d      xs d|!r|!n| j                  d      xs g ddB|<   |j/                  |       5 |r#t*        j-                  d$t        |      |d d#        d;>Bfd%	}"t1        |      D #$cg c]&  \  }#^}}}$ |"|||$r|$d   nd      s|#|||$r|$d   ndf( }%}}}#}$t        |      }&dd&lm9m}'m:  |'       E j                  d'      }(|(r|(j                  di       ni })|)j                  d(d      }*|)j                  d)d      }+dH|*s|+rLd*|* d+|+ H|)j                  d,g       D ]/  }$|$j                  d-      d.k(  sHd/|$j                  dd       z  H1 |)j                  d0d      },|,rHd1|, z  Hd;9:AD EHfd2	}-|%rAt;        d3t        |%            }.t=        |.4      5 }/i }0|%D #1ci c]
  \  }#}}}1|#|1 }2}}}#}1|%D ]6  \  }#}3}4}1|0rt?        j@                  d       |#|0|/jC                  |-|#|3|4|1      <   8 tE        |0      D ]  }5|5jG                         }6|6\  }7}3}4}8|83t*        jI                  d5|3|4        jK                  t        B      |&       O|4|8d<   |8BtM        |3|4|2j                  |7d            <    jK                  t        B      |&        jO                  d6B|d7tQ        B      d8        	 d d d        t        B      |&|&t        B      z
   jS                         d9tU               ddBitQ        B      d:tV        iiS # t        $ r g }
Y w xY wc c}}}w c c}}}w c c}}}w c c}}w c c}}w c c}$}}}#w c c}1}}}#w # 1 sw Y   xY w)<Nentity_detailr   r   rm   rY  u   entity_detail 결과 없음ro   rp   rM  rL  gpt_detailsrl   rO   r#   r   r  )r  r   description_sourcer  r   r  u   ∅c                     d }|r	j                  |      }|r ||      S |si S j                  |  d|       }|si S |\  }}|r	|r||k7  ri S  ||      S )ui  `entity_merge` 원자료 — **신원이 맞을 때만** 돌려준다.

            ★short_id 가 신원이다(`ENTITY_INSTANCE_IDENTITY_CONTRACT`).
             대상에 short_id 가 있고 원자료 쪽도 제 short_id 를 가졌는데
             서로 다르면 **다른 개체**다 — 이름이 같아도 안 쓴다.
            ★(이름, 종류)가 원자료에서 **유일하지 않으면** 빈 채로 둔다.
             모호한 자리를 메우는 것보다 비워 두는 것이 낫다.
            ★이름 **단독**으로는 안 찾는다 — 종류가 다르면 다른 개체다.
            c                 f    | r.t        | j                  d      xs d      j                         r| S i S )uK   설명이 **있을 때만** 원자료로 친다 — 없으면 「없음」.r  r#   )rS   r   r-   )recs    r   _usablez>EntityT2iStep._execute.<locals>._merge_source.<locals>._usable  s-    3sww}5;<BBDJ	r    r  r6   )
r   rP   rQ   r  r   pairr  src_sid_mf_by_name
_mf_by_sids
           r   _merge_sourcez-EntityT2iStep._execute.<locals>._merge_source  st      nnS)"3<'	??eWAeW#56D	LCw7c>	3<r    c                     |rj                  |      ndxs* j                  |  d|       xs j                  | i       }|r|S  | ||      S )u  이 엔티티의 **원자료**. 상세가 이기고, 없으면 `entity_merge`.

            ★열쇠 순서는 조립부와 같다(short_id → 이름:종류 → 이름).
            ★빈 dict 는 「없음」이다 — 그때만 대체 원천으로 간다.
            Nr  r6   )r   rP   rQ   r   r  rL  s       r   _source_detailz.EntityT2iStep._execute.<locals>._source_detail  sh     03N&&s+ 4$((E7!E7);<4$((3  
 s33r    r  r  r  r#  	completedr  c              3   *   K   | ]  ^}}}||f  y wr\  ra   )r8   r!  r"  _rs       r   r9   z)EntityT2iStep._execute.<locals>.<genexpr>  s     F+1a"1vs   c                     t        |xs i j                  d      xs d      }dt        |       v r"t        |       j                  dd      \  }}||fS t        |xs i j                  d      xs |       }||fS )uK   열쇠(short_id · _qkey · 이름)에서 (name, type) 짝을 되찾는다.r   r#   r`   r  rO   )rS   r   split)r  _valr   _t_n_nms         r   _pair_ofz(EntityT2iStep._execute.<locals>._pair_of  su    tzr&&}5;<C3t9$T15BBxtzr&&v.6$7C:r    r   c                     | v ry | |      }| v s|v r:j                  |d      dkD  r$t        |xs i j                  d      xs d      v S yy)NTr   r  r  r#   F)r   rS   )r  r  r  _pair_countr  _sids
want_pairswant_qs      r   _keptz%EntityT2iStep._execute.<locals>._kept  sd    u}D$'Dv~!3??4+a/
//
;ArBeKKr    uk   entity_t2i: 현재 대상에 없는 완료 기록 %d건 폐기 (archive 복원 잔존 추정) — 표본 %s   un   entity_t2i: 설명이 빈 완료 기록 %d건을 `entity_merge` 원자료로 메웠다(호출 0) — 표본 %sc                 Z   |r|v ryj                  t        | |            }|:j                  |       }|'t        |xs i j                  d      xs d      |k7  rd }|yj                  | |fd      dkD  r2t        |      xr% t        |xs i j                  d      xs d      |k(  S y)NTr   r#   Fr   r  r  )r   rT   rS   r,   )r  r  _sidr   r  r   s       r   _alreadyz(EntityT2iStep._execute.<locals>._alreadyU  s    ((5R=)C{hhrl?sCI2??=+I+OR'PTV'VC{Bx+a/DzTc39"//**E*K&LPT&TTr    )ENTITY_DETAIL_SCHEMA_load_systemr  r   r  r  u   

[세계관]
시대: r	  r
  r  r  r  t2i_contextu   

[T2I 시각 컨텍스트]
c                     |||      }d}|r|j                  dd      }dj                  |j                  dg             }|d| d| z  }t        |j                  d            \  }}	||z  }t        |j                  d            \  }
}||
z  }|r{t        j                  d	|t        |      |D cg c]P  }|j                  d
      |j                  d      t        |j                  d      xs dj                               dR c}       |	rt        j                  d|t        |	      |	D cg c]`  }|j                  d      |j                  d      t        |j                  d      xs dj                               |j                  d      db c}       t        dfd||d|      }t        d      D ]#  }	 t        d|j                  dj                               }|xs i }|j                  d      xs i }ddlm}  |||      }|t!               v r@ddlm} ddlm} ddlm} 	  ||||xs  j                  t-        ||      d      xs |       | ||||xs j                  t-        ||      d      |j                  dd      t5        |      |j                  d#d      |j                  dg       |d$|j                  d%      d&k(  rd%d&ini fc S  |t!               v r| ||d fS |xs i }| |||j                  t-        ||      d      |j                  dd      t=        |j                  d      t>              r|j                  dg       ng dd d d(d)fS c c}w c c}w # ||f$ r<}t/        |d d       xs t1        |      }t        j3                  d!||d"z   ||        d }~ww xY w# t6        $ r$ |d'k  rt9        j:                  d'|d"z   z         Y &w xY w)*Nr#   r  r  r   u.   

[시나리오 기반 상세 정보]
설명: u   
시각적 특징: r=   r   u^   entity_t2i %s: build_scale %d개 렌더 제외 (근거 인용 또는 견줌 문장 없음): %sr"   r$   r*   )r"   r$   rE   ut   entity_t2i %s: distinctive_visual_traits %d개 렌더 제외 (evidence-gate, literal_visual+source_quote 아님): %srB   r?   interpretation_reason)rB   rt  rE   reason
entity_t2ic                       di | S )N)turn_entity_detailra   )kwr  s    r   <lambda>z:EntityT2iStep._execute.<locals>._gen_t2i.<locals>.<lambda>  s    L$L$Lr    )entity_namer   )renderfieldsproject_block
call_blockr  r  metadata_jsonr   )normalize_metadata_for_type)validate_entity_metadata_shaperm   )SpaceProfileError)r  rr   uF   entity_t2i %s post-validate failed (attempt %d/3) for %r: %s — retryr  
t2i_prompt)rO   r  r  r   r  r   r  r  rl   r  )r   visual_identity)rO   r  r  r   r  r  ) r   r:   rG   r<   r  r  r   r,   r-   r
   rW  r   rk   r  re   r  rf   r  ry   rn   app.core.bg_state_vocabr  rT   rv   rS   r  r   rx   timesleepr   r   )idxr   rP   rQ   
ent_detaildetail_blockdesctraits_dblock	_excluded_sblock_s_excludedr(   turn_msgattemptdetailr   mdr  r  rn   r  r   exc_msgr  r  r  name_to_sidr   r  world_contexts                           r   _gen_t2iz(EntityT2iStep._execute.<locals>._gen_t2i  s\   'uc:J L!~~mR8:>>/2#FG#f$8B &CNN#>?&" ''>NN=1($ 'KKJs;/
 #.	/ #.Q	 *+})=&'eeJ&7'+ uu^4:AAC(EF #.	/	0 KKRs9~ &/ &/ *+w().C(D-11553H3NB2U2U2W-X*+%%0G*H	 &/	 ,L',UC+'H !8b6,)&3$,(<'+':':$0&*&>&>&@F2 %*CO4:B* 5UB?B 0 22 =M": %rC4l;??5QVX]K^`b;c4lgl$ $)(+(W{uUE?RTV/W+.77="+E-?-D*0**\2*F+.77="+E-/ #&''*>"?>"Q !5nE$&	 [ $\ (**UE400"C ! +eU0CR H#&77="#=EOPSPWPWXgPhjnEoSWW_b%Auw"$264%P	 k/d !)*;< " '.c9d&C&Os3xG"NN!7 %w{E7
 ""D ! 6{

1!#456sF   4AM(1A%M-A2N86-M2#A>N82N597N00N55N88)O%$O%
   rI  uQ   entity_t2i: %r (%s) failed all 3 attempts — excluded from done (failed_count++)running)r  rM  )statusr   )r	  r
  r  r  rN  r#   ),rw   r   ry   rn   rN   r   r+   rS   r-   r   r   rx   r   r   rT   r  load_checkpointrO  r  r,   r  r  r  r   rY  rP  r  r  r  r\  r   r  r  r^  r   r9  errorupdate_progressrU   save_checkpointr   r   r   r[   )Ir   r   r   rn   rM  	_merge_cp_kindr   _er  _vt_recr  _etr  _previtemr   r!  r"  r  r  r  re  r  rg  r   stale_filled_k_v_sd_src_keep_vtr  rj  r  	remainingr:  r  r  r  r  r  t2i_ctxr  rJ  executorfuturesrQ   _sid_of_idxr   rP   futurer9  r  r   r  r  r  r  r  r  r  r  r  r   rL  r  r  r  r  r  sI   `                                                        @@@@@@@@@@@@@@@@r   r!  zEntityT2iStep._execute_  sp   ))/:488F+0$5  F|N3f))*:;btF|?O?OP]_a?b  ..~>D"	
 13

 13)^ME6!f-388?E2E!"d+"&&..B/557 ff_-3c3'!"jjo $&66-#8,6sD,ASr*8
 266*-34::<"&&/526<<>H&'+Jt$
 au~#e4E>)-tK%&584+?(,K%K F ,P	 <	4$  D4yA~$q'7;AwE$q'4734&&tAwQ8 ! !!# dh& FF62"";3 	( 2>>+1a"%1+>1=NR"1RUN3FFF
 1==!Qq!f=
			 		 		 #'::<C<41auQ{Q<ECCJbq	
 *.EAq!1E Z;Gtzz|,B!"d+rvvm,2399;
 "&&..B/"&&/526"&&,23$S#s3488M28b9??A 66/28b#'88M#:#@b2:h,0HH_,E,K*8R r"5 -6 EL'"1+/	" "+<!8
!8:Aq1Aq11Q4"6 1qqtb*!8 	 

 L!	
 	
 % ++,@A-36::fb)ll5"%h+&7uJvhOM\\'2.55%2!'7mR8P7Q%RRM / ,,}b1>wiHHM~	 ~	@ b#i.1K#<>GHi*:1b"cq#viH,5(AueS

1OPGHOOHasKL -6
 +73F#]]_F.4+Ct |B!5
 ,,SY> */D'JNDue[__S"-EFG((TE:((&/-10<% #+4.%	% 4 =L  #4y %!CI-,,.		
 "$'	
 [$ U(4. U(*SU	
 		
c % ! !D ?N >. D FF
D I =<sm   6_21`
``#``` ``1`+`%

`5`-C>`52` `-`55`>r"  )
r   r   r   r   rS   r   r   r   r!  r{  r|  s   @r   r~  r~  T  s(    26c 6S
c3h S
r    r~  r  r"  )Fr   r,  r   loggingr  rZ  r   r   pathlibr   typingr   r   r   r	   app.core.prompt_assemblyr
   r   r   ru   r   r   app.modules.llm.llm_clientr   rw  r   r   r   	getLoggerr   r  rS   r   r   rT  r%   r<   rG   rI   r  rN   r[   rU   r^   rT   	frozensetrf   r   r   r   r   r   r   r   r&  r/  r3  r>  r@  rQ  rT  rW  r  r  r  r  rX  r]  r  r~  ra   r    r   <module>r     s   [     ?  , , 9 + 9 6 1 5			8	$DcN tCy $tCH~./	#Y.$tCH~./
4S#Xd38n!556&*$tCH~./*
3T#s(^$$%*Z0r'T#s(^(<=0r
3T#s(^$$%0rf H -= )3 3C 3c 33 3
m m m
" "C "C "$) $$84S> $8N4S> d38n  l lds s L LC L"KT
 K3 K4: K"NK-z NKb5J,j 5Jp5J(* 5Jp*
(* *
ZT
 T#s(^ T
 %-!1: %-P%- 0* %-P%-,j %-V@
' @
L=
' =
FC
&
 C
L:d4j :$t* :(+:03:N   T\' T\t^
$j ^
r    