little-king-41579
03/07/2026, 3:24 AMtall-airline-16756
03/07/2026, 3:24 AMtall-airline-16756
03/07/2026, 3:25 AMtts_node method in a custom Agent subclass, apply your text processing to the async text stream, then call the default TTS node so the processed text is converted to speech. This runs for that Agent across its sessions and fits into the STT → LLM → TTS pipeline.
Why this works
• tts_node receives the LLM output as an async iterable of text chunks, so you can intercept and modify text as it streams.
• Calling Agent.default.tts_node() reuses the default TTS behavior while letting you inject the modified text.
Example implementation
1. Subclass Agent.
2. Override tts_node to transform the async text stream, then forward it to the default TTS node.
from typing import AsyncIterable
from livekit.agents import Agent, ModelSettings
import rtc # replace with actual AudioFrame import if different
class MyAgent(Agent):
async def tts_node(
self, text: AsyncIterable[str], model_settings: ModelSettings
) -> AsyncIterable[rtc.AudioFrame]:
async def processed_text() -> AsyncIterable[str]:
async for chunk in text:
# Example processing:
# - normalize whitespace
# - expand abbreviations
# - apply profanity filter or SSML insertion
modified = chunk.strip().replace("API", "A P I").replace("LLM", "L L M")
# If you need SSML, you can wrap or transform here:
# modified = modified.replace("important", "<break time='200ms'/>important")
yield modified
# Use default TTS node to synthesize audio from processed text
async for frame in Agent.default.tts_node(self, processed_text(), model_settings):
yield frame
Notes and tips
• Because text is an async iterable, your processing can be streaming-friendly (no need to buffer entire responses).
• You can perform tasks like profanity filtering, abbreviation expansion, SSML insertion, phonetic/IPA mapping, pronunciation overrides, or language normalization.
• If you need different processing per AgentTask or per-session, inspect session/task context available in your Agent implementation and branch logic accordingly.
• If you must apply the same processing across many agent classes, factor the processing logic into a shared helper function and call it from each agent’s tts_node.
If you’d like, tell me:
• whether you need SSML support, profanity filtering, or pronunciation maps, and
• whether processing should be synchronous per-chunk or require context across chunks (e.g., join chunks to rewrite whole sentences).
I can provide a ready-to-use snippet for your exact use case.
Sources: Pipeline nodes and hooks | LiveKit Documentationlittle-king-41579
03/07/2026, 3:29 AMtall-airline-16756
03/07/2026, 3:29 AMtts_node) across all agents in a multi-agent system, and how to do this for AgentTask.
Short answer
• Yes — you can add a custom tts_node (or other pipeline nodes) for every agent by defining it on a shared base Agent class that all agents inherit from.
• AgentTask instances use the agent/session pipeline, so they automatically get those overrides. You can also override nodes directly in an AgentTask subclass when you need task-specific behavior.
How to apply this across all agents (recommended)
1. Create a base agent that overrides tts_node with your preprocessing logic.
2. Have all agent classes inherit from that base class so the custom node runs for every agent and during handoffs.
Example (base agent approach)
from typing import AsyncIterable
from livekit.agents import Agent, ModelSettings
import rtc # replace with actual AudioFrame import
class BaseTTSAgent(Agent):
async def tts_node(
self, text: AsyncIterable[str], model_settings: ModelSettings
) -> AsyncIterable[rtc.AudioFrame]:
async def preprocess(stream: AsyncIterable[str]) -> AsyncIterable[str]:
async for chunk in stream:
# Example processing: normalize and expand abbreviations
modified = chunk.strip().replace("API", "A P I").replace("LLM", "L L M")
yield modified
async for frame in Agent.default.tts_node(self, preprocess(text), model_settings):
yield frame
# All your agents inherit:
class AgentA(BaseTTSAgent):
...
class AgentB(BaseTTSAgent):
...
How AgentTask gets the node
• AgentTask runs inside the agent/session pipeline; if the agent starting the task has a custom tts_node, the task will use it automatically.
• If you need task-specific processing, you can override the node in the AgentTask subclass directly:
Example (task-specific override)
from livekit.agents import AgentTask
class MyTask(AgentTask[bool]):
async def tts_node(self, text, model_settings):
async def preprocess(stream):
async for chunk in stream:
# Task-specific transform
yield chunk.replace("foo", "bar")
async for frame in Agent.default.tts_node(self, preprocess(text), model_settings):
yield frame
Notes and recommendations
• Keep preprocessing streaming-friendly (work on each text chunk) unless you need whole-response context — then buffer and reassemble chunks.
• Factor shared processing into a helper function or module and call it from the base agent (DRY).
• Use Agent.default.tts_node() to reuse default TTS behavior after processing.
• If you need different behavior for specific handoffs, implement branching logic inside the base tts_node using session/task context.
If you’d like, tell me:
• Do you want one uniform preprocessing for every agent, or different rules per agent/task?
• Do you need SSML insertion, profanity filtering, or pronunciation overrides?
With that I can provide a tailored snippet (streaming vs. whole-response) and show how to wire it into your AgentSession or TaskGroup.
Sources: Pipeline nodes and hooks | LiveKit Documentation | Agent speech and audio | LiveKit Documentation | Agents and handoffs | LiveKit Documentation | Tasks and task groups | LiveKit Documentation