
    -ǚ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mZ ddlmZ ddlmZ ddlmZ  ej,                  e      Zdd	lmZ dd
lmZ ddlmZ ddlm Z m!Z!m"Z"m#Z#m$Z$m%Z%m&Z& ddl'Z(ddl)m*Z* ddl+m,Z, ddl-m.Z. ddl/m0Z0 ddl1m2Z2 de3fdZ4de3fdZ5 ejl                  d      Z7de
de
de
fdZ8de
dee3   fdZ9d)de
de3de3fdZ:de
dee   fdZ; G d d      Z<d*d ed!ed"e=ddfd#Z>d$e3de3fd%Z?d&e3d'e3de3fd(Z@y)+uK   내보내기 서비스 — 웹북 생성 및 PDF 렌더링 파이프라인.    N)datetimetimezone)Path)AnyDictListOptionalSet)Session)settings)resolve_image_path)AppError)t)ActivityLogger)EpisodeEntityCanonEntityEpisodeLink
ImageAsset
SceneStillWebbookPackage
WorldGuide)PDFRenderer)PDFValidator)ProgressTracker)ProvenanceRecorder)WebbookGeneratorreturnc                  d    t        j                  t        j                        j	                         S N)r   nowr   utc	isoformat     Q/Users/manta/Documents/Projects/TheRoad-I1/backend/app/services/export_service.py_nowr&   '   s    <<%//11r$   c                  <    t        t        j                               S r   )struuiduuid4r#   r$   r%   _new_idr+   +   s    tzz|r$   z#^(?:[CLP]\d{2,3}|C\d{2,3}O\d{2,3})$valuedefaultc                     | | dk(  r|S t        | t        t        f      r| S t        | t              s|S 	 t	        j
                  |       S # t        t        j                  f$ r |cY S w xY w)zILoad JSON-like DB fields while tolerating already-decoded current shapes. )
isinstancelistdictr(   jsonloads	TypeErrorJSONDecodeError)r,   r-   s     r%   _safe_json_loadr7   6   sg    }%$&eS!zz%  t++, s   A
 
A('A(c                    t        | g       }t        |t              r;|j                  d      xs( |j                  d      xs |j                  d      xs g }t        |t              s
t               S t               }|D ]  }d}t        |t              r|j                         }nnt        |t              r^dD ]Y  }|j                  |      }t        |t              s%t        j                  |j                               sI|j                         } n |rt        j                  |      sd|v r&|j                  d      r|j                  dd      d	   }|j                  d
      s|j                  |        |S )zMExtract C/L/P short ids from current and legacy visible_entities_json shapes.visible_entitiesentitiesidsr/   )short_idid	entity_idOC   r   )r@   LP)r7   r0   r2   getr1   setr(   strip_SHORT_ID_RE	fullmatch
startswithsplitadd)r,   rawoutitemsidkey	candidates          r%   _visible_short_ids_from_jsonrR   D   s;   
%
$C#tGG&' wwz"wwu~ 	 	 c4 uECdC **,Cd#6 HHSM	i-,2H2HIZ2[#//+C	 7
 ,005#:#..-))C#A&C>>/*GGCL   Jr$   fallbackc                    t        | g       }t        |t              r)|j                  d      xs |j                  d      xs |g}g }t        |t              r|D ]  }t        |t
              r|}n:t        |t              r)|j                  d      xs |j                  d      xs d}nPt        |t
              sa|j                         sr|j                  |j                                 dj                  |      xs |xs dS )zOExtract prompt text from current rich variation arrays and older prompt shapes.t2i_variations
variations
t2i_promptpromptr/    )	r7   r0   r2   rD   r1   r(   rF   appendjoin)r,   rS   rL   textsrN   texts         r%   _t2i_text_from_variations_jsonr^   e   s    
%
$C#tgg&'I377<+@ISEE#tD$$D$'xx-I(1CIr$$TZZ\*  88E?,h,",r$   	file_pathc                     | syt        t        |             }|r|j                         r|S t        t        |             }|j                         r|S dS )zRResolve ImageAsset.file_path for both absolute ORM reads and legacy relative rows.N)r   r(   existsr   )r_   prL   s      r%   _existing_asset_pathrc   x   sG    3y>*AQXXZ
s9~
C**,3(D(r$   c                   *   e Zd ZdZdedededdfdZdedefd	Zde	fd
Z
de	fdZ	 	 	 ddedededee   deeef   f
dZ	 ddedee   dee   fdZ	 ddedee   deeef   fdZ	 	 ddedee   dedefdZddede	dedefdZdeeeef      fdZy)ExportServiceu*   웹북 생성 및 PDF 렌더링 서비스.db
project_idactor_idr   Nc                 N    || _         t        |      | _        || _        || _        y r   )_dbr   _logger_project_id	_actor_id)selfrf   rg   rh   s       r%   __init__zExportService.__init__   s&     %b)%!r$   
episode_idc                 
   | j                   j                  t              j                  t        j                  |k(  t        j
                  | j                  k(        j                         }|st        dt        d      d      |S )Nzepisode.not_found  codemessagestatus_code)
rj   queryr   filterr=   rg   rl   firstr   r   )rn   rp   eps      r%   _get_episodezExportService._get_episode   sl    HHNN7#VGJJ*,g.@.@DDTDT.TUUW 	
 (-. 
 	r$   c                 N    t        t        j                        | j                  z  S r   )r   r   projects_dirrl   )rn   s    r%   _get_project_dirzExportService._get_project_dir   s    H))*T-=-===r$   c                 X    | j                         dz  dz  }|j                  dd       |S )NassetsexportsT)parentsexist_ok)r~   mkdir)rn   ds     r%   _get_exports_dirzExportService._get_exports_dir   s/    !!#h.:	t,r$   web_episode_countsections_per_episodeipc           
         | j                  |      }|j                  st        dt        d      d      ddlm}  |       st        dt        d      d      |j                  xs d}ddlm}  || j                  | j                  |      }	|	rY| j                  j                  t              j                  t        j                  j                  |	            j!                         ng }
