
    Jj.                       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mZ ddlmZmZmZmZ ddlmZ ddlmZ ddlmZ  ej        e          ZddZdd	l m!Z! dd
l"m#Z#m$Z$m%Z%m&Z& ddl'm(Z( d Z)d Z*d Z+d Z,d Z-d Z.d Z/dZ0dZ1dZ2dZ3dZ4dZ5 e6dh          Z7dZ8dZ9dZ:dZ;dZ<dZ=dZ>d Z?d!Z@d"ZAd#ZBd$ZCd%ZDd&ZEd'ZFd(ZGd)ZHd*ZId+ZJdZKd,ZLd-ZMd.ZNd'ZOd/ZPd%ZQd0ZRd1ZSd2eTfd3ZU eU            ZVd4d5d6d7d8d9d7d:d:d4d;
ZWeeTeXf         eYd<<   d4d4d7d7d7d7d=d>d?ZZeeTeXf         eYd@<   ddAedBe[d2e[fdCZ\d8Z]e]Z^	 ddDeeT         dEeeeTef                  d2eXfdFZ_d2eeTef         fdGZ`dEeeTef         d2eTfdHZa e6h dI          ZbdJZcdKZd e6h dL          Zed4ZfdEeeTef         dMeTd2eeTef         fdNZgdEeeTef         dMeTd2eeTef         fdOZhdPeeTef         d2e[fdQZidDeTdEeeTef         d2eeeTef                  fdRZjdSeTdTeTdDeTdEeeTef         d2eeT         f
dUZkdDeTd2e[fdVZldEeeTef         fdWZmdPeeTef         d2enfdXZo	 ddPeeTef         dTeeT         d2eTfdYZpdPeeTef         d2e[fdZZqd[eTd\eXd2eeT         fd]ZrdAeTd^eeT         d2eTfd_Zsd[eTd`eeTeTf         d2eTfdaZtdbeju        d2dfdcZvddeTdeend2ejw        fdfZxdgedPeeTef         d2efdhZydSeTdTeTdieTdPeeTef         dEeeTef         d2eTfdjZzddEeeeTef                  d2e[fdkZ{d2e[fdlZ|dmeTd2eeT         fdnZ}dSeTdTeTdEeeTef         d2eTfdoZ~dSeTdTeTdEeeTef         d2eTfdpZdSeTdTeTdEeeTef         d2eTfdqZdrZdsZ ej        dtdu                    e          z   dvz   du                    e          z   dwz   ej        x          Z ej        dyej        x          ZddAedBe[d2e[fdzZdSeTd2eTfd{ZdSeTdTeTdEeeTef         d2eTfd|ZdSeTdTeTdEeeTef         d2eTfd}ZdSeTdTeTdEeeTef         d2eTfd~ZeQeReSfdedeXdeXdeXd2ef
dZdeeTef         d2ee         fdZdeeTef         d2eTfdZdeTd2e[fdZdeeTef         deTd2e[fdZdeTd2eTfdZded2eTfdZddSeTdeTd2eTfdZ	 ddSeTdeeTef         deeT         d2eTfdZdSeTdTeTdEeeTef         d2eTfdZd2e[fdZd2e[fdZd2eTfdZd2eTfdZdSeTdTeTdEeeTef         d2eTfdZi aeeTef         eYd<   d2e[fdZd2efdZdeTded2eTfdZdSeTdTeTdEeeTef         d2eTfdZi aeeTef         eYd<   dSeTdTeTdEeeTef         d2eTfdZ	 ddSeTdTeeT         d2eTfdZd2e[fdZd2eeTeTe[f         fdZd2e[f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        d          Z ej        dej        x          Z ej        dej        x          Z ej        d          Z ej        d          ZdSeTd2eTfdZ	 ddej        dej        dej        deeeTgdf                  fdZedk    r- ed            ed           d Z ed            ed ee)d          rdnd             ed ee*d          rdnd             ed ed¦          rdndě             ed ee+dƦ          rdndǛ             ed e&            rdndț             ed edʦ          rdnd̛             ed e            rdndΛ             ed e|            rdndћ             edeV             e`            Z eae          Z ede            ddlmZmZ ddddddڜdd e             dܝdڜdݜdSgdޜdߜZ ej        dded ed           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 MINIMAX_API_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)Path)CallableDictAnyOptional)urljoin)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/rurouni/.hermes/hermes-agent/tools/tts_tool.pyr   r   ;   sl    (EEEEEEE ( ( (yw'''''(N4  Em77.s   	 ++)resolve_managed_tool_gateway)managed_nous_tools_enabled%nous_tool_gateway_unavailable_messageprefers_gatewayresolve_openai_audio_api_key)hermes_xai_user_agentc                      	 ddl m}   | dd           n9# t          $ r Y n-t          $ r!}t          t	          |                    d}~w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stredge_tts)_lazy_ensureer$   s      r   _import_edge_ttsr'   V   s    "::::::Z.....    " " "#a&&!!!"OOOO    
A	AAAc                      	 ddl m} m}  |dd           n9# t          $ r Y n-t          $ r!}t          t          |                    d}~w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.elevenlabsFr   N)
ElevenLabs)r!   r*   r   r   r"   r#   elevenlabs.clientr+   )r*   r   r&   r+   s       r   _import_elevenlabsr-   b   s    ">>>>>>>>.....    	 " " "#a&&!!!",,,,,,s    
A	AA		Ac                      ddl m}  | S )zCLazy import OpenAI client. Returns the class or raises ImportError.r   )OpenAI)openair/   )OpenAIClients    r   _import_openai_clientr2   w   s    ------    c                      	 ddl m}   | dd           n9# t          $ r Y n-t          $ r!}t          t	          |                    d}~w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.mistralFr   N)Mistral)r!   r   r   r"   r#   mistralai.clientr5   )r   r&   r5   s      r   _import_mistral_clientr7   |   s    "******}U+++++    " " "#a&&!!!"((((((Nr(   c                      ddl } | S )zJLazy import sounddevice. Returns the module or raises ImportError/OSError.r   N)sounddevice)sds    r   _import_sounddevicer;      s    Ir3   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    ######r3   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)piperrC   rB   s    r   _import_piperrE      s     !     r3   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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_tags      returnc                  @    ddl m}  t           | dd                    S )Nr   get_hermes_dirzcache/audioaudio_cache)hermes_constantsrZ   r#   rY   s    r   _get_default_output_dirr]      s.    //////~~m];;<<<r3   i  i   i:  i'  i   }  i  )
rF   r0   xaiminimaxmistralgemini
elevenlabsneuttsr?   rD   PROVIDER_MAX_TEXT_LENGTHi0u  i@  )	eleven_v3eleven_ttv_v3rH   eleven_multilingual_v1eleven_english_sts_v2eleven_english_sts_v1eleven_flash_v2rI    ELEVENLABS_MODEL_MAX_TEXT_LENGTHr   r   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#   striplower)r   r   
normalizeds      r   _config_boolr      s    % }%#u&& E{{% [[]]((**
>>>4@@@5Nr3   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   rc   model_id)FALLBACK_MAX_TEXT_LENGTHr}   r|   rx   getdictry   rz   DEFAULT_ELEVENLABS_MODEL_IDrl   r#   re   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##r3   c                  
   	 ddl m}   |             }|                    d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   r   r   loggerdebugr"   warning)r   configr&   s      r   _load_tts_configr   N  s    	111111zz%$$$   PQQQ			   6DIII						s!   %( &B	BA=7B=Bc                     |                      d          pt                                                                          S )z%Get the configured TTS provider name.r   )r   DEFAULT_PROVIDERr}   r|   )r   s    r   _get_providerr   a  s2    NN:&&:*:AACCIIKKKr3   >
   r_   rF   rD   rb   rd   r0   r`   ra   r?   rc   x   mp3>   r   oggwavflacr   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.)rx   r   r   )r   r   sections      r   _get_provider_sectionr     sC    j$'' 	nnT""G $//777R7r3   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   rx   r   r   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Ir3   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.Ftype command)rx   r   r#   r   r|   r}   ry   )r   ptyper   s      r   r   r     s    fd## u

6""(b))//117799E )##ujj##Ggs##=W]]__(=(==r3   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** 4r3   textoutput_pathc                    |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   )r   r   r   format)r}   r|   r   r   r   agent.tts_registryr   hermes_cli.pluginsr   r"   r   r   rx   r   r   !DEFAULT_COMMAND_TTS_OUTPUT_FORMATinfo
synthesizer#   rz   r{   )r   r   r   r   r   r   r   plugin_providerexcr   r   r   fmtwrittens                 r   _dispatch_to_plugin_providerr     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   r   Nz5tts plugin voice_compatible check failed for '%s': %s)
r}   r|   r   r   r   ry   voice_compatibler"   r   r   )r   r   r   r   r   s        r   $_plugin_provider_is_voice_compatibler   3  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   )rx   r   r   itemsr#   r}   r   r   )r   r   r   r   s       r   _iter_command_providersr   M  s      j$'' %j+>>Io2,,..    	cdC   	 TZZ\\9N%N%N*3//  Ci   r3   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   )r   #DEFAULT_COMMAND_TTS_TIMEOUT_SECONDSr{   	TypeError
ValueError)r   rawr   s      r   _get_command_tts_timeoutr   X  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   r   )	r   suffixr}   r|   lstripCOMMAND_TTS_OUTPUT_FORMATSr   r   r#   )r   r   r   r   r   s        r   _get_command_tts_output_formatr   d  s    
  k"")//117799@@EE///M

8 	-::o&&	-, 
 c((..


 
 
"
"
)
)#
.
.C333339ZZr3   c                     |                      dd          }t          |t                    r(|                                                                dv S t          |          S )zEReturn True only when the user explicitly opted in to voice delivery.r   F>   rn   ro   rp   rq   )r   rx   r#   r|   r}   ry   )r   r   s     r    _is_command_tts_voice_compatibler   v  sU    JJ)511E% C{{}}""$$(BBB;;r3   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   '"\TrU    )r   r   quoteescapedichars         r   _shell_quote_contextr   ~  s      EG	A
h,,"C<<s{{c\\ S[[EES[[EET\\FA	Q% h,,& Lr3   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)replacer   r   
subprocesslist2cmdlineshlexr   )r   r   s     r   _quote_command_tts_placeholderr     s    }}S'***WT6""WS%  WS%  WS%  	
 
