r
hi
t
Thanks for reaching out, Gaurav. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Hi — welcome! How can I help you today? If you’re reaching out about LiveKit, you can tell me what you’re trying to do (e.g., set up a room, fix connectivity, integrate SDKs, billing, or something else).
r
why is my session starting with 2 voices
t
Thanks — I can help with that. A few quick questions to diagnose why your session starts with two voices: 1. Which platform/SDK are you using (Web, iOS, Android, React Native)? 2. Are you seeing two distinct participants in the UI, or one participant but two audio streams/voices? 3. Is this happening for all rooms or just one room/session? 4. Are you publishing local microphone plus screen share audio or using an audio mixer/virtual mic? 5. Any recent changes (SDK update, custom code for tracks, or browser/device settings)? Send your answers and, if possible, the SDK logs or a short description of the published tracks (track IDs/types). I’ll suggest targeted troubleshooting steps next.
r
can I give you my code
t
Yes — please share the relevant parts of your code so I can help. The most helpful snippets are: • The code that joins the room (connect/create room) and any auth token generation. • The code that creates/publishes audio tracks (microphone, screen share, or any mixers/virtual mics). • Any event handlers for trackPublished/trackSubscribed/participantConnected. • Any logic that manipulates tracks (cloning, replacing, or re-attaching to DOM). • Any SDK logs or console output showing track IDs or publish/subscription events. Paste code inline (or as a short file). If it’s long, start with the minimal reproducible snippet that shows the publish/join flow and the UI code that plays audio. Also tell me your platform (Web/iOS/Android) and SDK version. I'll look for duplicate publishing, cloned tracks, or subscription issues.
r
i am going to give you the code in parts. ok?
t
Perfect — that works. Please paste the first part of your code (start with the room-join/auth and audio publish logic). Also include: • Platform (Web/iOS/Android/React Native) • SDK version • Any console logs shown when the session starts with two voices I’ll review and point out anything that could cause duplicate audio.
r
part 1:
Copy code
from dotenv import load_dotenv
import logging

from livekit import agents, rtc
from livekit.agents import AgentServer, AgentSession, Agent, function_tool, RunContext

from livekit.plugins import google

# Import the OneAssist API tool
from oneassist_api_tool import oneassist_chat

load_dotenv(".env.local")

# Set up logging - Enable DEBUG for more detailed logs
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)

logger = logging.getLogger(__name__)

# Also enable LiveKit agents debugging
logging.getLogger("livekit.agents").setLevel(logging.DEBUG)