|
D cg c]D  }|j                  |j"                  |j$                  |j&                  xs d	|j(                  xs d
dF }}ddlm} | j                  j                  t.              j                  t.        j0                  | j                  k(  t.        j2                  |k(   |t.        j4                  j7                  d      t.        j4                  dk7              j9                  t.        j:                        j!                         }|D cg c]H  }|j                  |j:                  |j<                  xs d	|j>                  xs d	|j@                  xs d	dJ }}| j                  j                  tB              j                  tB        j0                  | j                  k(  tB        j2                  |k(        j9                  tB        jD                  jG                               jI                         }|rtK        jL                  |jN                        ni }tQ        | j                  | j                        }tS        | j                  |d| j                        }|jU                  ddd       ddl+m,} | j                  j                  |      j                  |j0                  | j                  k(        jI                         }d}|r,|jZ                  r 	 tK        jL                  |jZ                        }tc        |      }	 |je                  dd|      5 }|jg                  ti        |j                        ti        |      ti        |      ||d       |jk                  |j                  ||||||      }ti        |jm                  dg             }|jo                  d|i       ddd       tq        ts               | j                  |tK        jt                  d      dtw                      }| j                  jy                  |       | j                  j{                          |j}                          | j                  j                  | j                  d!d"|| j                  ||d#|$       |S c c}w c c}w # t\        $ r,}t^        ja                  d| j                  |       Y d}~d}~ww xY w# 1 sw Y   xY w# t\        $ r }|j                  t        |              d}~ww xY w)%z(Generate webbook package for an episode.zanalysis.no_text  rs   r   has_openai_keyanalysis.openai_key_missingko)active_episode_canon_idsr/   {})r=   nameentity_typedescriptionstable_traitsor_Nstale)r=   still_indexscreenplay_scene_heading
beat_titlestill_frame_promptwebbooku   웹북 텍스트 생성 중rA   )ProjectSettingsuN   llm_config_json parse failed for project %s: %s — 기본 설정으로 진행)project_llm_configwebbook_generationwebbook_generatorrp   )fulltext_charsr:   stillsr   r   )fulltextr:   r   world_guidelanguager   r   episodesepisodes_generatedF)ensure_asciiv5)r=   rg   rp   package_jsonprompt_version
created_atzexport.generate_webbookepisode)r   r   rh   actionresource_typeresource_idrg   detail
ip_address)Dr{   r   r   r   app.core.openai_keysr   r   app.core.entity_identityr   rj   rl   rw   r   rx   r=   in_allr   r   r   r   
sqlalchemyr   r   rg   rp   statusis_order_byr   r   r   r   r   r   descry   r3   r4   
guide_jsonr   r   updateapp.models.projectr   llm_config_json	Exceptionloggerwarningr   start_operation	set_inputlengeneraterD   
set_outputr   r+   dumpsr&   rK   commitcompletefailr(   rk   logrm   )rn   rp   r   r   r   r   r   r   r   	canon_idsentities_ormer:   r   
stills_ormsr   	wg_recordr   
provenanceprogressr   _ps_llm_configexc	generatoroppackageep_count	wb_records                                 r%   generate_webbookzExportService.generate_webbook   s    ##J/',-  	8278  ##+t 	F,HHd&&
4	  HHNN;'VKNN&&y12SU 	 "	
 " dd }} }}2!"!8D " 	 	
 	#HHNN:&V%%)9)99%%3J%%))$/1B1Bg1MN
 Xj,,-SU 	$  	
   dd }},-,F,F,L"ll0b&'&:&:&@b   	 	
 HHNN:&VJ))T-=-==z?T?TXb?bcXj++0023UW	 	 1:DJJy++,r 	
 ($2B2BC
"488ZDDTDTU5q!< 	7hhnn_-44_5O5OSWScSc5cdjjl3&&H"jj)<)<= %D	&	++$&9j , &)'*:*:&; #H!&k):,@  $,,$--%! +%&7)= -  w{{:r:;3X>?). '9++%!ZZeD#6I HHLL#HHOO
 	^^,#"''%6(<  	 	
 g	
2	
D  Hoqu  rB  rB  DG  H  HH F  	MM#c(#	sX   .A	U?AU U ,V$  BVBV$ 	V(!VVV!V$ $	W-WWc                    | j                  |       | j                  j                  t              j	                  t        j
                  | j                  k(  t        j                  |k(        j                  t        j                  j                               j                         }|st        dt        d      d      t        j                  |j                         }| j                  j                  t"              j	                  t"        j
                  | j                  k(  t"        j                  |k(  t"        j$                  dk(        j'                         }i }|D ]I  }|j(                  s|j*                  dv s|j-                  |j(                  g       j/                  |       K i }|j1                         D ]  \  }	}
t3        d |
D        d      }|r|j4                  ||	<   +|
D cg c]  }|j6                  xs ddk(  s| }}|r'|j9                  d	 d
       |d   j4                  ||	<   w|
j9                  d d
       |
