
    epj'j                       U d Z ddlZddlZddlZddlZddlZddlZddlZddlZddl	Z	ddl
Z
ddlZddl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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 m!Z! ddl"m#Z# dd	l$m%Z%  ej&        e'          Z(dd
Z)de*de*de*fdZ+ddl,m-Z- ddl.m/Z/m0Z0m1Z1m2Z2 ddl3m4Z4 d Z5d Z6dee*ef         dee*ef         fdZ7d Z8d Z9d Z:d Z;d Z<dZ=dZ>dZ?dZ@dZAd ZB eCd h          ZDd!ZEd"ZFd#ZGd$ZHd%ZId&ZJd'ZKd(ZLd)ZMd*ZNd+ZOd,ZPd-ZQd.ZRd/ZSd0ZTd1ZUd2ZVd3ZWd4ZXdZYd0ZZd5Z[d6Z\d7Z]d0Z^d8Z_d9Z`d.Zad:Zbd;Zcd<Zdd=Zede*fd>Zf ef            Zgd?d@dAdBdCdDdBdEdEd?dF
Zhee*eif         ejdG<   d?d?dBdBdBdBdHdIdJZkee*eif         ejdK<   ddLed9eldelfdMZmdNedelfdOZndNeddfdPZoddQdNedRe*dSeei         depfdTZqddQdNedRe*dSeei         dee*ef         fdUZrddQdNedVe*dRe*dSeei         ddf
dWZsdCZtetZu	 ddXee*         dYeee*ef                  deifdZZvdee*ef         fd[ZwdYee*ef         de*fd\Zx ed]^           G d_ d`                      ZydYee*ef         deyfdaZz eCh db          Z{dcZ|ddZ} eCh de          Z~d?Z eCh df          ZdYee*ef         dge*dee*ef         fdhZdYee*ef         dge*dee*ef         fdiZdjee*ef         delfdkZdXe*dYee*ef         deee*ef                  fdlZdme*dVe*dXe*dYee*ef         dee*         f
dnZdXe*delfdoZdYee*ef         fdpZdjee*ef         defdqZ	 ddjee*ef         dVee*         de*fdrZdjee*ef         delfdsZdte*dueidee*         fdvZdLe*dwee*         de*fdxZdte*dyee*e*f         de*fdzZd{ej        ddfd|Zdjee*ef         defd}Z	 dd~e*dedee         dej        fdZdedjee*ef         defdZdme*dVe*de*djee*ef         dYee*ef         de*fdZddYeee*ef                  delfdZdelfdZde*dee*         fdZde*de*dee*         fdZde*de*fdZde*de*fdZdme*dVe*dYee*ef         de*fdZdme*dVe*dYee*ef         de*fdZdVe*de*fdZddddddddme*dVe*dYee*ef         dee*         dee*         dee*         dee*         dee         dee*         de*fdZdme*dVe*dYee*ef         de*fdZdZdZ e	j        dd                    e          z   dz   d                    e          z   dz   e	j                  Z e	j        de	j                  ZddLed9eldelfdZdme*de*fdZdme*dVe*dYee*ef         de*fdZdme*dVe*dYee*ef         de*fdZdme*dVe*dYee*ef         de*fdZeaebecfdepdeideideidepf
dZdee*ef         dee         fdZdee*ef         de*fdZde*delfdZdee*ef         de*delfdZde*de*fdZdNede*fdZddme*de*de*fdZ	 ddme*dee*ef         dee*         de*fdZdme*dVe*dYee*ef         de*fdZdelfdZdelfdZde*fdZde*fdZdme*dVe*dYee*ef         de*fdZdZdee*ef         de*deg ef         defdĄZi Zee*ef         ejd<   delfdƄZdefdǄZde*dede*fdɄZdme*dVe*dYee*ef         de*fdʄZi Zee*ef         ejd<   dme*dVe*dYee*ef         de*fd̄Z	 	 	 	 ddme*dVee*         dee         dee*         dXee*         de*fd̈́Zdelfd΄Zdee*e*elf         fdτZdelfdЄZ e	j        dѦ          Z e	j        dҦ          Z e	j        dӦ          Z e	j        dԦ          Z e	j        dզ          Z e	j        d֦          Z e	j        de	j                  Z e	j        de	j                  Z e	j        d٦          Z e	j        dڦ          Z e	j        dۦ          Z e	j        de	j                  Zdme*de*fd݄Z G dބ dߦ          Z	 	 ddej        dej        dej        deee*gdf                  dXee*         f
dZe'dk    r^ ed            ed           d Z ed            ed ee5d          rdnd             ed ee6d          rdnd             ed e+dd          rdnd             ed ee8d          rdnd             ed e2            rdnd             ew            Z	  eze          Zdej         dej         dZn# e$ rZde dZY dZ[ndZ[ww xY w ede             ed e            rdnd             eސd  e            rdnd             eސdeg             exe          Z eސde            ddlmZmZ dddd	d
dd	d e%             dddddd	ddd	ddddmgddZ ej        dded eȐd           dS (  a  
Text-to-Speech Tool Module

Built-in TTS providers:
- Edge TTS (default, free, no API key): Microsoft Edge neural voices
- ElevenLabs (premium): High-quality voices, needs ELEVENLABS_API_KEY
- OpenAI TTS: Good quality, needs OPENAI_API_KEY
- MiniMax TTS: High-quality with voice cloning, needs the selected region's key
- Mistral (Voxtral TTS): Multilingual, native Opus, needs MISTRAL_API_KEY
- Google Gemini TTS: Controllable, 30 prebuilt voices, needs GEMINI_API_KEY
- xAI TTS: Grok voices, uses xAI Grok OAuth credentials or XAI_API_KEY
- NeuTTS (local, free, no API key): On-device TTS via neutts
- KittenTTS (local, free, no API key): On-device 25MB model
- Piper (local, free, no API key): OHF-Voice/piper1-gpl neural VITS, 44 languages

Custom command providers:
- Users can declare any number of named providers with ``type: command``
  under ``tts.providers.<name>`` in ``~/.hermes/config.yaml``. Hermes
  writes the input text to a temp file and runs the configured shell
  command, which must produce the audio file at the expected path.
  See the Local Command section of ``website/docs/user-guide/features/tts.md``.

Output formats:
- Opus (.ogg) for Telegram voice bubbles (requires ffmpeg for Edge TTS)
- MP3 (.mp3) for everything else (CLI, Discord, WhatsApp)

Configuration is loaded from ~/.hermes/config.yaml under the 'tts:' key.
The user chooses the provider and voice; the model just sends text.

Usage:
    from tools.tts_tool import text_to_speech_tool, check_tts_requirements

    result = text_to_speech_tool(text="Hello world")
    N)FutureThreadPoolExecutor)	dataclassfield)Path)CallableDictAnyIteratorOptional)urljoinurlparse)windows_hide_flags)display_hermes_homec                 ~    	 ddl m} n%# t          $ r t          j        | |          cY S w xY w ||           }||n|S )a  Read env values through the live config module.

    Tests may monkeypatch and later restore ``hermes_cli.config.get_env_value``
    before this module is imported. Resolve the helper at call time so TTS does
    not keep a stale imported function for the rest of the test process.
    r   )get_env_value)hermes_cli.configr   ImportErrorosgetenv)namedefault_get_env_valuevalues       4/home/thesage/.hermes/hermes-agent/tools/tts_tool.pyr   r   ?   sl    (EEEEEEE ( ( (yw'''''(N4  Em77.s   	 ++env_varprovider_idreturnc                     	 ddl m} n@# t          $ r3 t          t	          |           pd                                          cY S w xY w || |t                    S )u}  Resolve a TTS provider API key via the shared voice-key resolver.

    Delegates to ``tools.tool_backend_helpers.resolve_provider_secret`` —
    the single owner of STT/TTS key resolution (config > env/.env > the
    credential pool populated by ``hermes auth add <provider_id>``).
    Resolved at call time so tests that reload the helpers module see the
    live function.
    r   )resolve_provider_secret )
env_getter)tools.tool_backend_helpersr    r   strr   strip)r   r   r    s      r   _resolve_provider_keyr&   N   s|    9FFFFFFF 9 9 9=))/R0066888889""7KMRRRRs   	 :AA)resolve_managed_tool_gateway)managed_nous_tools_enabled%nous_tool_gateway_unavailable_messageprefers_gatewayresolve_openai_audio_api_key)hermes_xai_user_agentc                  n    	 ddl m}   | dd           n# t          $ r Y nt          $ r Y nw xY wddl}|S )z?Lazy import edge_tts. Returns the module or raises ImportError.r   ensureztts.edgeFpromptN)tools.lazy_depsr/   r   	Exceptionedge_tts)_lazy_ensurer4   s     r   _import_edge_ttsr6   k   sv    ::::::Z.....      OOOO    
.	..c                  v    	 ddl m} m}  |dd           n# t          $ r Y nt          $ r Y nw xY wddlm} |S )a  Lazy import ElevenLabs client. Returns the class or raises ImportError.

    Calls :func:`tools.lazy_deps.ensure` first so the SDK gets installed on
    demand if the user picked ElevenLabs as their TTS provider but never ran
    the post-setup hook (e.g. enabled it by editing config.yaml directly).
    Raises ``ImportError`` on lazy-install failure so existing callers'
    error-handling paths keep working.
    r   )FeatureUnavailabler/   ztts.elevenlabsFr0   )
ElevenLabs)r2   r9   r/   r   r3   elevenlabs.clientr:   )r9   r/   r:   s      r   _import_elevenlabsr<   w   s    >>>>>>>>.....    	   ,,,,,,s    
0	00	el_configc                    |                      d          pd                    d          }|si S |                      d          pd                    d          }|st          j        dd|          }ddlm} d	 |||
          iS )ar  Build ElevenLabs client kwargs honoring config base_url/wss_url.

    ``tts.elevenlabs.base_url`` (and optionally ``wss_url``) redirect the SDK
    to a self-hosted / proxy endpoint via an ``ElevenLabsEnvironment``. When
    neither is set the SDK default environment is used. ``wss_url`` defaults
    to the ``base_url`` host with a ``wss://`` scheme when omitted.
    base_urlr!   /wss_urlz^httpwsr   )ElevenLabsEnvironmentenvironment)basewss)getrstripresubelevenlabs.environmentrC   )r=   r?   rA   rC   s       r   _elevenlabs_environment_kwargsrL      s     j))/R77<<H 	}}Y''-255c::G 3&422<<<<<<00hGLLLMM    c                      ddl m}  | S )zCLazy import OpenAI client. Returns the class or raises ImportError.r   )OpenAI)openairO   )OpenAIClients    r   _import_openai_clientrR      s    ------rM   c                  r    	 ddl m}   | dd           n# t          $ r Y nt          $ r Y nw xY wddlm} |S )aj  Lazy import Mistral client. Returns the class or raises ImportError.

    Calls :func:`tools.lazy_deps.ensure` first so the ``mistralai`` SDK gets
    installed on demand if the user picked Mistral as their STT/TTS provider
    but never ran the post-setup hook (e.g. enabled it by editing config.yaml
    directly). Mirrors the ElevenLabs lazy-import path.
    r   r.   ztts.mistralFr0   )Mistral)r2   r/   r   r3   mistralai.clientrT   )r/   rT   s     r   _import_mistral_clientrV      s    ******}U+++++      ((((((Nr7   c                      ddl } | S )zJLazy import sounddevice. Returns the module or raises ImportError/OSError.r   N)sounddevice)sds    r   _import_sounddevicerZ      s    IrM   c                      ddl m}  | S )z?Lazy import KittenTTS. Returns the class or raises ImportError.r   	KittenTTS)	kittenttsr]   r\   s    r   _import_kittenttsr_      s    ######rM   c                      ddl m}  | S )at  Lazy import Piper. Returns the PiperVoice class or raises ImportError.

    Piper is an optional, fully-local neural TTS engine (Home Assistant /
    Open Home Foundation). ``pip install piper-tts`` provides cross-platform
    wheels (Linux / macOS / Windows, x86_64 + ARM64) with embedded espeak-ng.
    Voice models (.onnx + .onnx.json) are downloaded on first use.
    r   
PiperVoice)piperrb   ra   s    r   _import_piperrd      s     !     rM   edgezen-US-AriaNeuralpNInz6obpgDQGcFmaJgBeleven_multilingual_v2eleven_flash_v2_5zgpt-4o-mini-ttsz!KittenML/kitten-tts-nano-0.8-int8Jasperzen_US-lessac-mediumalloyzhttps://api.openai.com/v1zspeech-02-hdEnglish_expressive_narratorz https://api.minimax.io/v1/t2a_v2z"https://api.minimaxi.com/v1/t2a_v2zvoxtral-mini-tts-2603z$c69964a6-ab8b-4f8a-9465-ec0925096ec8eveen]    Fzhttps://api.x.ai/v1ffffff?g      ?      ?zgemini-2.5-flash-preview-ttsKorez0https://generativelanguage.googleapis.com/v1betatts_audio_tagsr         i      c                  @    ddl m}  t           | dd                    S )Nr   get_hermes_dirzcache/audioaudio_cache)hermes_constantsry   r$   rx   s    r   _get_default_output_dirr|     s.    //////~~m];;<<<rM   i  i   i:  i'  i   }  i  )
re   rP   xaiminimaxmistralgemini
elevenlabsneuttsr^   rc   PROVIDER_MAX_TEXT_LENGTHi0u  i@  )	eleven_v3eleven_ttv_v3rg   eleven_multilingual_v1eleven_english_sts_v2eleven_english_sts_v1eleven_flash_v2rh    ELEVENLABS_MODEL_MAX_TEXT_LENGTHr   c                     t          | t                    r| S | |S t          | t          t          f          rt          |           S t          | t                    r2|                                                                 }|dv rdS |dv rdS |S )zNCoerce common YAML/env bool spellings without treating random strings as true.N>   1onyestrueenabledT>   0noofffalsedisabledF)
isinstanceboolintfloatr$   r%   lower)r   r   
normalizeds      r   _config_boolr   ,  s    % }%#u&& E{{% [[]]((**
>>>4@@@5NrM   responsec                     t          | dd           }t          |          sdS t          |           }|j                            d          rdS dt          |          v S )Niter_contentFz	requests.T)getattrcallabletype
__module__
startswithvars)r   r   response_types      r   _response_has_explicit_streamr   =  sd    8^T::LL!! uNNM**;77 tT-0000rM   c                     t          | dd           }t          |          r	  |             d S # t          $ r Y d S w xY wd S )Nclose)r   r   r3   )r   r   s     r   _close_responser   G  sa    Hgt,,E 	EGGGGG 	 	 	DD	 s   
. 
<<)limitlabelr   c                (   |t           n|}g }d}	 t          |           r|                     t                    }nt	          |                               dt          t          |           dd                    }t          |t                    r|
                    dd          }t          |t          t          f          r|fnd	}|D ]}|st          |t                    r|
                    dd          }t          |          }|t          |          z  }||k    r$t          |            t          | d
| d          |                    |           d                    |          t          |            S # t          |            w xY w)z3Read an upstream TTS response with a hard byte cap.Nr   )
chunk_sizecontentrM   utf-8replaceerrors z response exceeds z bytes)TTS_RESPONSE_BODY_LIMIT_BYTESr   r   TTS_RESPONSE_BODY_CHUNK_BYTESr   rG   r   r   r   r$   encodebytes	bytearraylenr   RuntimeErrorappendjoin)r   r   r   chunkstotaliteratorr   chunks           r   _read_tts_response_bytesr   P  s    .3]))EFE"(22 	U,,8U,VVHH8nn((GDNNIWZ4[4[\\G'3'' D!...CC%/%9K%L%LTzzRTH 
	! 
	!E %%% @WY??%LLESZZEu}})))"e#L#Lu#L#L#LMMMMM%    xx!!!!!!!!s   EF   Fc                    t          | ||          }|r't          j        |                    d                    S t	          |           sCt          | dd           }t          |          r# |            }t          |t                    r|ni S i S )Nr   r   r   json)	r   r   loadsdecoder   r   r   r   dict)r   r   r   rawjson_readerparseds         r   _read_tts_response_jsonr   s  s     #85
F
F
FC
 /z#**W--...
 )22 >h55K   	> []]F'55=662=IrM   output_pathc                    t          | ||          }t          |d          5 }|                    |           d d d            d S # 1 swxY w Y   d S )Nr   wb)r   openwrite)r   r   r   r   audio_bytesfs         r   _write_tts_response_to_filer     s     +85NNNK	k4	 	  A	                 s   AA
A
provider
tts_configc                 l   | st           S |                                                                 }|pi }t          |                    |          t
                    r|                    |          ni }|r|                    d          nd}t          |t                    rd}t          |t                    r|dk    r|S |dk    r[|pi                     d          pt          }t                              t          |                                                    }|r|S |t          v rt          |         S |t          vrot          ||          }t          |          rP|                    d          }	t          |	t                    rd}	t          |	t                    r|	dk    r|	S t          S t           S )a  Return the input-character cap for *provider*.

    Resolution order:
      1. ``tts.<provider>.max_text_length`` (user override in config.yaml)
      2. ``tts.providers.<provider>.max_text_length`` for user-declared
         command providers
      3. ElevenLabs model-aware table (keyed on configured ``model_id``)
      4. ``PROVIDER_MAX_TEXT_LENGTH`` default
      5. ``DEFAULT_COMMAND_TTS_MAX_TEXT_LENGTH`` when the provider is a
         command-type user provider without an explicit cap
      6. ``FALLBACK_MAX_TEXT_LENGTH`` (4000)

    Non-positive or non-integer overrides fall through to the default so a
    broken config can't accidentally disable truncation entirely.
    max_text_lengthNr   r   model_id)FALLBACK_MAX_TEXT_LENGTHr   r%   r   rG   r   r   r   DEFAULT_ELEVENLABS_MODEL_IDr   r$   r   BUILTIN_TTS_PROVIDERS_get_named_provider_config_is_command_provider_config#DEFAULT_COMMAND_TTS_MAX_TEXT_LENGTH)
r   r   keycfgprov_cfgoverrider   mappednamednamed_overrides
             r   _resolve_max_text_lengthr     s   &  (''
..


 
 
"
"C

C  *#''#,,==Eswws|||2H2:Dx||-...H(D!! (C   X\\
lN''
33R7R155c(mm6I6I6K6KLL 	M
&&&',, '''*344&u-- 	7"YY'899N.$// &!%.#.. &>A3E3E%%66##rM   c                     	 ddl m}   |             }|                    d          pi S # t          $ r t                              d           i cY S t          $ r)}t                              d|d           i cY d}~S d}~ww xY w)	z
    Load TTS configuration from ~/.hermes/config.yaml.

    Returns a dict with provider settings. Falls back to defaults
    for any missing fields.
    r   )load_configttsz9hermes_cli.config not available, using default TTS configzFailed to load TTS config: %sTexc_infoN)r   r   rG   r   loggerdebugr3   warning)r   configes      r   _load_tts_configr     s    	111111zz%  &B&   PQQQ			   6DIII						s!   &) &B	BA>8B>Bc                     |                      d          pt                                                                          S )a'  Get the explicitly configured TTS provider or the free default.

    Inference credentials do not imply consent to paid speech generation.
    Users opt into cloud TTS by setting ``tts.provider`` (normally through
    ``hermes tools``); otherwise the historical Edge backend remains active.
    r   )rG   DEFAULT_PROVIDERr   r%   )r   s    r   _get_providerr     s4     NN:&&:*:AACCIIKKKrM   T)frozenc                   T    e Zd ZU dZeed<   eed<   eed<    ed          Zeed<   dS )	_MiniMaxTTSRuntimezA region-bound MiniMax endpoint and credential.

    The credential is excluded from ``repr`` so diagnostics cannot expose it
    accidentally.
    regionendpointcredential_sourceF)reprapi_keyN)__name__r   __qualname____doc__r$   __annotations__r   r   r   rM   r   r   r     sY           KKKMMM5e$$$GS$$$$$rM   r   c                    |                      di           }t          |t                    si }dt          t	          dd          pd                                          fdt          t	          dd          pd                                          fd}t          t          d}t          |                     d          pd                                                                          }|r||vrt          d          |r|}n$|d         d	         rd}n|d
         d	         rd
}nd}||         \  }}|st          | d|          t          |                     d          p||                                                   }t          |          j        pd                                }	t          ddh          t          dh          d}
|dk    rd
nd}|	|
|         v rt          d|d|          t          ||||          S )a  Select MiniMax TTS region, endpoint, and credential atomically.

    An explicit ``tts.minimax.region`` wins. Without one, the legacy global
    credential wins when present; a China credential is selected only when it
    is the sole configured MiniMax credential.
    r   MINIMAX_API_KEYr!   MINIMAX_CN_API_KEY)globalcnr   z+tts.minimax.region must be 'global' or 'cn'r  rt   r  z  not set for MiniMax TTS region r?   zapi.minimax.iozapi.minimax.chatzapi.minimaxi.comz#tts.minimax.base_url points to the z  MiniMax endpoint but region is )r   r   r   r   )rG   r   r   r$   r&   r%   DEFAULT_MINIMAX_BASE_URLDEFAULT_MINIMAX_CN_BASE_URLr   
ValueErrorr   hostname	frozensetr   )r   	mm_configcredentials	endpointsconfigured_regionr   r   r   r   endpoint_hostofficial_region_hostsother_regions               r   _resolve_minimax_tts_runtimer     s    y"--Ii&& 	 %&7CCIrJJPPRR

 !%&:IFFL"MMSSUU
	 	K +) I
 IMM(339r::@@BBHHJJ H.i??FGGG "	X	q	! 	T	1	 !,V!4w 
 LL&LL
 
 	
 9==,,A	&0ABBHHJJHh''06B==??M-/ABCC+,--  "X--448L-l;;;(, ( (#( (
 
 	

 +	   rM   >   r~   re   rc   r   r   rP   r   r   	deepinfrar^   r   x   mp3>   aacamrm4ar  oggwavflacopus>   feishumatrixsignaltelegramwhatsappr   c                     t          | t                    si S |                     |          }t          |t                    r|ni S )zBReturn a provider config block if it's a dict, else an empty dict.)r   r   rG   )r   r   sections      r   _get_provider_sectionr&    sC    j$'' 	nnT""G $//777R7rM   c                 
   t          | d          }t          |t                    r|                    |          nd}t          |t                    r|S |                                t
          vrt          | |          }|r|S i S )a  Return the config dict for a user-declared provider.

    Looks up ``tts.providers.<name>`` first (the canonical location), and
    falls back to ``tts.<name>`` so users who followed the built-in layout
    still work. Returns an empty dict when the provider is not declared.
    	providersN)r&  r   r   rG   r   r   )r   r   r(  r%  legacys        r   r   r     s     &j+>>I%/	4%@%@JimmD!!!dG'4    zz||000&z488 	MIrM   r   c                 j   t          | t                    sdS t          |                     d          pd                                                                          }|r|dk    rdS |                     d          }t          |t                    o t          |                                          S )z;Return True when *config* declares a command-type provider.Fr   r!   command)r   r   r$   rG   r%   r   r   )r   ptyper+  s      r   r   r     s    fd## u

6""(b))//117799E )##ujj##Ggs##=W]]__(=(==rM   c                     | sdS |                                                                  }|t          v rdS t          ||          }t	          |          r|S dS )zReturn the provider config if *provider* resolves to a command type.

    Built-in provider names are rejected (they have native handlers).
    Returns None when the name is a built-in, unknown, or not a command
    type.
    N)r   r%   r   r   r   )r   r   r   r   s       r    _resolve_command_provider_configr.    sf      t
..


 
 
"
"C
###t'
C88F"6** 4rM   textc                    |sdS |                                                                 }|t          v rdS t          t	          ||                    rdS 	 ddlm} ddlm}  |              ||          }| |d            ||          }n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w|dS t          |t                    r|                    d          nd}	t          |t                    r|                    d	          nd}
t          |t                    r|                    d
          nd}t          |t                    r|                    dt                    nt          }t                              d|           |                    | |t          |	t$                    r|	r|	ndt          |
t$                    r|
r|
ndt          |t&          t(          f          rt)          |          nd|r!t%          |                                           nd          }t          |t$                    r|r|n|S )u  Route the call to a plugin-registered TTS provider, or return None.

    Returns the path to the written audio file on dispatch, or ``None``
    to fall through to the next resolution layer (built-in dispatch or
    Edge TTS default).

    Resolution invariants enforced here (matches issue #30398):

    1. Built-in provider names short-circuit — never reach the plugin
       registry. The caller is responsible for the elif chain that
       handles ``edge``/``openai``/etc.; this function explicitly
       rejects those names defensively.
    2. Command-type providers declared under
       ``tts.providers.<name>: type: command`` (PR #17843) win over a
       plugin with the same name. The caller passes us only when its
       own command-provider check returned None — we re-verify here so
       a refactor of the caller can't silently break the invariant.
    3. Plugin dispatch fires only when ``provider`` matches a registered
       :class:`TTSProvider` whose ``name`` equals the configured value.
       Unknown names return None (caller falls through to Edge default).

    Plugin exceptions are caught and re-raised — the outer
    ``text_to_speech_tool`` try/except converts them to the standard
    error envelope, matching how command-provider failures surface.
    Nr   get_provider_ensure_plugins_discoveredT)forcez2tts plugin dispatch skipped (discovery failed): %svoicemodelspeedoutput_formatz2Generating speech with plugin TTS provider '%s'...r  )r6  r7  r8  format)r   r%   r   r   r   agent.tts_registryr2  hermes_cli.pluginsr4  r3   r   r   r   r   rG   !DEFAULT_COMMAND_TTS_OUTPUT_FORMATinfo
synthesizer$   r   r   )r/  r   r   r   r   r2  r4  plugin_providerexcr6  r7  r8  fmtwrittens                 r   _dispatch_to_plugin_providerrD    s   >  t
..


 
 
"
"C
###t ##=j##N#NOO t333333AAAAAA""$$$&,s++" '&T2222*l3//O   I3OOOttttt t (2*d'C'CMJNN7###E'1*d'C'CMJNN7###E'1*d'C'CMJNN7###E j$''	/
(IJJJ.  KK<c   ((!%--A%AeeT!%--A%AeeT(e==GeElll4#&1s3xx~~E )  G !#..K7K77Ks   :B 
CB<<Cc                 (   | sdS |                                                                  }|t          v rdS 	 ddlm}  ||          }|dS t          |j                  S # t          $ r'}t          	                    d||           Y d}~dS d}~ww xY w)a  Return True when the registered plugin provider opts into voice
    bubble delivery via its ``voice_compatible`` property.

    Defensive: any registry or property access failure means False
    (matches the safe default for the command-provider path).
    Fr   r1  Nz5tts plugin voice_compatible check failed for '%s': %s)
r   r%   r   r;  r2  r   voice_compatibler3   r   r   )r   r   r2  r@  rA  s        r   $_plugin_provider_is_voice_compatiblerG     s      u
..


 
 
"
"C
###u333333&,s++"5O4555   CS#	
 	
 	
 uuuuu	s   A  A   
B*BBc              #     K   t          | t                    sdS t          | d          }|pi                                 D ]J\  }}t          |t                    r0|                                t          vrt          |          r||fV  KdS )zDYield (name, config) pairs for every declared command-type provider.Nr(  )r   r   r&  itemsr$   r   r   r   )r   r(  r   r   s       r   _iter_command_providersrJ  :  s      j$'' %j+>>Io2,,..    	cdC   	 TZZ\\9N%N%N*3//  Ci   rM   c                    |                      d|                      dt                              }	 t          |          }n+# t          t          f$ r t          t                    cY S w xY w|dk    rt          t                    S |S )z5Return timeout in seconds, falling back when invalid.timeouttimeout_secondsr   )rG   #DEFAULT_COMMAND_TTS_TIMEOUT_SECONDSr   	TypeErrorr
  )r   r   r   s      r   _get_command_tts_timeoutrP  E  s    
**Y

+<>a b b
c
cC:c

z" : : :899999:zz8999Ls   A %A)(A)c                    |rVt          |          j                                                                                            d          }|t
          v r|S |                     d          p|                     d          pt          }t          |                                                                                              d          }|t
          v r|nt          S )z6Return the validated output format (mp3/wav/ogg/flac)..r:  r9  )	r   suffixr   r%   lstripCOMMAND_TTS_OUTPUT_FORMATSrG   r=  r$   )r   r   rS  r   rB  s        r   _get_command_tts_output_formatrV  Q  s    
  k"")//117799@@EE///M

8 	-::o&&	-, 
 c((..


 
 
"
"
)
)#
.
.C333339ZZrM   c                     |                      dd          }t          |t                    r(|                                                                dv S t          |          S )zEReturn True only when the user explicitly opted in to voice delivery.rF  F>   r   r   r   r   )rG   r   r$   r%   r   r   )r   r   s     r    _is_command_tts_voice_compatiblerX  c  sU    JJ)511E% C{{}}""$$(BBB;;rM   command_templatepositionc                     d}d}d}||k     r\| |         }|dk    r	|dk    rd}n:|dk    r|rd}n/|dk    rd}n&|dk    rd}n|dk    rd}n|dk    rd}n|dk    r|dz  }|dz  }||k     \|S )	zReturn the shell quote character active right before *position*.

    Returns ``"'"`` / ``'"'`` when inside a single- / double-quoted region
    of the template, ``None`` for bare context.
    NFr   '"\Trt   r   )rY  rZ  quoteescapedichars         r   _shell_quote_contextrc  k  s      EG	A
h,,"C<<s{{c\\ S[[EES[[EET\\FA	Q% h,,& LrM   quote_contextc                 \   |dk    r|                      dd          S |dk    rR|                      dd                               dd                               dd                               d	d
          S t          j        dk    rt          j        | g          S t          j        |           S )zGQuote a placeholder value for its position in a shell command template.r\  z'\''r]  r^  z\\z\"$z\$`z\`nt)r   r   r   
subprocesslist2cmdlineshlexr_  )r   rd  s     r   _quote_command_tts_placeholderrl    s    }}S'***WT6""WS%  WS%  WS%  	
 
w$&w///;urM   placeholdersc                     d                     d D                       }t          j        d| d| d          }g dt          j        t                   dt          f fd}|                    |           }|                    d	d
                              dd          }D ]\  }}|                    ||          }|S )z@Replace supported placeholders while preserving ``{{`` / ``}}``.|c              3   >   K   | ]}t          j        |          V  d S N)rI   escape).0r   s     r   	<genexpr>z/_render_command_tts_template.<locals>.<genexpr>  s*      >>RYt__>>>>>>rM   z(?<!\$)(?:\{\{(?P<double>z)\}\}|\{(?P<single>z)\})matchr   c                    |                      d          p|                      d          }dt                     d}                    |t          |         t	          |                                                     f           |S )Ndoublesingle__HERMES_TTS_PLACEHOLDER___)groupr   r   rl  rc  start)ru  r   tokenrY  rm  replacementss      r   replace_matchz3_render_command_tts_template.<locals>.replace_match  s    {{8$$=H(=(=AC,=,=AAA*T"$%5u{{}}EE 
 	 	 	 rM   z{{{z}}})r   rI   compileMatchr$   rJ   r   )	rY  rm  namespatternr  renderedr}  r   r~  s	   ``      @r   _render_command_tts_templater    s    
 HH>>>>>>>EjPuPPEPPP G +-L
RXc] 
s 
 
 
 
 
 
 
 
 {{=*:;;Hc**224==H$ 2 2u##E511OrM   procc           	         |                                  dS t          j        dk    rv	 t          j        ddddt          | j                  gt          j        t          j        dt          j                   n$# t          $ r | 	                                 Y nw xY wdS d	dl
}	 |                    | j                  }|                    d
          D ]'}	 |                                 # |j        $ r Y $w xY w|                                 n0# |j        $ r Y dS t          $ r |                                  Y nw xY w	 |                     d           dS # t          j        $ r Y nw xY w	 |                    | j                  }|                    d
          D ]'}	 |	                                 # |j        $ r Y $w xY w|	                                 dS # |j        $ r Y dS t          $ r | 	                                 Y dS w xY w)zCBest-effort termination of a shell process and all of its children.Nrh  taskkillz/Fz/Tz/PID   )stdoutstderrrL  stdinr   T)	recursiveru   rL  )pollr   r   ri  runr$   pidDEVNULLr3   killpsutilProcesschildren	terminateNoSuchProcesswaitTimeoutExpired)r  r  parentchilds       r   #_terminate_command_tts_process_treer    s   yy{{	w$		NT4TX?!)!) (      	 	 	IIKKKKK	MMM))__t_44 	 	E!!!!'         		!	$   ))__t_44 	 	E

'         		s   AA7 7BB"2D C*)D *
C74D 6C77D 
D<D<;D< E E*)E*.2G !F65G 6
G G GG 
H
)H
	H
c                     |                      d          }t          |t          t          f          sg S d |D             S )a  Return the provider's ``env_passthrough`` allowlist (opt-out of scrub).

    Command providers legitimately reference their own API keys in the shell
    template (curl one-liners). The child env is scrubbed of Hermes secrets by
    default; ``env_passthrough: [MY_API_KEY, ...]`` copies the named variables
    back from the parent environment so a trusted template keeps working.
    env_passthroughc                     g | ]D}t          |                                          #t          |                                          ES r   )r$   r%   )rs  items     r   
<listcomp>z5_command_provider_env_passthrough.<locals>.<listcomp>  s=    CCC$T1B1BCCIIOOCCCrM   )rG   r   listtuple)r   r   s     r   !_command_provider_env_passthroughr    sE     **&
'
'CcD%=)) 	CC#CCCCrM   r+  rL  r  c           	         ddl m} ddlm}  |d          }|pg D ](}t          j                            |          }||||<   )dt          j        t          j        ddd	 ||          d
}t          j	        dk    rt          t          dd          |d<   nd|d<   t          j        | fi |dt          j        i}	t          j                    g g d}
ddh}dt          dt           ddffd}t#          j        |d|	j        fd          t#          j        |d|	j        fd          g}|D ]}|                                 t-          j                    |z   }d}|r|t-          j                    z
  }|dk    rd}n	                     t1          d|                    \  }}n# t          j        $ r Y [w xY w||                    |           w|
|                             |           t-          j                    |z   }||sQ	 |	                    t;          d|t-          j                    z
                       n# t          j        $ r d}Y nw xY w|rt?          |	           |D ]}|                     d           	 	 !                                \  }}n# t          j        $ r Y n"w xY w|r|
|                             |           Ld                     |
d                   }d                     |
d                   }	 t          j        | |          # t          j        $ r}t          j        | |||          |d}~ww xY wd                     |
d                   }d                     |
d                   }|	j"        rt          j#        |	j"        | ||          t          j$        | |	j"        ||          S )zRun a command-provider shell command with process-tree idle cleanup.

    Child env is scrubbed of Hermes secrets (salvage of #56332) while still
    propagating delegated-child lineage markers when applicable.
    r   )delegated_child_subprocess_env)hermes_subprocess_envF)inherit_credentialsNTr   r   )shellr  r  r/  encodingr   envrh  CREATE_NEW_PROCESS_GROUPcreationflagsstart_new_sessionr  )r  r  r  r  r   streamr   c                    t          |dd           pd}t          t          |dd           dd           }	 	 ||                    d          }n" |d          }|                    |d          }|sn                    | |f           U	                     | d f           d S #                     | d f           w xY w)	Nr  r   bufferread1Trv   r   r   )r   readr   put)r   r  r  r  r   dataoutput_queues         r   read_streamz%_run_command_tts.<locals>.read_stream"  s    6:t44?$77$GG	+0="KK..EE 5<<D KKKCCE   $///0  dD\*****LdD\****s   AB& &B?targetargsdaemong?r  g              ?r!   )outputr  )%agent.delegation_contextr  tools.environments.localr  r   environrG   ri  PIPEr   r   Popenr  queueQueuer$   r
   	threadingThreadr  r  r|  time	monotonicminEmptydiscardr   r  maxr  r  r   
get_nowait
returncodeCalledProcessErrorCompletedProcess)r+  rL  r  r  r  scrubbedr   r   popen_kwargsr  r   open_streamsr  readersreaderdeadline	timed_out	remainingr   r   r  r  rA  r  s                          @r   _run_command_ttsr    s    HGGGGG>>>>>>$$???H$" " "
s##!HSM// --h77
$ 
$L 
w$(/
<VXY(Z(Z_%%,0()GNN|NN:;MNNND=B[]]L.0B#?#?Fh'L+# +s +t + + + + + +" 	DK(	
 	
 	

 	DK(	
 	
 	
G   ~')HI
 .t~///	>>I	&**3tY3G3G*HHKD%%{ 	 	 	H	=  &&&tE""">##g-  .  	IIc#x$.2B2B'BCCIDDDD( 	 	 	III	  +D111 	% 	%FKKK$$$$	+*5577ee;    +t##E***	+ )**)**	+GW===( 	 	 	+	  
 	 WWVH%&&FWWVH%&&F 
+O	
 
 
 	
 &wPPPsN   >'F& &F87F8	8I IIJ J10J1	L M-MMpathc                 P    t          |          }|                     d|           S )zKReturn an output path whose extension matches the provider's output_format.rR  )rV  with_suffix)r  r   rB  s      r   #_configured_command_tts_output_pathr  |  s)    
(
0
0CIII&&&rM   provider_namec                    t          |                    d          pd                                          }|st          d| d          t	          |                                          }|j                            dd           |                                r|	                                 t          |          }t          |t          |                    }|                    d|                    dd                    }	t          j                    5 }
t	          |
          dz  }|                    | d	
           t          |          t          |          t          |          |t          |                    dd                    t          |                    dd                    t          |	          d}t          ||          }	 t!          ||t#          |                     n# t$          j        $ r}t)          d| d|dd          |d}~wt$          j        $ r}g }|j        r/|                    d|j                                                    |j        r/|                    d|j                                                    d                    |          pd}t)          d| d|j         d|           |d}~ww xY w	 ddd           n# 1 swxY w Y   |                                r|                                j        dk    rt)          d| d|           t          |          S )a
  Generate speech by running a user-configured shell command.

    Returns the absolute path of the audio file the command wrote.
    Raises ``ValueError`` when the provider config is invalid, and
    ``RuntimeError`` for timeouts / non-zero exits / empty output.
    r+  r!   ztts.providers.z.command is not configuredTparentsexist_okr8  z	input.txtr   r  r6  r7  )
input_path	text_pathr   r:  r6  r7  r8  )r  zTTS provider 'z' timed out after gsNzstderr: zstdout: z; zno command outputz' exited with code z: r   z' produced no output at )r$   rG   r%   r
  r   
expanduserr  mkdirexistsunlinkrP  rV  tempfileTemporaryDirectory
write_textr  r  r  ri  r  r   r  r  r   r  r   r  statst_size)r/  r   r  r   r   rY  r  rL  r9  r8  tmpdirr  rm  r+  rA  detail_partsdetails                    r   _generate_command_ttsr    s    6::i006B77==?? 
F]FFF
 
 	
 +))++F
Mt444}} &v..G263v;;GGMJJw
w ; ;<<E		$	&	& #&LL;.	TG444 i..Yv;;#GR0011GR0011ZZ
 
 //?NN	 A& I I    
 ( 	 	 	NNN'NNNN  , 
	 
	 
	Lz E##$Csz/?/?/A/A$C$CDDDz E##$Csz/?/?/A/A$C$CDDDYY|,,C0CF. . .>. .%+. .  
	!# # # # # # # # # # # # # # #J ==?? 
fkkmm3q88L]LLFLL
 
 	
 v;;sD   B:K&G54K&5KHK.B#KKK&&K*-K*c                 T    | t                      } t          |           D ]\  }} dS dS )z=Return True when any command-type TTS provider is configured.NTF)r   rJ  )r   _name_cfgs      r   _has_any_command_tts_providerr    s:    %''
.z::  ttt5rM   c                  .    t          j        d          duS )z+Check if ffmpeg is available on the system.ffmpegN)shutilwhichr   rM   r   _has_ffmpegr     s    <!!--rM   mp3_pathc                     t                      sdS |                     dd          d         dz   }t          | |          S )z
    Convert an audio file (MP3/WAV/anything ffmpeg reads) to OGG Opus
    format for Telegram voice bubbles.

    Args:
        mp3_path: Path to the input audio file.

    Returns:
        Path to the .ogg file, or None if conversion fails.
    NrR  rt   r   .ogg)r   rsplit_ffmpeg_transcode_to_opus)r  ogg_paths     r   _convert_to_opusr    sB     == tsA&&q)F2H$Xx888rM   r  r  c                    t                      sdS t          j                            |           t          j                            |          k    }|r|dz   n|}	 t	          j        dd| ddddd	d
dddddddd|dgddt          j        t                                }|j        dk    rt          
                    d|j        |j                            dd          dd                    	 |rGt          j                            |          r*	 t          j        |           dS # t          $ r Y dS w xY wdS dS t          j                            |          rt          j                            |          dk    ra|rt          j        ||           ||rEt          j                            |          r'	 t          j        |           S # t          $ r Y S w xY wS S n# t          j        $ r t          
                    d           Y nXt&          $ r t          
                    d           Y n3t(          $ r'}t          
                    d|d            Y d}~nd}~ww xY w|rEt          j                            |          r&	 t          j        |           n^# t          $ r Y nRw xY wnM# |rEt          j                            |          r'	 t          j        |           w # t          $ r Y w w xY ww w xY wdS )!zTranscode *input_path* to real Ogg/Opus at *ogg_path* via ffmpeg.

    Safe when ``input_path == ogg_path`` (writes to a temp file, then
    replaces). Returns the output path on success, None on failure.
    Nz.tmp.oggr  -i-acodeclibopus-acr   -b:a48k-vbrr   -applicationvoip-compression_level10z-fr  -yT   capture_outputrL  r  r  r   z0ffmpeg conversion failed with return code %d: %sr   ignorer      z)ffmpeg OGG conversion timed out after 30szffmpeg not found in PATHz ffmpeg OGG conversion failed: %sr   )r   r   r  abspathri  r  r  r   r  r   r   r  r   r  removeOSErrorgetsizer   r  FileNotFoundErrorr3   )r  r  in_place	work_pathresultr   s         r   r  r    s    == twz**bgooh.G.GGH)1?:%%xItZICV%94u  $,..
 
 
 !!NNM +V]-A-A'RZ-A-[-[\`]`\`-ac c c  	y11 		)$$$$$   	 	 	 	 7>>)$$ 	)C)Ca)G)G 0
9h///  	y11 		)$$$$   	 	 $ D D DBCCCCC 3 3 3122222 M M M91tLLLLLLLLM  	y11 		)$$$$     	y11 		)$$$$   	 	
 4s   BG D$ $
D21D29AG 5G


GGJ( )IJ( #I+J( -	I6IJ( IJ( J 
J$#J$("K2K K2 
K-*K2,K--K2c                     ddl m} 	 t          | d          5 }|                    d          }ddd           n# 1 swxY w Y   n# t          $ r Y dS w xY w ||          pdS )a	  Return a container id ('ogg', 'wav', 'mp3', 'flac', ...) or 'unknown'.

    Delegates to the shared magic-byte sniffer in ``tools.audio_container``
    (one module owns container detection for both this outbound repair and
    the inbound gateway audio cache).
    r   )sniff_containerrb   Nunknown)tools.audio_containerr#  r   r  r  )r  r#  fhheads       r   _sniff_audio_containerr*  #  s     655555$ 	772;;D	 	 	 	 	 	 	 	 	 	 	 	 	 	 	   yy?4  -I-s.   A :A >A >A 
AAfile_strc                 f   |                      d          s| S t          |           }|dv r| S t                              d||            t	          | |           }|r|S | dd         dz   |z   }	 t          j        | |           t                              d| |           |S # t          $ r | cY S w xY w)a  Ensure a path claiming ``.ogg`` actually contains an Ogg container.

    When the bytes are MP3/WAV/FLAC (a backend ignored the opus request),
    transcode in place to real Ogg/Opus. On any failure, rename to the
    sniffed real extension so downstream players/platforms at least get an
    honest file instead of a 0-second voice bubble. Returns the (possibly
    updated) path.
    r  )r  r&  uI   TTS wrote %s bytes into a .ogg path (%s) — transcoding to real Ogg/OpusNrR  ub   Could not transcode %s to Ogg/Opus — renamed to %s so the file is delivered with its real format)	endswithr*  r   r>  r  r   r   r   r  )r+  	containerrepairedhonests       r   _repair_ogg_containerr2  4  s     V$$ &x00I&&&
KKS8   )8<<H  crc]S 9,F

8V$$$56>	
 	
 	
    s   .2B! !B0/B0c           	        K   t                      }|                    d          pi }|                    dt                    }t          |                    d|                    dd                              }d|i}|dk    rt	          |dz
  dz            }|dd|d<    |j        | fi |}	|	                    |           d	{V  |S )
z
    Generate audio using Edge TTS.

    Args:
        text: Text to convert.
        output_path: Where to save the MP3 file.
        tts_config: TTS config dict.

    Returns:
        Path to the saved audio file.
    re   r6  r8  rq   d   z+d%rateN)r6   rG   DEFAULT_EDGE_VOICEr   roundCommunicatesave)
r/  r   r   	_edge_ttsedge_configr6  r8  kwargspctcommunicates
             r   _generate_edge_ttsr@  [  s       !""I..((.BKOOG%788E+//':>>'3+G+GHHIIEuF||US[C'((v')'7777K


;
'
''''''''rM   c                 &   t          dd          pd}|st          d          |                    d          pi }|                    dt                    }|                    dt                    }|                    d          rd}nd	}t                      } |dd
|it          |          }	|	j        	                    | |||          }
t          |d          5 }|
D ]}|                    |           	 ddd           n# 1 swxY w Y   |S )z
    Generate audio using ElevenLabs.

    Args:
        text: Text to convert.
        output_path: Where to save the audio file.
        tts_config: TTS config dict.

    Returns:
        Path to the saved audio file.
    ELEVENLABS_API_KEYr   r!   z=ELEVENLABS_API_KEY not set. Get one at https://elevenlabs.io/voice_idr   r  opus_48000_64mp3_44100_128r   )r/  rC  r   r9  r   Nr   )r&   r
  rG   DEFAULT_ELEVENLABS_VOICE_IDr   r.  r<   rL   text_to_speechconvertr   r   )r/  r   r   r   r=   rC  r   r9  r:   clientaudio_generatorr   r   s                r   _generate_elevenlabsrK  y  s{    %%9<HHNBG ZXYYY|,,2I}}Z)DEEH}}Z)DEEH F## (''#%%JZUUU+I)+T+TUUF+33#	 4  O 
k4	 	  A$ 	 	EGGENNNN	               s   DD
D
c                     |                      d          rdS |                      d          rdS |                      d          rdS dS )zHPick an OpenAI-compatible TTS response format from the output extension.r  r  .wavr  .flacr  r  )r.  )r   s    r   _tts_response_format_from_pathrO    sX    F## vF## uG$$ v5rM   )r   r?   r7  r6  r8  instructionsr   r?   r7  r6  r8  rP  c          	         d}	d}
|du}|t                      \  }}	}
t          |t                    r|                    d          ndpi }||                    dt                    }||                    dt
                    }|                    d          }||p|	pt          }|Pt          |t                    r|                    dd          nd}t          |                    d|                    }|                    d	          }|
r6|s4|s2|t          vr)t          
                    d
|t          |           t          }t          |          }t                      } |||          }	 ||| |dt          t          j                              id}|dk    r!t!          dt#          d|                    |d<   |r||d<   |rd|i|d<    |j        j        j        di |}|                    |           |t-          |dd          }t/          |          r |             S S # t-          |dd          }t/          |          r |             w w xY w)uJ  Generate audio via the OpenAI ``audio.speech.create`` SDK shape.

    Optional kwargs let OpenAI-compatible backends (DeepInfra etc.) reuse
    this function — they resolve credentials/model themselves and pass
    them through, skipping the OpenAI-only ``_resolve_openai_audio_client_config``.

    Args:
        text: Text to convert.
        output_path: Where to save the audio file.
        tts_config: TTS config dict (used for ``tts.openai`` sub-block
            and the global ``speed`` default).
        api_key: Bearer token. When None, resolved from the OpenAI auth
            chain (config → env → managed gateway).
        base_url: API base URL. When None, falls back to
            ``tts.openai.base_url`` then the OpenAI default.
        model: Model id. When None, reads ``tts.openai.model``.
        voice: Voice id. When None, reads ``tts.openai.voice``.
        speed: Playback speed. When None, reads ``tts.openai.speed`` /
            ``tts.speed``.
        instructions: Optional voice-design guidance (tone, emotion, pacing,
            accent, whispering). Forwarded to `audio.speech.create` when
            truthy; omitted otherwise so ``tts-1``/``tts-1-hd`` and strict
            OpenAI-compatible servers that reject unknown kwargs are
            unaffected.

    Returns:
        Path to the saved audio file.
    NFrP   r7  r6  r?   r8  rq   languagezTTS: managed OpenAI audio gateway does not support model %r; falling back to %s. Set VOICE_TOOLS_OPENAI_KEY or OPENAI_API_KEY to use %r directly.)r   r?   zx-idempotency-key)r7  r6  inputresponse_formatextra_headers      ?      @rP  	lang_code
extra_bodyr   r   )#_resolve_openai_audio_client_configr   r   rG   DEFAULT_OPENAI_MODELDEFAULT_OPENAI_VOICEDEFAULT_OPENAI_BASE_URLr   MANAGED_OPENAI_TTS_MODELSr   r   rO  rR   r$   uuiduuid4r  r  audiospeechcreatestream_to_filer   r   )r/  r   r   r   r?   r7  r6  r8  rP  fallback_base
is_managedexplicit_base_url
oai_configconfig_base_urlspeed_defaultrR  rT  rQ   rI  create_kwargsr   r   s                         r   _generate_openai_ttsrl    s   V $(MJ ,-P-R-R*
 /9T.J.JT*..***PT[Y[J}w(<==}w(<== nnZ00O #NmN7N}8B:t8T8T]
w444Z]jnnWm<<==~~j))H 	%!%  % 222" '		
 	
 	
 %4[AAO(**L\'H===F.13tz||3D3DE)
 )
 C<<%(s3%?%?M'" 	9,8M.) 	B+6*AM,'-6<&->>>>,,,..E?? 	EGGGG	 ..E?? 	EGGGG	s   1BH) )-Ic                 z   t          dd          }|st          d          t          |t                    r|                    d          nd}t          |t                    si }ddlm}m} |                    d          }t          |t                    r|	                                s$ |d          }|st          d	          |d         }t          | ||| ||          ||                    d
t                    t          |                    d|                    dd                                        S )u  Resolve DeepInfra credentials/model, then delegate to the OpenAI handler.

    DeepInfra's audio endpoint is OpenAI-compatible, so there's no need
    to duplicate the SDK call — we just pass an explicit api_key /
    base_url / model / voice through. Model ids and the base URL come from
    the shared ``hermes_cli.models`` helpers so every DeepInfra surface
    resolves them identically.
    DEEPINFRA_API_KEYr  zXDEEPINFRA_API_KEY not set. Run `hermes setup` to configure, or set the env var directly.Nr   )deepinfra_base_urldeepinfra_model_idsr7  r   zNo DeepInfra TTS model available. Pin one in config.yaml under tts.deepinfra.model, or check connectivity to api.deepinfra.com so the live catalog can be fetched.r6  r8  rq   )r   r?   r7  r6  r8  )r&   r
  r   r   rG   hermes_cli.modelsro  rp  r$   r%   rl  DEFAULT_DEEPINFRA_TTS_VOICEr   )	r/  r   r   r   	di_configro  rp  r7  
candidatess	            r   _generate_deepinfra_ttsru  0  sd    $$7EEG 
+
 
 	
 0:*d/K/KU
{+++QUIi&& 	IIIIIIIIMM'""EeS!!  ((//
 	H  
 1##I..mmG%@AAIMM':>>'3+G+GHHII	 	 	 	rM   )pausez
long-pausezhum-tunelaughchucklegigglecrytskztongue-clickz	lip-smackbreathinhaleexhalesigh)softwhisperloudzbuild-intensityzdecrease-intensityzhigher-pitchzlower-pitchslowfastz	sing-songsingingzlaugh-speakemphasisz(\[(?:ro  z
)\]|</?(?:z)>)flagsu   ^(.{12,120}?[.!?…])\s+(?=\S)c                 $    t          | |          S )Nr   )r   )r   r   s     r   _xai_bool_configr    s    w////rM   c                    |                                  }|s| S |}t          j        dd|          }t          j        dd|          }t                              |          st
                              d|d          }t          j        dd|                                           }t                              |          r|S d	                    t                    }d	                    t                    }d
|z   dz   |z   dz   }	 ddl	m
}  |dd|ddd| dgd          }t          |                                           }t          j        d|t          j                  }	|	r'|	                    d                                           }|p|S # t          $ r'}
t                               d|
           |cY d}
~
S d}
~
ww xY w)a  Add xAI speech tags for more natural voice-mode replies.

    First applies a conservative local transform (inserts [pause] between
    paragraphs and after the first sentence). Then, if the result contains
    no explicit user/model speech tags, asks the configured auxiliary model
    to rewrite the transcript with a richer set of xAI-supported tags
    (laughs, sighs, whispers, soft/loud, slow/fast, etc.) so the voice
    output sounds more expressive. Falls back to the local result on any
    auxiliary-model failure.
    z\n\s*\n+z	 [pause] z\s*\n\s* z\1 [pause] rt   )countz\s{2,}, zYou rewrite transcripts for the xAI /v1/tts endpoint by inserting expressive speech tags.

Valid inline tags (use as `[tag]`): z1.
Valid wrapping tags (use as `[tag]...[/tag]`): u  .

Rules:
- Preserve the spoken words, order, and meaning.
- Do not add new spoken sentences or remove existing spoken words.
- Use inline `[tag]` for short modifiers (laughs, sighs, pause, etc.).
- Use wrapping `[tag]...[/tag]` for sustained effects (whisper, soft, slow, fast, loud, etc.).
- Do not use angle-bracket tags like `<tag>...</tag>` — xAI uses BBCode-style closing tags with `[/tag]`.
- Do not use SSML.
- Do not explain or comment.
- Return only the tagged TTS script.r   call_llmrs   systemroler   userzTRANSCRIPT TO TAG:
rp   taskmessagestemperature$```(?:[A-Za-z0-9_-]+)?\s*(.*?)\s*```r  z?xAI TTS audio tag rewrite failed; using locally-tagged text: %sN)r%   rI   rJ   _XAI_SPEECH_TAG_REsearch_XAI_FIRST_SENTENCE_REr   _XAI_INLINE_SPEECH_TAGS_XAI_WRAPPING_SPEECH_TAGSagent.auxiliary_clientr  "_extract_auxiliary_message_content	fullmatchDOTALLr{  r3   r   r   )r/  cleanlocalinlinewrappingsystem_promptr  r   taggedfencerA  s              r   _apply_xai_auto_speech_tagsr    s    JJLLE  EF;U33EF;U++E$$U++ K&**>5*JJF9c5))//11E   ''  YY.//Fyy233H	/17	8;:	: =E	E	H/	/ 3333338!!m<<,J5,J,JKK 
 
 
 4H==CCEEDfTVT]^^^ 	,[[^^))++F   VX[\\\s   BF 
GF?9G?Gc                    ddl }ddlm}  |            }t          |                    d          pd                                          }|st          d          |                    d          pi }t          |                    dt                                                              pt          }t          |                    d	t                                                              pt          }	t          |                    d
t                              }
t          |                    dt                              }t          |                    d|                    d                    t                    }|                    d|                    d                    }|0|dk    r*	 t          |          }n# t          t          f$ r d}Y nw xY w|(t!          t"          t%          t&          |                    }|                    d|                    d                    }|0|dk    r*	 t          |          }n# t          t          f$ r d}Y nw xY w|t!          dt%          d|                    }t          |                    d          t(                    }|rt+          |           } |                    d          dk    rOt          |                    d          pt,                                                                        d          }nrt          |                    d          p*|                    d          pt1          d          pt,                                                                        d          }|                    d          rdnd}| ||	d}|dk    s|
t          k    s|dk    r(|t          k    rd|i}|
r|
|d
<   |dk    r|r||d<   ||d<   ||t4          k    r||d<   ||t6          k    r||d<   |rd|d<   |                    | dd| d t;                      d!|d"d#          }|                                 t?          ||d$%           |S )&z
    Generate audio using xAI TTS.

    xAI exposes a dedicated /v1/tts endpoint instead of the OpenAI audio.speech
    API shape, so this is implemented as a separate backend.
    r   Nresolve_xai_http_credentialsr   r!   zSNo xAI credentials found. Configure xAI OAuth in `hermes model` or set XAI_API_KEY.r~   rC  rR  sample_ratebit_rateauto_speech_tagsspeech_tagsr8  optimize_streaming_latencyru   text_normalizationr   z	xai-oauthr?   r@   XAI_BASE_URLrM  r  r  )r/  rC  rR  codecr9  Tz/ttsBearer application/json)AuthorizationContent-Typez
User-Agent<   )headersr   rL  r  zxAI TTSr   ) requeststools.xai_httpr  r$   rG   r%   r
  DEFAULT_XAI_VOICE_IDDEFAULT_XAI_LANGUAGEr   DEFAULT_XAI_SAMPLE_RATEDEFAULT_XAI_BIT_RATEr  DEFAULT_XAI_AUTO_SPEECH_TAGSr   rO  r  DEFAULT_XAI_SPEED_MINr  DEFAULT_XAI_SPEED_MAX&DEFAULT_XAI_TEXT_NORMALIZATION_DEFAULTr  DEFAULT_XAI_BASE_URLrH   r   r.  DEFAULT_XAI_SPEED_DEFAULT.DEFAULT_XAI_OPTIMIZE_STREAMING_LATENCY_DEFAULTpostr,   raise_for_statusr   )r/  r   r   r  r  credsr   
xai_configrC  rR  r  r  r  r8  r  r  r?   r  payloadr9  r   s                        r   _generate_xai_ttsr    s    OOO;;;;;;((**E%))I&&,"--3355G pnooo&&,"J:>>*.BCCDDJJLLdPdH:>>*.BCCDDJJLLdPdHjnn]4KLLMMK:>>*.BCCDDH'):>>-+H+HII$  NN7JNN7$;$;<<EUb[[	%LLEE:& 	 	 	EEE	)3/De+L+LMM ",$344" " "-2LPR2R2R	.),-G)H)H&&:& 	. 	. 	.)-&&&	.!-%(C3M,N,N%O%O" *+,,.   1*400yy++uyy,,D0DEEKKMMTTUXYYNN:&& $yy$$$^,,$ $	
 

 %''&&++ 	 !))&11<EEuE G 	111UNNx+???)0%(8 	7+6M-(E>>h>(0M*%#0  U&???  	#.&*XXX0J,- -(,$%}}0w00./11
 

   
 
H +YGGGGs$   3G GG8I IIc           	      .   ddl }t          |          }|                    di           }t          |t                    si }|                    dt
                    }|                    dt                    }|j        }|                    dd          }	|                    dd          }
|                    d	d          }|                    d
d          }|                    dd          }|                    dd          }t          |                    d          pd          	                                p"t          d          pd	                                }|rd|vrd|v rdnd}| | d| }dd|j         d}d|v }|r|| ||	|
||d||dddd}n|| |d}|                    |||d d!"          }|r|                                 t          |d#$          }|                    d%i           }|                    d&d'          }|dk    r+|                    d(d)          }t          d*| d+|           |                    d,i                               d-d          }|st          d.          t                               |          }t%          |d/          5 }|                    |           ddd           n# 1 swxY w Y   |S |j                            d0d          }d1|v rt+          ||d#$           |S 	 t-          |d#$          }|r't/          j        |                    d2                    ni }|                    d%i           }|                    d&d'          }|dk    r+|                    d(d)          }t          d*| d+|           ni# t.          j        t6          t8          f$ rJ |                                 t          d3| d4d5t;                      v rt=          |          nd d6          w xY wt          d7          )8a  
    Generate audio using MiniMax TTS API.

    Supports two endpoints:
    - v1/text_to_speech: simple payload, returns raw audio (Content-Type: audio/mpeg)
    - v1/t2a_v2: nested voice_setting/audio_setting, returns JSON with hex-encoded audio

    Args:
        text: Text to convert (max 10,000 characters).
        output_path: Where to save the audio file.
        tts_config: TTS config dict.

    Returns:
        Path to the saved audio file.
    r   Nr   r7  rC  r8  rq   volpitchemotionneutralr  r}   bitratero   group_idr!   MINIMAX_GROUP_IDzGroupId=?&r  r  )r  r  t2a_v2)rC  r8  r  r  r  r  rt   )r  r  r:  channel)r7  r/  voice_settingaudio_setting)r7  r/  rC  r  T)r   r  rL  r  zMiniMax TTSr  	base_respstatus_code
status_msgunknown errorzMiniMax TTS API error (code ): r  ra  z%MiniMax TTS returned empty audio datar   r  zaudio/r   z.MiniMax TTS returned unexpected Content-Type 'z' (raw_bodyz bytes)z"MiniMax TTS returned no audio data)r  r  rG   r   r   DEFAULT_MINIMAX_MODELDEFAULT_MINIMAX_VOICE_IDr   r$   r%   r   r   r  r  r   r   r   fromhexr   r   r  r   r   r   r   r   JSONDecodeErrorUnicodeDecodeErrorrO  localsr   )r/  r   r   r  runtimer  r7  rC  r?   r8  r  r  r  r  r  r  sepr  	is_t2a_v2r  r   r!  r  r  r  	hex_audior   r   content_typer  s                                 r   _generate_minimax_ttsr  L  s     OOO*:66Gy"--Ii&& 	MM'#899E}}Z)ABBHHMM'3''E
--s
#
#CMM'1%%EmmIy11G--u55KmmIv..G 	IMM*%%+,,2244 	=,--3::<<   8Jh..H__cc#777X77 +47?44 G H$I 
 $"   +"	 
 
(  
 
 }}   H  ,A!!###(GGGJJ{B//	mmM266!"|_EEJZkZZjZZ[[[JJvr**..w;;	 	HFGGGmmI..+t$$ 	!GGK   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	!  '++NB??|##'+]SSSS	/NNNH=EMTZ 8 89992F

;33I#--r::Ka&]]<II
"#^+#^#^R\#^#^___   $&8)D 	 	 	%%'''L L L%/688%;%;CMMML L L  	 ?@@@s    "KKKBN A&Pc                    t          dd          pd}|st          d          |                    d          pi }|                    dt                    }|                    d          pt          }|                    d          }|                    d          rd	}n2|                    d
          rd}n|                    d          rd}nd}t                      }	d|i}
|r||
d<   	  |	di |
5 }|j        j        	                    || ||          }t          j        |j                  }ddd           n# 1 swxY w Y   n^# t          $ r  t          $ rG}t                              d|d           t!          dt#          |          j                   |d}~ww xY wt'          |d          5 }|                    |           ddd           n# 1 swxY w Y   |S )zGenerate audio using Mistral Voxtral TTS API.

    The API returns base64-encoded audio; this function decodes it
    and writes the raw bytes to *output_path*.
    Supports native Opus output for Telegram voice bubbles.
    MISTRAL_API_KEYr   r!   z?MISTRAL_API_KEY not set. Get one at https://console.mistral.ai/r7  rC  r?   r  r  rM  r  rN  r  r  r   
server_url)r7  rS  rC  rT  NzMistral TTS failed: %sTr   zMistral TTS failed: r   r   )r&   r
  rG   DEFAULT_MISTRAL_TTS_MODELDEFAULT_MISTRAL_TTS_VOICE_IDr.  rV   ra  rb  completebase64	b64decode
audio_datar3   r   errorr   r   r   r   r   )r/  r   r   r   	mi_configr7  rC  r?   rT  rT   client_kwargsrI  r   r   r   r   s                   r   _generate_mistral_ttsr    s    %%6	BBHbG \Z[[[y))/RIMM'#<==E}}Z((H,HH }}Z((HF##   			f	%	%  			g	&	&   $&&G%.$8M /&.l#MW%%}%% 	@|*33! /	 4  H !*8+>??K	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@     M M M-q4@@@D$q''2BDDEE1LM 
k4	 	  A	               sO   +E 3=D<0E <E  E E E F#AFF#6GGG	pcm_bytesr  channelssample_widthc                 @   ddl }||z  |z  }||z  }t          |           }|                    dddd|||||dz  	  	        }|                    dd	|          }	d
t          |          z   t          |	          z   |z   }
|                    dd|
d          }||z   |	z   | z   S )a  Wrap raw signed-little-endian PCM with a standard WAV RIFF header.

    Gemini TTS returns audio/L16;codec=pcm;rate=24000 -- raw PCM samples with
    no container. We add a minimal WAV header so the file is playable and
    ffmpeg can re-encode it to MP3/Opus downstream.
    r   Nz
<4sIHHIIHHs   fmt    rt      z<4sIs   data   z<4sI4ss   RIFFs   WAVE)structr   pack)r  r  r   r  r  	byte_rateblock_align	data_size	fmt_chunkdata_chunk_header	riff_sizeriff_headers               r   _wrap_pcm_as_wavr    s     MMMh&5I\)KII
	q
 
I FGY??C	NN"S):%;%;;iGI++hGDDK"%66BBrM   gemini_configc                    |                      d          }t          |t                    r|                                sdS t          j                            |                                          }t          |                                          }|	                                s;	 ddl
m}  |            |z  }n&# t          $ r t          j                    |z  }Y nw xY w|S )z7Return the configured persona prompt file path, if any.persona_prompt_fileNr   )get_hermes_home)rG   r   r$   r%   r   r  
expandvarsr   r  is_absoluter{   r  r3   cwd)r  r   expandedr  r  s        r   #_resolve_gemini_persona_prompt_pathr  /  s    


1
2
2Cc3 syy{{ tw!!#))++..H>>$$&&D %	%888888"?$$t+DD 	% 	% 	%8::$DDD	%Ks   (B< < CCc                     t          |           }|dS 	 |                    d                                          S # t          t          f$ r'}t
                              d||           Y d}~dS d}~ww xY w)zERead the Gemini persona prompt file, failing soft on config mistakes.Nr!   r   r  z4Gemini TTS persona prompt file unavailable at %s: %s)r  	read_textr%   r  r  r   r   )r  r  rA  s      r   _read_gemini_persona_promptr  @  s    .}==D|r~~w~//55777'(   B	
 	
 	

 rrrrrs   '= A5A00A5c                     | pd                                                                                     dd          d         }d|v od|v S )zIReturn True for Gemini TTS models known to support expressive audio tags.r!   r@   rt   r  z
gemini-3.1r   )r%   r   r  )r7  r   s     r   !_gemini_model_supports_audio_tagsr  P  sM    +2$$&&,,..55c1==bAJ:%=%:*==rM   c                    |                      d          }t          |t                    r|                     d          }t          |t                    }|sdS t          |          st                              d|           dS dS )N
audio_tagsr   r  FzrGemini TTS audio_tags enabled, but model %s is not known to support Gemini audio tags; skipping hidden tag rewriteT)rG   r   r   r   DEFAULT_GEMINI_AUDIO_TAGSr  r   r   )r  r7  r   r   s       r   _gemini_audio_tags_enabledr!  V  s    


L
)
)C#t !ggi  3(ABBBG u,U33 =	
 	
 	

 u4rM   r   c                     | pd                                 }t          j        d|t          j                  }|r'|                    d                                           }|S )Nr!   r  r  rt   )r%   rI   r  r  r{  )r   r  r  s      r   _clean_gemini_audio_tag_rewriter#  g  sX    ]!!##EL@%ryYYYE 'A$$&&LrM   c                    	 | j         d         }t          |dd           }t          |t                    r$t	          |                    d          pd          S t	          t          |dd          pd          S # t          $ r Y dS w xY w)Nr   messager   r!   )choicesr   r   r   r$   rG   r3   )r   choicer%  s      r   r  r  o  s    !!$&)T22gt$$ 	5w{{9--344477Ir228b999   rrs   AA9 A9 9
BBr!   persona_promptc                 ^   |                                  }|s| S d}|                                 pd}d| d| }	 ddlm}  |t          d|dd	|dgd
          }t	          t          |                    }|p| S # t          $ r'}	t                              d|	           | cY d}	~	S d}	~	ww xY w)z?Use the configured auxiliary model to insert Gemini audio tags.a  You rewrite transcripts for Gemini 3.1 Flash TTS by inserting expressive audio tags.

Audio tags are inline square-bracket modifiers such as [whispers], [excitedly], [very slow], [sarcastically], [laughs], [sighs], or [gasp]. There is no fixed allowlist. Use creative freeform tags generously but naturally to control tone, pace, emotional vibe, emphasis, section-level delivery, and non-verbal sounds. Use English audio tags even when the spoken transcript is not English.

Rules:
- Preserve the spoken words, order, and meaning.
- Do not add new spoken sentences or remove existing spoken words.
- Use square brackets for every audio tag.
- Do not use SSML or XML tags.
- Do not explain or comment.
- Return only the tagged TTS script.z(none)zPERSONA AND DIRECTOR CONTEXT:
z

TRANSCRIPT TO TAG:
r   r  r  r  r  rp   r  z<Gemini TTS audio tag rewrite failed; using untagged text: %sN)	r%   r  r  GEMINI_AUDIO_TAG_REWRITE_TASKr#  r  r3   r   r   )
r/  r(  
transcriptr  contextuser_promptr  r   r  rA  s
             r   _rewrite_gemini_tts_audio_tagsr.  z  s   J 	/ " ""$$0G		 	 	 	 3333338.!m<<K88 
 
 
 11ST\1]1]^^~   UWZ[[[s   A A; ;
B,B'!B,'B,c                    |                                  }|t          |          }|s|S d}t          j        dt          j                  t          j        dt          j                  f}|}|D ]H}|                    |          r1|                    ||          }| d|                                  c S I| d| d|                                  S )zHBuild the Gemini prompt from persona direction plus the live transcript.NzSynthesize speech from the TRANSCRIPT only. Treat AUDIO PROFILE, SCENE, DIRECTOR'S NOTES, and SAMPLE CONTEXT as performance direction; do not speak those sections aloud.z\{\{\s*transcript\s*\}\}r  z\{\s*transcript\s*\}

z

#### TRANSCRIPT
)r%   r  rI   r  
IGNORECASEr  rJ   )r/  r  r(  r+  preambleplaceholder_patternsr1   r  s           r   _compose_gemini_tts_promptr4    s    J4]CC 	-  	
.bmDDD

*"-@@@ F' 5 5>>&!! 	5[[V44F,,F,,2244444	5 MMNMMMMSSUUUrM   c                 6   ddl }t          dd          pt          dd          }|st          d          |                    d          pi }t	          |t
                    r|ni }t          |                    dt                                                              pt          }t          |                    dt                                                              pt          }t          |                    d	          pt          d
          pt                                                                        d          }	t          |          }
| }t          ||          rt          | |
          }t!          |||
          }t#          d|          }t%          |          |k    r3t&                              dt%          |          |           |d|         }dd|igigdgddd|iiidd}ddi}t+          |	          j        dk    r4	 ddl}t          |j                  }n# t2          $ r d}Y nw xY wd| |d<   |	 d| d}|                    |d|i||dd !          }|j        d"k    r&t9          |d#$          }	 |r<t;          j        |                    d%                                        d&i           }nXtA          |          sGtC          tE          |d'd                    r)|                                                    d&i           }ni }|                    d(          p|                    d%d)*          dd+         }n/# t2          $ r" |                    d%d)*          dd+         }Y nw xY wtG          d,|j         d-|           	 tI          |d#$          }|d.         d         d/         d         }tK          d0 |D             d          }|tG          d1          |                    d2          p|                    d3          pi }|                    d4d5          }n2# tL          tN          tP          f$ r}tG          d6|           |d}~ww xY w|stG          d7          tS          j*        |          }tW          |          }|,                                -                    d8          r?t]          |d9          5 }|/                    |           ddd           n# 1 swxY w Y   |S ta          j1        d8d:;          5 } | /                    |           | j2        }!ddd           n# 1 swxY w Y   	 tg          j4        d<          }"|"r|,                                -                    d=          r|"d>|!d?d@dAdBdCdDdEdFdGdHdIdJdKdLd&|g}#n	|"d>|!dKdLd&|g}#tk          j6        |#d dMtj          j7        tq                      N          }$|$j9        dk    r6|$j:                            d%dO*          dd+         }%tG          dP|%           n0t&                              dQ|           tg          j;        |!|           	 ty          j=        |!           n:# t|          $ r Y n.w xY w# 	 ty          j=        |!           w # t|          $ r Y w w xY wxY w|S )Ra  Generate audio using Google Gemini TTS.

    Gemini's generateContent endpoint with responseModalities=["AUDIO"] returns
    raw 24kHz mono 16-bit PCM (L16) as base64. We wrap it with a WAV RIFF
    header to produce a playable file, then ffmpeg-convert to MP3 / Opus if
    the caller requested those formats (same pattern as NeuTTS).

    Args:
        text: Text to convert (prompt-style; supports inline direction like
              "Say cheerfully:" and audio tags like [whispers]).
        output_path: Where to save the audio file (.wav, .mp3, or .ogg).
        tts_config: TTS config dict.

    Returns:
        Path to the saved audio file.
    r   NGEMINI_API_KEYr   GOOGLE_API_KEYzIGEMINI_API_KEY not set. Get one at https://aistudio.google.com/app/apikeyr7  r6  r?   GEMINI_BASE_URLr@   )r(  z@Gemini TTS composed prompt too long (%d chars), truncating to %dpartsr/  AUDIOvoiceConfigprebuiltVoiceConfig	voiceName)responseModalitiesspeechConfig)contentsgenerationConfigr  r  z!generativelanguage.googleapis.comz0.0.0zhermes-agent/zX-Goog-Api-Clientz/models/z:generateContentr   r  T)paramsr  r   rL  r  r  z
Gemini TTSr  r   r  r   r%  r   r   ,  zGemini TTS API error (HTTP r  rt  r   c              3   *   K   | ]}d |v sd|v 
|V  dS )
inlineDatainline_dataNr   )rs  ps     r   rt  z'_generate_gemini_tts.<locals>.<genexpr>5	  s7      WW|q/@/@MUVDVDV1DVDVDVDVWWrM   z+Gemini TTS response contained no audio datarE  rF  r  r!   z#Gemini TTS response was malformed: z$Gemini TTS returned empty audio datarM  r   FrS  deleter  r  r	  r
  r  r  r   r  r  r  r   r  r  r  r  r  	-loglevelr  r  r  zffmpeg conversion failed: zEffmpeg not found; writing raw WAV to %s (extension may be misleading))?r  r&   r
  rG   r   r   r$   DEFAULT_GEMINI_TTS_MODELr%   DEFAULT_GEMINI_TTS_VOICEr   DEFAULT_GEMINI_TTS_BASE_URLrH   r  r!  r.  r4  r   r   r   r   r   r  
hermes_cli__version__r3   r  r  r   r   r   r   r   r   r   r   r   nextKeyError
IndexErrorrO  r  r  r  r   r.  r   r   r  NamedTemporaryFiler   r  r  ri  r  r  r   r  r  copyfiler   r  r  )&r/  r   r   r  r   raw_gemini_configr  r7  r6  r?   r(  
tts_scriptprompt_textmax_lenr  r  _hermes_cli_hermes_versionr   r   r  errr  r  r9  
audio_partr  	audio_b64r   r  	wav_bytesr   tmpwav_pathr  cmdr!  r  s&                                         r   _generate_gemini_ttsrb    sB   " OOO 	.99 	= !18<<   
W
 
 	
 #x006B)34Et)L)LT%%RTM!!'+CDDEEKKMMiQiE!!'+CDDEEKKMMiQiE*%% 	'*++	'&  eggffSkk	 
 1??NJ!-77 Y3DXXX
,%  K
 'x<<G
;'!!Ng	
 	
 	
 "(7(+  5678#*))K+? 
 

 
G 12G"&III	&,,,,!+"9::OO 	& 	& 	&%OOO	&
 (I'H'H#$;;E;;;H}}w   H s""+HLIII		F j!9!9::>>wKK28<< 'RZ\bdhJiJiAjAj mmoo))'266WWY''[8??79?+U+UVZWZVZ+[FF 	F 	F 	F__WY_??EFFF	FK(*>KK6KK
 
 	
	M&x|DDD\"1%i09WWeWWWY]^^
LMMM--T1N1NTRTJJvr**		j), M M MDDDEE1LM  CABBB ++I ++I ##F++ +t$$ 	GGI	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 
	$F5	A	A	A S		)8              h'' 	3   ""++F33 
XD(y%E64"F,@$+w tXt[';W^CbPZPb  sE  sG  sG  H  H  HF A%%--gh-GGM"#H#H#HIII & NNW   OHk222	Ih 	 	 	D		Ih 	 	 	D	 s   H* *H98H9C
M )M>=M>BP5 5Q$QQ$S44S8;S8U  UUC=Y/ 
Y 
Y,+Y,/Z1ZZ
ZZZZc                  f    	 ddl } | j                            d          duS # t          $ r Y dS w xY w)z=Check if the neutts engine is importable (installed locally).r   Nr   Fimportlib.utilutil	find_specr3   	importlibs    r   _check_neutts_availablerj  y	  sP    ~''11==   uu   " 
00c                  f    	 ddl } | j                            d          duS # t          $ r Y dS w xY w)z@Check if the kittentts engine is importable (installed locally).r   Nr^   Frd  rh  s    r   _check_kittentts_availablerm  	  sP    ~''44D@@   uurk  c                  Z    t          t          t                    j        dz  dz            S )z9Return path to the bundled default voice reference audio.neutts_sampleszjo.wavr$   r   __file__r  r   rM   r   _default_neutts_ref_audiorr  	  $    tH~~$'77(BCCCrM   c                  Z    t          t          t                    j        dz  dz            S )z>Return path to the bundled default voice reference transcript.ro  zjo.txtrp  r   rM   r   _default_neutts_ref_textru  	  rs  rM   c                 >   ddl }|                    d          pi }|                    dd          pt                      }|                    dd          pt                      }|                    dd          }|                    d	d
          }|}	|                    d          s|                    dd          d         dz   }	t          t          t                    j	        dz            }
|j
        |
d| d|	d|d|d|d|g}t          j        |dddddt          j                  }|j        dk    rk|j                                        }d |                                D             }t%          dt'          d                              |          pd           |	|k    rxt+          j        d          }|rM|d|	d d!d"|g}t          j        |dd#t          j        t/                      $           t1          j        |	           nt1          j        |	|           |S )%a  Generate speech using the local NeuTTS engine.

    Runs synthesis in a subprocess via tools/neutts_synth.py to keep the
    ~500MB model in a separate process that exits after synthesis.
    Outputs WAV; the caller handles conversion for Telegram if needed.
    r   Nr   	ref_audior!   ref_textr7  zneuphonic/neutts-air-q4-ggufdevicecpurM  rR  rt   zneutts_synth.pyz--textz--outz--ref-audioz
--ref-textz--modelz--deviceTr   r   r  r  r/  r  r   rL  r  c                 <    g | ]}|                     d           |S )zOK:)r   )rs  ls     r   r  z$_generate_neutts.<locals>.<listcomp>	  s)    QQQQQ\\%=P=PQqQQQrM   zNeuTTS synthesis failed: 
   r  r  r	  r  rJ  r  r  checkrL  r  r  )sysrG   rr  ru  r.  r  r$   r   rq  r  
executableri  r  r  r  r  r%   
splitlinesr   chrr   r  r  r   r   r  rename)r/  r   r   r  neutts_configrw  rx  r7  ry  r`  synth_scriptra  r!  r  error_linesr  conv_cmds                    r   _generate_neuttsr  	  sD    JJJNN8,,2M!!+r22Q6O6Q6QI  R00N4L4N4NHg'EFFEx//F H'' :%%c1--a069tH~~,/@@AAL$yh5FC ^C4'Zcmp  yC  yK  L  L  LFA$$&&QQ&"3"3"5"5QQQes2ww||K7P7P7cTceefff ;h'' 	-hk7KXHN84:CUeweyeyzzzzIh Ih,,,rM      cacher   loadc                 ,   || v r |                      |          | |<   | |         S  |            }|| |<   t          |           t          k    rH|                      t          t	          |                     d           t          |           t          k    H|S )aj  Get ``key`` from ``cache`` or load it, keeping the cache LRU-bounded.

    Refreshes recency on a hit (insertion-ordered dict: pop + reinsert), loads
    on a miss, then evicts least-recently-used entries beyond the cap. An entry
    evicted while a caller still holds its returned reference stays alive for
    that caller; only the cache slot is released.
    N)popr   _TTS_MODEL_CACHE_MAXrP  iter)r  r   r  r   s       r   _tts_cache_get_or_loadr  	  s     e||YYs^^c
