+
    Pj#                      a  0 t $ R t^ RIHt ^ RIt^ RIt^ RIt^ RIt^ RIt^ RI	H
t
 ^ RItRt]
! ]4      P                  4       P                   P                   t]R,          tR R ltR*t]P*                  ! RRP-                  ]4      ,           R,           ]P.                  4      t]P*                  ! R]P.                  4      tRRR	RRRR
R
RR
RRRRRRRRRRRRRR/tRRR	RRRR
R
RR
RRRRRRRRRRRRRR/tR R ltR R ltR R lt]3R R llt]P@                  PC                  R R!4      t"]#! ]P@                  PC                  R"R#4      4      t$ ! R$ R%4      t%Rs&R&]'R'&   ]PP                  ! 4       t)R( R) lt*R# )+u  Pocket TTS wrapper for the gateway — the cloned Jarvis voice, served over WS.

Ports the load / synth / mood-clip logic from ``voice/tts_pocket.py`` (Kyutai
Pocket TTS, MIT — a 100M CALM model that runs ~7x realtime on two CPU cores,
~40ms to first audio, ~200MB RAM), plus the emotion-tag parsing from
``voice/tts_chatterbox.py``. Reproduced here so ``gateway/`` is a standalone
deployable with no dependency on the ``voice/`` package.

Key differences from the desktop module:
  * The model is LAZY-loaded on the first voice synth (``get_tts()``); a
    chat-only WS session never imports or loads it. ``pocket_tts`` / torch are
    NOT in the base ``requirements.txt`` — install ``requirements-voice.txt``
    on the voice-capable deployment (see README).
  * Voice refs are read from ``$VOICE_DIR`` — default: the repo's ``voices/``
    dir for local dev, a Railway volume in prod. Voiceprints NEVER ship in git.
  * Output is 16-bit PCM WAV bytes per sentence (``synth_wav``), ready to push
    straight down the socket after its ``sentence`` frame.

Env: VOICE_DIR (voice-ref directory), POCKET_VOICE (catalog fallback voice),
VOICE_REF (base clone wav basename, default jarvis_ref.wav),
POCKET_MAX_TOKENS (default 300 ≈ 24s cap).
)annotationsN)Pathi]  voicesc                   V ^8  d   QhRR/# )   returnr    )formats   "tts.py__annotate__r   *   s     C C4 C    c                 n    \        \        P                  P                  R 4      ;'       g    \        4      # )	VOICE_DIR)r   osenvironget_DEFAULT_VOICE_DIRr   r   r
   	voice_dirr   *   s#    

{+AA/ABBr   calmflatwarmhappyexcitedurgentalarmedsad	sarcasticdryseriousneutralz^\s*\[(|z)\]\s*z^\s*\[[a-z][a-z ,'-]{0,24}\]\s*c                    V ^8  d   QhRRRR/# r   textstrr   ztuple[str, str | None]r   )r	   s   "r
   r   r   S   s        6 r   c                B   \         P                  T ;'       g    R4      pV'       d@   WP                  4       R P                  4       VP	                  ^4      P                  4       3# \        P                  T ;'       g    R4      pV'       d   VP	                  ^ 4      P                  4       ^R P                  4       P                  4       pV'       d   VP                  4       R,          MRpWP                  4       R P                  4       V\        9   d   V3# R3# V R3# )zStrip a leading [emotion] tag; return (clean_text, tag_name_or_None).

Unknown tone tags are stripped too (a TTS must never read one aloud); their
last word is tried as a known tag ("[Mildly excited]" -> "excited"). N)
_TAG_REmatchendlstripgrouplower_ANY_TAG_REstripsplit_TAG_TO_STEM)r#   minnerkeys   &   r
   split_emotionr5   S   s    
 	djjb!AEEGH~$$&
(8(8(:::$**"%A
  "1R(..0668#(ekkmBbEEGH~$$&|0CNNNN:r   c                    V ^8  d   QhRRRR/# r"   r   )r	   s   "r
   r   r   c   s     ? ? ? 6 ?r   c                ^    \        V 4      w  rY'       d   \        P                  V4      3# R3# )zv(clean_text, protocol_emotion) for the `sentence` frame. protocol_emotion
is one of warm|dry|excited|sad|calm or None.N)r5   _TAG_TO_PROTOCOLr   )r#   cleantags   &  r
   emotion_labelr;   c   s1     t$JE#'',>>>>r   c                    V ^8  d   QhRRRR/# )r   r#   r$   r   z
str | Noner   )r	   s   "r
   r   r   j   s     2 23 2: 2r   c                Z    \        V 4      w  rV'       d   \        P                  V4      # R# )zHThe mood-clip stem to voice this (tagged) sentence with, or None (base).N)r5   r1   r   )r#   _cleanr:   s   &  r
   stem_forr?   j   s&    %KF$'<C 1T1r   c               $    V ^8  d   QhRRRRRR/# )r   samples