d   j4                  ||	<    | j;                         }t=               }t?        | j                  | j                        }tA        | j                  |d| j                        }|jC                  ddd       	 |jE                  dd|      5 }|jG                  tI        |jK                  dg             tI        |      d       |jM                  |||      }|jO                  dtI        |      i       ddd       |jQ                          D cg c]  }tW        |       }}| jX                  j[                  | j\                  dd|| j                  dtI        |      i|       |S c c}w # 1 sw Y   yxY w# tR        $ r }|jU                  tW        |              d}~ww xY wc c}w )z3Render PDFs from existing webbook package + images.zexport.no_webbookr   rs   scene)	generatedapprovedc              3   :   K   | ]  }|j                   s|  y wr   
is_primary).0is     r%   	<genexpr>z,ExportService.render_pdfs.<locals>.<genexpr>t  s     <t!q||At   Noriginalc                 "    | j                   xs dS Nr/   r   r   s    r%   <lambda>z+ExportService.render_pdfs.<locals>.<lambda>{  s    Q\\-?R-?r$   T)rP   reverser   c                 "    | j                   xs dS r   r   r   s    r%   r   z+ExportService.render_pdfs.<locals>.<lambda>  s    ALL$6B$6r$   
pdf_renderu   PDF 렌더링 중rA   pdf_renderingpdf_rendererr   r   )episodes_in_packageimages_available)r   images
output_dir	pdf_countzexport.render_pdfr   r   )/r{   rj   rw   r   rx   rg   rl   rp   r   r   r   ry   r   r   r3   r4   r   r   
asset_typer   still_idr   
setdefaultrZ   itemsnextr_   variant_typesortr   r   r   r   r   r   r   r   rD   
render_allr   r   r   r   r(   rk   r   rm   )rn   rp   r   r   r   
images_ormstill_imagesimg	image_mapr   imgsprimaryr   	originalsexports_dirrendererr   r   r   pathsr   rb   results                          r%   render_pdfszExportService.render_pdfsD  s    	*% HHNN>*V))T-=-==))Z7 Xn//4467UW 	 (-.  **Y334
 HHNN:&V%%)9)99%%3%%0
 SU 	 )+C||

.G G''b9@@E  %'	*002NHd<t<dCG&-&7&7	(#$(YDqQ^^-Izj,XDIY#?N&/l&<&<	(#II6IE"&q'"3"3Ih 3" ++-='$2B2BC
"488ZtGWGWX+Q2	++J , SZQS9T5Uknoxkyz{ ++#$* , 
 {CJ78 
 #((%Q#a&%(^^&#"''V- 	 	
 [ Z"   	MM#c(#	 )sC   *N8N8
O	 A'N=O	 !O5=OO	 		O2O--O2filenamec                    ddl m}  |       st        dt        d      d      | j	                         }t        |      j                  }||z  }|j                         j                  |j                               st        ddd      |j                         r|j                         st        dt        d      d	      t               }|j                  |      }| j                  j                  | j                  d
d|| j                   |j#                  dd      t%        |j#                  dg             d|       |S )Nr   r   r   r   rs   zexport.invalid_pathzInvalid filenamezexport.file_not_foundrr   zexport.validate_pdfexportoverall_qualityissues)r  issues_countr   )r   r   r   r   r   r   r   resolveis_relative_tora   is_filer   validaterk   r   rm   rl   rD   r   )	rn   r  r   r   r  safe_filenamepdf_path	validatorr  s	            r%   validate_pdfzExportService.validate_pdf  s3   
 	8278  ++-X++. !001D1D1FG 5?Q_bcc (8(8(:,12  !N	##H-^^(" ''#)::.?#C #FJJx$< =  	 	
 r$   r   c                 	   ddl }ddl}ddlm}m} ddlm} ddlm}	 ddl	m
}
  |
