
    i0                         d 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mZ ddlmZ ddlmZ ddlmZmZ  ej,                  e      Zd	Zd
ZdZdZ G d de      Zy)uE  shot_essence_extraction StepRunner — shot description을 essence/peripheral/atmospheric 3분류.

Phase 1b (v3 plan 2026-04-29). scene_detail이 모든 시각요소를 한 t2i_prompt에
넣어 카탈로그식 합성을 일으키는 문제를 해결하기 위해, shot description을 사전
분류하여 scene_detail은 essence만 prepend (peripheral/atmospheric은 다른
step이 처리).

설계:
- 입력: shot_validator + shot_selection — selected shot 만 처리
- 출력: per shot {essence, peripheral, atmospheric}
- 처리: shots를 BUNDLE_MAX (15)로 묶어 LLM 호출, MAX_WORKERS (4) 병렬
- 모델: gpt-5.5
- toggle: settings.shot_essence_enabled — applicability="if_shot_essence_enabled"
  (default False, run-all에서 자동 제외)

Phase 2 통합 지점:
- backend/app/core/steps/scene_context_loader.py — essence 체크포인트 로드
- backend/app/core/steps/detail_steps.py::SceneDetailStep._analyze_one —
  user_prompt 앞에 essence prepend (atmospheric은 chain bg ref가 처리)

체크포인트 출력:
  data.shots[].{scene_index, shot_index, essence: [str], peripheral: [str],
                atmospheric: [str], status: "ok"|"failed"}
  data.applicable_count: 입력 shot 수 (총 처리 대상)
  data.succeeded_count:  성공 shot 수 (status='ok')
  data.failed_count:     실패 shot 수 (status='failed')
    N)ThreadPoolExecutoras_completed)Path)AnyDictListOptionalTuple)
StepRunner)call_structured)load_promptload_schema            c                       e Zd ZdZdedee   fdZddeeef   fdZ	de
eeef      deded	edeee
eeef      f   f
d
Zde
eeef      dede
eeef      de
eeef      fdZy)ShotEssenceExtractionStepu6   Shot별 essence/peripheral/atmospheric 3분류 추출.step_idreturnc                     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.jsonzutf-8)encoding)
app.core.configr   r   projects_dir
project_id
episode_idexistsjsonloads	read_text)selfr   r   cps       a/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/shot_essence_extraction_step.py_load_prev_checkpointz/ShotEssenceExtractionStep._load_prev_checkpoint2   su    ,&&'$//9()+/??;'( 	
 99;::bllGl<==    c                    | j                  d      }|r!|j                  di       j                  d      sddlm}  |ddd	      | j                  d
      }i }|rO|j                  di       j                  d      r.|d   d   D ]#  }t	        |j                  dg             ||d   <   % g }|d   d   D ]  }|d   }||vrt
        j                  d|       #||   }	|j                  dg       D ]O  }
|
j                  d      |	v s|j                  ||
j                  dd      |
j                  dd      xs dd       Q  |sdddg dddddS t        dd| j                        }t        dd| j                        }g }t        dt        |      t              D ]  }|j                  |||t        z            t
        j                  dt        |      t        |             i }t        t               5 }t#        |      D ci c]+  \  }}|j%                  | j&                  ||dz   ||      |dz   - }}}t)        |      D ]  }|j+                         \  }}|||<    	 d d d        g }t-        |      D ]  }|j/                  ||           t1        d |D              }t1        d |D              }|rU|D cg c]$  }|j                  d      dk(  rd |d    d!|d    & }}t
        j                  d"|d#j3                  |             t
        j                  d$|t        |      |       |t        |      ||t        |      ||ddS c c}}w # 1 sw Y   xY wc c}w )%Nshot_validatordatascenesr   )AppErrorzstep.no_shotsu   shot_validator 결과 없음i  )codemessagestatus_codeshot_selectionselected_shot_indicesscene_indexuI   shot_essence_extraction: scene %d missing from shot_selection — skippedshots