class VoiceAssistant(Agent):
    def __init__(self) -> None:
        <http://logger.info|logger.info>("🤖 VoiceAssistant.__init__() called")
        super().__init__(
            instructions="""
            Primary Language: English (in Indian accent)
            
            You are Aristo, a helpful female voice AI assistant for OneAssist Consumer Pvt Ltd who talks in Indian accent.
            You assist users with questions about OneAssist services, plans, coverage, and benefits.
            
            IMPORTANT: When a user asks about OneAssist services, products, or support:
            1. Use the 'oneassist_chat' tool to get accurate information
            2. The tool will return the response from the OneAssist API
            3. Speak the response naturally to the user
            
            Your responses should be:
            - Concise and to the point
            - Without complex formatting, emojis, asterisks, or other symbols
            - Friendly and helpful with a sense of humor
            - Handle interruptions gracefully - if the user starts speaking while you're talking, stop and listen
            
NON-NEGOTIABLE RULES
1. TOOL CALLING DISCIPLINE
CRITICAL: ONLY call the oneassist_chat tool when you receive a COMPLETE, MEANINGFUL question from the user.

DO NOT call the tool for:
- Partial words, fragments, or incomplete sentences
- Single characters, punctuation marks (like ".", "..", "..."), or filler words (um, uh, hmm, ah, er)
- Greetings alone (hi, hello, hey) - respond naturally first
- Very short inputs (less than 3 words or 3 characters)
- Background noise or unclear audio

WAIT for the user to finish their complete thought before calling the tool.
If the input seems incomplete or unclear, ask for clarification instead of calling the tool.

For every COMPLETE user query about OneAssist services, products, or support:
- You MUST call the oneassist_chat function.
- You MUST send the user's query to the function in English only.
- Never answer using your own knowledge.
- Never generate clarification questions.
- Pass the user's query exactly as provided (after translating to English if needed).

2. RESPONSE FIDELITY (STRICT)
After receiving the function_response:
- It is your only source of truth.
- You must not add, remove, invent, modify, paraphrase, or summarize anything.
- If user_language is English, output function_response EXACTLY as-is.
- If user_language is not English, translate the function_response into
  user_language without altering meaning.

VOICE OUTPUT OVERRIDE
- Do NOT add natural-sounding tone, fillers, greetings, apologies, or
  conversational adjustments.
- English output MUST be spoken in an INDIAN ACCENT at all times.
- When speaking Indian languages, translate but DO NOT modify the meaning.
- English output must be EXACTLY the function_response.

3. SPEAKING STYLE
- For English: Always speak in an INDIAN ACCENT, without modifying the text.
- For Hindi or other Indian languages: No self-reference. Mix English words naturally
  (membership, service request, SR, plan, claim, payment, policy, weekend, etc.).
- Use feminine forms (e.g., "bata deti hoon", "kar sakti hoon").
- Hindi should sound like a normal Indian female agent.

4. PER-QUERY STATELESSNESS
Detect language separately for each new user message.

OPERATIONAL WORKFLOW

Step 1: Prepare Query
- If user_language is English: use message as-is.
- Otherwise: translate fully to English (english_query).

Step 2: Call Function
Call oneassist_chat with english_query.

Step 3: Receive Response
Let function_response be EXACT text returned. If empty, output nothing.

Step 4: Final Output to User
- If user_language is English: output function_response EXACTLY in an Indian accent.
- If user_language is not English: translate function_response without changing meaning.      
            """,
        )
        <http://logger.info|logger.info>("✅ VoiceAssistant initialized with instructions")
    
    @function_tool()
    async def oneassist_chat_tool(
        self,
        context: RunContext,
        message: str,
        detected_language: str = "en"
    ) -> str:
        """
        Call OneAssist chat API to get responses for user queries about OneAssist services.
        
        Use this tool whenever the user asks about:
        - OneAssist products, services, or plans
        - Coverage and benefits
        - Membership information
        - Support or assistance
        
        Args:
            message: The user's question or query (should be in English)
            detected_language: The language the user spoke in (defaults to 'en')
            
        Returns:
            The response from OneAssist API
        """
        <http://logger.info|logger.info>(f"🔧 Tool called: oneassist_chat | Message: '{message}' | Language: {detected_language}")
        
        # ✅ INPUT VALIDATION: Prevent premature tool calls for partial/incomplete inputs
        cleaned_message = message.strip()
        
        # Reject if too short (less than 3 characters)
        if not cleaned_message or len(cleaned_message) < 3:
            logger.warning(f"⚠️ Tool call rejected: message too short: '{message}'")
            return "I'm listening. Please continue with your question."
        
        # Reject if only punctuation or whitespace
        if not any(c.isalnum() for c in cleaned_message):
            logger.warning(f"⚠️ Tool call rejected: no alphanumeric characters: '{message}'")
            return "Could you please repeat that?"
        
        # Reject common filler words or incomplete phrases
        filler_words = {'um', 'uh', 'hmm', 'ah', 'er', 'hm', '.', '..', '...', 'umm', 'uhh', 'hmm'}
        if cleaned_message.lower() in filler_words:
            logger.warning(f"⚠️ Tool call rejected: filler word detected: '{message}'")
            return "I'm here. What would you like to know?"
        
        # ✅ Only proceed with valid, meaningful input
        # Call the OneAssist API function
        response = await oneassist_chat(message, detected_language)
        
        <http://logger.info|logger.info>(f"🔧 Tool response: {response[:100]}...")
        return response
    
    async def on_enter(self):
        """Called when the agent enters the conversation"""
        <http://logger.info|logger.info>("🎯 on_enter() called - Agent is entering the conversation")
        try:
            <http://logger.info|logger.info>("💬 Generating greeting...")
            await self.session.generate_reply(instructions="Greet the user warmly and offer your assistance.")
            <http://logger.info|logger.info>("✅ Greeting generated successfully")
        except Exception as e:
            logger.error(f"❌ Error in on_enter: {e}", exc_info=True)