| j                  | j                        }| j                  j                  t              j                   |t        j                               j#                  t        j$                  |k(  t        j&                  | j                  k(        j)                         }|r|j                   st+        ddd	
      | j                  j                  t              j                   |t        j                               j#                  t        j&                  | j                  k(        j-                  t        j.                        j1                         }|j3                         5 }t5        |      }|dz  }|j7                          t9        | j                  j                  t:        j<                  |j?                  t:        j$                              j#                  t:        j&                  | j                  k(  t:        j<                  jA                  |D cg c]  }|j$                   c}       |t:        jB                  jE                  d      t:        jB                  dk7              jG                  t:        j<                        j1                               }d}|D ]  }|j                   s|jI                  |j$                  d      dk(  r7tJ        jM                  d|j$                  |j.                  |jN                         f| jQ                  |||      }| d|j.                   d}||z  jS                  |d       |dz  } |dk(  rt+        ddd	
      | jU                         }tW        jX                  dd|      j[                         j]                  dd      xs d}|rdnd}t_        j`                         jc                  d      }|je                  d      D ]$  }d|jf                  v |k(  s|ji                          & || d| d| d z  }|jk                  tm        |jo                  d            d!|       ddd       | jp                  js                  | jt                  d"d#|| j                  tw        |      jf                  d$|%       tm        |      S c c}w # 1 sw Y   exY w)&u  씬별 원본 텍스트 + 이미지를 HTML ZIP으로 생성.

        에피소드별 HTML 파일 분리, 프로젝트명 기반 고정 파일명, 항상 덮어쓰기.

        original=False(기본): 모든 이미지를 JPEG 로 변환(용량 절감).
        original=True: 변환 없이 원본 파일(주로 PNG) 그대로 export — 무손실/풀해상도.
        JPG/PNG ZIP 은 파일명 태그로 구분해 둘 다 보존된다.
        r   N)funcr   )undefer)ProjectRegistry)ensure_english_namezexport.no_textu'   시나리오 텍스트가 없습니다.r   rs   r   r   zVexport.html_zip skip episode without analyzed scene_still: ep_id=%s ep_num=%s title=%r)r   z epz.htmlutf-8encodingrA   zexport.no_analyzed_episodesu<   ZIP 으로 내보낼 분석 완료 episode 가 없습니다.z[^a-zA-Z0-9 ]r/   rY   _ProjectPNGJPGz%Y%m%d_%H%Mz*.zip_PNG_.zipzipzexport.generate_html_zipr   )r   r,  r   )<shutiltempfiler   r  r   sqlalchemy.ormr   app.models.catalogr!  app.services.project_servicer"  rj   rl   rw   r   optionsr   rx   r=   rg   ry   r   r   episode_numberr   TemporaryDirectoryr   r   r2   r   rp   countr   r   r   group_byrD   r   infotitle_build_episode_html
write_textr   resubrF   replacer   r    strftimeglobr   unlinkmake_archiver(   with_suffixrk   r   rm   r   )rn   rp   r   r   r-  r.  r  r   r   r!  r"  project_namer   all_episodestmp_dirtmpimg_dirrz   still_countswrittenep_htmlep_filenamer  	safe_namekindtsold_zipzip_paths                               r%   generate_html_zipzExportService.generate_html_zip  s    	(*6D*488T5E5EF ((..)11G$$%

&JJ*$$"2"22
 %' 	 g.. 0:cqtuu HHNN7#WWW--./VG&&$*:*::;Xg,,-SU 	 ((*gw-CHnGMMO  z44djj6OP))T-=-==))--|.L|ruu|.LM
))--d3Z5F5F'5QR
 *//0	L G"{{##BEE1-2KKpr00"(( 222w2R!-c"2C2C1DEJ{"..w.I1 # !|6Z #  //1K/\BHHJRRSVX[\i`iI$5%D((7B '++G4w||+8NN$ 5 #	{!D62$d%CCHH$8$8$< =ucJi +l 	^^-#"'' #L 1(--H 	 	
 8}k /M +*s'   B/S>SGSASSSr   rG  c                 D  PQRSTUVWXYZ[\ ddl }ddlUddl[ddlm} ddlm}m} ddlm	}  |j                  t              T|j                  W|j                  }	|j                  xs d|	 }
| j                  j!                  t"              j%                  t"        j&                  | j(                  k(  t"        j*                  |j,                  k(   |t"        j.                  j1                  d      t"        j.                  dk7              j3                  t"        j4                        j7                         }|st9        |
d      S t;        t<        j>                        | j(                  z  d	z  d
z  |j,                  z  }g }dD ]z  }||z  dz  }|jA                         s	 tC        jD                  |jG                  d            }|jO                  di       jO                  dg       }tQ        |tR              su|sx|} n |D ci c])  }tQ        |tT              r|jO                  d      |d   |+ c}Z| j                  j!                  tV              j%                  tV        j&                  | j(                  k(  tV        j*                  |j,                  k(  tV        jX                  dk(        j3                  tV        jZ                        j7                         }ddh}i }|D ]  }|j.                  rA|j.                  |vr3Tj]                  d|j,                  |j^                  |j.                         P|j^                  s]|ja                  |j^                  g       jc                  |         |tR              }|D ]:  }|jd                  |jd                  n|j4                  }||   jc                  |       < |D ]  }||   jg                  d         dth        dtj        t"           dtl        fWZfd}dtl        dtj        tl           fUfd}dth        dtj        tl           d tj        tT           dtj        th           fTfd!}i }i } |d"#      5 } |jo                         D ]  \  }}! |||!      }" ||"      }#|!D cg c]1  }|jp                  xs d|jr                  xs |jt                  xs d$d%3 }$}tw        |!      dkD  r(tw        |#      d&kD  r| jy                  |||#|$      }%|||%<   tw        |#      xs d&gtw        |!      z  ||<     ||      D ]  }%||%   }	 |%j{                         ||<    	 ddd       | j                  j!                  tV              j%                  tV        j&                  | j(                  k(  tV        jX                  d'k(  tV        j~                  d&k(        j7                         }&|&D 'ch c]  }'|'j                  s|'j                   }(}'i })|(rm| j                  j!                  t              j%                  t        j,                  j                  |(            j7                         D ]  }*|*|)|*j,                  <    i X|&D ]-  }'|'j                  s|'j                  Xvs|'X|'j                  <   / | j                  j!                  tV              j%                  tV        j&                  | j(                  k(  tV        jX                  d'k(  tV        j                  j                  d(            j7                         }+i V|+D ]M  },Uj                  d)|,j                  xs d$      }-|-s&|,V|-j                  d&       d*|-j                  d+       <   O | j                  j!                  t              j%                  t        j&                  | j(                  k(        j7                         }.|.D /ci c]  }/|/j                  s|/j                  |/ c}/\t               Yd,tV        d-tl        dtl        fY[fd.Sd/tj        tV           dt        tj        t        tl        t        f         fd0}0|jo                         D 12ci c]  \  }1}2|1 |0|2       c}2}1RRj                         D 3'ch c]-  }3|3r)|3D ]$  }'|'jO                  d1      r|'jO                  d1      & / }4}3}'i Q|4rm| j                  j!                  tV              j%                  tV        j,                  j                  |4            j7                         D ]  }5|5Q|5j,                  <    d2d3d4d5d6d7d8d9d:d;d<d=Pd>t"        dtl        fPQRSUVX\fd?}6g }7t        |j                               }8|8D ]&  }||   }!|!d   j                  xs d@| }9 |||!      }" ||"      }#|jO                  |g       }:i };t        |!      D ]  \  }<}|jO                  |j,                  g       }=|=s&g }>t        |=      D ]  \  }?}t        |j                        }@|@s|jp                  |jp                  nd}Ar@j                  ndA}BdB|	 dC|dDdEAdFdG|?d&z   dF|B 	}C|Cz  }D|DjA                         s!r[j                  @D       nt        @D       g }E|j                  rEjc                  dH|j                          |jt                  rEjc                  dI|jt                          dJj                  dK ED              }F|>jc                  dLC dM|F dN        |>sUt        |jr                  xs |jt                  xs d$      ddO }Gtw        |>      }H |6|      }IdPt        |Hd+       dQd$j                  |>       |GrdRGz   dSz   nd$ I dS|;|<<     |tR              }Jt        |:      D ]!  \  }<}K|<|;v sJK   jc                  |;|<          # g }L|#rLt        |#d&      D ]<  \  }M}NLjc                  dTt        |N       dS       |MJv s)Lj                  JM          > nFLjc                  dTt        |"       dS       JjO                  d&g       D ]  }OLjc                  |O        |7jc                  dU| dVt        |9       dWtw        |!       dXd$j                  L       dY	       ) t9        |
dZj                  |7            S # tH        tB        jJ                  f$ r}TjM                  d||       Y d}~Fd}~ww xY wc c}w c c}w # t|        $ r d&gtw        ||         z  ||<   Y w xY w# 1 sw Y   xY wc c}'w c c}/w c c}2}1w c c}'}3w )[u
  단일 에피소드 HTML 생성 — shot 단위 이미지를 씬 텍스트에 인라인 삽입.

        original=True 면 이미지를 JPEG 변환 없이 원본 파일 그대로 복사하고
        <img src> 도 원본 확장자(.png 등)로 참조한다.
        r   N)defaultdict)ThreadPoolExecutoras_completedr   EPr   u   <p>씬 데이터 없음</p>checkpointsr   )