SzDFFEE#J
e**+
+
+		$tE{{##T*** e**+
+
+LrM   _piper_voice_cachec                  f    	 ddl } | j                            d          duS # t          $ r Y dS w xY w)z2Check whether the piper-tts package is importable.r   Nrc   Frd  rh  s    r   _check_piper_availabler  	  sP    ~''00<<   uurk  c                  r    ddl m}  t           | dd                    }|                    dd           |S )zReturn the directory where Hermes caches Piper voice models.

    Resolves to ``~/.hermes/cache/piper-voices/`` under the active
    HERMES_HOME so voice downloads follow profile boundaries.
    r   rx   zcache/piper-voicespiper_voices_cacheTr  )r{   ry   r   r  )ry   roots     r   _get_piper_voices_dirr  	  sL     0/////35IJJKKDJJtdJ+++KrM   download_dirc           
      j   | st           } t          |                                           }|j                                        dk    r#|                                rt          |          S ||  dz  }|                                r)||  dz                                  rt          |          S ddl}t          	                    d| |           	 t          j        |j        dd| dt          |          gd	d	d
ddt          j                  }n+# t          j        $ r}t          d|  d          |d}~ww xY w|j        dk    r:|j        pd                                pd}t          d|  d|dd                    |                                st          d| d          t          |          S )a}  Resolve *voice* (a model name or path) to a concrete .onnx file path.

    Accepts any of:
      - Absolute / expanded path to an .onnx file the user already has
      - A voice *name* like ``en_US-lessac-medium`` (downloads to
        ``download_dir`` on first use via ``python -m piper.download_voices``)

    Raises RuntimeError if the model can't be located or downloaded.
    z.onnxz