w$&w///;ur3   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)reescape).0r   s     r   	<genexpr>z/_render_command_tts_template.<locals>.<genexpr>  s*      >>RYt__>>>>>>r3   z(?<!\$)(?:\{\{(?P<double>z)\}\}|\{(?P<single>z)\})matchrW   c                    |                      d          p|                      d          }dt                     d}                    |t          |         t	          |                                                     f           |S )Ndoublesingle__HERMES_TTS_PLACEHOLDER___)grouplenappendr   r   start)r   r   tokenr   r   replacementss      r   replace_matchz3_render_command_tts_template.<locals>.replace_match  s    {{8$$=H(=(=AC,=,=AAA*T"$%5u{{}}EE 
 	 	 	 r3   z{{{z}}})joinr   compileMatchr#   subr   )	r   r   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Or3   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.Nr   taskkillz/Fz/Tz/PID   )stdoutstderrr   stdinr   T)	recursiverV   r   )pollr   r   r   runr#   pidDEVNULLr"   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
r   r   c                    dt           j        t           j        dd}t          j        dk    rt	          t           dd          |d<   nd|d<   t          j        | fi |dt           j        i}	 |                    |	          \  }}n# t           j        $ rz}t          |           	 |                    d
	          \  }}n2# t          $ r% t	          |dd          }t	          |dd          }Y nw xY wt          j        | |||          |d}~ww xY w|j        rt          j        |j        | ||          t          j        | |j        ||          S )zGRun a command-provider shell command with process-tree timeout cleanup.T)shellr  r  r   r   CREATE_NEW_PROCESS_GROUPr   creationflagsstart_new_sessionr  r  rU   outputNr  )r3  r  )r   PIPEr   r   getattrPopenr"  communicater*  r-  r"   
returncodeCalledProcessErrorCompletedProcess)r   r   popen_kwargsr  r  r  r   s          r   _run_command_ttsr<     s    //	$ $L 
w$(/
<VXY(Z(Z_%%,0()GNN|NN:;MNNND))')::$   +D111	2!--a-88NFFF 	2 	2 	2S(D11FS(D11FFF	2 '	
 
 

 	  
+O	
 
 
 	
 &wPPPs<   ,B DD
%B?>D
?,C.+D
-C..D

Dpathc                 P    t          |          }|                     d|           S )zKReturn an output path whose extension matches the provider's output_format.r   )r   with_suffix)r=  r   r   s      r   #_configured_command_tts_output_pathr@  (  s)    
(
0
0CIII&&&r3   provider_namec                 n   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!          ||           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_okr   z	input.txtutf-8encodingr   r   )
input_path	text_pathr   r   r   r   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#   r   r|   r   r   
expanduserr+  mkdirexistsunlinkr   r   tempfileTemporaryDirectory
write_textr  r<  r   r*  RuntimeErrorr9  r  r  r  r  r8  statst_size)r   r   rA  r   r   r   r3  r   r   r   tmpdirrJ  r   r   r   detail_partsdetails                    r   _generate_command_ttsrZ  .  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	Wg....( 	 	 	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. . .>. .%+. .  
	 /!              B ==?? 
fkkmm3q88L]LLFLL
 
 	
 v;;sD   B:KG&%K&K5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   r   )r   _name_cfgs      r   _has_any_command_tts_providerr^  r  s:    %''
