The livekit-plugins-camb package enables you to integrate Camb.ai’s MARS text-to-speech models into your LiveKit voice agents. This integration provides high-quality, low-latency voice synthesis for real-time conversational AI applications.
Add custom capabilities to your agent using the @function_tool decorator. The LLM automatically discovers these functions and calls them based on user conversation.
Copy
from livekit.agents.llm import function_toolfrom livekit.agents import RunContextclass MyAgent(Agent): def __init__(self) -> None: super().__init__( instructions="You are a helpful assistant that can look up weather.", ) @function_tool async def lookup_weather( self, context: RunContext, location: str, latitude: str, longitude: str ): """Called when the user asks for weather related information. Ensure the user's location (city or region) is provided. When given a location, please estimate the latitude and longitude of the location and do not ask the user for them. Args: location: The location they are asking for latitude: The latitude of the location, do not ask user for it longitude: The longitude of the location, do not ask user for it """ # Your weather lookup logic here return "sunny with a temperature of 70 degrees."
import loggingfrom dotenv import load_dotenvfrom livekit.agents import ( Agent, AgentServer, AgentSession, JobContext, JobProcess, MetricsCollectedEvent, RunContext, cli, metrics, room_io,)from livekit.agents.llm import function_toolfrom livekit.plugins import camb, silerofrom livekit.plugins.turn_detector.multilingual import MultilingualModellogger = logging.getLogger("basic-agent")load_dotenv()class MyAgent(Agent): def __init__(self) -> None: super().__init__( instructions="Your name is Kelly. You would interact with users via voice." "with that in mind keep your responses concise and to the point." "do not use emojis, asterisks, markdown, or other special characters in your responses." "You are curious and friendly, and have a sense of humor." "you will speak english to the user", ) async def on_enter(self): # when the agent is added to the session, it'll generate a reply # according to its instructions # Keep it uninterruptible so the client has time to calibrate AEC self.session.generate_reply(allow_interruptions=False) @function_tool async def lookup_weather( self, context: RunContext, location: str, latitude: str, longitude: str ): """Called when the user asks for weather related information. Ensure the user's location (city or region) is provided. When given a location, please estimate the latitude and longitude of the location and do not ask the user for them. Args: location: The location they are asking for latitude: The latitude of the location, do not ask user for it longitude: The longitude of the location, do not ask user for it """ logger.info(f"Looking up weather for {location}") return "sunny with a temperature of 70 degrees."server = AgentServer()def prewarm(proc: JobProcess): proc.userdata["vad"] = silero.VAD.load()server.setup_fnc = prewarm@server.rtc_session()async def entrypoint(ctx: JobContext): ctx.log_context_fields = {"room": ctx.room.name} session = AgentSession( stt="deepgram/nova-3", llm="openai/gpt-4.1-mini", tts=camb.TTS(), turn_detection=MultilingualModel(), vad=ctx.proc.userdata["vad"], preemptive_generation=True, resume_false_interruption=True, false_interruption_timeout=1.0, ) usage_collector = metrics.UsageCollector() @session.on("metrics_collected") def _on_metrics_collected(ev: MetricsCollectedEvent): metrics.log_metrics(ev.metrics) usage_collector.collect(ev.metrics) async def log_usage(): summary = usage_collector.get_summary() logger.info(f"Usage: {summary}") ctx.add_shutdown_callback(log_usage) await session.start( agent=MyAgent(), room=ctx.room, room_options=room_io.RoomOptions( audio_input=room_io.AudioInputOptions(), ), )if __name__ == "__main__": cli.run_app(server)
Ensure your CAMB_API_KEY environment variable is set correctly:
Copy
export CAMB_API_KEY=your_api_key_here
Or pass it directly:
Copy
tts=camb.TTS(api_key="your_api_key")
Voice Not Found
The voice ID must be an integer. Use list_voices() to find available voices:
Copy
voices = await camb.list_voices()print(voices)
Timeout Errors
TTS synthesis can take time for longer texts. The plugin automatically uses a minimum 60-second timeout.For very long texts, consider breaking them into smaller chunks.
Audio Quality Issues
Use mars-pro for highest quality (48kHz) or mars-flash for best latency (22.05kHz)