
    PmjpH                       U d Z ddlm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
mZmZmZmZ ddlmZ ddlmZmZmZ  ej        e          ZdZd=dZdZdZdaded<   d>dZd?dZ ej         d          Z! ej         dej"                  Z# G d d          Z$ G d de          Z%i Z&ded<   d@d!Z'dAd%Z(g d&Z)d'ed(<   	 dBdCd+Z* e'd,           G d- d.e%                      Z+dDd/Z, e'd0           G d1 d2e%                      Z-dEd6Z. e'd7           G d8 d9e%                      Z/ e'd:           G d; d<e%                      Z0dS )Fu  Provider-agnostic streaming TTS: sentence text → int16 PCM chunk iterator.

The keystone of Hermes' conversational voice UX. `stream_tts_to_speaker`
(``tools.tts_tool``) owns the sentence buffer, sounddevice output, and
stop/queue protocol; this module owns the *provider* half — turning one
sentence into audio the moment it's ready, so playback starts on sentence one
instead of after the whole reply.

Two provider shapes, one contract (int16 mono PCM at ``sample_rate``):

* **True streamers** (`StreamingTTSProvider.stream`) — chunked APIs
  (ElevenLabs pcm_24000, OpenAI pcm, …) that yield audio as it synthesizes.
  Lowest time-to-first-audio.
* **Everyone else** — providers with no chunked API still get per-*sentence*
  playback via the proven sync `text_to_speech_tool` path (handled by the
  dispatcher, not here), so edge (the default) is conversational too.

Adding a streamer is `@register("name")` on a `StreamingTTSProvider` subclass;
the dispatcher, config gate (`tts.<name>.streaming`), and resolver come free.
    )annotationsN)ABCabstractmethod)CallableDictIteratorListOptional)resolve_openai_audio_api_key)_get_provider_load_tts_configget_env_valuei   env_varstrprovider_idreturnc                n    	 ddl m}  || |          pdS # t          $ r t          |           pdcY S w xY w)u  Provider secret lookup: config > env/.env > credential pool.

    Thin, monkeypatchable seam over ``tools.tts_tool._resolve_provider_key``
    (which delegates to ``resolve_provider_secret``). ALL streaming-provider
    key lookups go through here — never bare ``get_env_value``.
    r   )_resolve_provider_key )tools.tts_toolr   	Exceptionr   )r   r   r   s      9/home/thesage/.hermes/hermes-agent/tools/tts_streaming.py_resolve_keyr   *   se    ,888888$$Wk::@b@ , , ,W%%++++,s    44zK[Note: the user interrupted your previous spoken reply before it finished.]g      ^@zOptional[float]_interrupted_atNonec                 ,    t          j                    ad S N)time	monotonicr        r   mark_speech_interruptedr"   I   s    n&&OOOr!   boolc                 \    t           dc} a | duot          j                    | z
  t          k     S )z9Pop the latch; True when a barge happened within the TTL.N)r   r   r   _INTERRUPT_TTL_S)ats    r   take_speech_interruptedr'   N   s2     *4BT>Fdn..36FFFr!   z(?<=[.!?])(?:\s|\n)|(?:\n\n)z<think[\s>].*?</think>)flagsc                  ,    e Zd ZdZdddZdd
ZddZdS )SentenceChunkera  Incremental sentence cutter for LLM token deltas.

    Shared by the speaker pipeline (`stream_tts_to_speaker`) and the
    speak-stream WebSocket so every surface cuts speech identically. Strips
    ``<think>`` blocks (even split across deltas) and merges fragments shorter
    than *min_len* into the following sentence, so "Ha!" rides along with the
    sentence after it instead of stalling as a tiny clip.
       min_lenintc                "    || _         d| _        d S )Nr   )r,   buf)selfr,   s     r   __init__zSentenceChunker.__init__c   s    r!   deltar   r   	List[str]c                H   t                               d| j        |z             | _        d| j        v rd| j        vrg S g }d}t                              | j        |          x}r| j        d|                                         }t          |                                          | j        k     r|                                }|	                    |           | j        |                                d         | _        d}t                              | j        |          x}|S )zBAbsorb *delta*; return every complete sentence now ready to speak.r   z<thinkz</think>r   N)