.z::  ttt5r3   c                  .    t          j        d          duS )z+Check if ffmpeg is available on the system.ffmpegN)shutilwhichr   r3   r   _has_ffmpegrc  ~  s    <!!--r3   mp3_pathc                    t                      sdS |                     dd          d         dz   }	 t          j        dd| dd	d
ddddd|dgddt          j        t                                }|j        dk    rEt                              d|j        |j	        
                    dd          dd                    dS t          j                            |          r%t          j                            |          dk    r|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dS )z
    Convert an MP3 file to OGG Opus format for Telegram voice bubbles.

    Args:
        mp3_path: Path to the input MP3 file.

    Returns:
        Path to the .ogg file, or None if conversion fails.
    Nr   rU   r   .oggr`  -i-acodeclibopus-acrn   -b:a64k-vbrru   -yT   capture_outputr   r  r1  z0ffmpeg conversion failed with return code %d: %srF  ignoreerrors   z)ffmpeg OGG conversion timed out after 30szffmpeg not found in PATHz ffmpeg OGG conversion failed: %sr   )rc  rsplitr   r   r"  r	   r8  r   r   r  decoder   r=  rO  getsizer*  FileNotFoundErrorr"   )rd  ogg_pathresultr&   s       r   _convert_to_opusr|    s    == tsA&&q)F2HMtXy)CxG$,..
 
 
 !!NNM +V]-A-A'RZ-A-[-[\`]`\`-ac c c47>>(## 	(A(AA(E(EO$ 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4s+   B
D =AD )F-#F	FE==Fc           	        K   t                      }|                    d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.
    rF   r   r   rR   d   z+d%rateN)r'   r   DEFAULT_EDGE_VOICEr{   roundCommunicatesave)
r   r   r   	_edge_ttsedge_configr   r   kwargspctr7  s
             r   _generate_edge_ttsr    s       !""I..,,KOOG%788E+//':>>'3+G+GHHIIEuF||US[C'((v')'7777K


;
'
''''''''r3   c                    t          d          pd}|st          d          |                    di           }|                    dt                    }|                    dt                    }|                    d          rd}nd	}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   z=ELEVENLABS_API_KEY not set. Get one at https://elevenlabs.io/rc   voice_idr   rf  opus_48000_64mp3_44100_128api_keyr   r  r   r   wbN)r   r   r   DEFAULT_ELEVENLABS_VOICE_IDr   endswithr-   text_to_speechconvertopenwrite)r   r   r   r  	el_configr  r   r   r+   clientaudio_generatorfchunks                r   _generate_elevenlabsr    se    1228bG ZXYYY|R00I}}Z)DEEH}}Z)DEEH F## (''#%%JZ(((F+33#	 4  O 
k4	 	  A$ 	 	EGGENNNN	               s   C99C= C=c           	         t                      \  }}}|                    di           }|                    dt                    }|                    dt                    }|                    d          }	|	r|	}t	          |                    d|                    dd                              }
|r4|	s2|t
          vr)t                              d|t          |           t          }|                    d          rd	}nd
}t                      } |||          }	 ||| |dt          t          j                              id}|
dk    r!t          dt          d|
                    |d<    |j        j        j        di |}|                    |           |t'          |dd          }t)          |          r |             S S # t'          |dd          }t)          |          r |             w w xY w)z
    Generate audio using OpenAI TTS.

    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.
    r0   r   r   base_urlr   rR   zTTS: 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.rf  opusr   )r  r  zx-idempotency-key)r   r   inputresponse_formatextra_headersg      ?g      @closeNr   )#_resolve_openai_audio_client_configr   DEFAULT_OPENAI_MODELDEFAULT_OPENAI_VOICEr{   MANAGED_OPENAI_TTS_MODELSr   r   r  r2   r#   uuiduuid4maxminaudiospeechcreatestream_to_filer5  callable)r   r   r   r  r  
is_managed
oai_configr   r   custom_base_urlr   r  r1   r  create_kwargsresponser  s                    r   _generate_openai_ttsr    s    %H$I$I!GXz"--JNN7$899ENN7$899E nnZ00O #"*..*..#*F*FGGHHE  %/ %e;T.T.T" '		
 	
 	
 % F##   (**L\'H===F.13tz||3D3DE
 
 C<<%(s3%?%?M'"-6<&->>>>,,,..E?? 	EGGGG	 ..E?? 	EGGGG	s   A<G -G0)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(\[(?:r   z
)\]|</?(?:z)>)flagsu   ^(.{12,120}?[.!?…])\s+(?=\S)c                 $    t          | |          S )Nr   )r   )r   r   s     r   _xai_bool_configr  _  s    w////r3   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] rU   )countz\s{2,}z, 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_llmrT   systemrolecontentuserzTRANSCRIPT TO TAG:
rQ   taskmessagestemperature$```(?:[A-Za-z0-9_-]+)?\s*(.*?)\s*```r  z?xAI TTS audio tag rewrite failed; using locally-tagged text: %sN)r|   r   r  _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  r"   r   r   )r   cleanlocalinlinewrappingsystem_promptr  r  taggedfencer   s              r   _apply_xai_auto_speech_tagsr  c  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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|                    }|rt)          |           } t          |                    d          p*|                    d          pt+          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<   ||t2          k    r||d<   ||t4          k    r||d<   |                    | dd| dt9                      d|d          }|                                 t=          |d           5 }|                    |j                    ddd           n# 1 swxY w Y   |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_   r  languagesample_ratebit_rateauto_speech_tagsspeech_tagsr   optimize_streaming_latencyrV   r  XAI_BASE_URL/.wavr   r   )r   r  r  codecr   z/ttsBearer application/json)AuthorizationContent-Typez
User-Agent<   )headersjsonr   r  )!requeststools.xai_httpr  r#   r   r|   r   DEFAULT_XAI_VOICE_IDDEFAULT_XAI_LANGUAGErz   DEFAULT_XAI_SAMPLE_RATEDEFAULT_XAI_BIT_RATEr  DEFAULT_XAI_AUTO_SPEECH_TAGSr{   r   r  DEFAULT_XAI_SPEED_MINr  DEFAULT_XAI_SPEED_MAXr  r   DEFAULT_XAI_BASE_URLrstripr  DEFAULT_XAI_SPEED_DEFAULT.DEFAULT_XAI_OPTIMIZE_STREAMING_LATENCY_DEFAULTpostr   raise_for_statusr  r  r  )r   r   r   r  r  credsr  
xai_configr  r  r  r  r  r   r  r  r  payloadr   r  r  s                        r   _generate_xai_ttsr    s    OOO;;;;;;((**E%))I&&,"--3355G pnooor**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z"" 	 99Z  	 ((	   	 
 eggffSkk  !))&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 	k4	 	  "A	 !!!" " " " " " " " " " " " " " " s6   2G GG7I IIO??PPc           	         ddl }t          d          pd}|st          d          |                    di           }|                    dt                    }|                    dt
                    }|                    d	t                    }|                    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| d}d|v }|r|| ||	|
||d||ddd d!}n|| |d"}|	                    |||d#$          }|r|
                                 |                                }|                    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 rDt          |d/          5 }|                    |j                   ddd           n# 1 swxY w Y   |S 	 |                                }|                    d%i           }|                    d&d'          }|dk    r+|                    d(d)          }t          d*| d+|           nJ# t&          $ r= |
                                 t          d2| d3t)          |j                   d4          w xY wt          d5          )6a  
    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   NMINIMAX_API_KEYr   z@MINIMAX_API_KEY not set. Get one at https://platform.minimax.io/r`   r   r  r  r   rR   volpitchemotionneutralr  r^   bitraterP   group_idMINIMAX_GROUP_IDzGroupId=?&r  r  )r  r  t2a_v2)r  r   r  r  r  r   rU   )r  r  r   channel)r   r   voice_settingaudio_setting)r   r   r  r   )r  r  r   	base_respstatus_code