scene_savescene_segmentationzmanifest.jsonr#  r$  uD   checkpoint manifest read failed: path=%s, exc=%s — fallback 진행datasegmentsscene_indexr   r   r   uA   skip image asset: id=%s, still_id=%s, status=%s — export 제외c                 6    | j                   | j                   S dS )Nr   )
shot_index)r   s    r%   r   z3ExportService._build_episode_html.<locals>.<lambda>  s    @X0_^_0_r$   )rP   si	si_stillsr   c                    	j                  |       }|rO|j                  d      xs( |j                  d      xs |j                  d      xs d}t        |t              r|r|S |D cg c]2  }|j                  $|j                  |j                  |j                  f4 }}|r8t        dt        d |D                    }t        d |D              }||kD  r|| S yc c}w )Nr]   content
scene_textr/   r   c              3   &   K   | ]	  \  }}|  y wr   r#   )r   ar&  s      r%   r   zUExportService._build_episode_html.<locals>._scene_text_from_stills.<locals>.<genexpr>  s     "8A1   c              3   &   K   | ]	  \  }}|  y wr   r#   )r   r&  bs      r%   r   zUExportService._build_episode_html.<locals>._scene_text_from_stills.<locals>.<genexpr>  s     /1!rf  )rD   r0   r(   segment_start_charsegment_end_charmaxmin)
r_  r`  segr]   r   rangesstartendr   seg_maps
           r%   _scene_text_from_stillszBExportService._build_episode_html.<locals>._scene_text_from_stills  s    ++b/Cwwv[#'')*<[@U[Y[dC(TK #"A''38J8J8V %%q'9'9:"  
 As"8"889///;#E#..s   (7Cr]   c                     j                  d| j                               }|D cg c]#  }|j                         s|j                         % c}S c c}w )u4   씬 텍스트를 빈 줄 기준으로 단락 분할.z\n\s*\n)rJ   rF   )r]   blocksrh  _res      r%   _split_paragraphsz<ExportService._build_episode_html.<locals>._split_paragraphs  s?    YYz4::<8F'-;v!AGGIv;;;s
   AA
paragraphsshotsc                    |r|st        |      gt        |      z  S t        |      dk(  rdgt        |      z  S ddlm} dj                  d t	        |      D              }dj                  d |D              }d|  dt        |       d	| d
| dt        |       d}ddddddiddidddgdddidgdd}	  |dd||d      }|j                  dg       D 	ci c]  }	|	d   |	d    }
}	g }d}|D ]I  }|
j                  |d   |      }t        |t        |t        |                  }|j                  |       |}K |S c c}	w # t        $ ry}j                  d| |       t        |      }t        t        |            D cg c]-  }t        |t        d|dz   |z  t        |      dz   z              / nc c}w c}cY d}~S d}~ww xY w)ub   LLM으로 각 shot 이미지의 삽입 위치 결정. 반환: shot별 after_paragraph (1-indexed).rA   r   )call_structured
c              3   8   K   | ]  \  }}d |dz    d|   yw)[rA   z] Nr#   )r   r   rb   s      r%   r   zSExportService._build_episode_html.<locals>._determine_placements.<locals>.<genexpr>  s'      R<QDAq1QqSEA3<Q   c              3   8   K   | ]  }d |d    d|d      yw)z- shot_r^  : r   Nr#   )r   shs     r%   r   zSExportService._build_episode_html.<locals>._determine_placements.<locals>.<genexpr>  s1      "B "\*+2b.?-@Ar~     씬 u   의 텍스트가 u   개 단락으로 나뉘어 있습니다.
각 샷 이미지를 어느 단락 뒤에 삽입하면 내용과 가장 어울리는지 결정하세요.

## 단락
u   

## 삽입할 샷
uu   

## 규칙
- 각 샷의 시각적 순간과 가장 관련 있는 단락 바로 뒤에 배치
- after_paragraph는 1~u    범위 (해당 번호 단락 뒤에 삽입)
- 샷 순서 유지: 앞 샷의 위치 ≤ 뒤 샷의 위치
- 같은 위치에 여러 샷 가능object
placementsarraytypeinteger)r^  after_paragraphr^  r  F)r  
propertiesrequiredadditionalProperties)r  r   image_placementua   시나리오 텍스트에 스틸 이미지를 배치하는 전문가. JSON으로만 답하세요.g?)stepsystem_promptuser_promptresponse_schematemperaturez+Image placement LLM failed for scene %d: %sN)r   app.modules.llm.llm_clientrz  r[   	enumeraterD   rk  rl  rZ   r   r   range)r\  rw  rx  rz  numbered	shot_listr  schemar  rb   placement_map	positionsprev_posr  posr   nr   _logs                     r%   _determine_placementsz@ExportService._build_episode_html.<locals>._determine_placements  s?   
J(3u:55:!#sSZ''Byy RIj<Q RRH		 "" I
 {m#4S_4E F&Z (%%.K 0* +.j/): ;78  !  '$,/5y.A4:I3F+ *67H(I49"# *N(-#F&d(* #F +$* # QWPZPZ[gikPl mPl1<!4E2F!FPl m	B'++B|,<hGChCZ(ABC$$S)"H	  
 !  !n  dJKY\]
OQVWZ[`WaQbcQbAAs1q1ukc%j1n&EFGQbcc	ds=   :!E E+AE E 	G4G2F65G=GG   )max_workersr/   )r^  r   rA   	referencez%composite:%z#composite:([a-f0-9-]+):([a-f0-9-]+):   assetlabelc                    t        | j                        }|syr|j                  nd}d| j                  d d  | }|z  }|vr4r	j	                  ||       nt        ||d       j                  |       d| dt        |       d	S )
Nr/   .jpgref_r  K   )qualityz'<div class="ref-item"><img src="images/z5" onclick="openModal(this)" /><div class="ref-label">z9</div><div class="meta" style="display:none"></div></div>)rc   r_   suffixr=   copy2_copy_as_jpegrK   _html_escape)
r  r  srcextfnamedstrG  r   
ref_copiedr-  s
         r%   	_copy_refz4ExportService._build_episode_html.<locals>._copy_refD  s    &u7C (#**fC588BQ<..EE/CJ&LLc*!#sB7u%$$)7 +**6u*=)> ?r$   r   c                 R   t        d | D        | r| d   nd      }|y	 t        j                  |j                  xs d      }|j                  d      }t        |t              sy|D cg c]  }t        |t              s| c}S # t        t        j
                  f$ r Y yw xY wc c}w )u   primary asset 의 actual_attached_refs — 키 부재(legacy)=None,
            기록 있음=list([] 포함: 진짜 무참조 T2I 샷).c              3   :   K   | ]  }|j                   s|  y wr   r   )r   re  s     r%   r   zRExportService._build_episode_html.<locals>._parse_attached_refs.<locals>.<genexpr>e  s     3FqallFr   r   Nr   actual_attached_refs)