shot_indexdescription )r3   r5   r6   )r4   applicable_countsucceeded_countfailed_count)completed_countr8   r:   r+   shot_essence_extractionsystem)dbschemau9   shot_essence_extraction: %d selected shots → %d bundles)max_workers   c              3   J   K   | ]  }|j                  d       dk(  sd  ywstatusokrA   Nget.0rs     r&   	<genexpr>z5ShotEssenceExtractionStep._execute.<locals>.<genexpr>   s      J;a!%%/T2I;   ##c              3   J   K   | ]  }|j                  d       dk(  sd  ywrD   failedrA   NrF   rH   s     r&   rK   z5ShotEssenceExtractionStep._execute.<locals>.<genexpr>   s      K1quuX(/JQrL   rD   rO   S_Shotz,shot_essence_extraction: %d shots failed: %sz, z:shot_essence_extraction: %d/%d shots succeeded (%d failed))r'   rG   app.core.errorsr-   setloggerwarningappendr   r>   r   rangelenBUNDLE_MAX_SHOTSinfor   MAX_WORKERS	enumeratesubmit_call_bundler   resultsortedextendsumjoin)r$   modeshot_cpr-   sel_cpselected_maps	all_shotssiselshsystem_promptr?   bundlesiresults_by_idxpoolbundlefuturesfutcall_idxr4   all_results	succeededrO   rJ   failed_keyss                              r&   _executez"ShotEssenceExtractionStep._execute=   s   ,,-=>gkk&"599(C0$6  ++,<=')fjj,00:F^H-14QUU;RTV5W1XQ}-. .
 +-	*A=!B%_ r"CeeGR(66,'3.$$')&(ff\1&=')vvmR'@'FB&  ) +" #$$% !()'($%		
 
 $$=xDGGT6TWWM /1q#i.*:;ANN9Qq+;';<= < 	G	NCL	

 ;=K8D "+7!3!3IAv D--vq1umVTVWZ[V[[!3   $G,"%**,%+0x( - 9 -/'A~a01 (
 J;JJ	KKK %$Ah8(C Am$%U1\?*;<$   NN>		+.
 	Hs9~v	
  ) #I"$$'	N#, &		

 
	
C 98(s$   7M0M
6+M:)M
MMrr   ru   rm   r?   c                    |D ch c]  }|d   |d   f }}g }|D ]%  }|j                  d|d    d|d    d|d           ' ddj                  |      z   }d	|d
   d    d|d   d    }	d}
t        t        dz         D ]  }	 t	        d|||| j
                  d| | j                               }|j                  dg       }| j                  |||      }t        j                  d||	t        |      t        d |D              t        d |D                     ||fc S  t        j                  d||	t        |
       |D cg c]  }|d   |d   g g g dd }}||fS c c}w # t        $ r_}|}
|t        k  rIt        |dz   z  }t        j                  d||	|dz   t        ||       t!        j"                  |       Y d}~?Y d}~ d}~ww xY wc c}w )u!  단일 번들 LLM 호출. retry 포함. 최종 실패 시 stub(status='failed') 반환.

        반환 결과는 항상 입력 bundle과 동일한 (scene_index, shot_index) set.
        LLM 응답에 누락/extra/중복이 있어도 입력 keys 기준으로 정렬 + stub 채움.
        r3   r5   z
--- Scene z Shot z ---
description: r6   u   아래 샷들의 description을 essence/peripheral/atmospheric 3분류로 나누세요.
각 샷마다 scene_index/shot_index/essence/peripheral/atmospheric 모두 출력.