server = AgentServer()
t
Thanks — I’ve received Part 1 (agent and tool code). This looks like the voice assistant server side. A few quick clarifying questions and next requests so I can diagnose the “two voices” issue: 1. Platform and SDK version for the client that joins the LiveKit room (Web/iOS/Android/React Native and SDK version). 2. Do you hear two voices as two separate participants, or one participant speaking twice (duplicate audio)? 3. Share the client-side join/publish code (especially audio track creation/publish and any code that plays remote tracks). You said you’ll send code in parts — please send the client-side parts next. 4. Any logs from the client or server around join/publish/subscribed events (track IDs, participant IDs, publish/subscribed messages)? Enable DEBUG logs if possible. 5. Are you publishing both a mic track and a synthetic TTS/assistant track simultaneously? Or using any audio mixer/virtual device? Send the next code part (client join + audio publish + event handlers) and a short sample of relevant logs and I’ll inspect for duplicate publishing/cloning or subscription playback issues.
r
part 2:
Copy code
@server.rtc_session(
    agent_name="voice-assistant"
    # Note: Using default to dispatch when participant connects
)
async def my_agent(ctx: agents.JobContext):
    <http://logger.info|logger.info>(f"🚀 Agent entrypoint called for room: {ctx.room.name}")
    
    try:
        # Initialize AgentSession with Gemini Realtime model
        <http://logger.info|logger.info>("📝 Creating AgentSession with Gemini Realtime model...")
        session = AgentSession(
            llm=google.realtime.RealtimeModel(
                model="gemini-2.5-flash-native-audio-preview-12-2025",
                # model="gemini-live-2.5-flash-native-audio",
                voice="Aoede",
                temperature=0.8,
            )
        )
        <http://logger.info|logger.info>("✅ AgentSession created")
        
        # Set up event handlers for transcription and tool calls
        setup_session_handlers(session)

        # Connect to room first
        <http://logger.info|logger.info>("🔌 Connecting to room...")
        await ctx.connect()
        <http://logger.info|logger.info>(f"✅ Connected to room. Participants: {len(ctx.room.remote_participants)}")
        
        # Log participant info
        for participant in ctx.room.remote_participants.values():
            <http://logger.info|logger.info>(f"👤 Participant: {participant.identity} (kind: {participant.kind})")
        
        # Set up room-level event handlers for transcription data
        setup_room_handlers(ctx.room)
        
        # Start the session - this will trigger the agent's on_enter method for greeting
        <http://logger.info|logger.info>("▶️  Starting AgentSession...")
        await session.start(
            room=ctx.room,
            agent=VoiceAssistant(),
        )
        <http://logger.info|logger.info>("✅ AgentSession started successfully")
        
        # Keep the agent running until the session closes naturally
        <http://logger.info|logger.info>("⏳ Waiting for session to complete...")
        
    except Exception as e:
        logger.error(f"❌ Error in agent entrypoint: {e}", exc_info=True)
        raise
    finally:
        <http://logger.info|logger.info>(f"🔚 Agent session ended for room: {ctx.room.name}")