r   r3   r4   pipeline_metadata_jsonr5   r6   rD   r0   r1   r2   )r   
meta_assetpmrefsrs        r%   _parse_attached_refsz?ExportService._build_episode_html.<locals>._parse_attached_refs_  s    
 3F3#q	J !ZZ
 A A ITJ 6601DdD)#;t!z!T':At;; t334 
 <s   #B (B$>B$B! B!asset_idu   합성(인물+아웃룩)u   얼굴u   소품u	   아웃룩u   배경u   구도 가이드u   포즈 가이드u   포즈 가이드(실내)u   상태 변형u   이전 샷 프레임u#   이전 샷 프레임(고정 인물))reference_compositereference_facereference_propreference_outfitbackground_rendercomposition_guideregistered_pose_guideindoor_pose_guidecharacter_state_variantscene_prev_frameimmobilized_prev_framestc           
      f   j                  | j                        }|g }t               }|D ]  }|j                  d      }|r||v r|j                  |       j                  |      }|s@t	        |j                  d      xs d      } |j                  ||xs d            }|s~|j                  |        |rddj                  |       dS yt        | j                        }	t        | j                  | j                  xs d      }
t        j                  d|
            }g }t               }t        |	      D ]:  }j                  |      }|s|j                  dk(  r|D cg c]  }|j                  d	      d
   |k(  s| }}|r|D ]  }d	|j                  d	      d   z   }j                  |      }|rT|j                   d|j                   }|v r5||vr1|j                  |        |   |      }|r|j                  |       |j                  |vs|j                  v s|j                  |j                         |j                   |j                     | d|j                                  E|j                  |vsU|j                  v se|j                  |j                         |j                   |j                     | d|j                                 |j                  dv s|j                  |vs|j                  v s|j                  |j                          |j                     | d|j                          }|s*|j                  |       = |D cg c]  }|s|	 }}|syddj                  |       dS c c}w c c}w )u   샷별 참조 이미지 HTML — SOT=actual_attached_refs(실제 첨부),
            metadata 부재 legacy still 만 VE + T2I C##O## 휴리스틱.r  roler/   u   참조z-<div class="shot-refs"><div class="ref-grid"></div></div>zC\d{2,3}O\d{2,3}	characterr?   r   rA   r  rY   )locationprop)rD   r=   rE   rK   r(   rZ   r[   rR   visible_entities_jsonr^   t2i_variations_jsont2i_prompt_cinematicfindallsortedr   rJ   r   )r  attachedsot_tagsseen_idsrefaidr  r  tagve_sidst2i_text	co_combosref_tagsseenrO   eccochar_comboso_sido_ecckeyr   _ROLE_CAPTION_attached_assets_attached_by_stillr  ru  composite_mapref_by_entitysid_to_entitys                         r%   _build_shot_refsz;ExportService._build_episode_html.<locals>._build_shot_refs  s    *--bee4H#&( ##C''*-C#/ LL%,005E  swwv4"5D#E=+<+<T4CS8+TUC , $ 1131B0C D!" 223K3KLG5&&''-2H CKK(;XFGIH5Dg"&&s+>>[009"U	"RXXc]1=MQT=T2	K"U""-B$'"((3-*:$:E#0#4#4U#;D#*,%%$''';#'=#8T=M$(HHTN*3M$4G*LC'*(0(<$,!uuD0RUUm5K $ (	-:NSVRWWXY[Y`Y`XaPb0c d #.  55,-1G HHRUUO$OOImBEE6JseSTUWU\U\T]L^,_`^^';;uuD(RUUm-C'bee(<Qrwwi>PQ$OOC0A 'B $,18aq8H1))+):(; <= #V6 2s   -N)