.onnx.jsonr   Nz0[Piper] Downloading voice '%s' to %s (first use)z-mzpiper.download_voicesz--download-dirTr   r   rC  r{  z/Piper voice download timed out after 300s for 'r\  r!   zno stderr outputz!Piper voice download failed for 'z': i  z#Piper voice download completed but uh    is missing — check voice name (see: https://github.com/OHF-Voice/piper1-gpl/blob/main/docs/VOICES.md))DEFAULT_PIPER_VOICEr   r  rS  r   r  r$   r  r   r>  ri  r  r  r  r  r   r  r  r%   )r6  r  	candidatecached_sysr!  rA  r  s           r   _resolve_piper_voice_pathr  
  s     $# U&&((I7**y/?/?/A/A*9~~ uOOO+F}} Le+?+?+??GGII 6{{ 
KKBE<XXX
_d$;Us<002dWYX[$	
 
 
 $   FeFFF
 
	
 A-%2,,..D2DHHH&#,HH
 
 	
 ==?? 
)& ) ) )
 
 	

 v;;s   =D D7D22D7c                    t                      ddl}t          |t                    r|                    d          pi ni                     d          pt
          }t                              d          pt                                                                }|	                    dd           t                              dd	                    t          ||                              d
d          }t          |t                    st          |t                    sd}n|} d }fd}	t          t          ||	          }
d}t          fddD                       }|r	 ddlm}  |t%                              dd                    t%                              dd                    t%                              dd                    t%                              dd                    t                              dd                    |          }n*# t&          $ r t(                              d           Y nw xY w|}|                    d          s|                    dd          d         dz   }|                    |d          5 }||
                    | ||           n|
                    | |           ddd           n# 1 swxY w Y   ||k    rt5          j        d          }|r^|d|d d!d"|g}t9          j        |dd#t8          j        t?                      $           	 tA          j!        |           n%# tD          $ r Y nw xY wtA          j#        ||           |S )%zGenerate speech using the local Piper engine.

    Loads the voice model once per process (cached by absolute path) and
    writes a WAV file. Caller is responsible for converting to MP3/Opus
    via ffmpeg when a different output format is required.
    r   Nrc   r6  
