
    aESj0                    &   d Z ddlmZ ddlZddlZddlmZ ddlZddl	m
Z
mZmZ dZ ee      j                         j                   j                   Zedz  dz  Zej&                  j)                  d	d
      Z eej&                  j)                  dd            Zi dddddddddddddddddddddddddddddddddddddddddd d d dd!Z eh d"      Z eh d#      ZdZd$\  ZZd,d%Zd-d&Z d.d'Z!d/d(Z"d) Z# G d* d+      Z$y)0u  Pocket TTS (Kyutai, MIT) — the Mac voice: cloned, streaming, CPU-only.

Chatterbox on Apple Silicon is structurally slow (MPS fallback ops, ~3GB,
thermal throttle on the fanless Air). Pocket TTS is a 100M CALM model that
runs ~7x realtime on TWO CPU cores of an M4 Air with ~40ms to first audio
chunk — so the Mac gets the cloned Jarvis voice without touching the GPU
or the RAM budget (~200MB).

Voice: voices/jarvis_ref.wav is cloned when the gated HF weights are
available (accept terms at https://huggingface.co/kyutai/pocket-tts, then
`hf auth login`). Without them it falls back to a catalog voice
(POCKET_VOICE, default "george") — never mute.

Emotion: Pocket has no exaggeration dial, so the [emotion] tag protocol
maps to *reference-clip swaps*: drop voices/jarvis_warm.wav,
jarvis_excited.wav, jarvis_dry.wav, jarvis_sad.wav, jarvis_calm.wav
(generate them once with Chatterbox on the RTX box, exaggeration dialed
per mood) and each tag speaks through the matching clone. Missing clips
just use the base voice. Emotion states preload in a background thread
after startup so the first [excited] sentence doesn't stall.

Env: POCKET_VOICE (catalog fallback voice), VOICE_REF (base clone wav,
default voices/jarvis_ref.wav), POCKET_MAX_TOKENS (default 300 ≈ 24s cap).
    )annotationsN)Path)_ANY_TAG_RE_TAG_REsplit_emotioni]  voiceszjarvis_ref.wavPOCKET_VOICEgeorgePOCKET_MAX_TOKENS300calmflatseriouswarmhappyexcitedurgentalarmedsadtired	sarcasticdryshocked	surprisedcurious
interested	intriguedamusedannoyedtender)r   playfulcheekyr   	irritated	impatienttestyr    gentlesoftneutral>   r   r   r"   r   r!   >   r%   r   r   r   r   r$   r#   r   )      c                6    | d uxr | t         k\  xs	 | t        k  S N)_LATE_START	_LATE_END)hours    >/Users/ahmed/devFolder/Ultron/claude-voice/voice/tts_pocket.py_late_nightr1   M   s!    tI!4!Hy8HI    c                 B    ddl m }  | j                         j                  S )Nr   datetime)r5   nowr/   r4   s    r0   _local_hourr7   Q   s    !<<>r2   c                   t        j                  | xs d      }|r6|j                  d      j                         }|t        j                  |      fS t        j                  | xs d      }|rp|j                  d      j                         dd j                         j                         }|r|j                         d   nd}|xs dt        j                  |      fS y)u   (emotion word, mood-clip stem) from a leading [emotion] tag — the exact
    lookup _state_for did inline, factored out so tone-biasing can reuse it.
    Improvised tags map via their last word; unknown ones give (word, None).    r   N)NN)	r   matchgrouplower_TAG_TO_STEMgetr   stripsplit)textmwinners       r0   
_parse_tagrG   V   s     	djb!AGGAJ,""1%%%$*"%A
  "1R(..0668!&EKKM"B	TL,,Q///r2   c                    | j                   ry| j                  r| j                  rdS dS | j                  r(t	        |      r| j
                  ry| j                  rdS dS y)u   No usable brain tag → pick a mood clip from Ahmed's vocal tone. Returns
    a jarvis_<stem> key, or None for the base voice (today's untagged delivery)
    when his tone is unremarkable.r   r   r   r    r   N)laughinguphighdownr1   slowlow)toner/   s     r0   
_tone_stemrP   f   sR     }}ww IIy383yyt88x//r2   c                f    |$| t         v r| t        vr|j                  rt        S |S t	        ||      S )u(  Fuse the brain's tag with Ahmed's live tone. `tag_word` = the brain's
    emotion word (or None); `base_stem` = the clip it maps to (None = neutral /
    no clip). A specific tag wins — except a bouncy one softens to `warm` when
    he's clearly down; urgent/annoyed/shocked are never softened.)_BOUNCE_TAGS_NEVER_DAMPrL   
