
    `gj                       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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 Z0dZ1dZ2dZ3dZ4dZ5dZ6 e7dh      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/ZQd0ZRd%ZSd1ZTd2ZUd3eVfd4ZW eW       ZXd5d6d7d8d9d:d8d;d;d5d<
ZYeeVeZf   e[d=<   d5d5d8d8d8d8d>d?d@Z\eeVeZf   e[dA<   ddBed0e]d3e]fdCZ^d9Z_e_Z`	 ddDeeV   dEeeeVef      d3eZfdFZad3eeVef   fdGZbdEeeVef   d3eVfdHZc e7h dI      ZddJZedKZf e7h dL      Zgd5ZhdEeeVef   dMeVd3eeVef   fdNZidEeeVef   dMeVd3eeVef   fdOZjdPeeVef   d3e]fdQZkdDeVdEeeVef   d3eeeVef      fdRZldSeVdTeVdDeVdEeeVef   d3eeV   f
dUZmdDeVd3e]fdVZndEeeVef   fdWZodPeeVef   d3epfdXZq	 ddPeeVef   dTeeV   d3eVfdYZrdPeeVef   d3e]fdZZsd[eVd\eZd3eeV   fd]ZtdBeVd^eeV   d3eVfd_Zud[eVd`eeVeVf   d3eVfdaZvdbej                  d3dfdcZxddeVdeepd3ej                  fdfZzdgedPeeVef   d3efdhZ{dSeVdTeVdieVdPeeVef   dEeeVef   d3eVfdjZ|ddEeeeVef      d3e]fdkZ}d3e]fdlZ~dmeVd3eeV   fdnZdSeVdTeVdEeeVef   d3eVfdoZdSeVdTeVdEeeVef   d3eVfdpZdTeVd3eVfdqZddddddrdSeVdTeVdEeeVef   dseeV   dteeV   dueeV   dveeV   dweep   d3eVfdxZdSeVdTeVdEeeVef   d3eVfdyZdzZd{Z ej                  d|d}j                  e      z   d~z   d}j                  e      z   dz   ej                        Z ej                  dej                        ZddBed0e]d3e]fdZdSeVd3eVfdZdSeVdTeVdEeeVef   d3eVfdZdSeVdTeVdEeeVef   d3eVfdZdSeVdTeVdEeeVef   d3eVfdZeSeTeUfdedeZdeZdeZd3ef
dZdeeVef   d3ee   fdZdeeVef   d3eVfdZdueVd3e]fdZdeeVef   dueVd3e]fdZdeVd3eVfdZded3eVfdZddSeVdeVd3eVfdZ	 ddSeVdeeVef   deeV   d3eVfdZdSeVdTeVdEeeVef   d3eVfdZd3e]fdZd3e]fdZd3eVfdZd3eVfdZdSeVdTeVdEeeVef   d3eVfdZi aeeVef   e[d<   d3e]fdZd3efdZdveVded3eVfdZdSeVdTeVdEeeVef   d3eVfdZi aeeVef   e[d<   dSeVdTeVdEeeVef   d3eVfdZ	 ddSeVdTeeV   d3eVfdZd3e]fdZd3eeVeVe]f   fdZd3e]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jj                        Z ej                  dejj                        Z ej                  d      Z ej                  d      ZdSeVd3eVfdZ	 ddejv                  dejx                  dejx                  deeeVg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֛         edeX         eb       Z ece      Z ede        ddlmZmZ ddddddߜdd e        ddߜddSgddZ eÐj                  dded ed       y)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urlparse)windows_hide_flags)display_hermes_homec                 v    	 ddl m}  ||       }||S |S # t        $ r t        j                  | |      cY S w xY w)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       A/root/.hermes/venv/lib/python3.12/site-packages/tools/tts_tool.pyr   r   ;   sG    (E 4 Em7..  (yyw''(s    88)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       ddl}|S # t        $ r Y t        $ r}t        t	        |            d}~ww xY w)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   sJ    ":Z.
 O   "#a&!!"s    	AA?Ac                      	 ddl m} m}  |dd       ddlm} |S # t        $ r Y t        $ r}t        t        |            d}~ww xY w)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   sN    ">. -   	 "#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       ddlm} |S # t        $ r Y t        $ r}t        t	        |            d}~ww xY w)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   |   sJ    "*}U+
 )N   "#a&!!"s    	AAAA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_tagsr         returnc                  2    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   c                     t        | t              r| S | |S t        | t        t        f      rt        |       S t        | t              r(| j                         j                         }|dv ry|dv ry|S )zNCoerce common YAML/env bool spellings without treating random strings as true.>   1onyestrueenabledT>   0noofffalsedisabledF)
isinstanceboolintfloatr$   striplower)r   r   
normalizeds      r   _config_boolr      sn    %}%#u&E{%[[]((*
>>@@Nr3   provider
tts_configc                    | st         S | j                         j                         }|xs i }t        |j	                  |      t
              r|j	                  |      ni }|r|j	                  d      nd}t        |t              rd}t        |t              r|dkD  r|S |dk(  rM|xs i j	                  d      xs t        }t        j	                  t        |      j                               }|r|S |t        v r	t        |   S |t        vrWt        ||      }t        |      r@|j	                  d      }	t        |	t              rd}	t        |	t              r|	dkD  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  *#''#,=swws|2H2:x||-.H(D!(C X\
lN''
3R7R155c(m6I6I6KLM
&&',, ''*34&u-"YY'89N.$/!%.#.>A3E%%66##r3   c                      	 ddl m}   |        }|j                  d      xs i S # t        $ r t        j                  d       i cY S t        $ r$}t        j                  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   P  sg    	1zz% &B& PQ	 6DI	s!   !$  A2A2A-'A2-A2c                 l    | j                  d      xs t        j                         j                         S )a'  Get the explicitly configured TTS provider or the free default.

    Inference credentials do not imply consent to paid speech generation.
    Users opt into cloud TTS by setting ``tts.provider`` (normally through
    ``hermes tools``); otherwise the historical Edge backend remains active.
    r   )r   DEFAULT_PROVIDERr}   r|   )r   s    r   _get_providerr   c  s+     NN:&:*:AACIIKKr3   >   r_   rF   rD   rb   rd   r0   r`   ra   	deepinfrar?   rc   x   mp3>   r   oggwavflacr   c                 p    t        | t              si S | j                  |      }t        |t              r|S 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     s3    j$'	nnT"G $/77R7r3   c                     t        | d      }t        |t              r|j                  |      nd}t        |t              r|S |j	                         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     sb     &j+>I%/	4%@immD!dG'4  zz|00&z48MIr3   r   c                     t        | t              syt        | j                  d      xs d      j	                         j                         }|r|dk7  ry| j                  d      }t        |t              xr t        |j	                               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     sq    fd#

6"(b)//1779E)#jj#Ggs#=W]]_(==r3   c                     | sy| j                         j                         }|t        v ryt        ||      }t	        |      r|S y)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     sG     
..

 
 
"C
##'
C8F"6*r3   textoutput_pathc                    |sy|j                         j                         }|t        v ryt        t	        ||            ry	 ddlm} ddlm}  |         ||      }| |d        ||      }|yt        |t              r|j                  d      nd}	t        |t              r|j                  d	      nd}
t        |t              r|j                  d
      nd}t        |t              r|j                  dt              nt        }t        j!                  d|       |j#                  | |t        |	t$              r|	r|	ndt        |
t$              r|
r|
ndt        |t&        t(        f      rt)        |      nd|rt%        |      j                         nd      }t        |t$              r|r|S |S # t        $ r }t        j                  d|       Y d}~yd}~ww xY w)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   > 
..

 
 
"C
## ##=j##NO3A"$&s+" 'T2*3/O  (2*d'CJNN7#E'1*d'CJNN7#E'1*d'CJNN7#E j$' 	(IJ.  KK<c ((!%-%eT!%-%eT(e=eEl4#&s3x~~E ) G !#.77KKC  I3Os   .F/ /	G8GGc                     | sy| j                         j                         }|t        v ry	 ddlm}  ||      }|yt        |j                        S # t        $ r!}t        j                  d||       Y d}~y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   ;  s     
..

 
 
"C
##3&s+"O4455 CS#	
 	s   A A 	A<A77A<c              #      K   t        | t              syt        | d      }|xs i j                         D ]?  \  }}t        |t              s|j                         t        vs.t        |      s:||f A yw)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   U  sh     j$'%j+>Io2,,.  	cdC TZZ\9N%N*3/Ci s   AA7A7"A7.	A7c                     | j                  d| j                  dt                    }	 t        |      }|dk  rt        t              S |S # t        t        f$ r t        t              cY S w xY w)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   `  si    
**Y

+<>a b
cC:c
 z899L	 z" :899:s   A	 	A*)A*c                 ~   |rJt        |      j                  j                         j                         j	                  d      }|t
        v r|S | j                  d      xs | j                  d      xs t        }t        |      j                         j                         j	                  d      }|t
        v r|S 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   l  s    
 k"))//1779@@E//M

8 	-::o&	-, 
 c(..

 
 
"
)
)#
.C333Z9ZZr3   c                     | j                  dd      }t        |t              r |j                         j	                         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   ~  sB    JJ)51E%{{}""$(BBB;r3   command_templatepositionc                     d}d}d}||k  rQ| |   }|dk(  r|dk(  r7d}n4|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  rQ|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[ES[ET\FA	Q% h,& Lr3   quote_contextc                 :   |dk(  r| j                  dd      S |dk(  rB| j                  dd      j                  dd      j                  dd      j                  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% 	
 
ww$&&w//;;ur3   placeholdersc                 b    dj                  d D              }t        j                  d| d| d      }g dt        j                  t           dt        f fd}|j                  |       }|j                  d	d
      j                  dd      }D ]  \  }}|j                  ||      } |S )z@Replace supported placeholders while preserving ``{{`` / ``}}``.|c              3   F   K   | ]  }t        j                  |        y wN)reescape).0r   s     r   	<genexpr>z/_render_command_tts_template.<locals>.<genexpr>  s     >RYYt_>s   !z(?<!\$)(?:\{\{(?P<double>z)\}\}|\{(?P<single>z)\})matchrW   c                     | j                  d      xs | j                  d      }dt               d}j                  |t        |   t	        | j                                     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  sq    {{8$=H(=+C,=+>bA*T"$%5u{{}E
 	 r3   z{{{z}}})joinr   compileMatchr$   subr   )	r   r   namespatternr  renderedr  r   r  s	   ``      @r   _render_command_tts_templater    s    
 HH>>>Ejj&ug-CE7%PG +-L
RXXc] 
s 
 {{=*:;Hc*224=H$ 2u##E512Or3   procc           	         | j                         yt        j                  dk(  r^	 t        j                  ddddt        | j                        gt        j                  t        j                  dt        j                         yd	dl
}	 |j                  | j                        }|j                  d
      D ]  }	 |j                           |j                          	 | j                  d       y# t        $ r | j                          Y yw xY w# |j                  $ r Y jw xY w# |j                  $ r Y yt        $ r | j                          Y sw xY w# t        j                   $ r Y nw xY w	 |j                  | j                        }|j                  d
      D ](  }	 |j                          # |j                  $ r Y &w xY w |j                          y# |j                  $ r Y yt        $ r | j                          Y y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{	ww$		NNT4TXX?!))!)) (( 	)__t_4 	E!	
 			!	)  	IIK	 ''     $$ )__t_4 	E

'' 	
 	  		s   AC/ 0D# 9D	D# E /D
DD D# D  D# #E4EEE('E(,0G F.-G .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}	 |j                  |	      \  }}|j                  r#t        j                  |j                  | ||      t        j                  | |j                  ||      S # t         j                  $ rj}t        |       	 |j                  d
	      \  }}n)# t        $ r t	        |dd      }t	        |dd      }Y nw xY wt        j                  | |||      |d}~ww xY w)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  )r5  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 
ww$(/
<VXY(Z_%,0()GN|N:;M;MND))'): ++OO	
 	
 &&wPP+ $$ +D1	2!--a-8NFF 	2S(D1FS(D1F	2 ''	

 	s<   :C   E3E?DE#D;8E:D;;EEpathc                 @    t        |      }| j                  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_pathrB  0  s#    
(
0CauI&&r3   provider_namec                    t        |j                  d      xs d      j                         }|st        d| d      t	        |      j                         }|j                  j                  dd       |j                         r|j                          t        |      }t        |t        |            }|j                  d|j                  dd            }	t        j                         5 }
t	        |
      dz  }|j                  | d	
       t        |      t        |      t        |      |t        |j                  dd            t        |j                  dd            t        |	      d}t        ||      }	 t!        ||       	 ddd       |j                         r|j5                         j6                  dk  rt'        d| d|       t        |      S # t"        j$                  $ r}t'        d| d|dd      |d}~wt"        j(                  $ r}g }|j*                  r,|j-                  d|j*                  j                                 |j.                  r,|j-                  d|j.                  j                                 dj1                  |      xs d}t'        d| d|j2                   d|       |d}~ww xY w# 1 sw Y   >xY w)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,  RuntimeErrorr;  r  r	  r  r  r:  statst_size)r   r   rC  r   r   r   r5  r   r   r   tmpdirrL  r   r   r   detail_partsdetails                    r   _generate_command_ttsr\  6  s    6::i06B7==?]O+EF
 	
 +))+F