voices_dirTr  use_cudaF
speaker_idz::cuda=c                      t                               d                                         } t                               d           | S )Nz[Piper] Loading voice: %s)r  z[Piper] Voice loaded)r   r>  r  )vrb   
model_pathr  s    r   _load_piper_voicez._generate_piper_tts.<locals>._load_piper_voiceX
  sE    /<<<OOJO::*+++rM   c              3       K   | ]}|v V  	d S rq  r   )rs  kpiper_configs     r   rt  z&_generate_piper_tts.<locals>.<genexpr>d
  s<       
 
 	
\
 
 
 
 
 
rM   )length_scalenoise_scalenoise_w_scalevolumenormalize_audior  )SynthesisConfigr  rq   r  gMbX?r  g?r  r  uZ   [Piper] SynthesisConfig not available in this piper-tts version — advanced knobs ignoredrM  rR  rt   r   )
syn_configr  r	  r  rJ  r  r  r  )$rd   waver   r   rG   r  r   r  r  r  r   r  r   r  r  anyrc   r  r   r   r   r   r.  r  r   synthesize_wavr  r  ri  r  r  r   r   r  r  r  )r/  r   r   r  
voice_namer  _raw_speakerr  	cache_keyr  r6  r  has_advancedr  r`  wav_filer  r  rb   r  r  r  s                     @@@@r   _generate_piper_ttsr  8
  s6    JKKK4>z44P4PX:>>'**0bVXL!!'**A.AJ((66Q:O:Q:QRR]]__Ltd333L$$Z7788H*:|DDJ
  ##L!44L,%% "Zc-J-J "

!

 00h00I       ##5yBSTTE
 J 
 
 
 


 
 
 
 
L  	------("<#3#3NC#H#HII!,"2"2=%"H"HII#L$4$4_c$J$JKK\--h<<== $\%5%56G%N%N O O%  JJ  	 	 	NN5    	 H'' :%%c1--a069	8T	"	" 1h!  xJ GGGG  x000	1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 ;h'' 		-hk7KXHN84:CUeweyeyzzzz	(####    Ih,,,s7   6B<H3 3$II)2K''K+.K+M 
M)(M)_kittentts_model_cachec                    t                      |                    di           }|                    dt                    |                    dt                    }|                    dd          }|                    dd          }fd}t	          t
          |          }|                    | |||	          }	d
dl}
|}|                    d          s|	                    dd          d
         dz   }|

                    ||	d           ||k    rxt          j        d          }|rM|d|ddd|g}t          j        |ddt          j        t!                                 t#          j        |           nt#          j        ||           |S )at  Generate speech using KittenTTS local ONNX model.

    KittenTTS is a lightweight TTS engine (25-80MB models) that runs
    entirely on CPU without requiring a GPU or API key.

    Args:
        text: Text to convert to speech.
        output_path: Where to save the audio file.
        tts_config: TTS config dict.

    Returns:
        Path to the saved audio file.
    r^   r7  r6  r8  rq   
clean_textTc                      t                               d                      } t                               d           | S )Nz[KittenTTS] Loading model: %sz%[KittenTTS] Model loaded successfully)r   r>  )mr]   
model_names    r   _load_kittentts_modelz2_generate_kittentts.<locals>._load_kittentts_model
  s?    3Z@@@Ij!!;<<<rM   )r6  r8  r  r   NrM  rR  rt   rn   r  r	  r  rJ  r  r  r  )r_   rG   DEFAULT_KITTENTTS_MODELDEFAULT_KITTENTTS_VOICEr  r  generate	soundfiler.  r  r   r  r  ri  r  r  r   r   r  r  )r/  r   r   	kt_configr6  r8  r  r  r7  ra  sfr`  r  r  r]   r  s                 @@r   _generate_kittenttsr  
  s    "##I{B//Iw(?@@JMM'#:;;EMM'3''E|T22J      ##9:G\]]E NN4uEjNQQE H'' :%%c1--a069HHXue$$$ ;h'' 	-hk7KXHN84:CUeweyeyzzzzIh Ih,,,rM   c           	      &     r                                  st          dd          S 	 ddlm}  | d           n$# t          $ r                                    Y nw xY w st          dd          S t                      |?t          d	t          d
