How to Add a New TTS Backend to Speech-to-Speech: Handler Pattern Guide

To add a new TTS backend to the Speech-to-Speech system, you must create an arguments class for CLI configuration, implement a handler class extending BaseTTSHandler, and register both in backend_registry.py using BackendRegistry.register_tts_backend().

The Hugging Face Speech-to-Speech (STS) repository uses a clean handler pattern that separates configuration from runtime logic. This architecture allows developers to integrate custom text-to-speech engines without modifying the core pipeline. By following the existing patterns found in qwen3_tts_handler.py and pocket_tts_handler.py, you can add a new TTS backend that automatically inherits CLI argument parsing, error handling, and streaming capabilities.

Step 1: Create the Argument Class for CLI Options

All backend-specific configuration in Speech-to-Speech is encapsulated in argument classes located in src/speech_to_speech/arguments_classes/. Your new backend requires a class that inherits from BaseArguments and implements two required methods: add_cli_arguments and as_dict.

The add_cli_arguments method attaches your backend's flags to the global argument parser, while as_dict serializes the configuration for the handler's consumption.


# src/speech_to_speech/arguments_classes/mytts_arguments.py

from .base_arguments import BaseArguments

class MyTTSArguments(BaseArguments):
    """CLI arguments for the MyTTS backend."""
    
    def __init__(self):
        self.model_name: str = "mytts/model"
        self.voice: str = "default"
        self.speed: float = 1.0

    @classmethod
    def add_cli_arguments(cls, parser):
        """Add arguments to the shared argparse parser."""
        group = parser.add_argument_group("MyTTS")
        group.add_argument("--mytts-model", default="mytts/model",
                           help="Identifier of the MyTTS model to use.")
        group.add_argument("--mytts-voice", default="default",
                           help="Voice name within the model.")
        group.add_argument("--mytts-speed", type=float, default=1.0,
                           help="Playback speed factor (1.0 = normal).")

    def as_dict(self) -> dict:
        """Return a plain-dict representation for the handler."""
        return {
            "model_name": self.model_name,
            "voice": self.voice,
            "speed": self.speed,
        }

Placing this file in the arguments_classes directory ensures that the STSClient can discover and instantiate your configuration when parsing command-line arguments or configuration files.

Step 2: Implement the TTS Handler Class

The runtime implementation belongs in src/speech_to_speech/TTS/ and must inherit from BaseTTSHandler defined in src/speech_to_speech/baseHandler.py. This base class provides logging infrastructure and standard interfaces that the pipeline expects.

Your handler must implement generate(self, text: str) -> bytes for synchronous synthesis. Optionally, implement async def stream_generate(self, text: str) for real-time streaming applications.


# src/speech_to_speech/TTS/mytts_handler.py

from ..baseHandler import BaseTTSHandler
from ..arguments_classes.mytts_arguments import MyTTSArguments

class MyTTSHandler(BaseTTSHandler):
    """Handler that talks to the MyTTS service."""
    
    def __init__(self, args: MyTTSArguments):
        super().__init__(args)
        self.client = None

    def _ensure_client(self):
        """Lazy-load the client library on first use."""
        if self.client is None:
            from mytts import MyTTSClient
            self.client = MyTTSClient(
                model=self.args.model_name,
                voice=self.args.voice,
                speed=self.args.speed
            )

    def generate(self, text: str) -> bytes:
        """Synchronous generation returning raw PCM bytes."""
        self._ensure_client()
        audio_np = self.client.synthesize(text)
        return audio_np.tobytes()

    async def stream_generate(self, text: str):
        """Async generator yielding audio chunks."""
        self._ensure_client()
        async for chunk in self.client.synthesize_stream(text):
            yield chunk.tobytes()

Notice that the constructor accepts an instance of the arguments class created in Step 1. The registry automatically injects this dependency when instantiating your handler.

Step 3: Register the Backend in the Registry

Registration connects your symbolic backend name to the implementation classes. Open src/speech_to_speech/backend_registry.py and add a call to BackendRegistry.register_tts_backend() that maps your chosen name to the handler and arguments classes.


# src/speech_to_speech/backend_registry.py

from .arguments_classes.mytts_arguments import MyTTSArguments
from .TTS.mytts_handler import MyTTSHandler

# Existing registrations:

# BackendRegistry.register_tts_backend("qwen3", Qwen3TTSHandler, Qwen3TTSArguments)

# New registration:

BackendRegistry.register_tts_backend(
    name="mytts",
    handler_cls=MyTTSHandler,
    arguments_cls=MyTTSArguments,
)

The name parameter becomes the value users pass to the --tts-backend command-line flag. Once registered, the system can instantiate your handler and inject the parsed arguments automatically.

Optional: Add Dependencies and Testing

If your backend requires external packages, declare them in pyproject.toml under the [project] dependencies section. This ensures installation when users run pip install speech-to-speech.

Verify your integration by creating a test in tests/ following the pattern in test_qwen3_tts_handler_backend.py:


# tests/test_mytts_handler_backend.py

from speech_to_speech.backend_registry import BackendRegistry
from speech_to_speech.arguments_classes.mytts_arguments import MyTTSArguments

def test_mytts_handler_is_registered():
    handler_cls, args_cls = BackendRegistry.get_tts_backend("mytts")
    assert handler_cls is not None
    assert args_cls is MyTTSArguments

Running pytest tests/test_mytts_handler_backend.py -v confirms that the registry correctly resolves your backend name to the appropriate classes.

Using Your New Backend

After completing the implementation, users can invoke your backend via the CLI:

python -m speech_to_speech \
  --tts-backend mytts \
  --mytts-model mytts/v1 \
  --mytts-voice "alice" \
  --mytts-speed 1.2 \
  --prompt "Hello, world!"

The arguments --mytts-model, --mytts-voice, and --mytts-speed appear automatically in --help output due to the add_cli_arguments implementation in your arguments class.

Summary

  • Create an arguments class in src/speech_to_speech/arguments_classes/ that inherits from BaseArguments and implements add_cli_arguments and as_dict to expose CLI options.
  • Implement the handler in src/speech_to_speech/TTS/ by subclassing BaseTTSHandler and providing generate for synchronous operation and optionally stream_generate for async streaming.
  • Register the pair in src/speech_to_speech/backend_registry.py using BackendRegistry.register_tts_backend(name, HandlerClass, ArgumentsClass).
  • Add external dependencies to pyproject.toml if your TTS engine requires additional packages.
  • Test the registration by asserting that BackendRegistry.get_tts_backend() returns your classes for the registered name.

Frequently Asked Questions

What base class should my TTS handler extend?

Your handler must extend BaseTTSHandler from src/speech_to_speech/baseHandler.py. This base class provides standardized logging, argument injection, and interface contracts that the Speech-to-Speech pipeline requires for all TTS backends.

How does the system discover my new backend's command-line arguments?

The STSClient discovers arguments through the add_cli_arguments class method defined in your arguments class. When you register the backend in backend_registry.py, the system knows to call this method on your class during argument parsing, automatically adding your flags to the global parser.

Can I implement only synchronous generation without streaming?

Yes. You must implement generate(self, text: str) -> bytes, which returns complete audio data as bytes. The stream_generate method is optional; implement it only if your underlying TTS engine supports chunked audio generation and you want to enable real-time streaming capabilities.

Where should I place external dependencies for my backend?

Declare any required third-party packages in the [project] section of pyproject.toml under the dependencies list. This ensures that when users install the speech-to-speech package, your backend's requirements are satisfied automatically.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →