
    `gj[(                        d Z ddlmZ ddlZddlZddlmZmZmZm	Z	m
Z
  ej                  e      ZdZ eh d      Z G d dej"                        Zd
d	Zy)uH  
Text-to-Speech Provider ABC
============================

Defines the pluggable-backend interface for text-to-speech synthesis.
Providers register instances via
``PluginContext.register_tts_provider()``; the active one (selected via
``tts.provider`` in ``config.yaml``) services every ``text_to_speech``
tool call **only when the configured name is neither a built-in nor a
command-type provider declared under ``tts.providers.<name>``**.

Three coexisting TTS extension surfaces — in resolution order:

1. **Built-in providers** (``BUILTIN_TTS_PROVIDERS`` in
   :mod:`tools.tts_tool`) — native Python implementations (edge, openai,
   elevenlabs, …). **Always win** — plugins cannot shadow them.
2. **Command-type providers** declared under ``tts.providers.<name>:
   type: command`` (PR #17843, commit ``2facea7f7``). Wire any local
   CLI into Hermes with shell-template placeholders. **Wins over a
   same-name plugin** — config is more local than plugin install.
3. **Plugin-registered providers** (this ABC). For backends that need a
   Python SDK, streaming bytes, OAuth refresh, or voice-listing APIs
   the shell-template grammar can't reasonably express.

Built-ins-always-win is enforced at registration time
(:func:`agent.tts_registry.register_provider` rejects names in
``BUILTIN_TTS_PROVIDERS`` with a warning) AND at dispatch time
(:func:`tools.tts_tool._dispatch_to_plugin_provider` re-checks
defensively). The dispatcher also rejects plugin dispatch when a same-
name command provider is configured.

Providers live in ``<repo>/plugins/tts/<name>/`` (built-in plugins, no
shipped today) or ``~/.hermes/plugins/tts/<name>/`` (user-installed).
None ship in-tree as of issue #30398 — the hook is additive
infrastructure waiting for a real consumer (Cartesia, Fish Audio, …).

Response contract
-----------------
:meth:`TTSProvider.synthesize` writes the audio bytes to ``output_path``
and returns the path as a string. Implementations should raise on
failure — the dispatcher converts exceptions into the standard
``{success: False, error: …}`` JSON envelope the rest of Hermes
expects.
    )annotationsN)AnyDictIteratorListOptionalmp3>   r	   oggwavflacopusc                     e Zd ZdZeej                  dd              Zedd       ZddZ	ddZ
ddZddZddZdd	Zej                  d
d
d
ed	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dd       Zd
d
dd	 	 	 	 	 	 	 	 	 	 	 ddZedd       Zy
)TTSProvideru   Abstract base class for a text-to-speech backend.

    Subclasses must implement :attr:`name` and :meth:`synthesize`.
    Everything else has sane defaults — override only what your provider
    needs.
    c                     y)a  Stable short identifier used in ``tts.provider`` config.

        Lowercase, no spaces. Examples: ``cartesia``, ``fishaudio``,
        ``deepgram``. Names that collide with a built-in TTS provider
        (``edge``, ``openai``, ``elevenlabs``, ``minimax``, ``gemini``,
        ``mistral``, ``xai``, ``piper``, ``kittentts``, ``neutts``) are
        rejected at registration time.
        N selfs    E/root/.hermes/venv/lib/python3.12/site-packages/agent/tts_provider.pynamezTTSProvider.nameH           c                6    | j                   j                         S )zHuman-readable label shown in ``hermes tools``.

        Defaults to ``name.title()`` (e.g. ``Cartesia`` for ``cartesia``).
        )r   titler   s    r   display_namezTTSProvider.display_nameT   s     yy  r   c                     y)ui  Return True when this provider can service calls.

        Typically checks for a required API key + that the SDK is
        importable. Default: True (providers with no external
        dependencies are always available).

        Must NOT raise — used by the picker and ``hermes setup`` for
        availability displays and should fail gracefully.
        Tr   r   s    r   is_availablezTTSProvider.is_available\   s     r   c                    g S )u  Return voice catalog entries.

        Each entry::

            {
                "id": "voice-abc-123",                # required
                "display": "Aria — neutral female",    # optional; defaults to id
                "language": "en-US",                   # optional
                "gender": "female",                    # optional
                "preview_url": "https://...mp3",       # optional
            }

        Default: empty list (provider has no enumerable voices or
        doesn't surface them via API).
        r   r   s    r   list_voiceszTTSProvider.list_voicesh   s	      	r   c                    g S )a  Return model catalog entries.

        Each entry::

            {
                "id": "sonic-2",                       # required
                "display": "Sonic 2",                  # optional
                "languages": ["en", "es", "fr"],       # optional
                "max_text_length": 5000,               # optional
            }

        Default: empty list (provider has a single fixed model or
        doesn't expose model selection).
        r   r   s    r   list_modelszTTSProvider.list_modelsz   s	     	r   c                $    | j                   ddg dS )aH  Return provider metadata for the ``hermes tools`` picker.

        Used by ``tools_config.py`` to inject this provider as a row in
        the Text-to-Speech provider list. Shape::

            {
                "name": "Cartesia",                    # picker label
                "badge": "paid",                       # optional short tag
                "tag": "Ultra-low-latency streaming",  # optional subtitle
                "env_vars": [                          # keys to prompt for
                    {"key": "CARTESIA_API_KEY",
                     "prompt": "Cartesia API key",
                     "url": "https://play.cartesia.ai/console"},
                ],
            }

        Default: minimal entry derived from ``display_name`` with no
        env vars. Override to expose API key prompts and custom badges.
         )r   badgetagenv_vars)r   r   s    r   get_setup_schemazTTSProvider.get_setup_schema   s     * %%	
 	
r   c                P    | j                         }|r|d   j                  d      S y)z7Return the default model id, or None if not applicable.r   idN)r    get)r   modelss     r   default_modelzTTSProvider.default_model   )    !!#!9==&&r   c                P    | j                         }|r|d   j                  d      S y)z7Return the default voice id, or None if not applicable.r   r(   N)r   r)   )r   voicess     r   default_voicezTTSProvider.default_voice   r,   r   N)voicemodelspeedformatc                    y)uy  Synthesize ``text`` and write audio bytes to ``output_path``.

        Returns the absolute path to the written file as a string
        (typically just echoes ``output_path``). Raises on failure —
        the dispatcher converts exceptions to the standard
        ``{success: False, error: ...}`` JSON envelope.

        Args:
            text: The text to synthesize. Already truncated to the
                provider's max length by the dispatcher.
            output_path: Absolute path where the audio file should be
                written. Parent directory is guaranteed to exist.
            voice: Voice identifier from :meth:`list_voices`, or None
                to use :meth:`default_voice`.
            model: Model identifier from :meth:`list_models`, or None
                to use :meth:`default_model`.
            speed: Optional speech-rate multiplier (1.0 = normal).
                Providers that don't support speed control should
                ignore this argument.
            format: Output audio format. Implementations should match
                the requested format when possible; if unsupported,
                pick the closest equivalent and ensure ``output_path``
                ends with the correct extension.
            **extra: Forward-compat parameters future schema versions
                may expose. Implementations should ignore unknown keys.
        Nr   )r   textoutput_pathr0   r1   r2   r3   extras           r   
synthesizezTTSProvider.synthesize   r   r   r   )r0   r1   r3   c               4    t        d| j                  d      )a  Stream synthesized audio bytes.

        Optional. Providers that don't support streaming raise
        :class:`NotImplementedError` (the default) and the dispatcher
        falls back to :meth:`synthesize` + read-whole-file.

        Args mirror :meth:`synthesize`. Default ``format`` is ``opus``
        because the primary streaming use case is voice-bubble
        delivery (Telegram et al.) which requires Opus.
        zTTS provider zu does not implement streaming synthesis. Use synthesize() instead, or implement stream() if your backend supports it.)NotImplementedErrorr   )r   r5   r0   r1   r3   r7   s         r   streamzTTSProvider.stream   s&    & "DII= )+ +
 	
r   c                     y)u  Whether output is suitable for voice-bubble delivery.

        Mirrors the ``tts.providers.<name>.voice_compatible`` field
        from PR #17843. When True, the gateway's voice-message
        delivery pipeline runs ffmpeg conversion to Opus if needed.
        When False, output is delivered as a regular audio attachment.

        Default: False (safe — providers opt in explicitly).
        Fr   r   s    r   voice_compatiblezTTSProvider.voice_compatible   s     r   )returnstr)r>   bool)r>   zList[Dict[str, Any]])r>   zDict[str, Any])r>   Optional[str])r5   r?   r6   r?   r0   rA   r1   rA   r2   zOptional[float]r3   r?   r7   r   r>   r?   )r5   r?   r0   rA   r1   rA   r3   r?   r7   r   r>   zIterator[bytes])__name__
__module____qualname____doc__propertyabcabstractmethodr   r   r   r   r    r&   r+   r/   DEFAULT_OUTPUT_FORMATr8   r;   r=   r   r   r   r   r   @   s0       ! !
$"
6 	  $#!%+$$ $
 $ $ $ $ $ 
$ $T  $#

 	

 
 
 
 

2 
 
r   r   c                    t        | t              st        S | j                         j	                         }|t
        v r|S t        S )zClamp an output_format value to the valid set.

    Invalid values are coerced to :data:`DEFAULT_OUTPUT_FORMAT` rather
    than rejected so the tool surface is forgiving of agent mistakes.
    )
isinstancer?   rI   striplowerVALID_OUTPUT_FORMATS)valuevs     r   resolve_output_formatrQ     s<     eS!$$A    r   )rO   rA   r>   r?   )rE   
__future__r   rG   loggingtypingr   r   r   r   r   	getLoggerrB   loggerrI   	frozensetrN   ABCr   rQ   r   r   r   <module>rY      sZ   +Z # 
  6 6			8	$   !FG #'' N!r   