MMt4}}&v.G263v;GMJJw
w ;<E		$	$	& &L;.	TG4 i.Yv;#GR01GR01Z
 //?N	Wg.!B ==?fkkm33q8]O+CF8L
 	
 v;) (( 	 /A'!AN ,, 
	Lzz##hszz/?/?/A.B$CDzz##hszz/?/?/A.B$CDYY|,C0CF /B>>""VH. 
	+ s8   *BK9GJ?*G>>J?B&J::J??KKc                 D    | 
t               } t        |       D ]  \  }} y y)z=Return True when any command-type TTS provider is configured.TF)r   r   )r   _name_cfgs      r   _has_any_command_tts_providerr`  z  s.    %'
.z: tr3   c                  0    t        j                  d      duS )z+Check if ffmpeg is available on the system.ffmpegN)shutilwhichr   r3   r   _has_ffmpegre    s    <<!--r3   mp3_pathc                    t               sy| j                  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k7  r@t        j                  d|j                  |j                  j                  dd      dd        yt        j                  j                  |      r$t        j                  j                  |      dkD  r|S y# t        j                  $ r t        j                  d       Y yt         $ r t        j                  d       Y yt"        $ r"}t        j                  d|d       Y d}~yd}~ww xY w)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   .oggrb  -i-acodeclibopus-acrn   -b:a64k-vbrru   -yT   capture_outputr   r  r3  z0ffmpeg conversion failed with return code %d: %srH  ignoreerrors   z)ffmpeg OGG conversion timed out after 30szffmpeg not found in PATHz ffmpeg OGG conversion failed: %sr   )re  rsplitr   r"  r$  r
   r:  r   r   r  decoder   r?  rQ  getsizer,  FileNotFoundErrorr#   )rf  ogg_pathresultr'   s       r   _convert_to_opusr~    sV    =sA&q)F2HMtXy)CxG$$,.
 !NNM ++V]]-A-A'RZ-A-[\`]`-ac77>>(#(AA(EO  $$ DBC
 	  312   M91tLLMs+   BC5 1AC5 5(E(E(>E(E##E(c           	      p  K   t               }|j                  d      xs i }|j                  dt              }t        |j                  d|j                  dd                  }d|i}|dk7  rt	        |dz
  dz        }|dd|d<    |j
                  | fi |}	|	j                  |       d	{    |S 7 w)
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pctr9  s
             r   _generate_edge_ttsr    s      !"I..(.BKOOG%78E+//':>>'3+GHIEuF|US[C'(81v')''77K


;
''' (s   B+B6-B4.B6c                    t        d      xs d}|st        d      |j                  d      xs i }|j                  dt              }|j                  dt              }|j                  d      rd}nd	}t               } ||
      }	|	j                  j                  | |||      }
t        |d      5 }|
D ]  }|j                  |        	 ddd       |S # 1 sw Y   |S xY w)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   rh  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    s     128bGXYY|,2I}}Z)DEH}}Z)DEH F#''#%J(F++33#	 4 O 
k4	  A$ 	EGGEN	 	 s   2CC c                 p    | j                  d      ry| j                  d      ry| j                  d      ryy)zHPick an OpenAI-compatible TTS response format from the output extension.rh  opus.wavr   .flacr   r   )r  )r   s    r   _tts_response_format_from_pathr    s8    F#F#G$r3   r  base_urlr   r   r   r  r  r   r   r   c          	         d}d}	|du}
|t               \  }}}	t        |t              r|j                  d      ndxs i }||j                  dt              }||j                  dt
              }|j                  d      }||xs
 |xs t        }|?t        |t              r|j                  dd      nd}t        |j                  d|            }|	r.|
s,|s*|t        vr"t        j                  d	|t        |       t        }t        |      }t               } |||
      }	 ||| |dt        t        j                               id}|dk7  rt!        dt#        d|            |d<    |j$                  j&                  j(                  di |}|j+                  |       |t-        |dd      }t/        |      r |        S S # t-        |dd      }t/        |      r |        w w xY w)u  Generate audio via the OpenAI ``audio.speech.create`` SDK shape.

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

    Args:
        text: Text to convert.
        output_path: Where to save the audio file.
        tts_config: TTS config dict (used for ``tts.openai`` sub-block
            and the global ``speed`` default).
        api_key: Bearer token. When None, resolved from the OpenAI auth
            chain (config → env → managed gateway).
        base_url: API base URL. When None, falls back to
            ``tts.openai.base_url`` then the OpenAI default.
        model: Model id. When None, reads ``tts.openai.model``.
        voice: Voice id. When None, reads ``tts.openai.voice``.
        speed: Playback speed. When None, reads ``tts.openai.speed`` /
            ``tts.speed``.

    Returns:
        Path to the saved audio file.
    NFr0   r   r   r  r   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.)r  r  zx-idempotency-key)r   r   inputresponse_formatextra_headersg      ?g      @closer   )#_resolve_openai_audio_client_configrx   r   r   DEFAULT_OPENAI_MODELDEFAULT_OPENAI_VOICEDEFAULT_OPENAI_BASE_URLr{   MANAGED_OPENAI_TTS_MODELSr   r   r  r2   r$   uuiduuid4maxminaudiospeechcreatestream_to_filer7  callable)r   r   r   r  r  r   r   r   fallback_base
is_managedexplicit_base_url
oai_configconfig_base_urlspeed_defaultr  r1   r  create_kwargsresponser  s                       r   _generate_openai_ttsr    s   J $(MJ ,-P-R*
 /9T.J*..*PT[Y[J}w(<=}w(<= nnZ0O #NmN7N}8B:t8T
w4Z]jnnWm<= 	!22" '		
 %4[AO(*L'H=F.13tzz|3DE)
 C<%(s3%?M'"-6<<&&-->>,.E?G  .E?G s   "A;F> >"G c                    t        d      xs dj                         }|st        d      t        |t              r|j                  d      nd}t        |t              si }ddlm}m} |j                  d      }t        |t              r|j                         s |d	      }|st        d
      |d   }t        | ||| ||      ||j                  dt              t        |j                  d|j                  dd                        S )u  Resolve DeepInfra credentials/model, then delegate to the OpenAI handler.

    DeepInfra's audio endpoint is OpenAI-compatible, so there's no need
    to duplicate the SDK call — we just pass an explicit api_key /
    base_url / model / voice through. Model ids and the base URL come from
    the shared ``hermes_cli.models`` helpers so every DeepInfra surface
    resolves them identically.
    DEEPINFRA_API_KEYr   zXDEEPINFRA_API_KEY not set. Run `hermes setup` to configure, or set the env var directly.r   Nr   )deepinfra_base_urldeepinfra_model_idsr   r   zNo DeepInfra TTS model available. Pin one in config.yaml under tts.deepinfra.model, or check connectivity to api.deepinfra.com so the live catalog can be fetched.r   r   rR   r  )r   r|   r   rx   r   r   hermes_cli.modelsr  r  r$   r  DEFAULT_DEEPINFRA_TTS_VOICEr{   )	r   r   r   r  	di_configr  r  r   
candidatess	            r   _generate_deepinfra_ttsr  }  s     017R>>@G+
 	
 0:*d/K
{+QUIi&	IMM'"EeS!(/
H 
 1#I.mmG%@AIMM':>>'3+GHI	 	r3   )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                 .   | j                         }|s| S |}t        j                  dd|      }t        j                  dd|      }t        j	                  |      st
        j                  d|d      }t        j                  dd|      j                         }t        j	                  |      r|S d	j                  t              }d	j                  t              }d
|z   dz   |z   dz   }	 ddl	m
}  |dd|ddd| dgd      }t        |      j                         }t        j                  d|t        j                        }	|	r|	j                  d      j                         }|xs |S # t        $ r"}
t         j#                  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    s    JJLE EFF;U3EFF;U+E$$U+&**>5*JFF9c5)//1E   ' YY./Fyy23H	/17	8;:	: =E	E	H/	/ 3!!m<.B5',JK 
 4H=CCEDfTVT]T]^[[^))+F VX[\s   'BE) )	F2F	FFc                    ddl }ddlm}  |       }t        |j	                  d      xs d      j                         }|st        d      |j	                  d      xs i }t        |j	                  dt                    j                         xs t        }t        |j	                  d	t                    j                         xs t        }	t        |j	                  d
t                    }
t        |j	                  dt                    }t        |j	                  d|j	                  d            t              }|j	                  d|j	                  d            }||dk7  r	 t        |      }|t!        t"        t%        t&        |            }|j	                  d|j	                  d            }||dk7  r	 t        |      }|t!        dt%        d|            }|rt)        |       } t        |j	                  d      xs& |j	                  d      xs t+        d      xs t,              j                         j/                  d      }|j1                  d      rdnd}| ||	d}|dk7  s|
t        k7  s|dk(  r%|t        k7  rd|i}|
r|
|d
<   |dk(  r|r||d<   ||d<   ||t2        k7  r||d<   ||t4        k7  r||d<   |j7                  | dd| dt9               d|d      }|j;                          t=        |d       5 }|j?                  |j@                         ddd       |S # t        t        f$ r d}Y w xY w# t        t        f$ r d}Y w xY w# 1 sw Y   |S xY w)!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/r  r   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(    st    ;(*E%))I&,"-335Gnoo&,"J:>>*.BCDJJLdPdH:>>*.BCDJJLdPdHjnn]4KLMK:>>*.BCDH'):>>-+HI$ NN7JNN7$;<EUb[	%LE )3/De+LM ",$34" "-2LPR2R	.),-G)H& "-%(C3M,N%O"*40z" 	 99Z 	 (	   	
 egffSk  !))&1EuEG 	11UNx+??)0%(8+6M-(E>h(0M*%#0  U&??  	#.&*XX0J,-}}*D&wi0./1

   	H 	k4	  "A	  !" S :& 	E	 :& 	.)-&	.r" s0    L 4L7 9ML43L47MMMc           	         ddl }t        d      xs d}|st        d      |j                  di       }|j                  dt              }|j                  dt
              }|j                  d	t              }|j                  d
d      }	|j                  dd      }
|j                  dd      }|j                  dd      }|j                  dd      }|j                  dd      }t        |j                  d      xs d      j                         xs t        d      xs dj                         }|rd|vrd|v rdnd}| | d| }dd| d}d|v }|r|| ||	|
||d||ddd d!}n|| |d"}|j                  |||d#$      }|r|j                          |j                         }|j                  d%i       }|j                  d&d'      }|dk7  r#|j                  d(d)      }t        d*| d+|       |j                  d,i       j                  d-d      }|st        d.      t        j                  |      }t        |d/      5 }|j!                  |       ddd       |S |j"                  j                  d0d      }d1|v r2t        |d/      5 }|j!                  |j$                         ddd       |S 	 |j                         }|j                  d%i       }|j                  d&d'      }|dk7  r#|j                  d(d)      }t        d*| d+|       	 t        d5      # 1 sw Y   |S xY w# 1 sw Y   |S xY w# t&        $ r6 |j                          t        d2| d3t)        |j$                         d4      w xY w)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  rV  bytesfromhexr  r  r  r  r#   r  )r   r   r   r  r  	mm_configr   r  r  r   r+  r,  r-  r  r/  r0  sepr  	is_t2a_v2r'  r  r}  r8  r9  r;  	hex_audioaudio_bytesr  content_types                                r   _generate_minimax_ttsrJ    s     ./52G[\\y"-IMM'#89E}}Z)ABH}}Z)ABHMM'3'E
--s
#CMM'1%EmmIy1G--u5KmmIv.G 	IMM*%+,224 	=,-3::<  Jh.H_c#ZuHXJ7 +"7),G H$I $"  +"	
(  
 }}XGWb}QH!!#JJ{B/	mmM26!"|_EJ!=k]#j\Z[[JJvr*..w;	FGGmmI.+t$ 	!GGK 	!  ''++NB?|#k4( *A(()*	]]_F

;3I#--r:Ka&]]<I
"%A+cR\Q]#^__   ?@@9	!*  	%%'@ O(()*'3 	s%   =LL!,AL. L!L+.?M-c                 2   t        d      xs d}|st        d      |j                  d      xs i }|j                  dt              }|j                  d      xs t        }|j                  d      rd}n*|j                  d	      rd
}n|j                  d      rd}nd}t               }	  ||      5 }	|	j                  j                  j                  || ||      }
t        j                  |
j                        }ddd       t'        |d      5 }|j)                         ddd       |S # 1 sw Y   1xY w# t        $ r  t        $ r?}t        j                  d|d       t!        dt#        |      j$                         |d}~ww xY w# 1 sw Y   |S xY w)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  rh  r  r  r   r  r   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   errorrV  r   __name__r  r  )r   r   r   r  	mi_configr   r  r  r5   r  r  rH  r'   r  s                 r   _generate_mistral_ttsrV    s    ./52GZ[[y)/RIMM'#<=E}}Z(H,HHF# 			f	%			g	& $&GMW% 	@||**33! /	 4 H !**8+>+>?K	@ 
k4	  A	 #	@ 	@   M-q4@1$q'2B2B1CDE1LM s=   *	D8 3A	D,<D8 F,D51D8 8F	
:FF	F	pcm_bytesr  channelssample_widthc                    ddl }||z  |z  }||z  }t        |       }|j                  dddd|||||dz  	      }|j                  dd	|      }	d
t        |      z   t        |	      z   |z   }
|j                  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)rW  r  rX  rY  r^  	byte_rateblock_align	data_size	fmt_chunkdata_chunk_header	riff_sizeriff_headers               r   _wrap_pcm_as_wavrg  B  s     h&5I\)KII
	q
I FGY?C	N"S):%;;iGI++hGDK"%66BBr3   gemini_configc                    | j                  d      }t        |t              r|j                         syt        j
                  j                  |j                               }t        |      j                         }|j                         s	 ddl
m}  |       |z  }|S |S # t        $ r t        j                         |z  }Y |S w xY w)z7Return the configured persona prompt file path, if any.persona_prompt_fileNr   )get_hermes_home)r   rx   r$   r|   r   r?  
expandvarsr   rO  is_absoluter\   rk  r#   cwd)rh  r   expandedr?  rk  s        r   #_resolve_gemini_persona_prompt_pathrp  d  s    


1
2Cc3syy{ww!!#))+.H>$$&D	%8"$t+D K4K  	%88:$DK	%s   
B  CCc                     t        |       }|y	 |j                  d      j                         S # t        t        f$ r!}t
        j                  d||       Y d}~yd}~ww xY w)zERead the Gemini persona prompt file, failing soft on config mistakes.Nr   rH  rI  z4Gemini TTS persona prompt file unavailable at %s: %s)rp  	read_textr|   OSErrorUnicodeDecodeErrorr   r   )rh  r?  r   s      r   _read_gemini_persona_promptru  u  sc    .}=D|~~w~/5577'( B	

 s   0 A AA c                     | xs dj                         j                         j                  dd      d   }d|v xr 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}   rx  )r   r~   s     r   !_gemini_model_supports_audio_tagsrw    sD    +2$$&,,.55c1=bAJ:%=%:*==r3   c                     | j                  d      }t        |t              r|j                  d      }t        |t              }|syt        |      st        j                  d|       yy)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_TAGSrw  r   r   )rh  r   r   rr   s       r   _gemini_audio_tags_enabledr{    sa    


L
)C#tggi 3(ABG,U3=	

 r3   r  c                     | xs dj                         }t        j                  d|t        j                        }|r|j	                  d      j                         }|S )Nr   r  r  rU   )r|   r   r  r  r  )r  r  r  s      r   _clean_gemini_audio_tag_rewriter}    sI    ]!!#ELL@%ryyYEA$$&Lr3   r  c                     	 | j                   d   }t        |dd       }t        |t              rt	        |j                  d      xs d      S t	        t        |dd      xs d      S # t        $ r Y yw xY w)Nr   messager  r   )choicesr7  rx   r   r$   r   r#   )r  choicer  s      r   r  r    ss    !!!$&)T2gt$w{{9-34477Ir28b99 s   A	A& A& &	A21A2persona_promptc                 2   | j                         }|s| S d}|j                         xs d}d| d| }	 ddlm}  |t        d|dd	|dgd
      }t	        t        |            }|xs | S # t        $ r"}	t        j                  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_TASKr}  r  r#   r   r   )
r   r  
transcriptr  contextuser_promptr  r  r   r   s
             r   _rewrite_gemini_tts_audio_tagsr    s    J	/ " ""$0G)) ,	 3.!m<K8 
 11ST\1]^~ UWZ[s   6A+ +	B4BBBc                    | j                         }|t        |      }|s|S d}t        j                  dt        j                        t        j                  dt        j                        f}|}|D ]<  }|j                  |      s|j                  ||      }| d| j                         c S  | d| d| j                         S )zHBuild the Gemini prompt from persona direction plus the live transcript.zSynthesize 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|   ru  r   r  
IGNORECASEr  r  )r   rh  r  r  preambleplaceholder_patternsr!   r  s           r   _compose_gemini_tts_promptr    s     J4]C	-  	

.bmmD


*"--@ F' 5>>&![[V4FZtF8,22445
 ZtN++@MSSUUr3   c                 
   ddl }t        d      xs t        d      xs dj                         }|st        d      |j	                  d      xs i }t        |t              r|ni }t        |j	                  dt                    j                         xs t        }t        |j	                  d	t                    j                         xs t        }t        |j	                  d
      xs t        d      xs t              j                         j                  d      }	t        |      }
| }t        ||      rt        | |
      }t        |||
      }t!        d|      }t#        |      |kD  r%t$        j'                  dt#        |      |       |d| }dd|igigdgddd|iiidd}ddi}t)        |	      j*                  dk(  r"	 ddl}t        |j.                        }d| |d<   |	 d| d}|j3                  |d|i||d !      }|j4                  d"k7  r^	 |j7                         j	                  d#i       }|j	                  d$      xs |j8                  dd% }t;        d&|j4                   d'|       	 |j7                         }|d(   d   d)   d   }t=        d* |D        d      }|t;        d+      |j	                  d,      xs |j	                  d-      xs i }|j	                  d.d      }|st;        d0      tE        jF                  |      }tI        |      }|jK                         jM                  d1      r(tO        |d2      5 }|jQ                  |       ddd       |S tS        jT                  d1d34      5 }|jQ                  |       |jV                  } ddd       	 tY        jZ                  d5      }!|!r|jK                         jM                  d6      r|!d7 d8d9d:d;d<d=d>d?d@dAd#|g}"n	|!d7 d@dAd#|g}"t]        j^                  |"dBdCt\        j`                  tc               D      }#|#jd                  dk7  rZ|#jf                  ji                  dEdFG      dd% }$t;        dH|$       t$        j'                  dI|       tY        jj                   |       	 tm        jn                  |        |S # t0        $ r d}Y w xY w# t0        $ r |j8                  dd% }Y uw xY w# t>        t@        tB        f$ r}t;        d/|       |d}~ww xY w# 1 sw Y   |S xY w# 1 sw Y   vxY w# tp        $ r Y |S w xY w# 	 tm        jn                          w # tp        $ r Y w w xY wxY w)Ja  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  )r  z@Gemini TTS composed prompt too long (%d chars), truncating to %dpartsr   AUDIOvoiceConfigprebuiltVoiceConfig	voiceName)responseModalitiesspeechConfig)contentsgenerationConfigr  r  z!generativelanguage.googleapis.comz0.0.0zhermes-agent/zX-Goog-Api-Clientz/models/z:generateContentr   r  )paramsr  r  r   rw  rS  r  ,  zGemini TTS API error (HTTP r=  r  r  c              3   2   K   | ]  }d |v sd|v s|  yw)
inlineDatainline_dataNr   )r   ps     r   r   z'_generate_gemini_tts.<locals>.<genexpr>`  s     W|q/@MUVDV1Ws   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   deleterb  rh  ri  rj  rk  rl  rn   rm  rn  ro  ru   rp  	-loglevelTrq  rr  rH  rt  ru  zffmpeg conversion failed: zEffmpeg not found; writing raw WAV to %s (extension may be misleading))9r  r   r|   r   r   rx   r   r$   DEFAULT_GEMINI_TTS_MODELDEFAULT_GEMINI_TTS_VOICEDEFAULT_GEMINI_TTS_BASE_URLr   ru  r{  r  r  r   r  r   r   r	   hostname
hermes_cli__version__r#   r#  r9  r  r   rV  nextKeyError
IndexErrorr   rP  rQ  rg  r}   r  r  r  rS  NamedTemporaryFiler   rc  rd  r   r"  r$  r
   r:  r  ry  copyfiler   removers  )%r   r   r   r  r  raw_gemini_configrh  r   r   r  r  
tts_scriptprompt_textmax_lenr'  r  _hermes_cli_hermes_versionendpointr  errr[  r>  r  
audio_partr  	audio_b64r'   rW  	wav_bytesr  tmpwav_pathrb  cmdr}  r  s%                                        r   _generate_gemini_ttsr    s   " -.W-@P2QWUW^^`GW
 	
 #x06B)34Et)L%RTM!!'+CDEKKMiQiE!!'+CDEKKMiQiE*% 	'*+	'& egffSk	 
 1?NJ!-73DX
,%K
 'x<G
;'!Ng	
 "(7+  5678#*))K+? 

G 12G""&II	&,!+"9"9:O *76G'H#$8E7*:;H}}w  H s"	)--/%%gr2CWWY'>8==#+>F )(*>*>)?s6(K
 	
	M}}\"1%i09WeWY]^
LMM-T1NTRTJJvr*	 ABB  +I +I ##F++t$ 	GGI	 
	$	$F5	A S		)88h'   "++F3D(y%E65+w tXt[';W^^CbPZPbPb  sE  sG  HF  A%--gh-GM"%?x#HIINNW OOHk2	IIh s  	&%O	&(  	)]]4C(F	) j), M@DE1LM	 @  			IIh 		s    R AR- /A8S ?S72TC*T! T R*)R*-S	S	S4 S//S47TT	TT!U#T98U9	UUUUc                  d    	 ddl } | j                  j                  d      duS # t        $ r Y y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    s6    ~~''1==     # 	//c                  d    	 ddl } | j                  j                  d      duS # t        $ r Y yw xY w)z@Check if the kittentts engine is importable (installed locally).r   Nr?   Fr  r  s    r   _check_kittentts_availabler    s6    ~~''4D@@ r  c                  R    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    "    tH~$$'77(BCCr3   c                  R    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    r  r3   c                 .   ddl }|j                  d      xs i }|j                  dd      xs
 t               }|j                  dd      xs
 t               }|j                  dd      }|j                  d	d
      }|}	|j	                  d      s|j                  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k7  rs|j                  j!                         }|j#                         D cg c]  }|j%                  d      r| }}t'        dt)        d      j+                  |      xs d       |	|k7  r}t-        j.                  d      }|rP|d|	ddd |g}t        j                  |dd!t        j                  t1               "       t3        j4                  |	       |S t3        j6                  |	|       |S c c}w )#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   rs  r   r   r  zOK:zNeuTTS synthesis failed: 
   r<  rb  ri  rp  r  rS  rq  checkr   r  r3  )sysr   r  r  r  rx  r$   r   r  r-  
executabler   r"  r$  r:  r  r|   
splitlines
startswithrV  chrr  rc  rd  r
   r   r  rename)r   r   r   r  neutts_configr  r  r   r  r  synth_scriptr  r}  r  lerror_linesrb  conv_cmds                     r   _generate_neuttsr    s    NN8,2M!!+r2Q6O6QI  R0N4L4NHg'EFEx/F H'%%c1-a069tH~,,/@@AL$yh5FC ^^C4T^TfTfgFA$$&"("3"3"5QQQ\\%=PqQQ6s2w||K7P7cTc6deff ;h'hk7KXHNN84:CUCUeweyzIIh
  IIh, Rs   HH_piper_voice_cachec                  d    	 ddl } | j                  j                  d      duS # t        $ r Y yw xY w)z2Check whether the piper-tts package is importable.r   NrD   Fr  r  s    r   _check_piper_availabler    s6    ~~''0<< r  c                  \    ddl m}  t         | dd            }|j                  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_cacheTrE  )r\   rZ   r   rP  )rZ   roots     r   _get_piper_voices_dirr    s/     035IJKDJJtdJ+Kr3   download_dirc           
         | st         } t        |       j                         }|j                  j	                         dk(  r|j                         rt        |      S ||  dz  }|j                         r!||  dz  j                         rt        |      S ddl}t        j                  d| |       	 t        j                  |j                  dd| dt        |      gd	d	d
t        j                        }|j                   dk7  r6|j"                  xs dj%                         xs d}t        d|  d|dd        |j                         st        d| d      t        |      S # t        j                  $ r}t        d|  d      |d}~ww xY w)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   rO  r   r}   rQ  r$   r  r   r   r   r"  r  r$  r,  rV  r:  r  r|   )r   r  	candidatecached_sysr}  r   r  s           r   _resolve_piper_voice_pathr    s    # U&&(I7*y/?/?/A9~ ugUO+F}}LeWJ+??GGI6{ 
KKBE<X
__d$;Us<02dC$$	
 A--%2,,.D2D/wc&#,H
 	
 ==?1& :( )
 	

 v;# $$ =eWAF
	s   /AE F2FFc                 b   t               }ddl}t        |t              r|j	                  d      xs i ni j	                  d      xs t
        }t        j	                  d      xs
 t                     j                         }|j                  dd       t        j	                  dd	            }t        ||      }j	                  d
d      }	t        |	t              st        |	t              sd}
n|	}
| d| }|t        vrEt        j                  d|       |j!                  ||      t        |<   t        j                  d       t        |   }d}t#        fddD              }|r	 ddlm}  |t)        j	                  dd            t)        j	                  dd            t)        j	                  dd            t)        j	                  dd            t        j	                  dd            |
      }|}|j/                  d      s|j1                  dd      d   dz   }|j3                  |d      5 }||j5                  | ||       n|j5                  | |       ddd       ||k7  r~t7        j8                  d       }|rQ|d!|d"d#d$|g}t;        j<                  |dd%t:        j>                  tA               &       	 tC        jD                  |       |S tC        jH                  ||       |S # t*        $ r t        j-                  d       Y w xY w# 1 sw Y   xY w# tF        $ r Y |S w xY w)'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_dirTrE  use_cudaF
speaker_idz::cuda=z[Piper] Loading voice: %s)r  z[Piper] Voice loadedc              3   &   K   | ]  }|v  
 y wr   r   )r   kpiper_configs     r   r   z&_generate_piper_tts.<locals>.<genexpr>r  s      
 	
\
s   )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_configrb  ri  rp  r  rS  rq  r  )%rE   waverx   r   r   r  r   r  rO  rP  ry   r  rz   r  r   r   loadanyrD   r  r{   r   r   r  rx  r  synthesize_wavrc  rd  r   r"  r$  r
   r   r  rs  r  )r   r   r   rC   r  
voice_namer  r  
model_path_raw_speakerr  	cache_keyr   r  has_advancedr  r  wav_filerb  r  r  s                       @r   _generate_piper_ttsr  H  s	    J4>z44P:>>'*0bVXL!!'*A.AJ((6Q:O:QR]]_Ltd3L$$Z78H*:|DJ
  ##L!4L,%Zc-J
!

 ,ghZ0I**/<(2
X(V9%*+y)E
 J 


 
L 	-("<#3#3NC#HI!,"2"2=%"HI#L$4$4_c$JK\--h<= $\%5%56G%N O%J H'%%c1-a069	8T	" 1h!  xJ G  x0	1 ;h'hk7KXHNN84:CUCUeweyz		(#  IIh,?  	NN5	1 1  
 s1   +BK0 9*LL! 0LLL!	L.-L._kittentts_model_cachec                 &   t               }|j                  di       }|j                  dt              }|j                  dt              }|j                  dd      }|j                  dd      }|t        vr:t
        j                  d|        ||      t        |<   t
        j                  d	       t        |   }	|	j                  | |||
      }
ddl}|}|j                  d      s|j                  dd      d   dz   }|j                  ||
d       ||k7  r}t        j                  d      }|rP|d|ddd|g}t        j                  |ddt        j                   t#                      t%        j&                  |       |S 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   rb  ri  rp  r  rS  rq  r  )r@   r   DEFAULT_KITTENTTS_MODELDEFAULT_KITTENTTS_VOICEr  r   r   generate	soundfiler  rx  r  rc  rd  r   r"  r$  r
   r   r  r  )r   r   r   r>   	kt_config
model_namer   r   r  r   r  sfr  rb  r  s                  r   _generate_kittenttsr    sr    "#I{B/Iw(?@JMM'#:;EMM'3'E|T2J //3Z@-6z-Bz*;<":.E NN4uEjNQE H'%%c1-a069HHXue$ ;h'hk7KXHNN84:CUCUeweyzIIh
  IIh,r3   c                 j     r j                         st        dd      S t               t              }t	        |      }t        |      }t               |kD  r&t        j                  d|t               |        d|  ddl	m
}  |dd	      j                         }|d
k(  }|rTddlm}  ||      rt        j                  dd| ddd      S t!        |      j#                         }	|t%        |	|      }	nt&        j&                  j)                         j+                  d      }
t!        t,              }|j/                  dd       |t1        |      }|d|
 d| z  }	n|r|dv r
|d|
 dz  }	n	|d|
 dz  }	|	j2                  j/                  dd       t5        |	      	 |'t        j7                  d|       t9         ||      n|t:        vrt=         |      x}	 |n|dk(  r/	 t?                t        j7                  d       tC                n|dk(  r/	 tE                t        j7                  d       tG                nT|dk(  r/	 tE                t        j7                  d!       tI                n |d"k(  r$t        j7                  d#       tK                n|d$k(  r$t        j7                  d%       tM                n|d&k(  r/	 tO                t        j7                  d(       tQ                n|d)k(  r$t        j7                  d*       tS                nq|d+k(  rHtU               st        j                  dd,dd      S t        j7                  d-       tW                n$|d.k(  r.	 tY                t        j7                  d0       t[                n|d1k(  r.	 t]                t        j7                  d3       t_                nd}	 ta                |rft        j7                  d4       	 ddl1}|jd                  jg                  d56      5 }|ji                   fd7      jk                  d89       ddd       nItU               r%t        j7                  d:       d+}tW                nt        j                  dd;dd      S tt        jv                  jy                        r"tt        jv                  j{                        dk(  rt        j                  dd<| d=dd      S d}|=t}        |      rj                  d      st              }|r|j                  d      }n|t:        vr?t        |      }|rtj                  d      st              }|r|j                  d      }nB|r'|d>v r#j                  d      st              }|r|d}n|dv r|xr j                  d      }tt        jv                  j{                        }t        j7                  d?|d@|       dA }|rdB| }t        j                  d|||dCd      S # t@        $ r t        j                  dddd      cY S w xY w# t@        $ r t        j                  dddd      cY S w xY w# t@        $ r t        j                  dd dd      cY S w xY w# t@        $ r t        j                  dd'dd      cY S w xY w# t@        $ r t        j                  dd/dd      cY S w xY w# t@        $ r t        j                  dd2dd      cY S w xY w# t@        $ r d}Y @w xY w# 1 sw Y   xY w# tl        $ r$ to        jp                  ts                      Y w xY w# t        $ r5}dD| dE| }t        j                  dF|       t        |d      cY d}~S d}~wt        $ r7}dG| dE| }t        j                  dF|dH       t        |d      cY d}~S d}~wt        $ r7}dI| dE| }t        j                  dF|dH       t        |d      cY d}~S d}~ww xY w)Jac  
    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  rS  )ensure_asciiz%Y%m%d_%H%M%STrE  tts_r   >   rb   r0   ra   rc   rh  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;DeepInfra TTS uses the 'openai' SDK but it isn't installed.z'Generating speech with DeepInfra TTS...r`   z%Generating speech with MiniMax TTS...r_   z!Generating speech with xAI TTS...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                  D    t        j                  t                     S r   )asyncior"  r  )file_strr   r   s   r   <lambda>z%text_to_speech_tool.<locals>.<lambda>	  s    GKK0B4S]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 ()Fr|   
tool_errorr   r   r   r   r  r   r   gateway.session_contextr  r}   tools.path_securityr  r  dumpsr   rO  rB  datetimenowstrftimeDEFAULT_OUTPUT_DIRrP  r   r-  r$   r   r\  r   r   r-   r   r  r2   r  r  rJ  r(  r7   rV  r  r  r  r@   r  rE   r  r(   concurrent.futuresfuturesThreadPoolExecutorsubmitr}  rV  r$  r"  r  r   r?  rQ  rz  r   r  r~  r   r   rS  r{  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_toolrD    s   * tzz|,e<<!#JZ(H ?xT 'x<G
4y7Lc$i	
 HW~ 88"=CCEHZ'I  	@";/:: E"m $== "# # %002	". <2I %%))+44_E	)*dT2".01HICD1SE"::I 8'TTD4"88ID4"88I 4$79~H_4".KKEx -h*A:H 228h* L 	8
 $H%'"$ KK>? x<!'%' KK>? x<$'%' KKAB#D(J?"KK?@!$*=KK;<dHj9"'&( KKGH!$*=!KKEF x<!*,zz$F# !&	' '
 KKBCT8Z8$'!# KKLMh
; ' KKABh
; "N' " @AP-#++>>1>M -QU_ &&,- )*WX# x<zz$E# !&	' ' ww~~h'277??8+D+I:: H
RST "# # !". 00GH((0 0 :I #,#+#4#4V#< 22
 'K8&T#&((0 0 :I #,#+#4#4V#< VV%%f-(2I$#' FF(FX->->v-FGGOOH-	BHQZ[\P]`hi XJ'	.yk:Izz!"  0
  	[  'zz$# !&' ''  'zz$[# !&' ''  'zz$Z# !&' ''&  'zz$H# !&	' ''4  'zz$J#
 !&' ''  'zz$5#
 !&' ''  '!&'- - $ PKK 24: NOPV  4/zQC@	T9%)U33 4.xjA3?	T9t4)U33 4-hZs1#>	T9t4)U33	4s  &A]8 2
X3 <)]8 &
Y 0)]8 
Z $A;]8  
Z. *A5]8  )]8 

[ (]8 =
\  %]8 -
\) 7]8  ] /'\;] A	]8 (A]8 D+]8 3#Y]8 Y]8 #Z?]8 Z]8 #Z+(]8 *Z++]8 .#[]8 []8 #[=:]8 <[==]8  #\&#]8 %\&&]8 )\84]8 7\88]8 ;] ] )]51]8 4]55]8 8	`2*^1+`21`2=,_/)`2/`2;,`-'`2-`2c                     t               } t        |       }t        ||       }|y|dk(  r	 t                y|dk(  r	 t                t        t        d            S |dk(  r	 t                t               S |dk(  r	 t                t        t        d            S |d	k(  rt        t        d
            S |dk(  r&	 ddlm} t         |       j                  d            S |dk(  r!t        t        d      xs t        d            S |dk(  r	 t                t        t        d            S |dk(  r
t               S |dk(  r
t!               S |dk(  r
t#               S 	 ddlm} ddlm}  |         ||      }t        |xr |j-                               S # t        $ r t               cY S w xY w# t        $ r Y yw xY w# t        $ r Y yw xY w# t        $ r Y yw xY w# t        $ r Y yw xY w# t        $ r Y yw xY w# t        $ r Y yw xY w)a  Return whether the explicitly resolved TTS provider can run.

    Availability must mirror :func:`text_to_speech_tool` dispatch. Unrelated
    cloud credentials do not make the default Edge backend usable, and an
    explicitly selected backend is checked on its own requirements.
    TrF   rc   Fr  r0   r   r  r`   r*  r_   r   r  r  rb   r  r  ra   rL  rd   r?   rD   r   r   )r   r   r   r(   r   r  r-   ry   r   r2   _has_openai_audio_backendr  r  r   r#   r7   r  r  r   r   r   r   is_available)r   r   command_configr  r   r   plugins          r   check_tts_requirementsrJ  2
  s0    "#JZ(H5h
KN!6	- <	  M"67888	!# )**;	!# M"56779M"34555	C46::9EFF 8M"23V}EU7VWW9	"$ M"34558&((;)++7%''3A"$h'F4v22455e  	-*,,	-
  		  		  		  		  		"  s|   
F 
F%  
F4 :
G 7$G 
G! 7G0 F"!F"%	F10F14	G ?G 	GG	GG!	G-,G-0	G<;G<c                  ,   t               } | rt        d      s	| t        dfS t        d      }|3d}t	               st        d      r|dt        d      z   z  }t        |      |j                  t        |j                  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-audioz8Neither VOICE_TOOLS_OPENAI_KEY nor OPENAI_API_KEY is setz. zmanaged OpenAI audio for TTSr  v1T)r   r   r  r   r   r   r   nous_user_tokenr   gateway_originr   )direct_api_keymanaged_gatewayr  s      r   r  r  z
  s     23Noe46==2>BOL%'?5+A72G !! 	''?1188=>a@$G r3   c                  B    t        t               xs t        d            S )zPReturn True when OpenAI audio can use direct credentials or the managed gateway.rL  )ry   r   r   r   r3   r   rF  rF  
  s    ,.^2N~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                    t         j                  d|       } t        j                  d|       } t        j                  d|       } t        j                  d|       } t
        j                  d|       } t        j                  d|       } t        j                  d|       } t        j                  d|       } t        j                  d|       } t        j                  d|       } | j                         S )z:Remove markdown formatting that shouldn't be spoken aloud.r  z\1r   r  )_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_ttsr^  
  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           
         |j                          	 ddt        t        t               }|j	                  d      xs i }|j	                  d      |j	                  d|j	                  d            t        di |di |dii      t        d      xs d}|st        j                  d       nE	 t               } ||	      /	 t               }|j                  ddd      j                          d}
d}d}d}g t#        j$                  dt"        j&                        }dt(        ff	d}d j+                         s	 | j	                  |      }|+|j3                  d|
      }
|
j5                         r ||
       n|
|z  }
|j3                  d|
      }
d|
v rd|
vrp	 t6        j9                  |
      }|nE|j;                         }|
d| }|
|d }
t1        |j5                               |k  r||
z   }
n	 ||       ]j+                         s	 	 | j=                          # t        $ r t        j                  d
       Y w xY w# t        t        f$ r#}	t        j                  d|	       dY d}	~	d}	~	wt         $ r#}	t        j                  d|	       dY d}	~	d}	~	ww xY w# t,        j.                  $ r t1        |
      |kD  r
 ||
       d}
Y w xY w# t,        j.                  $ r Y nw xY wn,# t         $ r }	t        j                  d|	       Y d}	~	nd}	~	ww xY w1	 j?                          jA                          n# t         $ r Y nw xY w|jC                          y# 1	 j?                          jA                          n# t         $ r Y nw xY w|jC                          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)
sampleraterX  dtypezsounddevice not available: %sz#sounddevice OutputStream failed: %s   r  g      ?z<think[\s>].*?</think>r  sentencec                   	 j                         ryt        |       j                         }|sy|j                         j	                  d      }
D ]&  }|j                         j	                  d      |k(  s& y 
j                  |        |        yt        |      kD  r|d }	 j                  j                  |d      }\|D ]V  }j                         r yddl	}|j                  ||j                        }j                  |j                  dd             X y 	|       y# t        $ r }t        j!                  d	|       Y d}~yd}~ww xY w)
z6Display sentence and optionally generate + play audio.Nz.!,	pcm_24000r  r   )rg  r:  rU   z!Streaming TTS sentence failed: %s)is_setr^  r|   r}   r   r	  r  r  r  numpy
frombufferre  r  reshaper#   r   r   )ri  cleanedcleaned_lowerprev
audio_iterr  _npaudio_arrayr   _play_via_tempfile_spoken_sentencesr  rb  r   output_streamr`  stream_max_lenr  s            r   _speak_sentencez.stream_tts_to_speaker.<locals>._speak_sentence  sc     "-h7==?G#MMO2259M) ::<&&u-> $$W-+ *~7|n,!/>2I#22:: %%"-	 ; 
 !,!+ H%,,.!+&)nnU#))n&L%++K,?,?A,FGH 'z:> IBCHHIs%   +7D2 #AD2 (	D2 2	E;EEc                 ~   d}	 ddl }t        j                  dd      }|j                  }|j	                  |d      5 }|j                  d       |j                  d       |j                  d	       | D ]%  }|j                         r n|j                  |       ' ddd       dd
l
m}  ||       |r	 t        j                   |       yy# 1 sw Y   1xY w# t        $ r }t        j                  d|       Y d}~Jd}~ww xY w# t"        $ r Y yw xY w# |r&	 t        j                   |       w # t"        $ r Y w w xY ww xY w)z0Write PCM chunks to a temp WAV file and play it.Nr   r  Fr  r  rU   rV   rO   )play_audio_filez!Temp-file TTS fallback failed: %s)r  rS  r  r   r  setnchannelssetsampwidthsetframeraterl  writeframestools.voice_moder|  r#   r   r   r   rR  rs  )	rs  stop_evttmp_pathr  r  wfr  r|  r   s	            r   rv  z1stream_tts_to_speaker.<locals>._play_via_tempfile5  s)   H11N88YYsD) .ROOA&OOA&OOE*!+ .#??,!u-.	. =) 		(+ . .  IBCHHI #  		(+"  sw   9C ACC 4D CC 	D  C;6D ;D  D 	DDD<D,+D<,	D85D<7D88D<r   z<thinkz</think>z Streaming TTS pipeline error: %s)"clearr  %DEFAULT_ELEVENLABS_STREAMING_MODEL_IDr   r   r   r   r   r   r-   r   r;   OutputStreamr
  rs  r   r#   r   r  r  r$   rl  queueEmptyr  r  r|   _SENTENCE_BOUNDARY_REr  end
get_nowaitstopr  set)r_  r`  ra  rb  r   r  r  r+   r:   r   sentence_bufmin_sentence_lenlong_flush_lenqueue_timeout_think_block_rerz  deltamend_posri  rv  rw  r  r   rx  ry  r  s    ` `                @@@@@@@r   stream_tts_to_speakerr  
  s   " {.8%'
NN<06B	==X6==!5!*z8!DF 2MzM<)LI)Lz8)LM

 !!56<"NNUV[/1
#G4 !),.B$&OO#(1G %4 %M "'') ')**%>biiP(	Ic (	I (	IT	4 ##%"}= }.222|D%%'#L1E!L
 +..r<@L <'Jl,J )00>9%%''1+GH5x~~'(+;;#+l#:L) ? ##%Z %%' [  [YZ[ $W- )LL!@#F$(M  )NN#H#N$(M)h ;; |$~5#L1#%LX ;;   @93??@ $""$##%  $""$##% s  B"L
 >H: L
 .I AL
 J> +B;L
 'L
 )K0 9L
 :IL
 IL
 J;.JL
 J;J60L
 6J;;L
 >+K-)L
 ,K--L
 0LL
 LL
 	M: 
	L3L.)M: .L33M: 9 M 	M&%M&:N?> NN?	N+(N?*N++N?__main__u   🔊 Text-to-Speech Tool Modulez2==================================================c                 2    	  |         y# t         $ r Y yw xY w)NTF)r   )importerlabels     r   _checkr    s!    	J 		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  r  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                 Z    t        | j                  dd      | j                  d            S )Nr   r   r   r  )rD  r   )argskws     r   r&  r&    s&    2XXfb!HH]+ - r3   u   🔊)r   toolsetschemahandlercheck_fnemojir   )F)r   )__doc__r$  rP  r/  r  loggingr   r  r   r   rc  r   rS  	threadingr  pathlibr   typingr   r   r   r   urllib.parser   r	   hermes_cli._subprocess_compatr
   r\   r   	getLoggerrT  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   r  r  	frozensetr  r  r  r  r  r  r?  r@  rA  rM  rN  r  r  r  r  r  r  r  r  r!  r"  r  r  r  rz  r  r  GEMINI_TTS_SAMPLE_RATEGEMINI_TTS_CHANNELSGEMINI_TTS_SAMPLE_WIDTHr$   r]   r2  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  r8  r/  r<  r>  rB  r\  r`  re  r~  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r(  rJ  rV  rB  rg  rp  ru  rw  r{  r}  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  rD  rJ  tupler  rF  r  rT  rU  rV  rW  rX  rY  	MULTILINErZ  r[  r\  r]  r^  QueueEventr  printr  r   r   tools.registryr  r+  
TTS_SCHEMAregisterr   r3   r   <module>r     s  !F      	  	        0 0 * < 0			8	$/ D  1
*
$	  ' 4 6 (; %( 
 &'8&9: = " +  5 & 8 = 3 E     $ ,     23 .9 ! P !  0 '    = = -.  , $sCx.  ##""	4  $sCx. 	 d t "    +
 ,05$sm5$c3h(5$ 	5$v$sCx. &Ld38n L LR " #   '* #$) !&'DE &* #8d38n 8C 8DcN 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v3 4 4 S#X  	T#s(^ 	 	 "&[cN[#[ 	[$T#s(^  3 # (3- ># hsm PS "sCx. 	<1j.>.> 14 1h%Qc %QE %Qj6Q6Q %QP'd 'DcN 't 'A
AA A cN	A
 S#XA 	AHhtCH~.F RV .T .
"s "x} "P3 S d3PS8n Y\ <(s ( ($sCx. (UX (V  ( ""!d
dd S#Xd
 c]d smd C=d C=d E?d 	dd,# ,C ,T#s(^ ,X[ ,d     RZZ011MACHHMfDggjpp
--  $$ERYYW 0C 0$ 04 0Ac Ac AHjC jc jtCH~ jRU j`~A ~A# ~A4S> ~AVY ~AH+ +# +4S> +VY +f .'/	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S>V SMV 		V>^s ^ ^$sCx. ^UX ^J D D3 D
D# D
23 2S 2d38n 2QT 2x &( DcN ' 	t 	2S 2 2 2j_c _ _c3h _TW _N *, S#X +4c 4 4c3h 4TW 4x "&@4
@4#@4 	@4L
E EPU3T>-B >`4 ` #

#BC  -.2::./
"**_
%2::&'RZZ%
"**Z(RZZ	6


>>	G	

9%# # & 9=	NNN OON xt45	Nh z	
+,	(O 

$%	N&1A6*J;Pvw
xy	N&1CT*J;Pxy
z{	NM2F$G5YW
XY	N&1F*N;Tcd
ef	0258l
m	o 
NM:K,L=Rmn
op	N*@*B;Hop
qr	N+-;=gh
ij	/0
12FV$H	#H:
./ 0  ` !  s
 !!Z[n[pZq  rN   O	
 H
&   	- $
	r3   