melodic-zoo-29246
02/13/2026, 8:49 PMtall-airline-16756
02/13/2026, 8:49 PMtall-airline-16756
02/13/2026, 8:49 PMtts options on the running session (e.g., session.update_options(tts=new_tts_instance)).
• This can work when your runtime exposes an options update API, but support for modifying every plugin field at runtime is not guaranteed. Use handoff if you need a reliable method.
Supported providers
• You can switch between any TTS providers available to your instance (LiveKit Inference TTS or external plugins like ElevenLabs, Cartesia, etc.). Each provider requires the appropriate plugin setup and credentials.
Quick checklist before switching
• Ensure the new TTS plugin is installed and configured (API key, voice selection).
• Confirm your handoff logic passes session/chat context to the new agent.
• Test the handoff in a staging session to validate voice, latency, and context preservation.
If you’d like, tell me:
• Which TTS provider you’re switching from and to (e.g., Cartesia → ElevenLabs), and
• Whether you’re using the LiveKit Agents SDK (Python/JS) or a different integration.
I can then give a small code snippet tailored to your environment (agent class example + handoff call) to implement the switch.
Sources: TTS Comparison | LiveKit Documentation | Agents and handoffs | LiveKit Documentation | Text-to-speech (TTS) models overview | LiveKit Documentation | Cartesia TTS plugin guide | LiveKit Documentationmelodic-zoo-29246
02/13/2026, 8:51 PMtall-airline-16756
02/13/2026, 8:51 PMtts plugin and preserves session history.
• Example of a small orchestration service (cron + webhook) that triggers the handoff when thresholds are hit.
Tell me your answers to questions 1–4 (or which defaults you prefer) and which language you want the example in, and I’ll produce the tailored design and code snippets.melodic-zoo-29246
02/13/2026, 8:52 PMtall-airline-16756
02/13/2026, 8:53 PMimport asyncio
import logging
from typing import List
from livekit import agents, rtc
from livekit.plugins import elevenlabs, cartesia, inworld # example plugin imports
logger = logging.getLogger("tts-fallback")
logging.basicConfig(level=<http://logging.INFO|logging.INFO>)
class TTSProviderWrapper:
def __init__(self, name: str, tts_instance, max_retries: int = 2, retry_delay: float = 1.0):
self.name = name
self.tts = tts_instance
self.max_retries = max_retries
self.retry_delay = retry_delay
async def synthesize_stream(self, text: str):
"""
Attempt to synthesize. Return async iterable/stream of audio or raise.
Adapt this method to the actual plugin API (some return bytes, some async generators).
"""
# Example: assume plugin provides async generator `synthesize(text)`
attempt = 0
last_exc = None
while attempt <= self.max_retries:
try:
<http://logger.info|logger.info>(f"Trying TTS provider {self.name} (attempt {attempt+1})")
return self.tts.synthesize(text) # may be async generator or awaitable
except Exception as e:
last_exc = e
logger.warning(f"TTS {self.name} attempt {attempt+1} failed: {e}")
attempt += 1
if attempt <= self.max_retries:
await asyncio.sleep(self.retry_delay * attempt)
raise last_exc
async def publish_audio_stream(room: rtc.Room, audio_stream):
"""
Publish audio stream to the LiveKit room.
Adapt depending on audio_stream shape. Here we assume an async iterable yields audio frames/bytes.
"""
async for chunk in audio_stream:
await room.local_participant.publish_audio(chunk)
async def speak_with_fallback(
room: rtc.Room,
text: str,
providers: List[TTSProviderWrapper],
):
last_error = None
for provider in providers:
try:
# Get an async stream/generator for audio from the provider
audio_stream = await provider.synthesize_stream(text)
# If synthesize_stream returned an async generator, publish it:
await publish_audio_stream(room, audio_stream)
<http://logger.info|logger.info>(f"Succeeded with TTS provider: {provider.name}")
return provider.name # success - return which provider was used
except Exception as e:
logger.warning(f"Provider {provider.name} failed: {e}")
last_error = e
# Optionally detect credit-exhaustion errors here and mark provider as disabled
# Example:
# if is_credit_exhaustion_error(e):
# disable_provider_in_registry(provider.name)
continue
# All providers failed
logger.error(f"All TTS providers failed. Last error: {last_error}")
raise last_error
# Example Agent entrypoint
async def agent_entry(ctx: agents.JobContext):
await ctx.connect()
room = ctx.room
# Initialize provider wrappers in the fallback order
providers = [
TTSProviderWrapper("Inworld", inworld.TTS(api_key="INWORLD_KEY")),
TTSProviderWrapper("Cartesia", cartesia.TTS(model="sonic-3", voice="v1")),
TTSProviderWrapper("ElevenLabs", elevenlabs.TTS(api_key="ELEVEN_KEY")),
]
# Example usage: speak a message, automatically falling back as needed
try:
used = await speak_with_fallback(room, "Hello — switching TTS providers automatically.", providers)
<http://logger.info|logger.info>(f"Spoken using provider: {used}")
except Exception:
# handle global failure (log, alert, play silence, etc.)
logger.exception("Failed to synthesize speech with all providers")
if __name__ == "__main__":
server = agents.ServiceService()
# register your agent entrypoint appropriately for your setup
server.register_agent(agents.JobRequestHandler(entrypoint=agent_entry))
server.start()
Practical notes and extensions
• Detecting credit exhaustion: check for provider-specific error codes/messages. When detected, mark that provider as temporarily disabled (in memory or in a small registry) so future requests skip it until you refresh its status.
• Monitoring and refresh: run a background task to poll each provider’s usage/credits API (if available) and re-enable providers when they have credits again.
• Order strategy: use a fixed priority list, or compute dynamic ordering (e.g., based on remaining balance, expiry date, latency).
• Backoff/retry tuning: adjust max_retries and retry_delay per provider; some transient network errors should be retried, but quota/credit errors should usually move immediately to the next provider.
• Preserve session/context: because the agent process handles the fallback, you keep session/chat context. If you implement agent handoffs instead (creating a new Agent with a different TTS), pass the chat_ctx so history is preserved.
If you want, I can:
• Provide a version that handles provider disable/enable state and a polling loop to check provider balances.
• Convert this into a more concrete snippet matching the TTS plugin APIs you use (Inworld/Cartesia/ElevenLabs) — tell me which plugin methods you see in your environment (e.g., whether synthesize is an async generator or returns bytes). Sources: Speechify TTS plugin guide | LiveKit Documentation | TTS Comparison | LiveKit Documentation | Introducing LiveKit Inference: A unified model interface for voice AI