def setup_session_handlers(session: AgentSession):
    """Set up event handlers for the AgentSession"""
    
    <http://logger.info|logger.info>("🎬 Setting up session event handlers...")
    
    @session.on("user_input_transcribed")
    def on_user_transcribed(event: agents.UserInputTranscribedEvent):
        """Called when user speech is transcribed"""
        if event.is_final:
            <http://logger.info|logger.info>(f"👤 User (final): {event.transcript}")
        else:
            logger.debug(f"👤 User (partial): {event.transcript}")
    
    @session.on("conversation_item_added")
    def on_conversation_item(event: agents.ConversationItemAddedEvent):
        """Called when an item is added to the conversation history"""
        <http://logger.info|logger.info>(f"💬 {event.item.role}: {event.item.text_content}")
    
    @session.on("function_tools_executed")
    def on_tools_executed(event: agents.FunctionToolsExecutedEvent):
        """Called after function tools are executed"""
        for call, output in event.zipped():
            <http://logger.info|logger.info>(f"🔧 Tool executed: {call.name}")
            <http://logger.info|logger.info>(f"   Arguments: {call.arguments}")
            <http://logger.info|logger.info>(f"   Output: {output.output if output.output else 'None'}...")
    
    @session.on("agent_state_changed")
    def on_agent_state(event: agents.AgentStateChangedEvent):
        """Called when agent state changes"""
        <http://logger.info|logger.info>(f"🤖 Agent state: {event.old_state} → {event.new_state}")
    
    @session.on("speech_created")
    def on_speech_created(event: agents.SpeechCreatedEvent):
        """Called when agent creates speech"""
        <http://logger.info|logger.info>(f"🗣️  Agent speech created (source: {event.source}, user_initiated: {event.user_initiated})")
    
    <http://logger.info|logger.info>("✅ Event handlers registered successfully")


def setup_room_handlers(room: rtc.Room):
    """Set up room-level event handlers to capture transcription data"""
    
    <http://logger.info|logger.info>("📡 Setting up room event handlers for transcription data...")
    
    @room.on("data_received")
    def on_data_received(data: rtc.DataPacket):
        """Called when data is received in the room"""
        logger.debug(f"📦 Data received: topic={data.topic}, size={len(data.data)} bytes")
        
        # Check if this is transcription data
        if data.topic == "lk.transcription":
            try:
                import json
                transcription = json.loads(data.data.decode('utf-8'))
                <http://logger.info|logger.info>(f"📝 Transcription: {transcription}")
            except Exception as e:
                logger.debug(f"Could not parse transcription data: {e}")
    
    @room.on("track_subscribed")
    def on_track_subscribed(
        track: rtc.Track,
        publication: rtc.TrackPublication,
        participant: rtc.RemoteParticipant
    ):
        """Called when a track is subscribed"""
        <http://logger.info|logger.info>(f"📥 Track subscribed: {track.kind} from {participant.identity}")
    
    @room.on("track_published")
    def on_track_published(
        publication: rtc.LocalTrackPublication,
        participant: rtc.Participant
    ):
        """Called when a track is published"""
        <http://logger.info|logger.info>(f"📤 Track published: {publication.kind} by {participant.identity}")
    
    <http://logger.info|logger.info>("✅ Room event handlers registered")


if __name__ == "__main__":
    agents.cli.run_app(server)
Copy code
[project]
name = "livekit-gemini-voicebot"
version = "0.1.0"
description = "LiveKit Gemini-powered speech-to-speech voice bot"
readme = "README.md"
requires-python = ">=3.9"
dependencies = [
    "livekit-agents[google]~=1.2",
    "python-dotenv",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build]