_THINK_BLOCK_REsubr/   SENTENCE_BOUNDARY_REsearchendlenstripr,   append)r0   r2   outstartmheads         r   feedzSentenceChunker.feedg   s   "&&r48e+;<<txJdh$>$>I'..tx???a 	8IaeeggI&D4::<<  4<//JJtx)DHE (..tx???a 	 
r!   c                    t                               d| j                                                  }d| _        |r|gng S )z0Drain the tail (end-of-text or long-idle flush).r   )r5   r6   r/   r;   )r0   tails     r   flushzSentenceChunker.flushx   s=    ""2tx006688%vv2%r!   N)r+   )r,   r-   )r2   r   r   r3   )r   r3   )__name__
__module____qualname____doc__r1   rA   rD   r    r!   r   r*   r*   Y   s_                "& & & & & &r!   r*   c                      e Zd ZU dZdZded<   dZded<   dZded<   ddZe	e
dd                        Ze
dd            ZdS )StreamingTTSProviderzDYields raw int16, little-endian, mono PCM chunks at ``sample_rate``.]  r-   sample_rate   channels   sample_width
tts_configr   sectionc                "    || _         || _        d S r   )rQ   rR   )r0   rQ   rR   s      r   r1   zStreamingTTSProvider.__init__   s    $r!   r   r#   c                     dS )z?True when this provider's credentials/SDK are usable right now.Nr    r    r!   r   	availablezStreamingTTSProvider.available         r!   textr   Iterator[bytes]c                    dS )z>Yield PCM chunks for ``text``. Raise on failure (caller logs).Nr    r0   rW   s     r   streamzStreamingTTSProvider.stream   rV   r!   N)rQ   r   rR   r   r   r#   rW   r   r   rX   )rE   rF   rG   rH   rL   __annotations__rN   rP   r1   staticmethodr   rU   r[   r    r!   r   rJ   rJ      s         NNKHL    N N N ^ \N M M M ^M M Mr!   rJ   z%Dict[str, type[StreamingTTSProvider]]	_REGISTRYnameBCallable[[type[StreamingTTSProvider]], type[StreamingTTSProvider]]c                     d fd}|S )Nclstype[StreamingTTSProvider]r   c                    | t           <   | S r   )r`   )rd   ra   s    r   _wrapzregister.<locals>._wrap   s    	$
r!   )rd   re   r   re   r    )ra   rg   s   ` r   registerrh      s(          Lr!   rQ   r   Optional[StreamingTTSProvider]c                   t                               |           }||                                sdS 	  |||                    |           pi           S # t          $ r'}t                              d| |           Y d}~dS d}~ww xY w)zCConstruct the registered streamer *name* if it's usable, else None.Nz%streaming provider %s init failed: %s)r`   getrU   r   loggerdebug)ra   rQ   rd   excs       r   _try_instantiatero      s    
--

C
{#--//{ts:z~~d339r:::   <dCHHHttttts    A 
BBB)
elevenlabsgeminiopenaixair3   _PROVIDER_PRIORITY	preferredOptional[str]c                   |                      d          pi }t          |                     d          pd                                                                          }|dk    r"t          D ]}t          ||           }||c S dS |rt          ||           S |pt          |                                                                           }t          ||           S )u  Return a ready streamer for the *configured* provider, else ``None``.

    Resolution order:

    1. ``tts.streaming.provider`` (config knob) when set:
       * a provider name pins that exact streamer (or ``None`` if unusable);
       * ``auto`` walks the priority list (``elevenlabs → gemini → openai
         → xai``) and returns the first usable streamer — an explicit
         opt-in to "give me the best chunked voice available".
    2. Otherwise the *configured* TTS provider (or ``preferred`` override).
       ``None`` means "no chunked API for this provider" — the dispatcher
       then speaks per-sentence via the sync path, preserving the user's
       chosen voice. We never silently swap to a different provider just
       to get streaming.
    	streamingproviderr   autoN)rk   r   lowerr;   rt   ro   r   )rQ   ru   streaming_cfgpinnedra   insts         r   resolve_streaming_providerr      s    & NN;//52M"":..4"55;;==CCEEF& 	 	D#D*55D  t 4