np.ndarrayrateintr   bytesr   )r	   s   "r
   r   r   s   s!      * C % r   c                Z   V e   \        V 4      ^ 8X  d   RpMb\        P                  ! \        P                  ! V \        P                  R7      RR4      pVR,          P                  R4      P                  4       p\        P                  ! 4       p\        P                  ! VR4      ;_uu_ 4       pVP                  ^4       VP                  ^4       VP                  V4       VP                  V4       RRR4       VP                  4       #   + '       g   i     TP                  4       # ; i)	zFEncode a float32 [-1, 1] mono array as a complete 16-bit PCM WAV blob.Nr   dtypeg      ?g    @z<i2wbg      )lennpclipasarrayfloat32astypetobytesioBytesIOwaveopensetnchannelssetsampwidthsetframeratewriteframesgetvalue)rA   rC   pcmclippedbufws   &&    r
   float32_to_wav_bytesr^   s   s    #g,!+''"**WBJJ?sK ((/779
**,C	3			q	q	t	c	 

 <<> 
	
 <<>s   .ADD*	POCKET_VOICEgeorgePOCKET_MAX_TOKENS300c                  b    ] tR t^tRtR R ltR R ltR R ltR R	 ltR
 R lt	R R lt
RtR# )	PocketTTSu   Loads pocket-tts, clones the base + mood voices, synthesizes WAV bytes.

Constructing this LOADS the model — only do so from ``get_tts()`` on the
first voice session.
c                   V ^8  d   QhRR/# )r   r   Noner   )r	   s   "r
   r   PocketTTS.__annotate__   s     
% 
%$ 
%r   c                	   ^ RI Hp VP                  4       V n        \        P
                  ! 4       V n        / V n        RV n        \        4       p\        P                  P                  RR4      p\        P                  P                  V4      '       d   TM\        W#,          4      pV P!                  V4      V n        V P%                  V4       R# )    )TTSModelF	VOICE_REFzjarvis_ref.wavN)
pocket_ttsrj   
load_model_model	threadingLock_lock_states_clonedr   r   r   r   pathisabsr$   
_load_base
_base_name_preload_emotions)selfrj   vdirref_namebase_refs   &    r
   __init__PocketTTS.__init__   s    '))+^^%
