
    hMj\                       S r SSKJr  SSKrSSKrSSKrSSKrSSKJr  SSK	J
r
  \R                  " S5      r " S S\5      r\R                  " S	S
5      r/ SQ/ SQ/ SQ/ SQ/ SQS.r/ SQrSSSSSS.rSSSSSS.rS r0 SS_SS_SS_S S_S!S_S"S_S#S#_S$S#_S%S#_S&S#_S'S#_S(S(_S)S(_S*S*_S+S*_S,S*_r\R                  " S-5      r   S9     S:S. jjrS;S/ jrS<S0 jr\" S15      R9                  S25      rS=S3 jrS>S?S4 jjrS@S5 jr      SA           SBS6 jjr!SCSDS7 jjr"       SE                   SFS8 jjr#g)Gu  TTS backends for data pipeline.

Two implementations:
    - ElevenLabs (paid, emotional prosody) — recommended
    - edge-tts (free fallback)

Env vars:
    ELEVENLABS_API_KEY  (required for elevenlabs)
    ELEVENLABS_VOICE_ID (optional, default to multilingual-friendly voice)
    )annotationsN)Path)Listttsc                      \ rS rSrSrSrg)QuotaExhausted   u   Raised when ElevenLabs returns 401/402/quota — non-retryable.
synth_all catches this once and aborts the whole batch so the user
can see a single clear message and re-run with a different key. N)__name__
__module____qualname____firstlineno____doc____static_attributes__r
       X/home/ubuntu/service/kemix-engine/package/face/animasync-face-v3/scripts/compiler/tts.pyr   r      s    Gr   r   ELEVENLABS_MODEL	eleven_v3)FCdKzv68Ofr4VUDcZXIyKsAmBSHXsuxsZ1lZZXlGoHB9Xhox1bqMl1TvkmelZubHeGTOAkECknc02Zmo)m3yAHyFEFKtbCIM5n7GFk9073AMdU5sAUtPMH1ilhWXqitL3DEOLD49pgNWRz2P4oCxSHhXan3ew4COv)TbMNBJ27fH2U0VgpSNko1W00IGEmNmwmsDeYy7agOEaq3WGNtNvFJ5co9mJExi3rF0t7dg7uN2M0WUhr)8tsLeAV5vPVuzCCvqbbUs0XGIcqmceN2l7kjsqoZiWLjl1zCuqXRkW6494ve8QclKarwLctvzN2plke3)XfNU2rGpBa01ckF309OY93nuHbke4dTER9x2pDwEYDDaC9XKjODs7hY78qEWBNr4zvrC1bGIdIstzjFQ)angersadnessjoyneutralsurprise)r   r   r   r!   r%   r   r   r   r!   r%   r   r   r   r"   r&   c                  ^ U  Vs/ s H3  oR                  S5      =(       d    SR                  5       (       d  M1  UPM5     nnU(       d  gSSKJn  U" 5       n0 mU H  nUR                  S5      =(       d    Sn[        R                  US5      nXF==   S-  ss'   UR                  S5      =(       d    / S	Qn[        U5      S
:  a  [        [        US   5      5      OSnTR                  US5      U-   TU'   M     [        UR                  5       5      n	UR                  5        V
Vs/ s H  u  pX:X  d  M  U
PM     nn
n[        U5      S:X  a  US   $ [        UU4S jS9$ s  snf s  snn
f )u  Pick the most-frequent base emotion across a scenario's turns.

`turns` is a list of dicts with 'emotion' (and optionally 'vad').
Returns the base name ('joy', 'sadness', etc.). Tie-break: highest
summed |arousal| of the tied bases. Empty input → 'neutral'.
text r,   r   )Counteremotion   vad)        r5   r5      r5   c                (   > TR                  U S5      $ )Nr5   )get)barousal_sums    r   <lambda>)dominant_base_for_turns.<locals>.<lambda>   s    ;??1c#:r   )key)r8   stripcollectionsr1   EMOTION_TO_BASElenabsfloatmaxvaluesitems)turnstrealr1   countsemobaser4   a	top_countr9   ctiedr:   s                @r   dominant_base_for_turnsrQ   g   s'    >u!v!4" ; ; =AuD>#YFKeeI+)""3	2eeEl-o"%c(a-Cc!fS'OOD#6:D  FMMO$I ,,.;.$!ANA.D;
4yA~Awt:;;% ? <s   0E E &E%5E%r,   r+   laughter
excitement	agreement	gratituder*   cryingsulkapologystruggler)   refusalr-   flustershyELEVENLABS_VOICE_IDc                l   U(       a  U$ [         (       a  [         $ US:X  a  [        nO9U(       a
  [        U   nO([        R	                  U =(       d    SS5      n[        U   nUc  US   $ [        [        R                  " UR                  S5      5      R                  5       S5      nXF[        U5      -     $ )u  Pick voice ID from pool keyed by emotion → base emotion.

seed: stable key (e.g. text or scenario_id+turn_idx) to deterministically
pick a voice from the pool. Without seed, picks first voice in pool.
Deterministic selection ensures same turn always gets same voice on re-run.

pool_override: force voice selection from a specific base-emotion pool
(e.g. 'neutral' for monologues — same voice across all emotions in turn).
female_monologuer,   r   utf-8   )SINGLE_VOICE_OVERRIDEFEMALE_MONOLOGUE_POOLVOICE_POOLS_BY_BASEr@   r8   inthashlibmd5encode	hexdigestrA   )r2   overrideseedpool_overridepoolrL   hs          r   voice_id_for_emotionro      s     $$**$	"=1""7#7iC"4(|AwGKKG,-7792>ACIr   c                    U c  SSSSS.$ U u  pn[        [        U5      5      n[        [        S[        SSS	U-  -   5      5      5      n[        [        S
[        SSSU-  -
  5      5      5      nSnUUUSS.$ )u  Map VAD → ElevenLabs voice_settings for expressive prosody.

For v3 + audio tags: tags carry the emotion, voice_settings carry
consistency. Stacking high `style` on top of expressive tags
triple-amplifies expression and produces distortion (raspy / accent
drift / foreigner-sounding) — that was the failure mode observed on
2026-05-07. We keep style modest and stability solid.

Intensity is |A| magnitude — both high-positive (excited) and
low-negative (sad) arousal scale prosody equally.
g?g      ?g333333?T)	stabilitysimilarity_booststyleuse_speaker_boostr5   g      ?g?g      ?g?g?)rB   rC   rD   min)r4   VAD	intensityrs   rq   rr   s           r   _build_voice_settingsrz      s     {!tdaeffGA!E!HI #c3tTD9,<%<=>?Ec$D$	1A*A BCDI,!	 r   c                P    U R                  5       n U (       a  U S   S;  a  U S-   $ U $ )u2  Ensure text ends with sentence-ending punctuation so v3 doesn't cut the tail.

Note: `、` (U+3001) is a Japanese reading-comma, not a Korean sentence
terminator — it was here by mistake. Korean uses ASCII `.?!` plus
sometimes `…`. CJK terminators `。？！` are kept as a safety net for
pasted text.
u   .?!。？！….)r>   )r/   s    r   _ensure_trailing_punctr~      s-     ::<DR 11czKr   reu9   [ㅠㅜ]{2,}|[ㅋㅎ]{2,}|[ㅏㅓㅗㅡㅣ]{2,}|[ㅡ]{2,}c                j   SSK n[        UR                  SU 5      5      n[        UR                  SU 5      5      nUR                  SSU 5      n US;   a  S nUR                  SXP5      n OUR                  SSU 5      n UR                  S	SU 5      n UR                  S
SU 5      R	                  5       n XU4$ )u  v3 reads bare Korean jamo runs (ㅠㅠ, ㅋㅋ, ㅎㅎ) literally — comes out
as "어어/유유/크크" instead of sobbing/laughing. Replace with v3 audio
tags so they render as real reactions.

ㅠㅠ / ㅜㅜ → [crying]     (inline, at the emoticon position. Deleting it
                          and relying on the head [crying]/[sorrowful] tag
                          produced NO audible sob at all, so inline stays.
                          It's imperfect — v3 occasionally rolls it as a
                          laugh or a sigh-with-gap — but at least the cry
                          is present. Re-roll bad takes rather than remove.)
ㅋㅋ / ㅎㅎ → stripped     (removed entirely, NOT tagged — the clip is
                          still emotion=laughter so the head [laughs]
                          tag carries the laugh; we just don't want the
                          literal ㅋㅋ jamo in the text at all)
bare vowel runs (ㅏㅏ, ㅡㅡ…) → stripped (garbage)

No leading period — that produced a hard sentence break before the
reaction (audible "speak…long pause…laugh" instead of "speak laugh").
The canonical tags fire mid-clause without it.

Returns (cleaned_text, had_sob, had_chuckle) — flags reserved but
head-tag suppression is OFF by default now. Head tag carries the
emotional prosody for the whole line; the body tag just stamps the
reaction sound. Suppressing the head was making the voiced emotion
drop out entirely.
r   Nu   [ㅠㅜ]{2,}u   [ㅋㅎ]{2,}z
 [crying] rR   r+   rS   c                F    [        U R                  S5      5      S:  a  S$ S$ )Nr      z [laughs harder] z
 [laughs] )rA   group)ms    r   _laugh'_scrub_korean_emoticons.<locals>._laugh  s!    *-aggaj/Q*>&PLPr    u   [ㅏㅓㅗㅡㅣ]{2,}z\s+)r   boolsearchsubr>   )r/   r2   r   had_sobhad_chuckler   s         r   _scrub_korean_emoticonsr      s    6 299_d34Gryy$78K66/<6D33	Qvvov4vvosD166*C6D66&#t$**,D+%%r   c                h    [        U R                  5       5      U:  a  U R                  S5      S-   $ U $ )u   v3 fails / hallucinates on very short inputs (e.g. "응.", "네.").

Pad with neutral filler so the model has enough context. We add an
ellipsis-style trailer that v3 reads as a brief pause, not extra speech.
r}   z...)rA   r>   rstrip)r/   	min_charss     r   _pad_short_textr     s0     4::<9${{3%''Kr   c                x   SSK nUR                  SSU 5      n [        X5      u  pn[        U 5      n [	        U 5      n U(       a  U(       a  US:X  a  U $ SSSSSS	S
SSSSSSSSS.nUR                  US5      =(       d    SnU(       a  US;   a  SnU(       a  US;   a  SnU(       a  SU SU  3R                  5       $ U $ )ud  Prepend emotion tag for v3 models (supports bracketed audio tags).

Adds a leading period buffer before the tag — v3 sometimes eats the first
token after an audio tag, which truncates the first word of real content.
Also ensures trailing punctuation so the model doesn't cut the final word.
Short texts are padded — v3 hallucinates on <12-char inputs.
r   Nz
\[[^\]]*\]r   r,   z	[happily]z[laughs]z	[excited]z[sorrowful]z[crying]z[sighs]z[resigned tone]z[frustrated]z[angry]z[flatly]z[surprised]z
[stammers]z
[whispers])r+   rR   rS   rT   rU   r*   rV   rW   rX   rY   r)   rZ   r-   r[   r\   r0   )r*   rV   rW   rX   r   z. )r   r   r   r   r~   r8   r>   )r/   r2   use_v3_tagsr   r   r   tag_maptags           r   _format_text_with_emotionr     s     66-d+D!8!GD;4 D!$'DgI)= " !# '$ #""G" ++gr
"
(bC 7FF w"CC(+RuAdV""$55r   c	                  ^^^^^^^^^#    SSK Jm  [        UTU=(       d    TR                  US9mT=(       d    [        mT=(       d    [
        R                  " S5      mT(       d  [        S5      eSTR                  5       ;   n	[        XU	5      m[        U5      m[        R                  " 5       n
UUUUUUU4S jnSSKnS	n[        U5       H  n U
R                  SU5      I Sh  vN     g   g N	! [          Ga  n[#        U5      mTR                  5       mS
T;   =(       dA    ST;   =(       d5    ST;   =(       d)    ST;   =(       d    ST;   =(       d    ST;   =(       d    ST;   nU(       as   [%        S5      R'                  ST S[)        U5      R*                   S[-        USS5       S[-        USS5       S3	SS9  O! [          a     Of = f[/        STSS  35      UeST;   =(       d    ST;   nS T;   =(       d    S!T;   =(       d    S"T;   n[1        U4S# jS$ 5       5      n[1        U4S% jS& 5       5      nU=(       d    U=(       d    U=(       d    UnU(       a  XS'-
  :  a  U(       a  S(OS)n[3        S*US+U-  -  UR5                  SS,5      -   5      nU(       a  S OU(       a  SO
U(       a  S-OS.n[6        R9                  S/U S0TSS1  S2US'-    S3U S4US5 S635        [        R:                  " U5      I Sh  vN     SnAGM5  e SnAff = f7f)7u"  Synthesize one clip with ElevenLabs, using emotion+VAD for prosody.

If voice_id is None, picks voice automatically based on emotion → base mapping.
voice_seed: stable key for deterministic voice pick from the pool.
voice_pool: override base-emotion pool (e.g. 'neutral' for monologues).
r   )
ElevenLabs)rj   rk   rl   ELEVENLABS_API_KEYz#ELEVENLABS_API_KEY env var requiredv3c                   > SS K n T" TS9nUR                  R                  TT	T
STS9nTR                  S5      nUR	                  S5       nU H  nUR                  U5        M     S S S 5         U R                  SSS	S
S[        U5      SSSSSS[        T5      /SS9  UR                  SS9  g ! , (       d  f       NN= f! [        U R                  4 a4  n[        R                  SU S35        UR                  T5         S nAg S nAff = f)Nr   )api_keymp3_44100_128)voice_idr/   model_idoutput_formatvoice_settingsz.raw.mp3wbffmpegz-yz	-loglevelerrorz-iz-afz adelay=180|180,apad=pad_dur=0.18z-c:a
libmp3lamez-b:a128kT)check)
missing_okz[tts] ffmpeg pad failed (z); saving raw audio)
subprocesstext_to_speechconvertwith_suffixopenwriterunstrunlinkFileNotFoundErrorCalledProcessErrorLOGwarningreplace)r   clientaudioraw_pathfchunker   r   formatted_textmodelout_pathr   r   s          r   _sync#synth_one_elevenlabs.<locals>._synco  s   G,%%--)) . 
 ''
3]]4 A  !	'NNdK#h-=L&&M   	 OOtO,# ! $ ":#@#@A 	'KK3A36IJKX&&	's$   B./>B? .
B<?D	*DD	N   401402403quotaunauthorizedpayment_requiredbillingz/tmp/tts_quota_error.txtz=== full exception ===
z

=== type ===
z

=== status_code attr ===
status_codezn/az

=== body attr ===
body
r`   )encodingzElevenLabs auth/quota error:    409already_running429
rate_limittoo_many_requestsc              3  ,   >#    U  H	  oT;   v   M     g 7fNr
   ).0rO   msgs     r   	<genexpr>'synth_one_elevenlabs.<locals>.<genexpr>  s     P+Oac+O   )z 500 z 502 z 503 z 504 c              3  ,   >#    U  H	  oT;   v   M     g 7fr   r
   )r   rO   lows     r   r   r     s     \+[ac+[r   )timeout
connectionzremote disconnectedr3   g       @g       @g      N@r6   g      ?5xxnet[tts] z
 on voice    u   …, retry /z in z.1fs)elevenlabs.clientr   ro   stemDEFAULT_EL_MODELosgetenvRuntimeErrorlowerr   rz   asyncioget_event_looprandomrangerun_in_executor	Exceptionr   r   
write_texttyper   getattrr   anyru   uniformr   infosleep)r/   r   r   r   r   r2   r4   
voice_seed
voice_poolr   loopr   r   max_retriesattemptr   is_quotais_409is_429is_5xxis_net	retryablerL   delaykindr   r   r   r   r   s    ````                    @@@@@r   synth_one_elevenlabsr  R  s
     - $Gh*4*E3=?H %%E8#78G@AA %++-'K.tkJN*3/N!!#D%' %'^ K%#	&&tU333 &3  	a&C))+C F F F3F*8C*?F*c1F5>#5E  	34??23% 8))-a)9)9(: ;55<Qu5U4V W..5a.G-HL ") @  ! $'DS#YK%PQWXXc\=%6#%=Fc\V\S%8V<OSV<VFP+OPPF\+[\\F<&<F<fIWQ6$s#D$!w,"7&..C:P"PQ &uVE&V[6$z(2A,{7ST9+UVWbVccghmnqgrrstummE***A 	s{   CLC2&C0'C2+L0C22L=A/L-AF=<L=
G
L	G

D+L5K86L;LLLLc                `  #    [         R                  " SSUSU S[        U5      [         R                  R                  [         R                  R
                  S9	I Sh  vN nUR                  5       I Sh  vN u  pEUR                  S:w  a  [        SUR                  5       SS	  35      eg NN N87f)
zFree fallback via edge-tts CLI.zedge-ttsz--voicez--textz--write-media)stdoutstderrNr   zedge-tts failed: r   )
r   create_subprocess_execr   r   DEVNULLPIPEcommunicate
returncoder   decode)r/   r   voiceproc_errs         r   synth_one_edger    s     //Iuhos8}!!))!!&& D
 ##%%FA!.szz|DS/A.BCDD 
 &s$   AB.B*B.3B,47B.,B.c	                  ^ ^^^^^^^^	^^^^^#    [        T 5      [        T5      :X  d   e[        R                  " U5      mS/[        T 5      -  mSmSSS.mTS:X  a  [        O[        mSUUUU	UUUUUU UUUU4S jjn
[        R
                  " [        [        T 5      5       Vs/ s H
  o" U5      PM     sn6 I Sh  vN   T(       a  [        ST S	[        T 5       S
35        TS   (       a  [        STS   SS  S35        T$ s  snf  NL7f)zParallel TTS for a batch. Returns success flag per item.

emotions/vads: per-item lists aligned with texts. If provided,
passed into the TTS call for emotional prosody.
Fr   r0   )flagreason
elevenlabsc                4  >#    T
 IS h  vN   TS   (       a   S S S 5      IS h  vN   g T	U    nUR                  5       (       a:  UR                  5       R                  S:  a  STU '   TS-  m S S S 5      IS h  vN   g  [        T5      nTS:X  aI  Tb  TU    US'   Tb  TU    US'   Tb  TU    b  TU    US'   Tb  TU    b  TU    US	'   Tb  TU    b  TU    US
'   T" TU    U40 UD6I S h  vN   STU '   S S S 5      IS h  vN   g  N N N N ! [         a5  nSTS'   [        U5      TS'   [        R                  SU 35         S nANSS nAf[         a%  n[        R                  SU  SU 35         S nANS nAff = f N|! , IS h  vN  (       d  f       g = f7f)Nr  i  Tr3   r  r2   r4   r   r   r   r  u*   [tts] QUOTA EXHAUSTED — aborting batch: z[tts] fail z: )
existsstatst_sizedictr   r   r   r   r   r   )ipath
per_kwargsr   abortedbackendemotionskwargsok	out_paths	semaphoreskippedsynth_fntextsvads	voice_idsvoice_poolsvoice_seedss       r   onesynth_all.<locals>.one  s    9v 99 Q<D{{}}!4!4t!;11 994!&\
l*+08
9-',0G
5)".;q>3M3>q>
<0".;q>3M3>q>
<0 ,11I1:1
:.uQx<<<<1- 99* =! L"&$'F!		FqcJKK 4k!Bqc23349 999s   FDFE>FDFAE>.F9D	:F A*D*D+	D4F?E< FF	FD
E9+EE>E9E4/E>4E99E><F>FFFFNr   r   z  turns skipped (already on disk)r  u<   
[tts] ⚠ batch aborted on quota/auth error.
      reason: r  r   uz   
      partial results saved. Re-run with a different ELEVENLABS_API_KEY to resume — already-done turns will be skipped.)r  re   )rA   r   	Semaphorer  r  gatherr   print)r+  r'  r#  concurrencyr$  r,  r/  r.  r-  r%  r0  r  r"  r&  r(  r)  r*  s   ``` ``````  @@@@@r   	synth_allr6    s     " u:Y'''!!+.I
3u:	BG+G'.,'>#NH4 4 4B ..5U+<=+<a3q6+<=
>>>wiqU,LMNv &x0#67 8UV 	W I >>s   BD$D5D<D	=AD)NNN)
r2   
str | Nonerj   r7  rk   r7  rl   r7  returnr   )r4   zlist | None)r/   r   r8  r   )r/   r   r2   r7  r8  ztuple[str, bool, bool])   )r/   r   r   re   r8  r   )r/   r   r2   r7  r   r   r8  r   )NNNNNNN)r/   r   r   r   r   r   r   r   r   r   r2   r   r4   listr   r   r   r   )zko-KR-SunHiNeural)r/   r   r   r   r  r   )r  r   NNNNN)r+  	List[str]r'  z
List[Path]r#  r   r5  re   r$  r;  r,  z
List[list]r/  r;  r.  r;  r-  r;  r8  z
List[bool])$r   
__future__r   r   rf   loggingr   pathlibr   typingr   	getLoggerr   r   r   r   r   rd   rc   FEMALE_BY_BASEMALE_BY_BASErQ   r@   rb   ro   rz   r~   
__import__compile_KOREAN_EMOTICON_REr   r   r   r  r  r6  r
   r   r   <module>rF     s  	 #    	  G\ G 99/= 3 J  '&&&& '&&&&<<y	5#U0<e  %e y	 "9	 17		
 y
 $Y W #G 
 &z 49* 		"78  FJ-16: *)3?B<< !&..@ 
,&^36l KOCGBF3737	r(+r=@r*-r;?r .1r .1	rj	E  !!BBB B 	B
 B B B B B Br   