3332z2299;;AACCDD*---r!   rp   c                  6    e Zd ZdZdZedd            Zdd	Zd
S )ElevenLabsStreameruD   ElevenLabs chunked HTTP → pcm_24000 (the original reference path).rK   r   r#   c                 <    t          t          dd                    S )NELEVENLABS_API_KEYrp   r#   r   r    r!   r   rU   zElevenLabsStreamer.available   s    L!5|DDEEEr!   rW   r   rX   c              #  d  K   ddl m}m}m}m}   |            ddt          dd          i || j                  }| j                            d|          }| j                            d| j                            d|                    }|j        	                    |||d	
          E d {V  d S )Nr   )%DEFAULT_ELEVENLABS_STREAMING_MODEL_IDDEFAULT_ELEVENLABS_VOICE_ID_elevenlabs_environment_kwargs_import_elevenlabsapi_keyr   rp   voice_idstreaming_model_idmodel_id	pcm_24000)rW   r   r   output_formatr    )
r   r   r   r   r   r   rR   rk   text_to_speechconvert)	r0   rW   r   r   r   r   clientr   r   s	            r   r[   zElevenLabsStreamer.stream   s,     	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 &##%% 
 
 !5|DD
,,T\::
 
 <##J0KLL<## LZ)NOO
 
 (00%	 1 
 
 	
 	
 	
 	
 	
 	
 	
 	
 	
r!   Nr\   r]   rE   rF   rG   rH   rL   r_   rU   r[   r    r!   r   r   r      sU        NNKF F F \F
 
 
 
 
 
r!   r   c                     	 t                                          d          pi } n# t          $ r Y dS w xY w|                     d          pdS )z@Return ``tts.openai.api_key`` from config.yaml, or empty string.rr   r   r   )r   rk   r   )
openai_cfgs    r   _openai_config_api_keyr      s`    &((,,X66<"

   rr>>)$$**s   #& 
44rr   c                  6    e Zd ZdZdZedd            Zdd	Zd
S )OpenAIStreamerz?OpenAI speech with ``response_format=pcm`` (24 kHz mono int16).rK   r   r#   c                 T    t          t                      pt                                S r   )r#   r   r   r    r!   r   rU   zOpenAIStreamer.available  s#    *,,N0L0N0NOOOr!   rW   r   rX   c              #    K   ddl m}  || j                            d          pt	                      | j                            d          pt          d          pd           }| j                            dd          }| j                            d	d
          }|j        j        j        	                    |||d          5 }t          |                                d          E d {V  d d d            d S # 1 swxY w Y   d S )Nr   )OpenAIr   base_urlOPENAI_BASE_URL)r   r   modelzgpt-4o-mini-ttsvoicealloypcm)r   r   inputresponse_formatzOpenAI streaming TTS)rr   r   rR   rk   r   r   audiospeechwith_streaming_responsecreate_capped
iter_bytes)r0   rW   r   r   r   r   responses          r   r[   zOpenAIStreamer.stream  s     !!!!!!\%%i00R4P4R4R  ,,  !233
 
 
   *;<<  '22\ 8??!	 @ 
 
 	N
 x22446LMMMMMMMMM	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	N 	Ns   )C99C= C=Nr\   r]   r   r    r!   r   r   r     s[        IIKP P P \PN N N N N Nr!   r   chunksrX   labelc              #     K   d}| D ]G}|t          |          z  }|t          k    r$t                              d|t                      dS |V  HdS )u  Pass chunks through, aborting past the 16 MiB per-sentence cap.

    The streaming mirror of ``_read_tts_response_bytes``'s bounded-body
    invariant: one sentence of PCM should never approach the cap, so
    exceeding it means a runaway/hostile upstream — stop pulling.
    r   z1%s exceeded %d bytes for one sentence; truncatingN)r:   _STREAM_SENTENCE_BYTE_CAPrl   warning)r   r   totalchunks       r   r   r   (  ss       E  U,,,NNN ";= = =FF r!   rq   c                  6    e Zd ZdZdZedd            Zdd	Zd
S )GeminiStreameru(  Gemini ``streamGenerateContent?alt=sse`` → base64 PCM chunks (24 kHz).

    Salvaged from PR #47588 (@Cdddo) and rebased onto the post-campaign
    infrastructure: credentials via the provider-secret resolver, requests
    (not httpx) with a bounded streamed body, and main's provider ABC.
    rK   r   r#   c                 \    t          t          dd          pt          dd                    S )NGEMINI_API_KEYrq   GOOGLE_API_KEYr   r    r!   r   rU   zGeminiStreamer.availableD  s5    )844 8,h77
 
 	
r!   rW   r   rX   c              #    	
K   dd l dd l	dd lddlm}m}m} t          dd          pt          dd          
t          | j	        
                    d|                                                    p|}t          | j	        
                    d|                                                    p|}t          | j	        
                    d          pt          d	          p|                                                              d
          }dd|igigdgddd|iiidd| d| dd	
fd}t           |            d          E d {V  d S )Nr   )DEFAULT_GEMINI_TTS_BASE_URLDEFAULT_GEMINI_TTS_MODELDEFAULT_GEMINI_TTS_VOICEr   rq   r   r   r   r   GEMINI_BASE_URL/partsrW   AUDIOvoiceConfigprebuiltVoiceConfig	voiceName)responseModalitiesspeechConfig)contentsgenerationConfigz/models/z:streamGenerateContentr   rX   c               3    K                        d	ddd          5 } |                                  |                     d          D ]}|r|                    d          s	                     |t          d          d                    }|d         d	         d
         d         }n## t          t          t          t          f$ r Y w xY w|D ]}|
                    d          p|
                    d          pi }|
                    dd          }|sG	 
                    |          V  `# t          t          f$ r%}t                              d|           Y d }~d }~ww xY w!	 d d d            d S # 1 swxY w Y   d S )Nsse)altkey<   T)paramsjsontimeoutr[   )decode_unicodezdata: 
candidatesr   contentr   
inlineDatainline_datadatar   z Gemini SSE: bad base64 audio: %s)postraise_for_status
iter_lines
startswithloadsr:   
ValueErrorKeyError
IndexError	TypeErrorrk   	b64decoderl   r   )r   lineeventr   partinlineb64rn   _jsonr   base64payloadrequestsurls           r   _sse_chunksz*GeminiStreamer.stream.<locals>._sse_chunksr  s;     $W55    T ))+++$//t/DD T TD !tx'@'@ ! ! %DX,@ A A %l 3A 6y A' J&*iH ! ! ! ! % T T!%,!7!7!X488M;R;R!XVX$jj44" %$T"("2"23"7"77777 *I6 T T T"NN+MsSSSSSSSSTTTT T T T T T T T T T T T T T T T T Tsb   AE;(AB-,E;-C
E;CAE;D21E;2E(	E#	E;#E(	(E;;E?E?zGemini streaming TTS)r   rX   )r   r   r   r   r   r   r   r   r   rR   rk   r;   r   rstripr   )r0   rW   r   r   r   r   r   r   r   r   r   r   r   r   r   s            @@@@@@r   r[   zGeminiStreamer.streamK  s     	
 	
 	
 	
 	
 	
 	
 	
 	
 	
 )844 8,h77 	 DL$$W.FGGHHNNPPlTlDL$$W.FGGHHNNPPlTlLZ(( +.//+*
 
 %''&&++	 	 "VTN#345'.i!-U/C$!! !

 

 @@5@@@	T 	T 	T 	T 	T 	T 	T 	T 	T 	T 	T6 ;;==*@AAAAAAAAAAAr!   Nr\   r]   r   r    r!   r   r   r   9  sc          K
 
 
 \
BB BB BB BB BB BBr!   r   rs   c                  N    e Zd ZdZdZedd            Zdd	ZddZddZ	ddZ
dS )XAIStreameru  xAI WebSocket TTS → binary PCM frames (24 kHz mono int16).

    Salvaged from PR #47588 (@Cdddo): xAI's chunked TTS API is
    WebSocket-only (``wss://api.x.ai/v1/tts``). Credentials route through
    ``resolve_xai_http_credentials`` (OAuth or XAI_API_KEY), same as the
    sync ``_generate_xai_tts`` path. The async WS loop is bridged to the
    sync iterator contract via ``_collect_async`` — the seam unit tests
    monkeypatch.
    rK   r   r#   c                     	 ddl m}   |             }t          t          |                    d          pd                                                    S # t          $ r Y dS w xY w)Nr   resolve_xai_http_credentialsr   r   F)tools.xai_httpr   r#   r   rk   r;   r   )r   credss     r   rU   zXAIStreamer.available  s|    	CCCCCC0022EEIIi006B77==??@@@ 	 	 	55	s   AA 
A#"A#rW   r   rX   c              #  v   K   t          t          |                     |                    d          E d {V  d S )NzxAI streaming TTS)r   iter_collect_asyncrZ   s     r   r[   zXAIStreamer.stream  sE      4 3 3D 9 9::<OPPPPPPPPPPPr!   List[bytes]c                Z    dd l }|                    |                     |                    S )Nr   )asynciorun_drain_async)r0   rW   r   s      r   r   zXAIStreamer._collect_async  s+    {{4,,T22333r!   c                t   K   g }|                      |          2 3 d {V }|                    |           6 |S r   )_async_framesr<   )r0   rW   framesframes       r   r   zXAIStreamer._drain_async  s`       --d33 	! 	! 	! 	! 	! 	! 	!%MM%     4s   7c           	      K   dd l }dd l}ddlm} ddlm}  |            }t          |                    d          pd                                          }|st          d          t          | j
                            d|                                                    p|}t          | j
                            d          pd	                                          }	|                    |	d
