
    2vj*                         d Z ddlZddlZddlmZ ddlmZmZmZm	Z	 ddl
mZ  ej                  e      Z G d d      Z G d d	ee      Z G d
 dee      Z G d dee      Zy)uD   씬 Phase StepRunner -- scene_segmentation, scene_split, scene_save.    N)Path)AnyDictListOptional)
StepRunnerc                   >    e Zd ZdZdefdZdefdZdedee   fdZ	y)_SceneStepMixinuB   씬 단계 공통 -- fulltext 로드, 이전 단계 결과 로드.returnc                     | j                  d      }|r)|j                  di       j                  d      r|d   d   S | j                         S )u6   text_cleanup 결과 사용, 없으면 원본 fulltext.text_cleanupdatacleaned_text)_load_prev_checkpointget_load_fulltext)selfcps     P/Users/manta/Documents/Projects/TheRoad-I1/backend/app/core/steps/scene_steps.py_load_cleaned_textz"_SceneStepMixin._load_cleaned_text   sK    ''7"&&$((8f:n--""$$    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)undeferAppErrorzstep.no_fulltextu'   시나리오 텍스트가 없습니다.  codemessagestatus_code)app.models.projectr   sqlalchemy.ormr   dbqueryoptionsfulltextfilterid
episode_idfirstapp.core.errorsr   )r   r   r   epr   s        r   r   z_SceneStepMixin._load_fulltext   s    .* GGMM'"WWW--./VGJJ$//12UW	 	 0'A 
 {{r   step_idc                     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.configr1   r   projects_dir
project_idr*   existsjsonloads	read_text)r   r.   r1   r   s       r   r   z%_SceneStepMixin._load_prev_checkpoint+   s    , &&'oo  oo	
   	 99;::bllGl<==r   N)
__name__
__module____qualname____doc__strr   r   r   r   r    r   r   r
   r
      s4    L%C % (S Xd^ r   r
   c                   ,    e Zd ZdZdZddeeef   fdZy)SceneSegmentationStepu  Step 1: 씬 세그먼테이션 — LLM 은 그 대본용 규칙을 쓰고, 코드가 경계를 찍는다.

    설계 = docs/superpowers/specs/2026-08-08-scene-segmentation-rule-authoring-design.md

    ## 무엇이 달라졌나 (2026-08-08)

    전에도 LLM 이 정규식을 만들었지만 **후보를 하나만 받고 검증이 평균 길이
    하나**였다. 그래서 "개수는 맞는데 경계가 틀린" 규칙이 그대로 통과했다 —
    22개 대본 실측에서 그런 결함을 넷 찾았고, 넷 다 숫자만 봐서는 안 드러났다.

    지금은 후보를 2~3개 받아 기계 계약(C1~C7)으로 재고, 통과한 후보끼리 **경계가
    갈린 자리를 양쪽 다** 되돌려주고 다시 쓰게 한다. 한쪽만 보면 정답을 통째로
    포함하는 넓은 규칙이 조용히 이긴다(재현함).

    ★상한을 다 쓰고도 차이가 남으면 성공으로 두지 않는다. 코드가 이미 아는
    차이를 남긴 채 확정하면 대본 전체가 밀린 채 하류로 내려간다.
       r   c                 D   ddl m} ddlm} | j	                         } ||| j
                  d| j                  | j                  | j                        \  }}}|D ]  }|j                  d      rt        j                  d|d   |d          2t        j                  d	|d   t        |j                  d
      xs g       t        d |j                  d
      xs g D              |j                  d      rd|d    nd        |sjddlm}	 |r|d   ni }
|
j                  d      rd|
d   d    d|
d   d    d}n|
j                  d      r|
d   }nd} |	dd| j
                   d| d      t        j                  d|j"                  d d |j$                  |j&                         g }t)         |||            D ]  \  }}t        |j*                        dkD  r8t        j                  d |d!z   t        |j*                        |j*                         |j-                  |d!z   |j*                  |j.                  |j0                  |j0                  |j.                  z
  d"        d!d!d|t        |      |j2                  |j"                  |j4                  |j6                  d#d$d%S )&Nr   )