N)N.N.r  r  rz   _s03d_shot02dr&  zType: zBeat: <br>c              3   :   K   | ]  }d t        |       d  yw)z<span>z</span>N)r  )r   ms     r%   r   z4ExportService._build_episode_html.<locals>.<genexpr>  s"     +bWaRSf\!_4EW,MWas   z'<div class="img-wrap"><img src="images/z0" onclick="openModal(this)" /><div class="meta">r  x   z<div class="inline-shot cols-z">z<div class="shot-caption">z</div>z<div class="para">uq   <section class="scene open"><h2 onclick="this.parentElement.classList.toggle('open')" style="cursor:pointer">씬 r  z <span class="shot-count">(uR   샷)</span> <span class="toggle-arrow">&#9654;</span></h2><div class="scene-body">z</div></section>r{  )[loggingr;  r-  collectionsrS  concurrent.futuresrT  rU  r   r   	getLogger__name__r   r3  r8  rj   rw   r   rx   rg   rl   rp   r=   r   r   r   r   r   _build_html_templater   r   r}   ra   r3   r4   	read_textOSErrorr6   r   rD   r0   r1   r2   r   r   r   debugr   r   rZ   r\  r  intr   r(   r   r^  r   r   r   submitr  r   r   r>   r   r   prompt_usedlikesearchgroupr<   rE   r	   r   r   valuesr  keysr   r  rc   r_   r  r  r  r  r[   r  rl  extend)]rn   r   rG  r   r  rS  rT  rU  r   ep_numr8  r   cp_rootr[  	step_namecp_pathcp_datar   raw_segmentsr   r  _ALLOWED_IMG_STATUSr  r  scene_stillsr  r_  rr  rv  r  placement_resultsfuturespoolr`  rc  rw  
shots_infofut
ref_imagesr  
entity_idsentities_mapr  composite_imagescir  all_entitiesr   r  st_idr   r  _attached_idsre  r  scenes_htmlsorted_scenesheadingr  shot_img_htmlshot_ir  img_tagsidxsrc_pathshot_idxr  r  r  
meta_lines	meta_htmlcaptionn_imgsref_html
insert_mapr  
body_partsp_idxpara
html_blockr  r  r  r  r  ru  r  r   r  r  rq  r-  r  s]     ``                                                                            @@@@@@@@@@@@@r%   r9  z!ExportService._build_episode_htmlA  s    	+G" w  *##''.2fX HHNN:&V%%)9)99%%3J%%))$/1B1Bg1MN
 Xj,,-SU 	 '/LMM &&'  jj	 	 =I	)O;G>>#	**W%6%6%6%HI #;;vr266z2FL,-,'# >( 
!T"quu]';'G ma
 HHNN:&V%%)9)99%%3%%0
 Xj++,SU 	  +J746Czzcjj0CC

WFFCLL#** ||''b9@@E  5@4EB#%>>#=2>>B##B'  B!!&_!` 	 	Z8H 	S 	&	<C 	<DI 	<
B	ds B	dS	 B	dRVW[R\ B	daefiaj B	dJ 35A.$!-!3!3!5I4RC
.z:
 '& $%<<#41QEYEYEo]^]i]iEomop&   y>A%#j/A*=++&;RZXC#%GCL .1_-A,BS^,S%b) "6 $G,S\H,/JJL%b) - /0 HHNN:&V%%)9)99%%4%%*
 SU 	 ,6E:aakk:
Ehhnn[1889K9KJ9WX\\^&(RUU# _/1A{{q{{-?-.akk* 
 HHNN:&V%%)9)99%%4&&++N;
 SU 	 02"B

A2>>CWUWXA>@Aaggaj\:; # xx~~k299""d&6&66

#% 	 IU0c1XYXbXbQ0c%
	Z 	 	 	 	6	<$	<d4S>*+	<, ".!3!3!5I
!5v '//!5I
 +113
3uuZ 	 EE*  3 	 
 35z*
))-89 
 *+ &	 $>&& +!)!3%7!;'6 6&K
N	 N	 N	 N	b |0023B$R(Il;;JbT{G0Y?J*:6J)--b"5I -/M'	2
#''r2 )$HC3CMMBH# 020Ir}}qH-5(//6C 2c(%~Qs1uSkRUQVWE!E/C::<#"LL37)(C8!#J''"))F33C3C2D*EF}}"))F2==/*BC &+bWa+b bIOO,,17 3--6K 8!"+ !06 *2+@+@+WBMM+WUWXY]Z]^G ]F/3H7FA7Gr778,-RY9GChN_ab#* 	" "&)I 3\ 0;4/@J(3]*sO**=+@A  4 J#,Z#;KE4%%(:<;M:Nf&UV
*"))*U*;< $<
 !!$6|J7O6PPV"WX",..B"7J%%j1 #8 d"\'23 4--0^,< =++-77:+>*? @O  b $E499[+ABB] T112  ZS 
X ! H-.C#l26F2G,G%b)H' /.@ F: 1dfI

s   %r.s	,s?56s+A)s?s(s?2tt/tt=t'2ts/sss?s<8s?;s<<s??t	c                    | j                         }g }|j                         s|S t        |j                               D ]  }|j	                         s|j
                  dv s#|j                         }|j                  |j                  dt        |      |j                  t        j                  |j                  t        j                        j!                         d        |S )zList all exported files.)z.pdfr+  N)tz)r  rp   r_   
size_bytesr   )r   ra   r  iterdirr  r  statrZ   r   r(   st_sizer   fromtimestampst_mtimer   r!   r"   )rn   r  r  fr4  s        r%   list_exportszExportService.list_exports?  s    ++-')!!#M++-.Ayy{qxx+;;vvx$%FF&*%(V&*ll&.&<&< MMhll'#)+
 / r$   )   
   Nr   )NF)F)r  