_DAMP_STEMrP   )tag_word	base_stemrO   r/   s       r0   _biased_stemrW   u   s9    
 $)DIIdD!!r2   c                      e Zd ZdZdZddZedd       ZddZddZ	e
d        Zej                  d        Zdd	Zdd
ZddZy)	PocketTTSzDrop-in for KokoroTTS/ChatterboxTTS: synth(text) -> float32 @ 24kHz.
    Also exposes synth_stream(text) yielding ~80ms chunks (~40ms latency).Tc                .   ddl m} |j                         | _        t	        j
                         | _        i | _        d| _        d| _	        t        j                  j                  dd      dk7  | _        t        j                  j                  dt        t                    }| j!                  |      | _        | j%                  | j"                         | _        | j)                  d	       t	        j*                  | j,                  d
      j/                          y )Nr   )TTSModelFg      ?TONE_TTS10	VOICE_REFzWarm up.T)targetdaemon)
pocket_ttsr[   
load_model_model	threadingLock_lock_states_cloned_speedosenvironr@   	_tone_ttsstr_DEFAULT_REF
_load_base
_base_name_is_default_plainsynthThread_preload_emotionsstart)selfr[   base_refs      r0   __init__zPocketTTS.__init__   s    '))+^^%
*, 
C8C?::>>+s</@A//(3 **4??;;

: 6 6tDJJLr2   c                    	 t         j                  j                  |       t         j                  j                  t        t                    k(  S # t
        $ r Y yw xY w)u   True when `name` is the canonical Jarvis clone (the only voice the
        mood clips belong to). Any other base voice speaks 'plain' — the jarvis
        mood clips would sound like a different person mid-sentence.F)rk   pathabspathrn   ro   	Exception)names    r0   rr   zPocketTTS._is_default   sC    
	77??4(BGGOOC<M,NNN 		s   AA 	AAc                    t         j                  j                  |      r^	 | j                  j	                  |      | j
                  d<   d| _        t        dt         j                  j                  |       d       |S | j                  j	                  t              | j
                  d<   t        S # t        $ rT}t        |      }d|j                         v rt        dt         d       nt        d|d	d
  dt         d       Y d	}~d	}~ww xY w)zClone from the ref wav if the gated weights allow it, else use a
        catalog voice. Returns a display name for logs/status.r9   Tz  voice: cloned from z	 (pocket)zvoice cloningu     voice: cloning weights are GATED — accept terms at huggingface.co/kyutai/pocket-tts then `hf auth login`; using catalog voice 'z' meanwhilez  voice: clone failed (NP   u   ) — catalog '')rk   r|   isfilerd   get_state_for_audio_promptrh   ri   printbasenamer~   rn   r>   _CATALOG_FALLBACK)rx   refemsgs       r0   rp   zPocketTTS._load_base   s     77>>#4#';;#I#I##NR #-bgg.>.>s.C-DINO
  ;;AAR    	4!f"ciik1  00= >
 3CH: >/03 4	4s   AB0 0	D9A
DDc           
     @   | j                   syt        j                  d t        j	                         D              D ]W  }t
        dz  d| dz  }|j                         s$	 | j                  j                  t        |            }|| j                  |<   Y | j                  D cg c]  }|s|	 }}|r(t        d
dj                  t        |             d	       yy# t        $ r2}t        d|j                   dt        |      dd  d	       Y d}~d}~ww xY wc c}w )zBuild voice states for whatever jarvis_<mood>.wav clips exist.
        Runs in the background after startup; each takes a few seconds.Nc              3  &   K   | ]	  }|s|  y wr,    ).0ss     r0   	<genexpr>z.PocketTTS._preload_emotions.<locals>.<genexpr>   s     !H-Ba!-Bs   r   jarvis_z.wavz  voice: mood clip 	 failed (<   )z  voice: mood clones ready (z, )ri   dictfromkeysr?   values_PROJECTis_filerd   r   rn   rh   r~   r   r   joinsorted)rx   stemr|   stater   kmoodss          r0   rv   zPocketTTS._preload_emotions   s    ||MM!H\-@-@-B!HHDh&74&)==D<<>P>>s4yI%*T" I !LL.LqAL.06%=1I0J!LM   P+DII;iAs}ANOOP.s$   !3C$D,D	D&(DDc                    | j                   S r,   )rq   )rx   s    r0   _voicezPocketTTS._voice   s    r2   c           	        t        |t              r|| j                  k(  ry 	 | j                  5  | j                  j                  |      | j                  d<   d d d        || _        | j                  |       | _        t        dt        j                  j                  |       d| j                  rdnd d       y # 1 sw Y   exY w# t        $ r(}t        d|dt        |      d d	  d       Y d }~y d }~ww xY w)