d| i          4 d {V 	 }
|
                    |                    ||dd                     d {V  	 	 |
                                 d {V }t          |t           t"          t$          f          rt!          |          W V  P	 |                    |          }n1# t(          t*          f$ r |dk    rY d d d           d {V  d S Y w xY w|                    d          }|dk    r	 d d d           d {V  d S |dk    rYt,                              d|                    d          p|                    d          p|           	 d d d           d {V  d S '# t0          $ r^}|j        j        dk    rY d }~d d d           d {V  d S t,                              d|           Y d }~d d d           d {V  d S d }~ww xY w# 1 d {V swxY w Y   d S )Nr   )DEFAULT_XAI_VOICE_IDr   r   r   z$No xAI credentials for streaming TTSr   streaming_urlzwss://api.x.ai/v1/ttsAuthorizationzBearer )extra_headersr   )rW   r   r   TdonetypeerrorzxAI WS error envelope: %smessageConnectionClosedzxAI WS receive failed: %s)r   
websocketsr   r   r   r   r   rk   r;   RuntimeErrorrR   connectsenddumpsrecv
isinstancebytes	bytearray
memoryviewr   r   r   rl   r   r   	__class__rE   )r0   rW   r   r  r   r   r   r   r   ws_urlwsr  envelopeetypern   s                  r   r   zXAIStreamer._async_frames  s     777777??????,,..eii	**0b117799 	GEFFFDL$$Z1EFFGGMMOOgSgL_--H1H
 