status_msgunknown errorzMiniMax TTS API error (code ): datar  z%MiniMax TTS returned empty audio datar  r  zaudio/z.MiniMax TTS returned unexpected Content-Type 'z' (z bytes)z"MiniMax TTS returned no audio data)r  r   r   r   DEFAULT_MINIMAX_MODELDEFAULT_MINIMAX_VOICE_IDDEFAULT_MINIMAX_BASE_URLr#   r|   r  r  r  rT  bytesfromhexr  r  r  r  r"   r  )r   r   r   r  r  	mm_configr   r  r  r   r  r  r  r  r  r  sepr  	is_t2a_v2r  r  r{  r%  r&  r(  	hex_audioaudio_bytesr  content_types                                r   _generate_minimax_ttsr7    s     OOO.//52G ][\\\y"--IMM'#899E}}Z)ABBH}}Z)ABBH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 +,7,, G H$I 
 $"   +"	 
 
(  
 
 }}XGWb}QQH ,A!!###JJ{B//	mmM266!"|_EEJZkZZjZZ[[[JJvr**..w;;	 	HFGGGmmI..+t$$ 	!GGK   	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	!  '++NB??|##k4(( *A()))* * * * * * * * * * * * * * *	]]__F

;33I#--r::Ka&]]<II
"#^+#^#^R\#^#^___    	 	 	%%'''3 3 3())3 3 3  	 ?@@@s2   /KKKL44L8;L8A1N4 4AO;c                    t          d          pd}|st          d          |                    di           }|                    dt                    }|                    d          pt          }|                    d          rd}n2|                    d	          rd
}n|                    d          rd}nd}t                      }	  ||          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   z?MISTRAL_API_KEY not set. Get one at https://console.mistral.ai/ra   r   r  rf  r  r  r   z.flacr   r   r  )r   r  r  r  NzMistral TTS failed: %sTr   zMistral TTS failed: r  )r   r   r   DEFAULT_MISTRAL_TTS_MODELDEFAULT_MISTRAL_TTS_VOICE_IDr  r7   r  r  completebase64	b64decode
audio_datar"   r   errorrT  r   __name__r  r  )r   r   r   r  	mi_configr   r  r  r5   r  r  r5  r&   r  s                 r   _generate_mistral_ttsrC    s    .//52G \Z[[[y"--IMM'#<==E}}Z((H,HHF##   			f	%	%  			g	&	&   $&&GMWW%%% 	@|*33! /	 4  H !*8+>??K	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@ 	@     M M M-q4@@@D$q''2BDDEE1LM 
k4	 	  A	               sO   	D* =DD* D""D* %D"&D* *F>AF  FF::F>F>	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    rU      z<4sIs   data   z<4sI4ss   RIFFs   WAVE)structr  pack)rD  r  rE  rF  rK  	byte_rateblock_align	data_size	fmt_chunkdata_chunk_header	riff_sizeriff_headers               r   _wrap_pcm_as_wavrT    s     MMMh&5I\)KII
	q
 
I FGY??C	NN"S):%;%;;iGI++hGDDK"%66BBr3   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)r   rx   r#   r|   r   r=  
expandvarsr   rM  is_absoluter\   rX  r"   cwd)rU  r   expandedr=  rX  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   rF  rG  z4Gemini TTS persona prompt file unavailable at %s: %s)r]  	read_textr|   OSErrorUnicodeDecodeErrorr   r   )rU  r=  r   s      r   _read_gemini_persona_promptrb    s    .}==D|r~~w~//55777'(   B	
 	
 	

 rrrrrs   '= A5A00A5r   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  rU   r'  z
gemini-3.1r   )r|   r}   rv  )r   r~   s     r   !_gemini_model_supports_audio_tagsrd    sM    +2$$&&,,..55c1==bAJ:%=%:*==r3   c                    |                      d          }t          |t                    r|                     d          }t          |t                    }|sdS t          |          st                              d|           dS dS )N
audio_tagsrr   r  FzrGemini TTS audio_tags enabled, but model %s is not known to support Gemini audio tags; skipping hidden tag rewriteT)r   rx   r   r   DEFAULT_GEMINI_AUDIO_TAGSrd  r   r   )rU  r   r   rr   s       r   _gemini_audio_tags_enabledrh    s    


L
)
)C#t !ggi  3(ABBBG u,U33 =	
 	
 	

 u4r3   r  c                     | pd                                 }t          j        d|t          j                  }|r'|                    d                                           }|S )Nr   r  r  rU   )r|   r   r  r  r  )r  r  r  s      r   _clean_gemini_audio_tag_rewriterj  &  sX    ]!!##EL@%ryYYYE 'A$$&&Lr3   r  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   )choicesr5  rx   r   r#   r   r"   )r  choicerl  s      r   r  r  .  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  rQ   r  z<Gemini TTS audio tag rewrite failed; using untagged text: %sN)	r|   r  r  GEMINI_AUDIO_TAG_REWRITE_TASKrj  r  r"   r   r   )
r   ro  
transcriptr  contextuser_promptr  r  r  r   s
             r   _rewrite_gemini_tts_audio_tagsru  9  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|   rb  r   r  