skip = true
t
Thanks — I’ve received Parts 1–3 (agent code, server hooks, and pyproject). I’m looking for the specific issue you mentioned: the session starts with two voices. A few targeted clarifying questions to narrow this down: 1. Do you hear two identical voices (duplicate audio) or two different voices (e.g., agent voice + user/other participant)? 2. Do you see two participants in the room logs or UI, or one participant with multiple tracks? (Share any console log lines from your server showing
Track published
/
Track subscribed
at session start.) 3. Does the double-voice occur immediately when the agent joins, or only after the agent's greeting runs? 4. Are you publishing any audio tracks elsewhere (mic, screen share, or a pre-recorded file) in your app or on the client side? 5. Which platform/SDK are your clients using (Web/iOS/Android) and what SDK version? If you can paste the server logs around the room join (including
📤 Track published
/
📥 Track subscribed
and
🗣️ Agent speech created
), and any client-side publish code that creates audio tracks, I’ll review for duplicate publishing (cloned/re-published tracks), voice-mixing, or subscription playback issues and give next steps.
r
1. Do you hear two identical voices (duplicate audio) or two different voices (e.g., agent voice + user/other participant)? 2 different voices (agent's voice only but 2 agents speaking the answer to 1 user) 1. Do you see two participants in the room logs or UI, or one participant with multiple tracks? (Share any console log lines from your server showing
Track published
/
Track subscribed
at session start.) 2. Does the double-voice occur immediately when the agent joins, or only after the agent's greeting runs? immediately when the agent joins 1. Which platform/SDK are your clients using (Web/iOS/Android) and what SDK version? Web. dependencies = [ "livekit-agents[google]~=1.2", "python-dotenv", ]
t
Thanks — this is helpful. I’m responding about the duplicate-agent voices that start immediately when the agent joins. To diagnose, please provide two things next: 1. Server logs around the agent join (include lines showing any of these:
Track published
,
Track subscribed
,
Agent speech created
,
AgentSession started
, and timestamps for the first ~30s). 2. The Web client code that joins the room and handles audio track publishing/subscription (especially any code that auto-plays subscribed audio or creates/publishes LocalAudioTrack). If it’s long, paste the parts that: ◦ connect to LiveKit (`connect`/`createLocalTracks`), ◦ publish tracks, ◦ handle
trackSubscribed
or attach tracks to audio elements. With those I’ll check for duplicate publishing, multiple agent sessions, or playback of the same track twice and give specific fixes.
r
I didnt hear any voice this time 2025-12-15 154703,247 - __mp_main__ - INFO - 📤 Track published: 1 by user-tvmq4mdy 2025-12-15 154703,247 - __mp_main__ - INFO - 📤 Track published: 1 by user-tvmq4mdy 154703.247 INFO __mp_main__ 📤 Track published: 1 by user-tvmq4mdy {"pid": 70946, "job_id": "AJ_r5EGjF3QjzAk", "room_id": "RM_67g53RMG3gRy"} 2025-12-15 154703,263 - __mp_main__ - INFO - 📥 Track subscribed: 1 from user-tvmq4mdy 2025-12-15 154703,263 - __mp_main__ - INFO - 📥 Track subscribed: 1 from user-tvmq4mdy 154703.263 INFO __mp_main__ 📥 Track subscribed: 1 from user-tvmq4mdy {"pid": 70946, "job_id": "AJ_r5EGjF3QjzAk", "room_id": "RM_67g53RMG3gRy"}
t
Thanks — I’m responding about the duplicate "Track published" / "Track subscribed" lines you shared. The duplicated log lines show the same track ID published and subscribed twice at the same timestamp, which typically means the same audio is being published or played twice. Likely causes to check on the Web client and agent setup: • The Web client is creating/publishing the audio track twice (two calls to
createLocalTracks
/
createLocalAudioTrack
or
publishTrack
). • The client attaches the same subscribed track to two audio elements or plays it twice in UI
trackSubscribed
handler. • Two AgentSession instances are joining the room concurrently (agent process restarted/duplicated), causing two synthetic-agent voices to speak the same reply. • Agent-side code publishes the agent audio and the SDK also publishes a separate TTS audio track (less likely given your logs, but check for any additional publish calls around session start). Next steps — please provide one of these (pick whichever is easiest): • The Web client code that connects/publishes audio and the
trackSubscribed
handler (show the parts that create/publish tracks and attach them to audio elements). OR • A 10–20s excerpt of server logs including timestamps from just before join through the first greeting that shows any
AgentSession started
,
speech_created
,
track_published
, and
track_subscribed
lines. I’ll look for duplicate publish calls, multiple agent joins, or double-attachment of subscribed tracks and give exact fixes.
r
give me clean annd efficient code that achieves what I am doing
t
Below is a clean, minimal server + web client example that follows your current design (LiveKit agent generating TTS via Gemini + a Web client that publishes a single mic track and plays subscribed audio). This example focuses on preventing duplicate audio by ensuring: • Only one AgentSession instance joins per room. • The server publishes only the agent's generated audio once. • The web client publishes exactly one local audio track and attaches each subscribed remote track to a single audio element (no double-attachment). • Handlers are idempotent (guard against registering twice). Server (Python) — minimal, efficient, and idempotent
Copy code
# server.py
import logging
from livekit import agents, rtc
from livekit.agents import AgentServer, AgentSession, Agent, function_tool, RunContext
from livekit.plugins import google
from dotenv import load_dotenv
load_dotenv(".env.local")