*,{::>>+/?@!ww}}X668C<P//(3t$r   c                    V ^8  d   QhRRRR/# )r   refr$   r   r   )r	   s   "r
   r   rg      s     ! !c !c !r   c                	   \         P                  P                  V4      '       d^    V P                  P	                  V4      V P
                  R &   RV n        \        R\         P                  P                  V4       24       V# V P                  P	                  \        4      V P
                  R &   \        #   \         d1   p\        R\        T4      R,           R\         R24        Rp?LhRp?ii ; i)r&   Tz  [tts] voice cloned from z  [tts] clone failed (:NP   Nu   ) — catalog ''N)r   rt   isfilern   get_state_for_audio_promptrr   rs   printbasename	Exceptionr$   _CATALOG_FALLBACK)ry   r   es   && r
   rv   PocketTTS._load_base   s    77>>#0#';;#I#I##NR #22773C3CC3H2IJK
  ;;AAR    0.s1vc{m <+,A/ 0 00s   AB5 5C0 &C++C0c                    V ^8  d   QhRRRR/# )r   rz   r   r   rf   r   )r	   s   "r
   r   rg      s     M Md Mt Mr   c           
     	   V P                   '       g   R # \        P                  R \        P	                  4        4       4       FZ  pVRV R2,          pVP                  4       '       g   K(   V P                  P                  \        V4      4      V P                  V&   K\  	  V P                   Uu. uF  qU'       g   K  VNK  	  ppV'       d*   \        RR	P                  \        V4      4       R24       R # R #   \         d8   p\        RTP                   R\        T4      R,           R24        R p?K  R p?ii ; iu upi )
Nc              3  8   "   T F  q'       g   K  Vx  K  	  R # 5i)Nr   ).0ss   & r
   	<genexpr>.PocketTTS._preload_emotions.<locals>.<genexpr>   s     !H-Ba!!-Bs   	
jarvis_z.wavz  [tts] mood clip z	 failed (:N<   N)z  [tts] mood clones ready (z, )rs   dictfromkeysr1   valuesis_filern   r   r$   rr   r   r   namejoinsorted)ry   rz   stemrt   r   kmoodss   &&     r
   rx   PocketTTS._preload_emotions   s    |||MM!H\-@-@-B!HHDGD6..D<<>>O%)[[%K%KI&T" I !LL.LqAL./		&-0H/IKL   O*499+Ys1vc{m1MNNO.s$   ,1C8/	D==D=8D:,D55D:c                   V ^8  d   QhRR/# )r   r#   r$   r   )r	   s   "r
   r   rg      s     E Es Er   c                	    \        V4      p\        V4      w  r4Y0P                  P                  T;'       g    R V P                  R ,          4      3# )r&   )r?   r5   rr   r   )ry   r#   r   r9   _tags   &&   r
   
_state_forPocketTTS._state_for   s@    ~#D)ll&&tzzr4<<3CDDDr   c                    V ^8  d   QhRRRR/# )r   r#   r$   r   rB   r   )r	   s   "r
   r   rg      s     I I# I* Ir   c                   V P                  V4      w  r#VP                  4       '       g'   \        P                  ! ^ \        P                  R7      # V P
                  ;_uu_ 4        V P                  P                  W2\        R7      pRRR4       XP                  4       P                  4       P                  4       P                  \        P                  4      P                  R4      #   + '       g   i     Lh; i)z?(optionally [emotion]-tagged) text -> float32 mono PCM @ 24kHz.rG   )
max_tokensNr'   )r   r/   rK   zerosrN   rq   rn   generate_audio_MAX_TOKENSdetachcpunumpyrO   reshape)ry   r#   r9   statewavs   &&   r
   synthPocketTTS.synth   s    t,{{}}88ARZZ00ZZZ++,,U8C - EC  zz|!'')00<DDRHH Zs   $"C&&C6	c                    V ^8  d   QhRRRR/# )r   r#   r$   r   rE   r   )r	   s   "r
   r   rg      s     C Cc Ce Cr   c                @    \        V P                  V4      \        4      # )zD(optionally [emotion]-tagged) text -> complete 16-bit PCM WAV bytes.)r^   r   SAMPLE_RATE)ry   r#   s   &&r
   	synth_wavPocketTTS.synth_wav   s    #DJJt$4kBBr   )rw   rs   rq   rn   rr   N)__name__
__module____qualname____firstlineno____doc__r}   rv   rx   r   r   r   __static_attributes__r   r   r
   rd   rd      s/    
%!M E
IC Cr   rd   zPocketTTS | None	_INSTANCEc                   V ^8  d   QhRR/# )r   r   rd   r   )r	   s   "r
   r   r      s       r   c                     \         f0   \        ;_uu_ 4        \         f   \        4       s RRR4       \         # \         #   + '       g   i     \         # ; i)u   Load-on-first-use accessor. Raises if pocket-tts isn't installed — the
caller turns that into an {"type":"error"} frame; chat still works.N)r   
_INIT_LOCKrd   r   r   r
   get_ttsr      s>     Z %K	  9 Z s	   >A	)r   r   r   r   r   r   r   r   r   r   r   r   )+__conditional_annotations__r   
__future__r   rQ   r   rero   rS   pathlibr   r   rK   r   __file__resolveparent
_REPO_ROOTr   r   	_EMOTIONScompiler   
IGNORECASEr(   r.   r1   r8   r5   r;   r?   r^   r   r   r   rD   r   rd   r   __annotations__rp   r   r   )r   s   @r
   <module>r      s  . # 	 	 	     (^##%,,33
(* C	 **Z#((9"55	A2==
Q jj;R]]K
 FFFIv
FGVy(Iy)	5ut FFFIv
FGVy(Iy)	5ut  ?2 ;F & JJNN>8< "**..!4e<=BC BCL #	 "^^
r   