Guide: lexigram-multimedia-tts
Learn how to use the text-to-speech subsystem effectively.
Overview
Section titled “Overview”lexigram-multimedia-tts synthesizes speech from text. Like its music sibling, it is backend-agnostic: your code depends on the TTSProvider contract, and TTSConfig.backend decides which of the seven backends answers — from a zero-config local HTTP server to hosted ElevenLabs/OpenAI APIs to four in-process local model servers.
The umbrella lexigram-multimedia auto-discovers it via entry points; it also works standalone through AudioTTSModule.
When to use it
Section titled “When to use it”- You need speech narration, voice-overs, accessibility audio, or IVR prompts.
- You want one code path that can switch engines (local → hosted) without rewriting call sites.
- You want graceful degradation: failures come back as
Result[MediaAsset, TTSError], not thrown exceptions, except for credential errors.
Choosing a backend at a glance
Section titled “Choosing a backend at a glance”backend | Type | Needs extra | Needs key/voice |
|---|---|---|---|
local-http | any conforming HTTP server | no | no |
elevenlabs | hosted API | [elevenlabs] | elevenlabs_voice_id + API key |
openai | hosted API | [openai] | API key |
chatterbox | local model server :5100 | [chatterbox-server] | no |
kokoro | local model server :5101 | [kokoro-server] | no |
f5-tts | local model server :5102 (voice cloning) | [f5-tts-server] | reference clip + transcript |
piper | local model server :5103 (CPU, lightest) | [piper-server] | no |
Core Concepts
Section titled “Core Concepts”TTSProvider— structural protocol (fromlexigram-contracts):async generate(request: TTSRequest) -> Result[MediaAsset, MultimediaError].TTSRequest— frozen request value:text,voice,format(default"mp3"),reference_audio_uri,emotion, andextrafor provider-specific data.MediaAsset— frozen result value (mime_type,provider,bytes_data/uri,metadata); checkhas_bytes/has_uri.TTSError— the package error family (leaf ofMultimediaError, codeLEX_ERR_MM_002). Returned inErr(...)for domain/transport failures.TTSAuthenticationError— raised-not-wrapped when an API key is rejected (401). It’s an infrastructure error, so it bypasses theResultpath.AudioTTSProvider— DI provider (name"tts") that readsTTSConfig, resolves secrets, builds the backend, registers it.- Reference servers —
aiohttpservers that load a model once and serve/generate+/health(lexigram-tts-*-servescripts). - Secrets — hosted providers get their API key by name from the secrets backend (
AsyncSecretStoreProtocol), never from config.
Typical Usage
Section titled “Typical Usage”Plain speech — zero config
Section titled “Plain speech — zero config”import asyncio
from lexigram import Applicationfrom lexigram.contracts.multimedia import TTSProvider, TTSRequestfrom lexigram.di.module import Module, modulefrom lexigram.multimedia.tts import AudioTTSModule
@module(imports=[AudioTTSModule.configure()])class AppModule(Module): pass
async def speak() -> None: async with Application.boot(modules=[AppModule]) as app: tts: TTSProvider = await app.container.resolve(TTSProvider) result = await tts.generate( TTSRequest(text="Welcome back. Your report is ready.", voice="alloy") ) if result.is_ok(): asset = result.unwrap() print(asset.provider, asset.mime_type) # local-http audio/mpeg
asyncio.run(speak())AudioTTSModule.configure()defaults tobackend="local-http"→LocalHttpTTSProviderathttp://localhost:5002.- One
POST /generatesends{text, voice, format}; the response becomesMediaAsset.bytes_data.
Hosted API with secrets
Section titled “Hosted API with secrets”from lexigram.multimedia.tts import AudioTTSModulefrom lexigram.multimedia.tts.config import TTSConfig
module = AudioTTSModule.configure( config=TTSConfig(backend="elevenlabs", elevenlabs_voice_id="21m00Tcm4TlvDq8ikWAM"))The provider resolves the key from the secrets backend using elevenlabs_api_key_secret_name (default "elevenlabs_api_key") via resolve_credential.
Common Patterns
Section titled “Common Patterns”Pattern: Voice cloning with F5-TTS
Section titled “Pattern: Voice cloning with F5-TTS”F5TTSProvider clones a voice from a reference clip. It requires both a reference_audio_uri (a URI the server fetches — http(s):// or file://, never inlined bytes) and the clip’s transcript in extra["reference_text"]. Missing either returns TTSError (a request-shape problem, not a crash).
request = TTSRequest( text="Now this is the clone speaking.", reference_audio_uri="https://cdn.example.com/voice_ref.wav", extra={"reference_text": "The original sentence the voice was cloned from."},)
request.formatis ignored — the F5-TTS server always returns native WAV.
Pattern: Emotion-guided speech via an OpenAI-compatible gateway
Section titled “Pattern: Emotion-guided speech via an OpenAI-compatible gateway”When OpenAITTSProvider receives a reference_audio_uri, it switches from the classic {model, input, voice} payload to the IndexTTS2 “clone” wire shape (metadata.audio_url + should_use_prompt_for_emotion), optionally adding an emotion_prompt:
request = TTSRequest( text="Great to meet you!", reference_audio_uri="https://cdn.example.com/ref.wav", emotion="cheerful",)Point openai_base_url at your gateway (default https://api.openai.com).
Pattern: Async job execution
Section titled “Pattern: Async job execution”Resolve TTSGenerationTask from the container and submit via lexigram-tasks. run() returns a JSON-serializable dict (never raw bytes) — safe for the JSON result store:
task: TTSGenerationTask = await app.container.resolve(TTSGenerationTask)job = await task.run({ "text": "Your order has shipped.", "voice": "alloy", "format": "mp3", "emotion": "happy",})# -> {"provider": ..., "mime_type": ..., "bytes_data": ..., "uri": ..., "metadata": ...}Pattern: Resilience without code changes
Section titled “Pattern: Resilience without code changes”If the container has RetryPolicyProtocol and CircuitBreakerProtocol, every TTS backend automatically runs its HTTP call via retry.execute(circuit_breaker.call, ...) — the provider resolves both during register() and injects them.
Integration
Section titled “Integration”lexigramcore —Application.boot(), provider lifecycle (register→boot), container singletons forTTSConfig,TTSProvider,TTSGenerationTask.lexigram-contracts—TTSProviderprotocol,TTSRequest/MediaAssettypes, and the error family (TTSError,MultimediaError,ProviderNotInstalledError).- Secrets backend — hosted providers (
elevenlabs,openai) resolve their API key by name throughAsyncSecretStoreProtocol+resolve_credential. lexigram-resilience— optionalRetryPolicyProtocol/CircuitBreakerProtocolinjection.lexigram-tasks—TTSGenerationTaskis a compatible handler for the async job path.lexigram-multimediaumbrella — entry pointslexigram.multimedia.subsystems: ttsandlexigram.multimedia.modules: ttsenable auto-discovery.
Best Practices
Section titled “Best Practices”- ✅ Use
backend="local-http"(orpiper) for development and CI — no keys, light compute, sub-second cold start. - ✅ Resolve
TTSProviderfrom the container — never instantiate backend classes manually. - ✅ Always check
result.is_ok()/result.unwrap_err();TTSAuthenticationErrorwill be raised, not returned. - ✅ Store API keys exclusively in the secrets backend and reference them by name (
elevenlabs_api_key,openai_api_key). - ✅ Run local model servers in a dedicated venv so torch/ONNX weights stay out of your app process.
- ✅ Keep F5-TTS reference audio as a URI the server fetches; never inline bytes through
reference_audio_uri. - ❌ Don’t call
result.unwrap()blindly — it raises onErr. - ❌ Don’t use
backend="elevenlabs"without settingelevenlabs_voice_id—AudioTTSProvider.register()raisesProviderNotInstalledError(“required when backend=‘elevenlabs’”). - ❌ Don’t expect
request.formatto control output on Chatterbox, F5-TTS, Kokoro, or Piper — they always return native WAV. - ❌ Don’t hardcode secrets in
application.yaml.
Next Steps
Section titled “Next Steps”- How-Tos — task-oriented recipes
- Configuration — every config key
- Architecture — internal design and extension points
- Troubleshooting — common failures and fixes