__module____qualname____doc__
OrmSessionr(   ro   r   r{   r   r~   r   r  r	   r   r   r   r   r  r  boolrQ  r9  r9  r#   r$   r%   re   re      s   4	"	" 	" 		"
 
	"s w >$ >$  "#$& [[ [ "	[
 SM[ 
c3h[@ !bb SMb 
c	bN !++ SM+ 
c3h	+` !	jj SMj 	j
 
jX|C7 |CT |CT |C^a |C|d4S>2 r$   re   r  r  r  c                 T   	 ddl m} |j                  |       5 }|j                  dk7  r|j	                  d      n|}|j                  |d|d       ddd       y# 1 sw Y   yxY w# t        $ r8}ddl}t        j                  d| ||       |j                  | |       Y d}~yd}~ww xY w)	u  PNG를 JPEG로 변환하여 복사. 실패 시 HTML이 참조하는 dst 경로에 원본 복사.

    fallback (Codex iter 2 B1): dst 의 확장자(.jpg)와 raw bytes(PNG) 가 mismatch
    되지만 HTML <img src="...jpg"> 참조 깨짐을 우선 방지. 운영자가 변환 실패를
    인지하도록 logger.warning 으로 가시화 (silent 회피, feedback_no_silent_fallback).
    r   )ImageRGBJPEGT)r  optimizeNu   _copy_as_jpeg PIL conversion failed: src=%s, dst=%s, exc=%s — raw 복사 fallback (MIME mismatch 가능, 운영자 확인 권장).)PILrB  openmodeconvertsaver   r-  r   r   r  )r  r  r  rB  imrgbr   r-  s           r%   r  r  Y  s    ZZ_')ww%'7"**U#RCHHS&'DHA __  Sc	

 	S#s4   A& 8AA& A#A& #A& &	B'/.B""B'r]   c                 L    t        j                  |       j                  dd      S )Nr{  r  )html_moduleescaper=  )r]   s    r%   r  r  o  s     d#++D&99r$   r8  bodyc                 f    dt        j                  |        dt        j                  |        d| dS )Nz<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>a  </title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif; background: #111; color: #e0e0e0; padding: 20px; }
h1 { text-align: center; padding: 30px 0; font-size: 28px; color: #fff; border-bottom: 1px solid #333; margin-bottom: 30px; }
.scene { margin-bottom: 40px; border: 1px solid #333; border-radius: 8px; overflow: hidden; }
.scene h2 { background: #1a1a2e; padding: 14px 20px; font-size: 18px; color: #a0c4ff; user-select: none; }
.scene h2 .toggle-arrow { font-size: 12px; margin-left: 8px; transition: transform 0.2s; display: inline-block; }
.scene h2 .shot-count { font-size: 13px; color: #666; font-weight: normal; }
.scene.open h2 .toggle-arrow { transform: rotate(90deg); }
.scene-body { display: none; padding: 16px 20px; background: #1a1a1a; }
.scene.open .scene-body { display: block; }
.para { font-size: 14px; line-height: 1.8; white-space: pre-wrap; margin-bottom: 12px; color: #e0e0e0; }
.inline-shot { display: grid; gap: 4px; margin: 16px 0; border-radius: 6px; overflow: hidden; }
.inline-shot.cols-1 { grid-template-columns: 1fr; }
.inline-shot.cols-2 { grid-template-columns: 1fr 1fr; }
.shot-caption { grid-column: 1 / -1; padding: 6px 10px; font-size: 12px; color: #999; background: #0d0d0d; font-style: italic; }
.shot-refs { grid-column: 1 / -1; padding: 6px 8px; background: #0a0a0a; border-top: 1px solid #222; }
.img-wrap { position: relative; overflow: hidden; }
.img-wrap img { width: 100%; display: block; cursor: pointer; transition: opacity 0.2s; }
.img-wrap img:hover { opacity: 0.85; }
.img-wrap .meta { display: none; }
.ref-section { padding: 12px 16px; background: #151515; border-top: 1px solid #333; }
.ref-title { font-size: 12px; font-weight: 600; color: #666; text-transform: uppercase; margin-bottom: 8px; }
.ref-grid { display: flex; flex-wrap: wrap; gap: 6px; }
.ref-item { width: 100px; flex-shrink: 0; }
.ref-item img { width: 100px; height: 100px; object-fit: cover; border-radius: 4px; cursor: pointer; }
.ref-item img:hover { opacity: 0.8; }
.ref-label { font-size: 10px; color: #888; text-align: center; margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.modal-overlay { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 1000; justify-content: center; align-items: center; flex-direction: column; cursor: pointer; }
.modal-overlay.active { display: flex; }
.modal-overlay img { max-width: 95vw; max-height: 85vh; object-fit: contain; border-radius: 4px; }
.modal-meta { margin-top: 12px; padding: 16px 24px; background: #1a1a1a; border-radius: 6px; max-width: 90vw; max-height: 20vh; overflow-y: auto; font-size: 12px; color: #aaa; line-height: 1.7; word-break: break-all; }
.modal-close { position: fixed; top: 20px; right: 30px; font-size: 36px; color: #fff; cursor: pointer; z-index: 1001; }
</style>
</head>
<body>
<h1>z</h1>
aJ  
<div class="modal-overlay" id="modal" onclick="closeModal()">
<span class="modal-close">&times;</span>
<img id="modal-img" src="" />
<div class="modal-meta" id="modal-meta"></div>
</div>
<script>
function openModal(el) {
  const modal = document.getElementById('modal');
  const modalImg = document.getElementById('modal-img');
  const modalMeta = document.getElementById('modal-meta');
  modalImg.src = el.src;
  const metaEl = el.parentElement.querySelector('.meta');
  modalMeta.innerHTML = metaEl ? metaEl.innerHTML : '';
  modal.classList.add('active');
  document.body.style.overflow = 'hidden';
}
function closeModal() {
  document.getElementById('modal').classList.remove('active');
  document.body.style.overflow = '';
}
document.addEventListener('keydown', e => { if (e.key === 'Escape') closeModal(); });
</script>
</body>
</html>)rN  rO  )r8  rP  s     r%   r  r  s  sO    
 	5!" %#J    WC Cr$   )r/   )P   )Ar>  r3   r  r;  r)   r   r   pathlibr   typingr   r   r   r	   r
   r/  r   r?  app.core.configr   app.core.file_pathsr   r  r  r   app.core.errorsr   app.i18n.loaderr   app.logging.activity_loggerr   r   r   r   r   r   r   r   r   htmlrN  app.modules.pdf_rendererr   app.modules.pdf_validatorr   app.modules.progress_trackerr   app.modules.provenancer   app.modules.webbook_generatorr   r(   r&   r+   compilerG   r7   rR   r^   rc   re   r  r  r  r  r#   r$   r%   <module>ra     sT   Q   	  '  1 1 0 $ 2			8	$ $  6    0 2 8 5 :2c 2  rzz@A3    C B-# - -c -&)C )HTN )S Slt $  d ,:s :s :D D3 D3 Dr$   