IGNORECASEr  r  )r   rU  ro  rr  preambleplaceholder_patternsr    r  s           r   _compose_gemini_tts_promptr{  i  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Ur3   c                 x   ddl }t          d          pt          d          pd                                }|st          d          |                    d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}|                    |d|iddi|d          }|j        dk    r	 |                                                    di           }|                    d           p|j        dd!         }n# t0          $ r |j        dd!         }Y nw xY wt3          d"|j         d#|           	 |                                }|d$         d         d%         d         }t5          d& |D             d          }|t3          d'          |                    d(          p|                    d)          pi }|                    d*d          }n2# t6          t8          t:          f$ r}t3          d+|           |d}~ww xY w|st3          d,          t=          j        |          }tA          |          }|!                                "                    d-          r?tG          |d.          5 }|$                    |           ddd           n# 1 swxY w Y   |S tK          j&        d-d/0          5 }|$                    |           |j'        }ddd           n# 1 swxY w Y   	 tQ          j)        d1          }|r|!                                "                    d2          r|d3|d4d5d6d7d8d9d:d;d<d=d|g}n	|d3|d<d=d|g}tU          j+        |d>d?tT          j,        t[                      @          } | j.        dk    r6| j/        0                    dAdBC          dd!         }!t3          dD|!           n0t$                              dE|           tQ          j1        ||           	 te          j3        |           n:# th          $ r Y n.w xY w# 	 te          j3        |           w # th          $ r Y w w xY wxY w|S )Fa  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_KEYGOOGLE_API_KEYr   zIGEMINI_API_KEY not set. Get one at https://aistudio.google.com/app/apikeyrb   r   r   r  GEMINI_BASE_URLr  )ro  z@Gemini TTS composed prompt too long (%d chars), truncating to %dpartsr   AUDIOvoiceConfigprebuiltVoiceConfig	voiceName)responseModalitiesspeechConfig)contentsgenerationConfigz/models/z:generateContentr   r  r  r   )paramsr  r  r   ru  r@  rl  ,  zGemini TTS API error (HTTP r*  
candidatesr  c              3   *   K   | ]}d |v sd|v 
|V  dS )
inlineDatainline_dataNr   )r   ps     r   r   z'_generate_gemini_tts.<locals>.<genexpr>  s7      WW|q/@/@MUVDVDV1DVDVDVDVWWr3   z+Gemini TTS response contained no audio datar  r  r+  z#Gemini TTS response was malformed: z$Gemini TTS returned empty audio datar  r  Fr   deleter`  rf  rg  rh  ri  rj  rn   rk  rl  rm  ru   rn  	-loglevelTro  rp  rF  rr  rs  zffmpeg conversion failed: zEffmpeg not found; writing raw WAV to %s (extension may be misleading))5r  r   r|   r   r   rx   r   r#   DEFAULT_GEMINI_TTS_MODELDEFAULT_GEMINI_TTS_VOICEDEFAULT_GEMINI_TTS_BASE_URLr  rb  rh  ru  r{  r   r  r   r   r  r&  r  r   r"   rT  nextKeyError
IndexErrorr   r=  r>  rT  r}   r  r  r  rQ  NamedTemporaryFiler   ra  rb  r   r   r"  r	   r8  r  rw  copyfiler   remover`  )"r   r   r   r  r  raw_gemini_configrU  r   r   r  ro  
tts_scriptprompt_textmax_lenr  endpointr  errrY  r+  r  
audio_partr  	audio_b64r&   rD  	wav_bytesr  tmpwav_pathr`  cmdr{  r  s"                                     r   _generate_gemini_ttsr    s   " OOO-..W-@P2Q2QWUW^^``G 
W
 
 	
 #x44)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 ;;E;;;H}}w!34   H s""	)--//%%gr22CWWY''>8=#+>FF 	) 	) 	)]4C4(FFF	)K(*>KK6KK
 
 	
	M}}\"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%E65+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   7AJ J J >BM N	1NN	7PP P<Q%%Q),Q)1C9V +V   
VVV7V'&V7'
V41V73V44V7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   Nrd   Fimportlib.utilutil	find_specr"   	importlibs    r   _check_neutts_availabler     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?   Fr  r  s    r   _check_kittentts_availabler  )  sP    ~''44D@@   uur  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   r3   r   _default_neutts_ref_audior  2  $    tH~~$'77(BCCCr3   c                  Z    t          t          t                    j        dz  dz            S )z>Return path to the bundled default voice reference transcript.r  zjo.txtr  r   r3   r   _default_neutts_ref_textr  7  r  r3   c                 8   ddl }|                    d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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   Nrd   	ref_audior   ref_textr   zneuphonic/neutts-air-q4-ggufdevicecpur  r   rU   zneutts_synth.pyz--textz--outz--ref-audioz
--ref-textz--modelz--deviceTr   rq  r   r   r  c                 <    g | ]}|                     d           |S )zOK:)
startswith)r   ls     r   
<listcomp>z$_generate_neutts.<locals>.<listcomp>`  s)    QQQQQ\\%=P=PQqQQQr3   zNeuTTS synthesis failed: 
   r)  r`  rg  rn  r  r@  ro  checkr   r  r1  )sysr   r  r  r  rv  r#   r   r  r+  
executabler   r   r"  r8  r  r|   
splitlinesrT  chrr  ra  rb  r	   r   r  rename)r   r   r   r  neutts_configr  r  r   r  r  synth_scriptr  r{  r  error_linesr`  conv_cmds                    r   _generate_neuttsr  <  s*    JJJNN8R00M!!+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T^TfgggF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,,,r3   _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   NrD   Fr  r  s    r   _check_piper_availabler  {  sP    ~''00<<   uur  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   rY   zcache/piper-voicespiper_voices_cacheTrC  )r\   rZ   r   rN  )rZ   roots     r   _get_piper_voices_dirr    sL     0/////35IJJKKDJJtdJ+++Kr3   r   download_dirc           
      f   | 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
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  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   rM  r   r}   rO  r#   r  r   r   r   r   r  r"  r*  rT  r8  r  r|   )r   r  	candidatecached_sysr{  r   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dC$	
 
 
 $   FeFFF
 
	
 A-%2,,..D2DHHH&#,HH
 
 	
 ==?? 
)& ) ) )
 
 	

 v;;s   ;D D5D00D5c                    t                      }ddl}t          |t                    r|                    d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| }|t          vrTt                              d|           |                    ||          t          |<   t                              d           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    rt7          j        d           }|r^|d!|d"d#d$|g}t;          j        |dd%t:          j        tA                      &           	 tC          j"        |           n%# tF          $ r Y nw xY wtC          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   NrD   r   
voices_dirTrC  use_cudaF
speaker_idz::cuda=z[Piper] Loading voice: %s)r  z[Piper] Voice loadedc              3       K   | ]}|v V  	d S r   r   )r   kpiper_configs     r   r   z&_generate_piper_tts.<locals>.<genexpr>  s<       
 
 	
\
 
 
 
 
 