%'' 	 %%?4Gg4G4G"H & 
 
 	 	 	 	 	 	 	 	''%++!#(' '           
$&GGIIooooooG!'E9j+IJJ !#Gnn,,,, !#(;;w#7#7&	2 ! ! !"f,,"%	 	 	 	 	 	 	 	 	 	 	 	 	 	& !! %LL00E-	 	 	 	 	 	 	 	 	 	 	 	 	 	. '''B'/||G'<'<'cY@W@W'c[ce e e5	 	 	 	 	 	 	 	 	 	 	 	 	 	$    =)-???FFF;	 	 	 	 	 	 	 	 	 	 	 	 	 	< :C@@@?	 	 	 	 	 	 	 	 	 	 	 	 	 	67	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	sz   3K	6AIFIG4II
GI=AII
K(K8K	K*K	KK		
KKNr\   r]   )rW   r   r   r   )rW   r   )rE   rF   rG   rH   rL   r_   rU   r[   r   r   r   r    r!   r   r   r     s          K   \Q Q Q Q
4 4 4 4
   0 0 0 0 0 0r!   r   )r   r   r   r   r   r   )r   r   r\   )ra   r   r   rb   )ra   r   rQ   r   r   ri   r   )rQ   r   ru   rv   r   ri   )r   r   )r   rX   r   r   r   rX   )1rH   
__future__r   loggingrer   abcr   r   typingr   r   r   r	   r
   tools.tool_backend_helpersr   r   r   r   r   	getLoggerrE   rl   r   r   SPEECH_INTERRUPTED_NOTEr%   r   r^   r"   r'   compiler7   DOTALLr5   r*   rJ   r`   rh   ro   rt   r   r   r   r   r   r   r   r    r!   r   <module>r      s    * # " " " " "  				  # # # # # # # # ; ; ; ; ; ; ; ; ; ; ; ; ; ; C C C C C C I I I I I I I I I I		8	$	$ - , , , ,2 R   #' ' ' ' '' ' ' '
G G G G "rz"ABB "*6biHHH#& #& #& #& #& #& #& #&TM M M M M3 M M M* 46	 5 5 5 5   	 	 	 	  !J I I  I I I I
  $. . . . .L 
,
 
 
 
 
- 
 
 
D+ + + + 
(N N N N N) N N N>   " 
(SB SB SB SB SB) SB SB SBl 
%W W W W W& W W W W Wr!   