apply_rule)author_and_choosescene_segmentation)
max_roundsstepproject_configr7   r*   erroru&   scene_segmentation %d회차 실패: %sroundu;   scene_segmentation %d회차: 후보 %d개 · 통과 %d개%s
candidatesc              3   D   K   | ]  }|j                  d       sd  yw)ok   Nr   ).0cs     r   	<genexpr>z1SceneSegmentationStep._execute.<locals>.<genexpr>h   s     I9!QUU4[A9s     retry_reasonu    · 재저작 이유= r   
unresolvedu4   후보끼리 경계가 갈린 채 남았다 (놓침 missedu    · 저 혼자 잡음 extra)u$   계약을 통과한 규칙이 없다zstep.segmentation_failedu   씬 세그먼테이션 실패 (u   회 시도): i  r   u<   scene_segmentation 확정: %s · %d개 · 헤딩 내용 %.2f<      u<   Scene %d heading 비정상적으로 김 (%d chars): %.60s...rQ   )scene_indexheading
start_charend_charlength)namepatternnumber_group	rationale)segmentstotal_scenesrulecompleted_countapplicable_countfailed_countr   )!app.modules.pipeline.segment_rulerF   (app.modules.pipeline.segment_rule_authorrG   r   	MAX_RETRYrK   r7   r*   r   loggerwarninginfolensumr,   r   re   match_countheading_content	enumerater`   appendra   rb   rd   rf   rg   )r   moderF   rG   r'   chosenverdictroundsrdr   lastwhyrh   isegs                  r   _executezSceneSegmentationStep._executeQ   s   @N**,"3~~%..#
 BvvgG!'{BwK9KKM7S!5!;<Irvvl39r9IIACAW(N);(<=]_	  0!'6":RDxx%""&|"4X">!? @,,0,>w,G+HK '"7m</9$..9IWZV[\  	RNN3B')<)<++	- 
8V <=FAs3;;#%] 1uc#++&6EOO 1u;;!nnLL,,7 	 >  ! !!)3x=&,kkfnn.4.A.A+1+;+;=>	
 	
r   Nresume)	r<   r=   r>   r?   rq   r   r@   r   r   rA   r   r   rC   rC   <   s$    $ IH
c3h H
r   rC   c                   4    e Zd ZdZdefdZddeeef   fdZ	y)SceneSplitStepu#   Step 5: 큰 씬 분할 (조건부).r   c                 p   | j                  d      }|r!|j                  di       j                  d      syddlm} | j                  j                  |      j                  |j                  | j                  k(        j                         }|r|j                  nd|d   d   }t        fd|D              S )	u-   threshold 초과 씬이 있을 때만 적용.rH   r   rh   Fr   ProjectSettingsX  c              3   H   K   | ]  }|j                  d d      kD    yw)rc   r   NrR   )rS   s	thresholds     r   rU   z5SceneSplitStep.check_applicability.<locals>.<genexpr>   s"     D8a1551%	18s   ")r   r   r"   r   r$   r%   r(   r7   r+   scene_split_thresholdany)r   seg_cpr   psrh   r   s        @r   check_applicabilityz"SceneSplitStep.check_applicability   s    ++,@AVZZ377
C6 GGMM/*VO..$//ABUW 	
 13B,,	&>*-D8DDDr   c           
      (   | j                  d      }|d   d   }| j                         ddlm} | j                  j                  |      j                  |j                  | j                  k(        j                         }|r|j                  ndddl
m ddlm}m} t        |      D 	cg c]  \  }}	|	j!                  d	d      kD  s||	f! }
}}	t        |      D 	cg c]  \  }}	|	j!                  d	d      k  s||	f! }}}	i }fd
}d}ddlm} t'        t)        |
      xs d|j*                        } ||      5 }|
D 	ci c]  \  }}	|j-                  |||	      | }}}	 ||      D ]1  }||   }	 |j/                         \  }}|||<   |t)        |      dz
  z  }3 	 d d d        g }t        |      D ]/  \  }}	||v r|j7                  ||          |j9                  |	       1 t        |      D ]  \  }}	|dz   |	d<    |dz   t)        |      d|t)        |      |ddS c c}	}w c c}	}w c c}	}w # t0        $ r=}t2        j5                  d||   j!                  d      |       ||   g||<   Y d }~
d }~ww xY w# 1 sw Y   xY w)NrH   r   rh   r   r   r   )_split_large_scene_by_llm)ThreadPoolExecutoras_completedrc   c                 R    |j                  d|d   |d          }|  ||      fS )Ntextra   rb   rR   )idxr   
scene_textr   r'   r   s      r   
_split_onez+SceneSplitStep._execute.<locals>._split_one   s8    #l2CC
O)TUJ1*c9MMMr   r0   rQ   )max_workerszSplit failed for scene %s: %sr_   )rh   ri   splitsrk   )r   r   r"   r   r$   r%   r(   r7   r+   r   'app.modules.pipeline.scene_extractor_v2r   concurrent.futuresr   r   ry   r   r5   r1   minru   max_concurrent_entity_detailsubmitresult	Exceptionrr   rs   extendrz   )r   r{   r   rh   r   r   r   r   r   r   
large_segs
small_segssplit_resultsr   split_countr1   r   poolfuturesfr   _subexcfinal_segmentsr   r'   r   s                            @@@r   r   zSceneSplitStep._execute   s   ++,@A&>*-**,6 GGMM/*VO..$//ABUW 	
 13B,,		
 	H-6x-@e-@61cCGGHVWDX[dDdq#h-@
e-6x-@f-@61cCGGHVWDX\eDeq#h-@
f/1	N ,#j/.Q0U0UVK8DHRS
fat{{:q#69
GS!'*aj9XXZFAs),M#&3s8a</K + 9 02)FAsM!%%mA&67%%c*	 *  /FAs!"QC 0  +Q #H* #N 3%		
 		
I ff T ! 9NN#BHSMDUDUVcDdfij*23-M#&9 98s`   .H-H-#H3H3JH9+J?)H?(J9J?	J2J :J JJJNr   )
r<   r=   r>   r?   boolr   r   r@   r   r   rA   r   r   r   r      s)    -ET E @
c3h @
r   r   c                   (    e Zd ZdZddeeef   fdZy)SceneSaveStepu  Step 6: 씬 저장 (DB + checkpoint). LLM 호출 없음.

    scene_segmentation 결과(오프셋)를 받아 fulltext를 슬라이스하여
    각 segment에 text 필드를 추가한다.
    이후 downstream 스텝은 seg["text"]만 사용하고 fulltext 로딩 불필요.
    r   c                    | j                  d      }|r*|j                  di       j                  d      r	|d   d   }nddlm}  |ddd	      | j	                         }|D ]7  }|j                  d
d      }|j                  dt        |            }||| |d<   9 t        |      t        |      d|t        |      ddS )NrH   r   rh   r   r   zstep.no_inputu$   씬 세그먼테이션 결과 없음r   r   ra   rb   r   )rh   ri   rk   )r   r   r,   r   r   ru   )	r   r{   r   rh   r   r'   r   startends	            r   r   zSceneSaveStep._execute   s    ++,@Afjj,00<f~j1H0$>  **,CGGL!,E''*c(m4C"5-CK   #8} #H!)3x=I	
 	
r   Nr   )r<   r=   r>   r?   r   r@   r   r   rA   r   r   r   r      s    
c3h 
r   r   )r?   r9   loggingpathlibr   typingr   r   r   r   app.core.step_runnerr   	getLoggerr<   rr   r
   rC   r   r   rA   r   r   <module>r      sg    J    , , +			8	$, ,^]
OZ ]
@S
_j S
l$
OZ $
r   