r3   )length_scalenoise_scalenoise_w_scalevolumenormalize_audior  )SynthesisConfigr  rR   r  gMbX?r  g?r  r  uZ   [Piper] SynthesisConfig not available in this piper-tts version — advanced knobs ignoredr  r   rU   r  )
syn_configr`  rg  rn  r  r@  ro  r  )%rE   waverx   r   r   r  r   r  rM  rN  ry   r  rz   r  r   r   loadanyrD   r  r{   r   r   r  rv  r  synthesize_wavra  rb  r   r   r"  r	   r   r  r`  r  )r   r   r   rC   r  
voice_namer  r  
model_path_raw_speakerr  	cache_keyr   r  has_advancedr  r  wav_filer`  r  r  s                       @r   _generate_piper_ttsr    sS    JKKK2<Z2N2NV:>>'2...TVL!!'**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***/<<<(2
X(V(V9%*+++y)E
 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   ?B<I< <$J#"J#22L00L47L4N% %
N21N2_kittentts_model_cachec                    t                      }|                    di           }|                    dt                    }|                    dt                    }|                    dd          }|                    dd          }|t          vrHt
                              d|            ||          t          |<   t
                              d	           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?   r   r   r   rR   
clean_textTz[KittenTTS] Loading model: %sz%[KittenTTS] Model loaded successfully)r   r   r  r   Nr  r   rU   rO   r`  rg  rn  r  r@  ro  r  )r@   r   DEFAULT_KITTENTTS_MODELDEFAULT_KITTENTTS_VOICEr  r   r   generate	soundfiler  rv  r  ra  rb  r   r   r"  r	   r   r  r  )r   r   r   r>   	kt_config
model_namer   r   r  r   r  sfr  r`  r  s                  r   _generate_kittenttsr  /  s    "##I{B//Iw(?@@JMM'#:;;EMM'3''E|T22J ///3Z@@@-6Yz-B-Bz*;<<<":.E NN4uEjNQQE H'' :%%c1--a069HHXue$$$ ;h'' 	-hk7KXHN84:CUeweyeyzzzzIh Ih,,,r3   c                 D     r                                  st          dd          S t                      t                    }t	          |          }t          |          }t                     |k    r4t                              d|t                     |            d|          ddl	m
}  |dd	                                          }|d
k    }|rbddlm}  ||          rt          j        dd| ddd          S t!          |                                          }	|t%          |	|          }	nt&          j                                                            d          }
t!          t,                    }|                    dd           |t1          |          }|d|
 d| z  }	n|r|dv r
|d|
 dz  }	n	|d|
 dz  }	|	j                            dd           t5          |	          	 |0t                              d|           t9           ||          n`|t:          vrt=           |          x}	 |n>|dk    rf	 t?                       n)# t@          $ r t          j        dddd          cY S w xY wt                              d           tC                      n|dk    rf	 tE                       n)# t@          $ r t          j        dddd          cY S w xY wt                              d           tG                      nf|dk    r-t                              d            tI                      n3|d!k    r-t                              d"           tK                      n |d#k    rf	 tM                       n)# t@          $ r t          j        dd$dd          cY S w xY wt                              d%           tO                      n|d&k    r-t                              d'           tQ                      na|d(k    rTtS                      st          j        dd)dd          S t                              d*           tU                      n|d+k    rf	 tW                       n)# t@          $ r t          j        dd,dd          cY S w xY wt                              d-           tY                      n|d.k    rf	 t[                       n)# t@          $ r t          j        dd/dd          cY S w xY wt                              d0           t]                      n/d}	 t_                       n# t@          $ r d}Y nw xY w|rt                              d1           	 ddl0}|j1        2                    d23          5 }|3                     fd4          4                    d56           ddd           n# 1 swxY w Y   n# tj          $ r& tm          j7        tq                                Y nYw xY wtS                      r.t                              d7           d(}tU                      nt          j        dd8dd          S tr          j:        ;                              r#tr          j:        <                              dk    rt          j        dd9| d:dd          S d}|Mt{          |          r=>                    d          st                    }|r|>                    d          }n|t:          vrOt          |          }|r=>                    d          st                    }|r|>                    d          }nL|r/|d;v r+>                    d          st                    }|r|d}n|dv r|o>                    d          }tr          j:        <                              }t                              d<|d=|           d> }|rd?| }t          j        d|||d@d          S # t          $ r>}dA| dB| }t          B                    dC|           t          |d          cY d}~S d}~wt          $ r@}dD| dB| }t          B                    dC|dE           t          |d          cY d}~S d}~wt          $ r@}dF| dB| }t          B                    dC|dE           t          |d          cY d}~S d}~ww xY w)Gac  
    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

    Returns:
        str: JSON result with success, file_path, and optionally MEDIA tag.
    zText is requiredF)successz>TTS text too long for provider %s (%d chars), truncating to %dNr   )get_session_envHERMES_SESSION_PLATFORMr   telegram)has_traversal_componentz/output_path contains '..' traversal component: zM. Use an absolute path or one relative to the current directory without '..'.)r  r@  )ensure_asciiz%Y%m%d_%H%M%STrC  tts_r   >   rb   r0   ra   rc   rf  z.mp3z3Generating speech with command TTS provider '%s'...rc   z`ElevenLabs provider selected but 'elevenlabs' package not installed. Run: pip install elevenlabsz$Generating speech with ElevenLabs...r0   z<OpenAI provider selected but 'openai' package not installed.z$Generating speech with OpenAI TTS...r`   z%Generating speech with MiniMax TTS...r_   z!Generating speech with xAI TTS...ra   ziMistral provider selected but 'mistralai' package not installed. Run: pip install 'hermes-agent[mistral]'z-Generating speech with Mistral Voxtral TTS...rb   z+Generating speech with Google Gemini TTS...rd   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)...rD   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...rU   )max_workersc                  J    t          j        t                               S r   )asyncior   r  )file_strr   r   s   r   <lambda>z%text_to_speech_tool.<locals>.<lambda>C	  s    GK0B4S]0^0^$_$_ r3   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_   rF   rD   rd   r`   r?   z,TTS audio saved: %s (%s bytes, provider: %s),zMEDIA:z[[audio_as_voice]]
)r  	file_path	media_tagr   r   zTTS configuration error (r*  z%szTTS dependency missing (r   zTTS generation failed ()Er|   
tool_errorr   r   r   r   r  r   r   gateway.session_contextr  r}   tools.path_securityr
  r  dumpsr   rM  r@  datetimenowstrftimeDEFAULT_OUTPUT_DIRrN  r   r+  r#   r   rZ  r   r   r-   r   r  r2   r  r7  r  r7   rC  r  r  r  r@   r  rE   r  r'   concurrent.futuresfuturesThreadPoolExecutorsubmitr{  rT  r  r   r  r   r=  rO  rx  r   r  r|  r   r   r@  ry  r"   )r   r   r   command_provider_configr  r  platform	want_opusr
  r  	timestampout_dirr   _plugin_pathedge_available
concurrentpoolr   	opus_pathplugin_voice_compatible	file_sizer  r&   	error_msgr  r   s   `                       @@r   text_to_speech_toolr/  i  sU   *  =tzz|| =,e<<<<!##JZ((H ?xTT 'x<<G
4yy7Lc$ii	
 	
 	
 HWH~ 8777778"==CCEEHZ'I  '9 	@?????"";// 	#: ="= = =  "# # # # %%0022	". <2 I %))++44_EE	)**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T4".KKEx   -h*A: HH 2228h*  L 	8
 $HH%%'"$$$$ ' ' 'z$# # !&' ' ' ' ' ''
 KK>??? x<<<<!!'%'''' ' ' 'z$[# # !&' ' ' ' ' ''
 KK>??? x<<<<""KK?@@@!$*====KK;<<<dHj9999""'&(((( ' ' 'z$H# # !&	' ' ' ' ' '' 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  "# # # # !". 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	4sz  ;Aa I% $a %#Ja 
J6a K a #K74a 6K77Ba N# "a ##O	a O		Ba 3a R a #S?a S6a 9T a #T.+a -T..2a !U0 /a 0U?<a >U??a X >/W9-X 9W==X  W=X a -X52a 4X55Aa Aa ,Ea 
d3b
d
d5cdd5dddc                     t                      rdS 	 t                       dS # t          $ r Y nw xY w	 t                       t	          d          rdS n# t          $ r Y nw xY w	 t                       t                      rdS n# t          $ r Y nw xY wt	          d          rdS 	 ddlm}   |             	                    d          rdS n# t          $ r Y nw xY wt	          d          st	          d          rdS 	 t                       t	          d	          rdS n# t          $ r Y nw xY wt                      rdS t                      rdS t                      rdS d
S )a,  
    Check if at least one TTS provider is available.

    Edge TTS needs no API key and is the default, so if the package
    is installed, TTS is available. A user-declared command provider
    also satisfies the requirement.

    Returns:
        bool: True if at least one provider can work.
    Tr  r  r   r  r  r}  r~  r9  F)r^  r'   r   r-   r   r2   _has_openai_audio_backendr  r  r   r"   r7   r  r  r  r  s    r   check_tts_requirementsr2  	  s    %&& tt   -.. 	4	   $&& 	4	   &'' t??????''))--i88 	4	   %&& -8H*I*I t   *++ 	4	      t!## t t5sS   " 
//A 
A A $B 
BB%#C 
CC<D 
D)(D)c                  b   t                      } | rt          d          s
| 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 )am  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.
    r   Fopenai-audioNz8Neither VOICE_TOOLS_OPENAI_KEY nor OPENAI_API_KEY is setz. zmanaged OpenAI audio for TTSr  v1T)r   r   DEFAULT_OPENAI_BASE_URLr   r   r   r   nous_user_tokenr   gateway_originr  )direct_api_keymanaged_gatewayrl  s      r   r  r  	  s     233N >oe44 >6==2>BBOL%'' 	?5+A+A 	72 G !!! 	'?188==@@@$GG r3   c                  V    t          t                      pt          d                    S )zPReturn True when OpenAI audio can use direct credentials or the managed gateway.r4  )ry   r   r   r   r3   r   r1  r1  	  s%    ,..^2N~2^2^___r3   z(?<=[.!?])(?:\s|\n)|(?:\n\n)z```[\s\S]*?```z\[([^\]]+)\]\([^)]+\)zhttps?://\S+z\*\*(.+?)\*\*z	\*(.+?)\*z`(.+?)`z^#+\s*z^\s*[-*]\s+z---+z\n{3,}c                 F   t                               d|           } t                              d|           } t                              d|           } t                              d|           } t
                              d|           } t                              d|           } t                              d|           } t                              d|           } t                              d|           } t                              d|           } |                                 S )z:Remove markdown formatting that shouldn't be spoken aloud.r  z\1r   rw  )_MD_CODE_BLOCKr  _MD_LINK_MD_URL_MD_BOLD
_MD_ITALIC_MD_INLINE_CODE
_MD_HEADER_MD_LIST_ITEM_MD_HR_MD_EXCESS_NLr|   )r   s    r   _strip_markdown_for_ttsrG  
  s    c4((D<<t$$D;;r4  D<<t$$D>>%&&Dud++D>>"d##DR&&D::b$DVT**D::<<r3   
text_queue
stop_eventtts_done_eventdisplay_callbackc           
         |                                  	 ddt          t          t                      }|                    di           }|                    d          |                    d|                    d                    t          di |di |dii          t          d          pd}|st                              d           n	 t                      } ||	          n*# t          $ r t                              d
           Y nw xY w	 t                      }|                    dd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}d}d}g t#          j        dt"          j                  }dt(          ff	d}d                                 s;	 |                     |          }n5# t,          j        $ r# t1          |
          |k    r ||
           d}
Y ^w xY w|6|                    d|
          }
|
                                r ||
           n|
|z  }
|                    d|
          }
d|
v rd|
vr	 t6                              |
          }|n_|                                }|
d|         }|
|d         }
t1          |                                          |k     r||
z   }
n ||           |                                ;	 	 |                                  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S # :	                                                                    n# t           $ r Y nw xY w|!                                 w xY w)a  Consume text deltas from *text_queue*, buffer them into sentences,
    and stream each sentence through ElevenLabs TTS to the speaker in
    real-time.

    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 (e.g. user interrupt).
        * *tts_done_event* is **set** in the ``finally`` block so callers
          waiting on it (continuous voice mode) know playback is finished.
    Nrc   r  streaming_model_idr   r  r   z8ELEVENLABS_API_KEY not set; streaming TTS audio disabledr  z8elevenlabs package not installed; streaming TTS disabledrO   rU   int16)
sampleraterE  dtypezsounddevice not available: %sz#sounddevice OutputStream failed: %s   r~  g      ?z<think[\s>].*?</think>r  sentencec                 H  	                                  rdS t          |                                           }|sdS |                                                    d          }
D ]0}|                                                    d          |k    r dS 1
                    |            |            dS t          |          k    r
|d         }	 j                            |d          }h|D ]a}                                 r nLddl	}|
                    ||j                  }                    |                    dd                     bdS dS  	|           dS # t          $ r&}t                              d	|           Y d}~dS d}~ww xY w)
z6Display sentence and optionally generate + play audio.Nz.!,	pcm_24000r  r   )rP  r'  rU   z!Streaming TTS sentence failed: %s)is_setrG  r|   r}   r  r  r  r  r  numpy
frombufferrN  r  reshaper"   r   r   )rR  cleanedcleaned_lowerprev
audio_iterr  _npaudio_arrayr   _play_via_tempfile_spoken_sentencesr  rK  r   output_streamrI  stream_max_lenr  s            r   _speak_sentencez.stream_tts_to_speaker.<locals>._speak_sentencen
  s     "" -h77==??G #MMOO22599M)  ::<<&&u-->>FF ?$$W---+  ***~7||n,,!/>/2I#2:: %%"-	 ;  
 !,!+ H H%,,.. "!E++++&)nnU#)n&L&L%++K,?,?A,F,FGGGGH H!E '&z:>>>>> I I IBCHHHHHHHHHIs   BE1 #E1 1
F!;FF!c                    d}	 ddl }t          j        dd          }|j        }|                    |d          5 }|                    d           |                    d           |                    d	           | D ]-}|                                r n|	                    |           .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|r(	 t          j        |           dS # t"          $ r Y dS w xY wdS # |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   r  Fr  r  rU   rV   rO   )play_audio_filez!Temp-file TTS fallback failed: %s)r  rQ  r  r   r  setnchannelssetsampwidthsetframeraterU  writeframestools.voice_modere  r"   r   r   r   rP  r`  )	r\  stop_evttmp_pathr  r  wfr  re  r   s	            r   r_  z1stream_tts_to_speaker.<locals>._play_via_tempfile
  s   H1NNN8YYsD)) .ROOA&&&OOA&&&OOE***!+ . .#??,, "!Eu----. . . . . . . . . . . . . . . =<<<<<)))) I I IBCHHHHHHHHI  	(+++++"    8 	(++++"   s   7C A0B7+C 7B;;C >B;?C D2 
DC>9D2 >DD2 	D 
D-,D-2E6E
E
EEEEr  z<thinkz</think>Tz Streaming TTS pipeline error: %s)"clearr  %DEFAULT_ELEVENLABS_STREAMING_MODEL_IDr   r   r   r   r   r   r-   r   r;   OutputStreamr  r`  r   r"   r   r  r  r#   rU  queueEmptyr  r  r|   _SENTENCE_BOUNDARY_REr  end
get_nowaitstopr  set)rH  rI  rJ  rK  r   r  r  r+   r:   r   sentence_bufmin_sentence_lenlong_flush_lenqueue_timeout_think_block_rerc  deltamend_posrR  r_  r`  r  r   ra  rb  r  s    ` `                @@@@@@@r   stream_tts_to_speakerr  #
  s   " {.8%''
NN<44	==X66==!5!*z8!D!DF F 2MzM<)LI)Lz8)L)LMM
 

 !!566<" 	)NNUVVVV[/11
#G444 [ [ [YZZZZZ[ !),..B$&OO#(1G %4 % %M "''))))#W- ) ) )LL!@#FFF$(MMMMMM  ) ) )NN#H#NNN$(MMMMMM) ')*%>biPPP(	Ic (	I (	I (	I (	I (	I (	I (	I (	I (	I (	I (	I (	I (	I (	IT	 	 	4 ##%% *	*"}==;   |$$~55#OL111#%L }.222|DD%%'' 2#OL111E!L
 +..r<@@L <''Jl,J,J*)00>>9%%'''1+GHH5x~~''((+;;;#+l#:L)))*? ##%% *	*Z	%%'''';   	   @ @ @93????????@ $""$$$##%%%%    $""$$$##%%%%   s  C N  C; :N ;$D"N !D""N (:E# "N #G
4FN G
#G N G

AN %H< ;N </I.+N -I..C2N !M6 5N 6NN NN P 
N<N72P 7N<<P (O+ +
O87O8Q#(P>=Q#>
QQ#
QQ#__main__u   🔊 Text-to-Speech Tool Modulez2==================================================c                 >    	  |              dS # t           $ r Y dS w xY w)NTF)r   )importerlabels     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:  r  rw  znot setz  OpenAI:     oaiznot installedz2not set (VOICE_TOOLS_OPENAI_KEY or OPENAI_API_KEY)z  MiniMax:    r  zAPI key setznot set (MINIMAX_API_KEY)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  r  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r   r   )r   
propertiesrequired)r   r  
parametersr   c                 r    t          |                     dd          |                     d                    S )Nr   r   r   r  )r/  r   )argskws     r   r  r  1  s6    2XXfb!!HH]++ -  -  - r3   u   🔊)r   toolsetschemahandlercheck_fnemojir   )F)r   )__doc__r  r=  r  r  loggingr   rq  r   r   ra  r   rQ  	threadingr  pathlibr   typingr   r   r   r   urllib.parser   hermes_cli._subprocess_compatr	   r\   r
   	getLoggerrA  r   r   tools.managed_tool_gatewayr   tools.tool_backend_helpersr   r   r   r   r  r   r'   r-   r2   r7   r;   r@   rE   r   r  r  r   ro  r  	frozensetr  r  r  r  r  r6  r,  r-  r.  r:  r;  r  r  r  r  r	  r  r
  r  r  r  r  r  r  rg  rq  GEMINI_TTS_SAMPLE_RATEGEMINI_TTS_CHANNELSGEMINI_TTS_SAMPLE_WIDTHr#   r]   r  re   rz   __annotations__rl   ry   r   r   MAX_TEXT_LENGTHr   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r{   r   r   r   r   r   r  r6  r-  r:  r<  r@  rZ  r^  rc  r|  r  r  r  r  r  r  r  rx  r  r  r  r  r  r  r7  rC  r/  rT  r]  rb  rd  rh  rj  r  ru  r{  r  r  r  r  r  r  r  r  r  r  r  r  r  r/  r2  tupler  r1  rs  r=  r>  r?  r@  rA  rB  	MULTILINErC  rD  rE  rF  rG  QueueEventr  printr  r   r   tools.registryr  r  