[분석 대상 샷]
rP   r   z-SNrA   r<   shot_essence_)steprm   user_promptresponse_schemaproject_configschema_nameopik_metadatar4   zAshot_essence_extraction call %d (%s, %d shots): %d ok / %d failedc              3   J   K   | ]  }|j                  d       dk(  sd  ywrC   rF   rH   s     r&   rK   z9ShotEssenceExtractionStep._call_bundle.<locals>.<genexpr>   s      E6aQUU8_-D6rL   c              3   J   K   | ]  }|j                  d       dk(  sd  ywrN   rF   rH   s     r&   rK   z9ShotEssenceExtractionStep._call_bundle.<locals>.<genexpr>   s      I6aQUU8_-H6rL   z@shot_essence_extraction call %d (%s) retry %d/%d (sleep %ds): %sz@shot_essence_extraction call %d (%s) FAILED after %d retries: %srO   r3   r5   essence
peripheralatmosphericrD   )rV   rc   rW   	MAX_RETRYr   r   build_opik_metadatarG   _merge_with_input_keysrT   rZ   rX   rb   	ExceptionRETRY_BACKOFF_BASErU   timesleep)r$   rr   ru   rm   r?   rl   
input_keysshots_linesr   scene_rangelast_excattemptr_   	raw_shotsmergedexcdelaystubss                     r&   r^   z&ShotEssenceExtractionStep._call_bundle   sM    GMMfr-("\*:;f
M!#Br-01<8H7I J  "= 124 $ ii$% 	 &)M232fRj6O5PQ(,Y]+G(2"/ +$*#'#6#6"/z :"&":":"< #JJw3	44VZSWk3v;E6EEI6II	  ''' ,B 	Nk9h	
 

   "-0 . !"  	 

 A NH  
Y&.'A+>ENNZ +w{Iuc JJu%
"

s%   EBE"G"	G
+AGG
r   r   c                    i }|D ]  }|j                  d      }|j                  d      }||t        j                  d       ?t        |      t        |      f}||vrt        j                  d|d   |d          w||v rt        j                  d|d   |d          |d   |d   t	        |j                  dg             t	        |j                  d	g             t	        |j                  d
g             dd||<    g }	|D ]`  }
|
d   |
d   f}||v r|	j                  ||          &t        j                  d|d   |d          |	j                  |d   |d   g g g dd       b |	S )u   LLM 응답을 입력 bundle 키 set과 1:1 매칭. 누락/extra/중복 모두 처리.

        - 입력에 없는 키: 무시 + warning
        - 응답 중복: 첫 번째만 사용 + warning
        - 응답 누락: stub(status='failed') 채움
        r3   r5   uH   shot_essence_extraction: response item missing scene/shot index — dropuD   shot_essence_extraction: extra key in response (S%d Shot%d) — dropr   rA   uI   shot_essence_extraction: duplicate response for S%d Shot%d — keep firstr   r   r   rE   r   uD   shot_essence_extraction: response missing S%d Shot%d — stub failedrO   )rG   rT   rU   intlistrV   )r$   rr   r   r   by_keyrJ   rj   shikeyr   rl   s              r&   r   z0ShotEssenceExtractionStep._merge_with_input_keys   s    9;A}%B%%%CzS[ijr7CH%C*$ZFCF f}_FCF "1v!!fi 45"155r#:;#AEE-$<=F3K' : (*Bm$b&67Cf}fSk*ZFCF #&q6"%a&!"$#%&  " r(   N)resume)__name__
__module____qualname____doc__strr	   r   r'   r   ry   r   r   dictr
   r^   rS   r    r(   r&   r   r   /   s    @	S 	Xd^ 	l
c3h l
\LT#s(^$L L 	L
 L 
sDc3h((	)L\<T#s(^$< < S#X'	<
 
d38n	<r(   r   )r   r!   loggingr   concurrent.futuresr   r   pathlibr   typingr   r   r   r	   r
   app.core.step_runnerr   app.modules.llm.llm_clientr   app.modules.prompt_loaderr   r   	getLoggerr   rT   rY   r[   r   r   r   r   r(   r&   <module>r      s`   6    ?  3 3 + 6 >			8	$ 	 F
 Fr(   