t          |                              }t                    |d<   |r'|
                                                                 }nt                    }t          |          }t          |          }t                     |k    r4t                              d|t                     |            d|          ddlm}	  |	dd          
                                }
|
t&          v }|rddlm}  ||          rt-          j        dd| ddd          S t1          |                                          }|t5          ||          }ddlm}  |t;          |                    rt-          j        dd| ddd          S nt<          j                                                             d          }t1          tB                    }|"                    dd           |tG          |          }|d| d| z  }n|r|dv r
|d| dz  }n	|d| dz  }|j$        "                    dd           t;          |          	 |0t          %                    d |           tM           ||          n|tN          vrtQ           |          x}	 |n|d!k    rf	 tS                       n)# tT          $ r t-          j        dd"dd          cY S w xY wt          %                    d#           tW                      n@|d$k    rh	 tY                       n)# tT          $ r t-          j        dd%dd          cY S w xY wt          %                    d&           t[           |'           n|d(k    rf	 tY                       n)# tT          $ r t-          j        dd)dd          cY S w xY wt          %                    d*           t]                      nf|d+k    r-t          %                    d,           t_                      n3|d-k    r-t          %                    d.           ta                      n |d/k    rf	 tc                       n)# tT          $ r t-          j        dd0dd          cY S w xY wt          %                    d1           te                      n|d2k    r-t          %                    d3           tg                      na|d4k    rTti                      st-          j        dd5dd          S t          %                    d6           tk                      n|d7k    rf	 tm                       n)# tT          $ r t-          j        dd8dd          cY S w xY wt          %                    d9           to                      n|d:k    rf	 tq                       n)# tT          $ r t-          j        dd;dd          cY S w xY wt          %                    d<           ts                      n/d}	 tu                       n# tT          $ r d}Y nw xY w|rt          %                    d=           	 ddl;}|j<        =                    d>?          5 }|>                     fd@          ?                    dAB           ddd           n# 1 swxY w Y   n# t          $ r& t          jB        t                                Y nYw xY wti                      r.t          %                    dC           d4}tk                      nt-          j        ddDdd          S t          jE        F                              r#t          jE        G                              dk    rt-          j        ddE| dFdd          S t                    d}|Mt          |          r=J                    d          st                    }|r|J                    d          }n|tN          vrOt          |          }|r=J                    d          st                    }|r|J                    d          }nL|r/|dGv r+J                    d          st                    }|r|d}n|dv r|oJ                    d          }t          jE        G                              }t          %                    dH|dI|           dJ }|rdK| }t-          j        d|||dLd          S # t          $ r>}dM| dN| }t          N                    dO|           t          |d          cY d}~S d}~wt          $ r@}dP| dN| }t          N                    dO|dQ           t          |d          cY d}~S d}~wt          $ r@}dR| dN| }t          N                    dO|dQ           t          |d          cY d}~S d}~ww xY w)Sa  
    Convert text to speech audio.

    Reads provider/voice config from ~/.hermes/config.yaml (tts: section).
    The model sends text; the user configures voice and provider.

    On messaging platforms, the returned MEDIA:<path> tag is intercepted
    by the send pipeline and delivered as a native voice message.
    In CLI mode, the file is saved to ~/voice-memos/.

    Args:
        text: The text to convert to speech.
        output_path: Optional custom save path. Defaults to ~/voice-memos/<timestamp>.mp3
        speed: Optional playback speed multiplier (0.25-4.0). Overrides config.yaml.
        instructions: Optional voice-design guidance (tone, emotion, pacing,
            accent, whispering). Forwarded to the OpenAI backend
            (gpt-4o-mini-tts and OpenAI-compatible servers). Silently
            ignored by backends that don't support it.
        provider: Optional TTS provider override. When set, bypasses the
            configured ``tts.provider`` and uses this provider instead.
            Accepts built-in names (``edge``, ``openai``, ``elevenlabs``,
            ``minimax``, ``xai``, ``mistral``, ``gemini``, ``neutts``,
            ``kittentts``, ``piper``), user-declared command provider names
            from ``tts.providers.<name>``, or plugin-registered provider
            names.  When ``None`` (the default), the configured provider
            from ``tts.provider`` in config.yaml is used.

    Returns:
        str: JSON result with success, file_path, and optionally MEDIA tag.
    zText is requiredF)successr   prepare_spoken_textN	max_charszText is empty after TTS cleanuprV  rW  r8  z>TTS text too long for provider %s (%d chars), truncating to %d)get_session_envHERMES_SESSION_PLATFORMr!   )has_traversal_componentz/output_path contains '..' traversal component: zM. Use an absolute path or one relative to the current directory without '..'.)r  r  )ensure_ascii)is_write_deniedz;output_path targets a protected credential or system path: z(. Choose a normal audio output location.z%Y%m%d_%H%M%S_%fTr  tts_rR  >   r   rP   r   r   r  .mp3z3Generating speech with command TTS provider '%s'...r   z`ElevenLabs provider selected but 'elevenlabs' package not installed. Run: pip install elevenlabsz$Generating speech with ElevenLabs...rP   z<OpenAI provider selected but 'openai' package not installed.z$Generating speech with OpenAI TTS...)rP  r  z;DeepInfra TTS uses the 'openai' SDK but it isn't installed.z'Generating speech with DeepInfra TTS...r   z%Generating speech with MiniMax TTS...r~   z!Generating speech with xAI TTS...r   zoMistral provider selected but 'mistralai' package not installed. Run `hermes setup` to install Mistral support.z-Generating speech with Mistral Voxtral TTS...r   z+Generating speech with Google Gemini TTS...r   zNeuTTS provider selected but neutts is not installed. Run hermes setup and choose NeuTTS, or install espeak-ng and run python -m pip install -U neutts[all].z(Generating speech with NeuTTS (local)...r^   zKittenTTS provider selected but 'kittentts' package not installed. Run 'hermes setup tts' and choose KittenTTS, or install manually: pip install https://github.com/KittenML/KittenTTS/releases/download/0.8.1/kittentts-0.8.1-py3-none-any.whlz2Generating speech with KittenTTS (local, ~25MB)...rc   zPiper provider selected but 'piper-tts' package not installed. Run 'hermes tools' and select Piper under TTS, or install manually: pip install piper-ttsz'Generating speech with Piper (local)...z"Generating speech with Edge TTS...rt   )max_workersc                  J    t          j        t                               S rq  )asyncior  r@  )r+  r/  r   s   r   <lambda>z%text_to_speech_tool.<locals>.<lambda>  s    GK0B4S]0^0^$_$_ rM   r  r  z9Edge TTS not available, falling back to NeuTTS (local)...zhNo TTS provider available. Install edge-tts (pip install edge-tts) or set up NeuTTS for local synthesis.z-TTS generation produced no output (provider: )>   r~   re   rc   r   r   r^   z,TTS audio saved: %s (%s bytes, provider: %s),zMEDIA:z[[audio_as_voice]]
)r  	file_path	media_tagr   rF  zTTS configuration error (r  z%szTTS dependency missing (r   zTTS generation failed ()Pr%   
tool_errortools.tts_text_normalizer  r3   r   r  r  r   r   r   r   r.  r   r   r   r   gateway.session_contextr  OPUS_VOICE_PLATFORMStools.path_securityr  r   dumpsr   r  r  agent.file_safetyr  r$   datetimenowstrftimeDEFAULT_OUTPUT_DIRr  rV  r  r>  r  r   rD  r<   r   rK  rR   rl  ru  r  r  rV   r  rb  rj  r  r_   r  rd   r  r6   concurrent.futuresfuturesr   submitr!  r   r  r  r@  r   r  r  r  r2  rX  r.  r  rG  r
  r  r  )r/  r   r8  rP  r   r  clampedcommand_provider_configrX  r  platform	want_opusr  r  r  	timestampout_dirrB  _plugin_pathedge_available
concurrentpoolrF  	opus_pathplugin_voice_compatible	file_sizer  r   	error_msgr+  r   s   `                            @@r   text_to_speech_toolr  
  sZ   J  =tzz|| =,e<<<<@@@@@@""44888   zz|| L;UKKKK!##J dCU5\\2233*%%
%
7  ->>##))++ ,, ?xTT 'x<<G
4yy7Lc$ii	
 	
 	
 HWH~ 8777778"==CCEEH00I  19 	@?????"";// 	#: ="= = =  "# # # # %%0022	". <2 I 	655555?3y>>** 	#: K K K K  "# # # #	# %))++445GHH	)**dT222".01HIIC":":":S":"::II  	98'TTT"8"8"8"88II"8"8"8"88I 4$7779~~Hg4".KKEx   -h*A: HH 2228h*  L 	8
 $HH%%'"$$$$ ' ' 'z$# # !&' ' ' ' ' ''
 KK>??? x<<<<!!'%'''' ' ' 'z$[# # !&' ' ' ' ' ''
 KK>??? x,WWWWW$$'%'''' ' ' 'z$Z# # !&' ' ' ' ' ''
 KKABBB#D(J????""KK?@@@!$*====KK;<<<dHj9999""'&(((( ' ' 'z$N# # !&	' ' ' ' ' '' KKGHHH!$*====!!KKEFFF x<<<<!!*,, 'z$F# # !&	' ' ' '
 KKBCCCT8Z8888$$'!#### ' ' 'z$J# #
 !&' ' ' ' ' '' KKLMMMh
;;;;  ' ' ' 'z$5# #
 !&' ' ' ' ' '' KKABBBh
;;;; "N' """" ' ' '!&'  '@AAAP----#+>>1>MM -QU______  &&,,,- - - - - - - - - - - - - - - $ P P PK 24: N NOOOOOP(** 	'WXXX# x<<<<z$E# # !&	' ' ' ' w~~h'' 	#27??8+D+D+I+I: TTTT  "# # # # )22 !". 00GHH =((00 - 0 : :I  -#,#+#4#4V#<#< 222
 'K8&T&T#& =((00 - 0 : :I  -#,#+#4#4V#<#< 
	GVVV%%f-- W )22I ($#' FFF(FX->->v-F-FGOOH--	BHQZN^N^`hiii (X''	 	;:y::Iz!"  0
 
    	  4 4 4@@@Q@@	T9%%%)U333333333 4 4 4?x??A??	T9t444)U333333333 4 4 4>h>>1>>	T9t444)U333333333	4s  A   A! A!/Af8 
M f8 #M?<f8 >M??6f8 6O f8 #O+(f8 *O++8f8 $P3 2f8 3#Qf8 QBf8 6T f8 #T+(f8 *T++Bf8 ;3f8 /W> =f8 >#X$!f8 #X$$6f8 Y* )f8 *#Zf8 Z2f8 [ f8 [!f8  [!!f8 ]'  /]]' ]]' "]#]' &f8 '-^f8 ^Af8 /Af8 E)f8 8
j3g;5j;j5i=jj5jjjc                  >   t                      } t          |           }t          ||           }|dS |dk    r/	 t                       dS # t          $ r t                      cY S w xY w|dk    r>	 t                       n# t          $ r Y dS w xY wt          t          dd                    S |dk    r/	 t                       n# t          $ r Y dS w xY wt                      S |dk    r>	 t                       n# t          $ r Y dS w xY wt          t          d	d                    S |d
k    r$	 t          |            n# t          $ r Y dS w xY wdS |dk    rB	 ddlm} t           |                                d                    S # t           $ r Y dS w xY w|dk    r-t          t          dd          pt          dd                    S |dk    r>	 t#                       n# t          $ r Y dS w xY wt          t          dd                    S |dk    rt                      S |dk    rt%                      S |dk    rt'                      S 	 ddlm} ddlm}  |              ||          }t          |o|                                          S # t           $ r Y dS w xY w)a  Return whether the explicitly resolved TTS provider can run.

    Availability must mirror :func:`text_to_speech_tool` dispatch. Unrelated
    cloud credentials do not make the default Edge backend usable, and an
    explicitly selected backend is checked on its own requirements.
    NTre   r   FrB  rP   r  rn  r   r~   r   r  r   r   r6  r7  r   r  r   r^   rc   r1  r3  )r   r   r.  r6   r   rj  r<   r   r&   rR   _has_openai_audio_backendr  r
  r  r  rG   r3   rV   rm  r  r;  r2  r<  r4  is_available)r   r   command_configr  r2  r4  plugins          r   check_tts_requirementsr  [  s    "##JZ((H5h
KKN!t6	-4 	- 	- 	-*,,,,,	-<	     	 	 	55	)*>MMNNN8	!#### 	 	 	55	(***;	!#### 	 	 	55	)*={KKLLL9	(4444 	 	 	55	t5	CCCCCC4466::9EEFFF 	 	 	55	8!"2H== A$%5x@@
 
 	
 9	"$$$$ 	 	 	55	)*;YGGHHH8&(((;)+++7%'''333333AAAAAA""$$$h''F4v2244555   uus   A	 	A$#A$.A= =
B
B2C 
CC'C6 6
DD+D; ;
E	E	/F 
FFG 
G-,G-
AJ 
JJc                  t   t                      } t          | t                    r|                     d          ndpi }|                    d          pd}|                    d          pd}|rt	          d          s||pt
          dfS t                      }|rt	          d          s||pt
          dfS t          d          }|Cd	}t                      st	          d          r|d
t          d          z   z  }t          |          |j        t          |j                            d           dd          dfS )a  Return ``(api_key, base_url, is_managed)`` for the OpenAI audio client.

    ``is_managed`` is True when the config resolves to the Nous managed audio
    gateway (a restricted proxy), so callers can coerce the request to what the
    gateway supports. When ``tts.use_gateway`` is set the gateway is preferred
    even if direct OpenAI credentials are present.

    Resolution order (mirrors the STT resolver):
    1. ``tts.openai.api_key`` / ``tts.openai.base_url`` from ``config.yaml``
    2. ``VOICE_TOOLS_OPENAI_KEY`` / ``OPENAI_API_KEY`` environment variables
       (still honoring ``tts.openai.base_url`` when set)
    3. Managed OpenAI audio tool gateway
    rP   Nr   r!   r?   r   Fopenai-audiozUNeither tts.openai.api_key in config nor VOICE_TOOLS_OPENAI_KEY/OPENAI_API_KEY is setz. zmanaged OpenAI audio for TTSr@   v1T)r   r   r   rG   r*   r]  r+   r'   r(   r)   r
  nous_user_tokenr   gateway_originrH   )r   
openai_cfgcfg_api_keycfg_base_urldirect_api_keymanaged_gatewayr%  s          r   rZ  rZ    st    "##J.8T.J.JT*..***PT[Y[J..++1rK>>*--3L M?511 M\D-DuLL133N Poe44 P G0G%OO2>BBO; 	 &'' 	?5+A+A 	72 G !!! 	'?188==@@@$GG rM   c                      t                                          d          pi } |                     d          rdS t          t                      pt	          d                    S )zTReturn True when OpenAI audio can use config/env credentials or the managed gateway.rP   r   Tr  )r   rG   r   r+   r'   )r  s    r   r  r    s\    "$$((228bJ~~i   t,..^2N~2^2^___rM   z```[\s\S]*?```z\[([^\]]+)\]\([^)]+\)zhttps?://\S+z\*\*(.+?)\*\*z	\*(.+?)\*z`(.+?)`z^#+\s*z^\s*[-*]\s+z---+z\n{3,}u"   [🀀-🫿☀-➿️‍󠀠-󠁿]+z<think[\s>].*?</think>c                    	 ddl m}  || d          S # t          $ r Y nw xY wt                              d|           } t
                              d|           } t                              d|           } t                              d|           } t                              d|           } t                              d|           } t                              d|           } t                              d|           } t                              d|           } t                              d|           } t                              d|           } t                              d|           } |                                 S )	a  Prepare text for speech via the shared cleaner in tts_text_normalize.

    One cleaner for every TTS path (tool, gateway auto-TTS, voice-mode
    streaming, web dashboard): strips <think> reasoning blocks, the
    file-mutation verifier footer, markdown, and emoji; expands units and
    symbols; and flattens newlines to sentence breaks so newline-sensitive
    providers (Kokoro) speak the whole script.  Falls back to the legacy
    regex pipeline if the normalizer ever fails.
    r   r  Nr  r  z\1r!   r0  )r  r  r3   _THINK_BLOCKrJ   _MD_CODE_BLOCK_MD_LINK_MD_URL_MD_BOLD
_MD_ITALIC_MD_INLINE_CODE
_MD_HEADER_MD_LIST_ITEM_MD_HR_EMOJI_MD_EXCESS_NLr%   )r/  r  s     r   _strip_markdown_for_ttsr    sK   @@@@@@""448888   C&&Dc4((D<<t$$D;;r4  D<<t$$D>>%&&Dud++D>>"d##DR&&D::b$D::c4  DVT**D::<<s    
""c                   n    e Zd ZdZdddej        defdZdedd	fd
Z	ddZ
dedee         fdZddZd	S )_SyncSentencePipelineu  Overlap per-sentence synthesis with playback for non-streaming providers.

    The universal sync fallback used to run strictly serially per sentence —
    synthesize, play, and only then start synthesizing the next sentence — so
    every sentence boundary added a full synthesis-time of dead air. For local
    model providers that cost dominates the conversation: a provider at
    real-time-factor ~1 spends as long silent between sentences as it does
    speaking. Chunked streamers already avoid this; this closes the same gap
    for everyone else (edge, piper, plugin providers, …) without touching the
    provider contract.

    Shape: one synthesis worker (single-threaded executor, so sentences are
    synthesized FIFO and providers never see concurrent calls from this loop —
    same effective concurrency as the serial path) feeding one playback worker
    through a small bounded queue. While sentence *n* plays, sentence *n+1* is
    already synthesizing. The bound keeps lookahead — and the temp files it
    implies — small, and gives natural backpressure to the caller.

    ``synthesize``/``play`` are resolved late (module global / import inside
    the worker) so tests that monkeypatch ``text_to_speech_tool`` or
    ``tools.voice_mode`` keep working unchanged.
    ru   )	lookahead
stop_eventr  c                   || _         t          j        t          d|                    | _        t          dd          | _        t          j        | j	        dd          | _
        | j
                                         d S )Nrt   maxsizeztts-sync-synth)r  thread_name_prefixztts-sync-playT)r  r   r  )_stopr  r  r  _queuer   	_executorr  r  _drain_playerr|  )selfr  r  s      r   __init__z_SyncSentencePipeline.__init__0  s    
CH;9%%D
 D
 D
 ,.>
 
 
 !';_T
 
 
 	rM   cleanedr   Nc                     | j                                         rdS | j                            | j        |          }| j                            ||f           dS )zAQueue one sentence. Blocks only when the lookahead bound is full.N)r#  is_setr%  r  _synthesize_to_tmpr$  r  )r(  r*  futures      r   speakz_SyncSentencePipeline.speak=  sV    : 	F&&t'>HH&)*****rM   c                     | j                             d           | j                                         | j                            d           dS )z@Flush queued sentences in order (skipped if stopped), then join.NT)r  )r$  r  r'  r   r%  shutdown)r(  s    r   r   z_SyncSentencePipeline.closeD  sI    T*****rM   c                 r   | j                                         rd S d }	 t          j        d          \  }}t	          j        |           t          ||           |S # t          $ rN}t          	                    d|           |r&	 t	          j
        |           n# t          $ r Y nw xY wY d }~d S d }~ww xY w)Nr  )rS  )r/  r   z*Sync per-sentence TTS synthesis failed: %s)r#  r,  r  mkstempr   r   r  r3   r   r   r  r  )r(  r*  tmp_pathfdrA  s        r   r-  z(_SyncSentencePipeline._synthesize_to_tmpJ  s    : 	4	#+6:::LBHRLLLW(CCCCO 	 	 	NNGMMM Ih''''   D44444	s;   >A 
B6(B1BB1
B(%B1'B((B11B6c                 h   	 | j                                         }|d S |\  }}d }	 |                                }|rl| j                                        sSt
          j                            |          r4t
          j                            |          dk    rddl	m
}  ||           n2# t          $ r%}t                              d|           Y d }~nd }~ww xY w|r&	 t          j        |           n># t          $ r Y n2w xY wn-# |r&	 t          j        |           w # t          $ r Y w w xY ww xY w2)NTr   play_audio_filez Sync per-sentence TTS failed: %s)r$  rG   r!  r#  r,  r   r  isfiler  tools.voice_moder8  r3   r   r   r  r  )r(  r  	_sentencer.  r4  r8  rA  s          r   r&  z_SyncSentencePipeline._drain\  s   	;??$$D| $IvH!==?? .TZ%6%6%8%8 .GNN844.GOOH5599@@@@@@#OH--- H H HA3GGGGGGGGH  	(++++"   8 	(++++"   !	sf   BB* )D *
C4CD CD C4 4
D DD/	DD/
D+(D/*D++D/r   N)r   r   r   r  r  Eventr   r)  r$   r/  r   r   r-  r&  r   rM   r   r  r    s         . IJ   9? #    +S +T + + + ++ + + +# (3-    $     rM   r  
text_queuer  tts_done_eventdisplay_callbackc                 	   ! |                                  d!	 dd d}dg t                      }ddlm}m}  |||            t                    nd!d 	 t          |pt          |          |          n# t          $ r dY nw xY wt          j
                    dk    rdn	 t                      }	|	                     j         j        d                                           nj# t           t"          f$ r'}
t$                              d|
           dY d}
~
n7d}
~
wt          $ r'}
t$                              d	|
           dY d}
~
nd}
~
ww xY w |            }d
}d}g t+          j                    g t/          j        d          d fddt2          t4                   ddddffdfdd# fd}dt6          ddf fdd} *t/          j        |d          }|                                 dt6          f!fd}d d$fd	                                s	 |                     |          }nO# t*          j        $ r= tA          |j!                  |k    r"|"                                D ]} ||           Y ww xY w|#|"                                D ]} ||           n7|#                    |          D ]} ||                                           	 	 | $                                 n# t*          j        $ r Y nw xY w,n2# t          $ r%}
t$                              d |
           Y d}
~
nd}
~
ww xY w!&	 !%                                 n# t          $ r Y nw xY w -|+&                    d           |'                    d!           D ]}|'                    d"           :	 (                                 %                                 n# t          $ r Y nw xY w|)                                 dS # !&	 !%                                 n# t          $ r Y nw xY w -|+&                    d           |'                    d!           D ]}|'                    d"           :	 (                                 %                                 n# t          $ r Y nw xY w|)                                 w xY w)%up  Consume text deltas from *text_queue*, buffer them into sentences, and
    speak each sentence the moment it's ready — the conversational path.

    Provider-agnostic. A registered streaming provider (ElevenLabs, OpenAI, …)
    plays chunked PCM through one sounddevice stream for the lowest latency;
    every other provider (edge, the default) is spoken per-sentence via the sync
    ``text_to_speech_tool`` path, so audio still starts on sentence one instead
    of after the whole reply.

    Protocol:
        * The producer puts ``str`` deltas onto *text_queue*.
        * A ``None`` sentinel signals end-of-text (flush remaining buffer).
        * *stop_event* can be set to abort early (barge-in / user interrupt).
        * *tts_done_event* is **set** in the ``finally`` block so callers
          waiting on it (continuous voice mode) know playback is finished.
    Nr   )SentenceChunkerresolve_streaming_provider)	preferredDarwinint16
samplerater   dtypeu2   sounddevice not available, streamer→tempfile: %sz#sounddevice OutputStream failed: %sr4  r  r  @   c                      t                      } |                     j        j        d          }|                                 |S )z0Create and start a fresh PortAudio OutputStream.rF  rG  )rZ   OutputStreamr  r   r|  )rY   
new_streamstreamers     r   _create_output_streamz4stream_tts_to_speaker.<locals>._create_output_stream  sO    $&&B#/!* )  J
 rM   
audio_iterchunk_queuezqueue.Queue[Optional[bytes]]r   c                    	 | D ]I}                                 rt                              d            n|                    |d           Jn2# t          $ r%}t                              d|           Y d}~nd}~ww xY w|                    d                                            dS # |                    d                                            w xY w)z-Consume a generator into a thread-safe queue.uP   TTS CUT: prefetch cancelled (stop_event set mid-sentence) — partial audio onlyg      >@r  zLTTS CUT: streaming TTS prefetch failed mid-sentence (partial audio only): %sN)r,  r   r>  r  r3   r   release)rP  rQ  r   rA  _prefetch_semr  s       r   _consume_to_queuez0stream_tts_to_speaker.<locals>._consume_to_queue  s   
(' 9 9E!((** C   OOE4O8888   /        %%%%%''''' %%%%%''''s0   AA B- 
A?A:5B- :A??B- -+Cc                  8   :	                                                                    n# t          $ r Y nw xY w	              } | t                              d           | S # t          $ r(}t                              d|           dY d}~dS d}~ww xY w)z@Close the broken PortAudio stream and try to create a fresh one.Nz6TTS: PortAudio output stream reinitialized after errorz'TTS: PortAudio stream reinit failed: %s)stopr   r3   r   r>  r   )rM  rA  rO  output_streams     r   _reinit_output_streamz4stream_tts_to_speaker.<locals>._reinit_output_stream  s     (!&&(((!''))))    D2244
 *L   "!   =s   !%ttttts#   (. 
;;'A' '
B1BBc                     J }ddl } 	 ddlm} n# t          $ r d }Y nw xY w |d           	 d}d}}	                                 }|n                                r.|Pg }	 |                                }|n|                    |           - t          |          j                   d}	 |                                }|n                                rn{||z   }	t          |	          t          |	          dz  z
  }
|
dk    r,	 |
                    |                     |	d|
         d	
                              dd                     n# t          $ r}t                              d|           ||k     r|dz  }             }|}	 |
                    |                     |	d|
         d	
                              dd                     n# t          $ r Y nw xY w|
t          |	          k     r
|	|
d         nd}Y d}~^nt                              d|           d}Y d}~n)d}~ww xY w|
t          |	          k     r
|	|
d         nd}-	  |d           dS #  |d           w xY w	                                 }|dS                                 r.g }	 |                                }|n|                    |           - t          |          j                   })z=Single consumer: play audio segments from the queue in order.Nr   )mark_audio_output_activec                     d S rq  r   )_actives    r   r[  zQstream_tts_to_speaker.<locals>._playback_worker.<locals>.mark_audio_output_active  s    #trM   Tr  rM   ru   z<i2)rI  r  rt   z4PortAudio write failed, attempting stream reinit: %szcTTS: PortAudio reinit exhausted after %d attempts, falling back to tempfile for remaining sentencesF)numpyr:  r[  r3   rG   r,  r   r  r  r   r   
frombufferreshaper   r   )_npr[  _max_reinit_reinit_count_current_streamrQ  _chunksr   _pcm_leftover_buf_aligned_len	write_exc_audio_queue_play_via_tempfilerY  rX  r  rN  s               r   _playback_workerz/stream_tts_to_speaker.<locals>._playback_worker  sT   '''(####$IIIIIII  $ $ $$ $ $ $ $$ )(...L4"#K$%M&3OF&2&6&6&8&8&.!%,,.. %$*2&(G6(3(9(9#(=$) 'u 5 5 5	6
 /. $Wz8;O   %(+4$/OO$5$5E$} %)0022 & %#05#8D+.t99D		A+FL+q00(!*$3$9$9(+,0,,?u )7 )* )**1'"a..%& %& %& %&
 (1 "!* "!* "!*$*NN)<(1%& %& %&
 (5{'B'B(5(::O:O:Q:Q+:+F-50?0E0E47NN8<]l]8K>C 5C 56 56 7>gb!nn	12 12 12 12 4= -5 -5 -504-5 4@#d))3K3K 15\]]0C0C58 -:
 -5HHHH ,G" )/-8 -8)* )* )* ;?$)EEEEE"!*H 8Dc$ii7O7O\]] 3 3UX *e4%F "J -,U33333,,U3333"."2"2"4"4K"*!((** !  G. + 1 1 =!u---	.
 '&Wz83G  st    %%C%I6 AE!  I6 !
I+2H=AG%$H=%
G2/H=1G22"H=I6 H=8I6 =I&I6 6Jtext_to_speakc                    
J 	 
                     |           }n3# t          $ r&}t                              d|           Y d}~dS d}~ww xY w                                 t          j                  }                    |           t          j	        ||fd          }	
                    |           |                                 dS )z=Synthesize *text_to_speak* and start prefetching immediately.Nz"Streaming TTS synthesis failed: %sr   Tr  )r  r3   r   r   acquirer  r  r  r  r  r   r|  )rm  rP  rA  rQ  t_CHUNK_QUEUE_MAXrj  rU  rT  _prefetch_threadsrN  s        r   _enqueue_audioz-stream_tts_to_speaker.<locals>._enqueue_audiov  s    '''%__];;

   CSIII !!###:?+N^:_:_:_K[))) ( +.  A
 $$Q'''GGIIIIIs    
AAAT)r  r  sentencec                                                     rdS t          |                                           }|sdS |                                                    d          }D ]0}|                                                    d          |k    r dS 1                    |            |            		                    |           dS rt          |          k    r
|d         } |           dS )z9Display sentence and route to the appropriate audio path.Nz.!,)r,  r  r%   r   rH   r   r/  r   )
rt  r*  cleaned_lowerprevrs  _spoken_sentencesr@  r  stream_max_lensync_pipelines
       r   _speak_sentencez.stream_tts_to_speaker.<locals>._speak_sentence  s#     "" -h77==??G #MMOO22599M)  ::<<&&u-->>FF ?$$W---+  *** (##G,,, 3#g,,"?"?!/>/2 N7#####rM   c              3     K   d}| D ]p}|                                 r nY||z   }t          |          t          |          dz  z
  }|dk    r|d|         V  |t          |          k     r
||d         nd}q|rdV  dS dS )z1Yield int16-aligned byte chunks from an iterable.rM   ru   N    )r,  r   )r   stop_evtleftoverr   bufaligned_lens         r   _align_int16_chunksz2stream_tts_to_speaker.<locals>._align_int16_chunks  s      H P P??$$ E&!#hh#c((Q,7!##l{l++++0;c#hh0F0F3{||,,C  rM   rn   c                 v   d}d}	 ddl }t          j        dd          }|j        }|                    |d          5 }|                    d           |                    d           |                    |            
| |          D ]}|                    |           	 ddd           n# 1 swxY w Y   |	                                 dd	l
m}  ||           n2# t          $ r%}	t                              d
|	           Y d}	~	nd}	~	ww xY w|&	 |	                                 n# t          $ r Y nw xY w|r(	 t          j        |           dS # t"          $ r Y dS w xY wdS # |&	 |	                                 n# t          $ r Y nw xY w|r&	 t          j        |           w # t"          $ r Y w w xY ww xY w)z0Write PCM chunks to a temp WAV file and play it.Nr   rM  FrH  r   rt   ru   r7  z!Temp-file TTS fallback failed: %s)r  r  rS  r   r   setnchannelssetsampwidthsetframeratewriteframesr   r:  r8  r3   r   r   r   r  r  )rP  r~  r  r_  r4  r  wfalignedr8  rA  r  s             r   rk  z1stream_tts_to_speaker.<locals>._play_via_tempfile  s   CH1NNN8YYsD)) 0ROOA&&&OOA&&&OOK000#6#6z8#L#L 0 0w////0	0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 		<<<<<<)))) I I IBCHHHHHHHHI ?		$    	(+++++"     ?		$    	(++++"   s   7C  A$B/#C  /B33C  6B37(C  E&  
D*D
E& 
DE& D* *
D76D7=E 
E! E!&F8*E?>F8?
F	F8FF8F'&F8'
F41F83F44F8r  z Streaming TTS pipeline error: %sg     r@g      $@r<  )rn   )*clearr   tools.tts_streamingrB  rC  r  r   r   r3   r  r  rZ   rL  r  r   r|  r   r  r   r   r   r  r  r  	Semaphorer   r   r$   r  r,  rG   r  r   r  flushfeedr  r   r  r   rW  set)"r>  r  r?  r@  r   _worker_threadr   rB  rC  rY   rA  chunkerlong_flush_lenqueue_timeoutrl  r{  deltart  rp  rq  r  rj  rU  rO  rs  rk  rT  rr  rY  rx  rX  ry  rN  rz  s"    ` `               @@@@@@@@@@@@@@@r   stream_tts_to_speakerr  t  s   . 59MG%''
 	TSSSSSSS--jHMMM
 >F=M-j999SW#!99j 9 9:" "  # # #!"#   H,, $),..B$&OO#+#7!)!2% %4 % %M
 "''))))#W- ) ) )LL!UWZ[[[$(MMMMMM  ) ) )NN#H#NNN$(MMMMMM) "/##') MRKMM46!+A..		 		 		 		 			( 	(7	( 	( 	( 	( 	( 	( 	( 	(0	 	 	 	 	 	.i	 i	 i	 i	 i	 i	 i	 i	 i	 i	 i	V	# 	$ 	 	 	 	 	 	 	 	 	 	 	& 6:&-5EdSSSN  """	$c 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$ 	$<	 	 	#	 #	 #	 #	 #	 #	J ##%% 	*"}==;   w{##n44$+MMOO 2 2'1111 } ' . .H#OH----#LL// * *))))% ##%% 	**	%%'''';   	   @ @ @93????????@ $##%%%%    N$>T"""..." 	! 	!AFF4F    $""$$$##%%%%   ' $##%%%%    N$>T"""..." 	! 	!AFF4F    $""$$$##%%%%   sI  AL: .B L: BL: BL: ;AD  ?L:  E'D3.L: 3E' E"L: "E''C"L: 
I!  L: !A	J-*L: ,J--A L: L# "L: #L52L: 4L55L: 9P0 :
M)M$P0 $M))P0 /N 
NN!(P
 

PP0S44Q	S4	
QS4QAS4&(SS4
SS4SS4__main__u   🔊 Text-to-Speech Tool Modulez2==================================================c                 >    	  |              dS # t           $ r Y dS w xY w)NTF)r   )importerr   s     r   _checkr    s9    	HJJJ4 	 	 	55	s   
 
z
Provider availability:z  Edge TTS:   	installedz$not installed (pip install edge-tts)z  ElevenLabs: elz&not installed (pip install elevenlabs)z    API Key:  rB  r   r  znot setz  OpenAI:     oaiznot installedz2not set (VOICE_TOOLS_OPENAI_KEY or OPENAI_API_KEY)zAPI key set (r  r  zunavailable (z  MiniMax:    z  Piper:      z%not installed (pip install piper-tts)z  ffmpeg:     u	   ✅ foundu(   ❌ not found (needed for Telegram Opus)z
  Output dir: z  Configured provider: )registryr  rG  a  Convert text to speech audio. Returns a MEDIA: path that the platform delivers as native audio. Compatible providers render as a voice bubble on Telegram; otherwise audio is sent as a regular attachment. In CLI mode, saves to ~/voice-memos/. Voice and provider are user-configured (built-in providers like edge/openai or custom command providers under tts.providers.<name>), not model-selected.objectstringzThe text to convert to speech. Provider-specific character caps apply and are enforced automatically (OpenAI 4096, xAI 15000, MiniMax 10000, ElevenLabs 5k-40k depending on model); over-long input is truncated.)r   descriptionz9Optional custom file path to save the audio. Defaults to z/audio_cache/<timestamp>.mp3numberzPlayback speed multiplier. 1.0 = normal, 0.5 = very slow (language learning), 2.0 = fast. Range: 0.25-4.0. Overrides the speed configured in config.yaml.a  Optional voice-design guidance: tone, emotion, pacing, accent, whispering, impressions (e.g. 'Speak in a cheerful, excited whisper'). Forwarded to the OpenAI backend (gpt-4o-mini-tts and OpenAI-compatible voice-design servers). Silently ignored by backends that don't support it.a+  Optional TTS provider override. Accepts built-in names (edge, openai, elevenlabs, minimax, xai, mistral, gemini, neutts, kittentts, piper), user-declared command provider names from tts.providers.<name>, or plugin-registered names. When omitted, the configured tts.provider from config.yaml is used.r/  r   r8  rP  r   )r   
propertiesrequired)r   r  
parametersr   c           	          t          |                     dd          |                     d          |                     d          |                     d          |                     d                    S )Nr/  r!   r   r8  rP  r   r  )r  rG   )r  kws     r   r  r  t  sc    2XXfb!!HH]++hhwXXn--*%% '  '  ' rM   u   🔊)r   toolsetschemahandlercheck_fnemojirq  )F)r!   )NNNN)NN)r  r  r  r  r   loggingr   r  r  rI   rk  r  ri  r  r  r  r_  r  r   r   dataclassesr   r   pathlibr   typingr   r	   r
   r   r   urllib.parser   r   hermes_cli._subprocess_compatr   r{   r   	getLoggerr   r   r   r$   r&   tools.managed_tool_gatewayr'   r#   r(   r)   r*   r+   r  r,   r6   r<   rL   rR   rV   rZ   r_   rd   r   r7  rF  r   %DEFAULT_ELEVENLABS_STREAMING_MODEL_IDr[  r  r^  r  r  r  r\  r]  r  r  r  r	  r  r  r  r  r  r  r  r  r  r  r  r  r  rK  rL  rM  r   r*  rr  GEMINI_TTS_SAMPLE_RATEGEMINI_TTS_CHANNELSGEMINI_TTS_SAMPLE_WIDTHr   r   r|   r  r   r   r  r   r   r   r   r   r   r   r   r   r   MAX_TEXT_LENGTHr   r   r   r   r  r   rN  r=  rU  r   r  r&  r   r   r.  rD  rG  rJ  r   rP  rV  rX  rc  rl  r  r  r  r  r  r  r  r  r  r  r   r  r  r*  r2  r@  rK  rO  rl  ru  r  r  r  r   r1  r  r  r  r  r  r  r  r  r  r  r  r  r!  r#  r  r.  r4  rb  rj  rm  rr  ru  r  r  r  r  r  r  r  r  r  r  r  r  r  rZ  r  r  r  r  r  r  r  	MULTILINEr  r  r  r  r  r  r  r  r  r=  r  printr  r   minimax_runtimer   r   minimax_statusr
  rA  r   tools.registryr  r  
TTS_SCHEMAregisterr   rM   r   <module>r     s  ! ! !F      				   				              9 9 9 9 9 9 9 9 ( ( ( ( ( ( ( (       : : : : : : : : : : : : : : * * * * * * * * < < < < < < 0 0 0 0 0 0		8	$	$/ / / /S3 SS SS S S S S D C C C C C            1 0 0 0 0 0
 
 
  ,Nd38n Nc3h N N N N"  
  $    	 	 	  ' 4 6 (; %( 
 &I'8&9:: = " +  5 & 8 = B 3 E     $ ,     23 . */ &9 ! P !  0 '     0  ) = = = = = -,..  , , $sCx.    ##""	4 	4  $sCx. 	 	 	  d t    "1C 1D 1 1 1 1c d      	 "  "  " "  " C=	 "
  "  "  "  "N  	    C=	
 
#s(^   4  	 	 			 		
 C=	 
	 	 	 	    +
 ,05$ 5$sm5$c3h(5$ 	5$ 5$ 5$ 5$v$sCx.    &Ld38n L L L L L $
% 
% 
% 
% 
% 
% 
% 
%AS#XAA A A AF "	 # # #    '* #$) !&Y>>>   '+ # !y " " "   8d38n 8C 8DcN 8 8 8 8S#X
 
#s(^   0>S#X >4 > > > >S#X d38n   *XL
XLXL XL S#X	XL
 c]XL XL XL XLv3 4    4 S#X        	T#s(^ 	 	 	 	 	 "&[ [cN[#[ 	[ [ [ [$T#s(^     3 # (3-    ># hsm PS    "sCx. 	   <1j.> 14 1 1 1 1hDd38n D D D D D" '+~Q ~Q~Q~Q d^~Q  	~Q ~Q ~Q ~QB'd 'DcN 't ' ' ' 'E
EE E cN	E
 S#XE 	E E E EP htCH~.F RV    .T . . . .
9s 9x} 9 9 9 9$)# ) )# ) ) ) )l. . . . . ."!C !C ! ! ! !N3 S d3PS8n Y\    <(s ( ($sCx. (UX ( ( ( (V     ( ""!"&o o o
oo S#Xo
 c]o smo C=o C=o E?o 3-o 	o o o oz,# ,C ,T#s(^ ,X[ , , , ,d     RZ0111MACHHMfDgDggjpp
-    $$ERYWWW 0 0C 0$ 04 0 0 0 0Ac Ac A A A AHvC vc vtCH~ vRU v v v vxDA DA# DA4S> DAVY DA DA DA DAT1 1# 14S> 1VY 1 1 1 1r .'/	C CCC C 	C
 C C C CDtCH~ (SW.    "tCH~ #     >S >T > > > >d38n S T    "S S         - - -c -3 - - - -f %)V V
VS>V SMV 		V V V V>is i i$sCx. iUX i i i i`    D    D3 D D D D
D# D D D D
23 2S 2d38n 2QT 2 2 2 2|  $sCx. s (2s7BS X[    * &( DcN ' ' '    	t 	 	 	 	2S 2 2 2 2 2 2jac a ac3h aTW a a a aR *, S#X + + +4c 4 4c3h 4TW 4 4 4 4x "&!"&"w4 w4
w4#w4 E?w4 3-	w4
 smw4 	w4 w4 w4 w4zL L L L L^,U3T>-B , , , ,^`4 ` ` ` ` -..2:.//
"*_
%
%2:&''RZ%%
"*Z((RZ	666

>>>>	G		
9%% 
L
 
 rz329EEE# #    <Y Y Y Y Y Y Y Y@ 9="a aaa Oa xt45	a
 sma a a aN z	E
+,,,	E(OOO   
E
$%%%	E
x&&1A6*J*Jv;;Pv
x
xyyy	E
z&&1CT*J*Jx;;Px
z
z{{{	E
n$9$9:NP\$]$]l55cl
n
nooo	E
e&&1F*N*Nc;;Tc
e
efff	E	o0022l558l	o 	o   F066v>>4O2 4 404 4 4 	  0 0 0////0	E
+>
+
+,,,	E
q*@*@*B*Bo;;Ho
q
qrrr	E
i++--g;;=g
i
ijjj	E
1/
1
1222}V$$H	E
.H
.
./// 0 / / / / / / /  ` !  s 
 !  O[n[n[p[p   O   O   O 
 !  { 
 !a  !Z	 	- 
  
B HG$ $( (
T  	' ' $
     s   *d	 	ddd