TTS_SCHEMAregisterr   r3   r   <module>r     s  ! ! !F      				  				                   0 0 0 0 0 0 0 0 0 0 0 0             < < < < < < 0 0 0 0 0 0		8	$	$/ / / / D C C C C C            1 0 0 0 0 0
 
 
  *  
  $    	 	 	  ' 4 6 (; %( 
 &I'8&9:: = " +  5 & 8 = 3 E     $ ,     23 .9 ! P !  0    = = = = = -,..  , , $sCx.    ##""	4 	4  $sCx. 	 	 	  d t    "    +
 ,05$ 5$sm5$c3h(5$ 	5$ 5$ 5$ 5$v$sCx.    &Ld38n L L L L LH "	 # # #    '* #$) !&Y'D'D'DEE &* #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%Qc %QE %Qj6Q %Q %Q %Q %QP'd 'DcN 't ' ' ' 'A
AA A cN	A
 S#XA 	A A A AH htCH~.F RV    .T . . . .
"s "x} " " " "P3 S d3PS8n Y\    <(s ( ($sCx. (UX ( ( ( (\<s < <$sCx. <UX < < < <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jC jc jtCH~ jRU j j j j`~A ~A# ~A4S> ~AVY ~A ~A ~A ~AH+ +# +4S> +VY + + + +f .'/	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>Qs Q Q$sCx. QUX Q Q Q Qp    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x &( DcN ' ' '    	t 	 	 	 	2S 2 2 2 2 2 2j_c _ _c3h _TW _ _ _ _N *, S#X + + +4c 4 4c3h 4TW 4 4 4 4x "&u4 u4
u4#u4 	u4 u4 u4 u4v	6 6 6 6 6rU3T>-B    >`4 ` ` ` ` #
#BCC  -..2:.//
"*_
%
%2:&''RZ%%
"*Z((RZ	666

>>>>	G		
9%%# #    & 9=	N NNN ON xt45	N N N Nh 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
XMM2F$G$GV55Y
X
XYYY	E
e&&1F*N*Nc;;Tc
e
efff	E	o0022l558l	o 	o   
E
oMM:K,L,Lm==Rm
o
oppp	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F}V$$H	E
.H
.
./// 0 / / / / / / /  ` !  s 
 !  O[n[n[p[p   O   O   O 	
 	
 H  
&  	- - $
	 	 	 	 	 	r3   