Nr9   z  voice: switched to z (u   plain — moods offzjarvis + moodsr   z  voice: switch to r   r   )
isinstancern   rq   rg   rd   r   rh   rr   rs   r   rk   r|   r   r~   )rx   vr   s      r0   r   zPocketTTS._voice   s    !S!Q$//%9	F#';;#I#I!#LR  DO"..q11DK)"''*:*:1*=)> ?/3{{+@PQQRT U	   	F'uIc!fSbk]!DEE	Fs/   C )B;A#C ;C C 	C8C33C8c                l   t        | dd      rt        |      \  }}|| j                  d   fS t        |xs d      \  }}t        | dd      r+ddlm} t        |||j                         t                     }n|}t        |      \  }}|| j                  j                  |xs d| j                  d         fS )up  Strip the [emotion] tag and pick the matching voice state.
        Improvised tags ("[Mildly excited]") map via their last word. With
        TONE_TTS on, Ahmed's live vocal tone also steers the pick when the line
        is untagged and softens a bouncy tag when he's plainly flat — while
        urgent/annoyed/shocked always speak at full tilt (see _biased_stem).rs   Fr9   rm   r   )prosody)
getattrr   rh   rG   voicer   rW   
tone_stater7   r@   )rx   rC   clean_rU   rV   r   r   s           r0   
_state_forzPocketTTS._state_for   s     45)$T*HE1$,,r***(4)4e,%) ' 2 2 4kmED D &qdll&&tzr4<<3CDDDr2   c                   | j                  |      \  }}|j                         s%t        j                  dt        j                        S | j
                  5  | j                  j                  ||t              }ddd       j                         j                         j                         j                  t        j                        j                  d      S # 1 sw Y   axY w)z>text (optionally '[emotion] ...') -> float32 mono PCM @ 24kHz.r   )dtype
max_tokensNr;   )r   rA   npzerosfloat32rg   rd   generate_audio_MAX_TOKENSdetachcpunumpyastypereshape)rx   rC   r   r   wavs        r0   rt   zPocketTTS.synth   s    t,u{{}88ARZZ00ZZ++,,u - 6C  zz|!'')00<DDRHH Zs   #CC"c              #    K   | j                  |      \  }}|j                         sy| j                  5  | j                  j	                  ||t
              D ]\  }|j                         j                         j                         j                  t        j                        j                  d       ^ 	 ddd       y# 1 sw Y   yxY ww)u   Yield float32 PCM chunks (~80ms each) as they're generated —
        first chunk in ~40ms on an M4. Holds the model lock for the whole
        sentence; callers stream one sentence at a time anyway.Nr   r;   )r   rA   rg   rd   generate_audio_streamr   r   r   r   r   r   r   r   )rx   rC   r   r   chunks        r0   synth_streamzPocketTTS.synth_stream   s      t,u{{}ZZ::5[ ; :lln((*00299JJ  ',: ZZs   2CBC 7	C C	CN)returnNone)r   rn   r   bool)r   rn   r   rn   )rC   rn   )rC   rn   r   z
np.ndarray)__name__
__module____qualname____doc__accepts_tagsrz   staticmethodrr   rp   rv   propertyr   setterr   rt   r   r   r2   r0   rY   rY      ss    N LM&  !.N&   ]]F FE*I,r2   rY   )r/   
int | Noner   r   )r   int)rC   rn   r   ztuple[str | None, str | None])r/   r   r   z
str | None)%r   
__future__r   rk   re   pathlibr   r   r   voice.tts_chatterboxr   r   r   SAMPLE_RATE__file__resolveparentr   ro   rl   r@   r   r   r   r?   	frozensetrR   rS   rT   r-   r.   r1   r7   rG   rP   rW   rY   r   r2   r0   <module>r      s  2 # 	    D D>!!#**11("%55JJNN>8< "**..!4e<=

FF$-v
FV y #I 09) 
5	 5	
 
 u y &y y '	 4?	 8xyy(H0 LM G H
 YJ
 
"I, I,r2   