logging.basicConfig(level=<http://logging.INFO|logging.INFO>, format="%(asctime)s - %(levelname)s - %(message)s")
logger = logging.getLogger(__name__)

class VoiceAssistant(Agent):
    def __init__(self):
        super().__init__(instructions="You are an assistant that answers user queries via TTS.")

    @function_tool()
    async def oneassist_chat_tool(self, context: RunContext, message: str, detected_language: str = "en"):
        # Placeholder: call the OneAssist API and return text response.
        # Keep tool logic minimal and return the response string.
        return await some_oneassist_api_call(message, detected_language)  # implement this

async def create_agent_session():
    return AgentSession(
        llm=google.realtime.RealtimeModel(
            model="gemini-2.5-flash-native-audio-preview-12-2025",
            voice="Aoede",
            temperature=0.8,
        )
    )

server = AgentServer()
_sessions_started = set()  # guard to avoid multiple sessions per room

@server.rtc_session(agent_name="voice-assistant")
async def my_agent(ctx: agents.JobContext):
    room_id = ctx.room.sid if hasattr(ctx.room, "sid") else getattr(ctx.room, "name", "unknown")
    <http://logger.info|logger.info>(f"Agent entrypoint for room: {room_id}")

    if room_id in _sessions_started:
        <http://logger.info|logger.info>("Agent session already started for this room; skipping duplicate start.")
        return
    _sessions_started.add(room_id)

    session = await create_agent_session()

    # Simple idempotent handlers
    def setup_session_handlers(s):
        if getattr(s, "_handlers_installed", False):
            return
        @s.on("speech_created")
        def _on_speech_created(event: agents.SpeechCreatedEvent):
            <http://logger.info|logger.info>("Agent speech created")
        s._handlers_installed = True

    def setup_room_handlers(room: rtc.Room):
        if getattr(room, "_handlers_installed", False):
            return
        @room.on("track_published")
        def _on_track_published(pub, participant):
            <http://logger.info|logger.info>(f"📤 Track published: {pub.kind} by {participant.identity}")
        @room.on("track_subscribed")
        def _on_track_subscribed(track, publication, participant):
            <http://logger.info|logger.info>(f"📥 Track subscribed: {track.kind} from {participant.identity}")
        room._handlers_installed = True

    setup_session_handlers(session)
    setup_room_handlers(ctx.room)

    await ctx.connect()
    await session.start(room=ctx.room, agent=VoiceAssistant())

    # Wait until session ends naturally
    <http://logger.info|logger.info>("Agent session started; awaiting completion.")
Key server notes: • Use a room-level guard (_sessions_started) so a second agent instance won’t start for the same room. • Mark handler installation on the session/room objects to avoid double-registering event handlers that could cause duplicate logs/actions. Web client (JS) — single publish, single playback
Copy code
<!-- index.html -->
<!doctype html>
<html>
  <body>
    <audio id="remote-audio" autoplay></audio>
    <script type="module">
      import { connect } from "<https://unpkg.com/livekit-client/dist/livekit.esm.js>";

      const url = "<LIVEKIT_WS_URL>";     // server-provided
      const token = "<ACCESS_TOKEN>";     // server-provided

      let localAudioPublished = false;
      const remoteAudioEl = document.getElementById("remote-audio");

      async function joinRoom() {
        const room = await connect(url, token, { autoSubscribe: true });

        // Publish exactly one local audio track
        if (!localAudioPublished) {
          try {
            const localTracks = await LiveKit.createLocalTracks({ audio: true, video: false });
            if (localTracks && localTracks[0]) {
              await room.localParticipant.publishTrack(localTracks[0]);
              localAudioPublished = true;
            }
          } catch (e) {
            console.error("Failed to create/publish local audio:", e);
          }
        }

        // Ensure we attach each subscribed track only once
        room.on("trackSubscribed", (track, publication, participant) => {
          if (track.kind !== "audio") return;
          // Avoid attaching the same MediaStream more than once
          if (remoteAudioEl.srcObject && remoteAudioEl.srcObject.id === track.mediaStreamTrack?.id) {
            console.debug("Remote track already attached.");
            return;
          }
          const stream = new MediaStream([track.mediaStreamTrack]);
          remoteAudioEl.srcObject = stream;
        });

        // Clean up when participant leaves
        room.on("participantDisconnected", (p) => {
          if (room.remoteParticipants.size === 0) {
            remoteAudioEl.srcObject = null;
          }
        });
      }

      joinRoom().catch(console.error);
    </script>
  </body>
</html>
Key client notes: • Publish a single local audio track and guard the publish with
localAudioPublished
to avoid duplicate publishes. • Attach each remote audio track to a single dedicated audio element and check the MediaStreamTrack id to avoid double-attachment. • Use
autoSubscribe: true
so you receive agent audio automatically; the handler just attaches it once. Common pitfalls that cause duplicate voices (and how this code avoids them) • Registering handlers multiple times: we store a flag on session/room to prevent re-registering. • Publishing the same local track multiple times: the client uses a flag and publishes exactly once. • Attaching the same subscribed track to multiple audio elements: we check
srcObject
and the
MediaStreamTrack
id before attaching. • Multiple agent instances joining the same room: server uses a room-id guard to skip duplicate agent startups. If you want, I can: • Provide a version of the client code using your specific LiveKit client bundle and exact import paths. • Show how to integrate token generation endpoints for your Web client. • Add more robust track lifecycle cleanup (handle trackUnsubscribed / participantDisconnected). Which of these would you like next?
r
I want noise cancellation
t
You're asking for noise cancellation—here's a concise, practical way to add it to your existing agent setup so inbound user audio is cleaned and duplicate/ambient voices are reduced. 1) Install the noise-cancellation plugin (Python)
Copy code
uv add "livekit-plugins-noise-cancellation~=0.2"
2) Enable Background Voice Cancellation (BVC) when starting the AgentSession • Add the plugin import and pass
room_options
with
audio_input.noise_cancellation=noise_cancellation.BVC()
so inbound audio into the agent is processed. Example patch for your server code (minimal changes):
Copy code
from livekit.plugins import noise_cancellation, room_io

# ... inside my_agent before session.start(...)
await session.start(
    room=ctx.room,
    agent=VoiceAssistant(),
    room_options=room_io.RoomOptions(
        audio_input=room_io.AudioInputOptions(
            noise_cancellation=noise_cancellation.BVC()
        )
    )
)
3) Notes and best practices • Use noise cancellation on the server/agent OR on the client — not both. Running it twice can distort audio. • Prefer BVC() for voice-AI scenarios where you want to remove background voices and improve STT accuracy. • If you have SIP/telephony callers, consider
BVCTelephony()
for telephony-optimized processing or use a conditional lambda to pick model per participant. • Keep handler registration idempotent (as in the code I previously provided) so you don’t start multiple AgentSessions or register handlers twice. If you want, I can: • Provide a full, patched server.py file with the noise-cancellation import and room options inserted, or • Show how to pick BVC vs BVCTelephony conditionally per participant. Which would you prefer? Sources: Enhanced noise cancellation | LiveKit docs | Voice AI quickstart | LiveKit docs | Turn detection and interruptions | LiveKit Docs