I’m having a problem. I created a very simple agen...
# ask-ai
d
I’m having a problem. I created a very simple agent running locally in console mode, and it works very well. However, when I stream audio from the front end to LiveKit Cloud, the logs show that the local agent can hear the audio and generate responses, but no sound is played on the front end. How should I debug this issue?
t
Thanks for reaching out, LUC. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I hear you: the agent hears and responds (logs show TTS), but the browser plays no sound. Below are focused debugging steps to find the root cause and fix it. 1) Confirm audio output is enabled on the agent • Ensure you did not set audio output off (e.g.,
audio_enabled=False
in RoomOutputOptions). If audio is disabled the agent will respond in text only. • Ensure TTS is configured for the session or that you explicitly call
session.say()
to produce audio. 2) Verify the agent actually published an audio track • Check agent logs / traces for a TTS node run and confirm it published an output track to the room. • Use Agent Observability (Agent Insights / Trace) to confirm TTS audio was generated and published. 3) Make sure the frontend subscribes and renders the audio track • Add the automatic renderer: include
<RoomAudioRenderer/>
(or
roomaudiorenderer
in your frontend) so room audio is played. • If using hooks, get the agent audio
audioTrack
(e.g., from
useVoiceAssistant
) and attach it to an
<audio>
element or visualizer so it actually plays. 4) Test full WebRTC flow (console vs cloud) • Console mode doesn’t exercise WebRTC publishing/subscription. Reproduce the issue with a frontend connected to LiveKit Cloud to validate publishing/subscription and playback. 5) Check for client-side audio issues • Ensure the browser has audio unmuted and permission to play media. • Confirm no other audio source or background audio player is overriding/muting the agent audio. 6) If you still see TTS generated but no playback, collect these diagnostics • Agent Observability trace showing the TTS node and publish event. • Frontend: whether an audio track is present on remote participant(s) and whether your renderer logs show subscription. • Browser console logs and any errors about autoplay, media, or WebRTC. If you want, share: • A short snippet of how you start the session (agent TTS config /
session.start
/
session.say
calls) • What frontend rendering you use (RoomAudioRenderer, custom
<audio>
, or hook) I’ll point out exactly what to change based on those snippets. Sources: How to Set Up Speech-to-Text Input with Text-Only Output in LiveKit Agents | OpenAI and LiveKit partner to turn Advanced Voice into an API | Playing Audio | LiveKit docs | https://blog.livekit.io/streamline-troubleshooting-with-agent-observability/#/portal/signup| | Agent speech and audio | LiveKit docs
d
Based on the following code, help me check it import asyncio import os from dotenv import load_dotenv from livekit import agents from livekit.agents.voice import AgentSession, Agent from livekit.plugins.silero import VAD from livekit.plugins.deepgram import STT as DeepgramSTT, TTS as DeepgramTTS from livekit.plugins.openai import LLM from livekit import api # Load environment load_dotenv() LIVEKIT_URL = os.getenv("LIVEKIT_URL") LIVEKIT_API_KEY = os.getenv("LIVEKIT_API_KEY") LIVEKIT_API_SECRET = os.getenv("LIVEKIT_API_SECRET") DEEPGRAM_API_KEY = os.getenv("DEEPGRAM_API_KEY") OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") AGENT_NAME = "" ROOM_NAME = "test_room" USER_NAME = "test_user" # ------------------------------- # Custom agent logic # ------------------------------- class Assistant(Agent): def __init__(self): super().__init__( id="voice_agent", instructions="You are a helpful, concise English-speaking assistant." ) # ------------------------------- # Main entrypoint # ------------------------------- async def entrypoint(ctx: agents.JobContext): # Connect to LiveKit Cloud await ctx.connect( # url=LIVEKIT_URL, # api_key=LIVEKIT_API_KEY, # api_secret=LIVEKIT_API_SECRET, ) # Create AgentSession with your models session = AgentSession( stt=DeepgramSTT( api_key=DEEPGRAM_API_KEY, model="nova-2-general", # Deepgram STT model language="en-US", ), llm=LLM( api_key=OPENAI_API_KEY, model="gpt-4o-mini", # LLM model ), tts=DeepgramTTS( api_key=DEEPGRAM_API_KEY, model="aura-asteria-en", # Deepgram TTS voice ), vad=VAD.load(), # Silero VAD for turn detection ) # Print LLM configuration print(f"Session LLM: {session.llm}") print(f"Session STT: {session.stt}") print(f"Session TTS: {session.tts}") print(f"Session VAD: {session.vad}") @session.on("user_input_transcribed") def on_user_input(ev): print(f"[STT] Transcribed: '{ev.transcript}' (final={ev.is_final}, language={ev.language})") @session.on("agent_state_changed") def on_agent_state(ev): print(f"[AGENT] State changed: {ev.old_state} -> {ev.new_state}") @session.on("conversation_item_added") def on_conversation_item(ev): print(f"[CHAT] Item added: {ev.item}") @session.on("speech_created") def on_speech_created(ev): print(f"[SPEECH] Created from '{ev.source}' (user_initiated={ev.user_initiated})") @session.on("track_published") def on_track_published(ev): print(f"[TRACK] Published: kind={ev.track.kind}") @session.on("error") def on_error(ev): print(f"[ERROR] {ev.error}") @session.on("close") def on_close(ev): print(f"[CLOSE] Session closed. Reason: {ev.reason}, Error: {ev.error}") # Start session await session.start( room=ctx.room, agent=Assistant(), ) print("RoomIO created:", session._room_io is not None) print("Room IO:", session._room_io) if session.output.audio is None: print("❌ Audio output NOT attached") else: print("✅ Audio output attached:", type(session.output.audio)) # ------------------------------- # CLI launch # ------------------------------- if name == "__main__": agents.cli.run_app( agents.WorkerOptions( entrypoint_fnc=entrypoint, port=8081, ) ) Here is format of js const { Room, RoomEvent } = window.LivekitClient; let room; // keep global for debugging let audioContext; let analyser; let checkAudioInterval; function detectAudio(stream) { const audioStatus = document.getElementById("audioStatus"); // Create audio context and analyser audioContext = new (window.AudioContext || window.webkitAudioContext)(); analyser = audioContext.createAnalyser(); const source = audioContext.createMediaStreamSource(stream); source.connect(analyser); analyser.fftSize = 256; const bufferLength = analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); // Check audio levels periodically checkAudioInterval = setInterval(() => { analyser.getByteFrequencyData(dataArray); // Calculate average volume const average = dataArray.reduce((a, b) => a + b) / bufferLength; // Show "talking" if volume is above threshold if (average > 10) { audioStatus.classList.add("active"); } else { audioStatus.classList.remove("active"); } }, 100); // Check every 100ms } function stopAudioDetection() { if (checkAudioInterval) { clearInterval(checkAudioInterval); checkAudioInterval = null; } if (audioContext) { audioContext.close(); audioContext = null; } const audioStatus = document.getElementById("audioStatus"); audioStatus.classList.remove("active"); } function attachAudioTrack(track, participant) { if (track.kind !== "audio") return; const audio = document.createElement("audio"); audio.autoplay = true; audio.playsInline = true; const stream = new MediaStream([track.mediaStreamTrack]); audio.srcObject = stream; document.body.appendChild(audio); console.log("🔊 Audio from", participant.identity); // Start audio detection detectAudio(stream); } function toggleButtons(connected) { const joinBtn = document.getElementById("joinBtn"); const disconnectBtn = document.getElementById("disconnectBtn"); if (connected) { joinBtn.classList.add("hidden"); disconnectBtn.classList.remove("hidden"); } else { joinBtn.classList.remove("hidden"); disconnectBtn.classList.add("hidden"); } } document.getElementById("joinBtn").onclick = async () => { const tokenServerUrl = document.getElementById("tokenServerUrl").value.trim(); const roomName = document.getElementById("roomName").value.trim(); const userName = document.getElementById("userName").value.trim(); const status = document.getElementById("status"); if (!tokenServerUrl) { status.innerText = "Missing Token Server URL"; return; } try { // Fetch token from API status.innerText = "Fetching token..."; console.log("🔑 Fetching token from server..."); const apiUrl = `${tokenServerUrl}/api/token?room=${encodeURIComponent(roomName)}&user=${encodeURIComponent(userName)}`; const response = await fetch(apiUrl); if (!response.ok) { throw new Error(
Failed to fetch token: ${response.statusText}
); } const data = await response.json(); if (!data.success) { throw new Error(data.error || "Failed to get token"); } console.log("✅ Token received"); console.log("📍 LiveKit URL:", data.url); console.log("🏠 Room:", data.room); console.log("👤 User:", data.user); // Connect to LiveKit status.innerText = "Connecting..."; room = new Room({ adaptiveStream: true, dynacast: true }); await room.connect(data.url, data.token, { autoSubscribe: true }); status.innerText = "Connected"; console.log("✅ Connected to room"); // Hide Join button, show Disconnect button toggleButtons(true); await room.localParticipant.setMicrophoneEnabled(true); console.log("🎤 Mic enabled"); room.on(RoomEvent.TrackSubscribed, (track, pub, participant) => { attachAudioTrack(track, participant); }); // Handle disconnection events room.on(RoomEvent.Disconnected, () => { status.innerText = "Disconnected"; console.log("❌ Disconnected from room"); toggleButtons(false); stopAudioDetection(); }); } catch (e) { console.error(e); status.innerText = "Connection failed"; } }; document.getElementById("disconnectBtn").onclick = async () => { const status = document.getElementById("status"); if (room) { status.innerText = "Disconnecting..."; console.log("🔌 Disconnecting from room"); await room.disconnect(); status.innerText = "Disconnected"; console.log("✅ Disconnected successfully"); // Show Join button, hide Disconnect button toggleButtons(false); // Stop audio detection stopAudioDetection(); room = null; } };
t
Thanks — this is helpful. I checked both snippets and the most likely cause is that your agent never connects to LiveKit Cloud, so it never publishes an audio track to the room. Below are concise checks and fixes tied to your code. 1) Un-comment and call ctx.connect() • In entrypoint you commented out:
Copy code
# await ctx.connect(
#     # url=LIVEKIT_URL,
#     # api_key=LIVEKIT_API_KEY,
#     # api_secret=LIVEKIT_API_SECRET,
# )
• Fix: call it with your env vars:
Copy code
await ctx.connect(
    url=LIVEKIT_URL,
    api_key=LIVEKIT_API_KEY,
    api_secret=LIVEKIT_API_SECRET,
)
Reason: the worker must connect to LiveKit to be dispatched to rooms and publish audio. 2) Confirm RoomIO and audio output after session.start • Right after
await session.start(...)
ensure these printouts show audio attached:
Copy code
print("RoomIO created:", session._room_io is not None)   # should be True
print("Audio output attached:", session.output.audio is not None)  # should be True
If audio is None, TTS or session init failed. 3) Ensure TTS is actually emitted • Either explicitly call
await session.say("...")
to force TTS during testing, or confirm your agent logic emits speech events and the session has
tts=DeepgramTTS(...)
(you already do). Confirm a TTS node appears in Agent Observability / traces. 4) Frontend subscription & rendering checks (your JS is mostly correct) • Add participant and track logs to confirm agent joined and published:
Copy code
room.on(RoomEvent.ParticipantConnected, p => console.log("Participant:", p.identity));
room.on(RoomEvent.TrackSubscribed, (track, pub, participant) => {
  console.log("TrackSubscribed:", { kind: track.kind, source: pub.source, participant: participant.identity });
});
• Your attachAudioTrack creating an <audio> element is fine. Confirm the console shows an agent participant and an audio track from that participant. 5) Browser autoplay / audio permissions • Ensure microphone/camera permissions, that tab is not muted, and no autoplay policy blocks audio. You set
autoplay
and
playsInline
correctly. 6) Useful diagnostics to collect if still silent • Backend: output of the prints from step (2) and any
speech_created
/
track_published
logs from your session handlers. • Frontend: console logs from the added Participant & TrackSubscribed listeners and any autoplay/media errors. If you want, paste: • the console output after uncommenting
ctx.connect()
and starting the agent • the frontend console logs for ParticipantConnected and TrackSubscribed I’ll review those and point out the exact next step.
d
why did I got this error ? Traceback (most recent call last): File "C:\Users\LucNM\AppData\Local\miniconda3\envs\voice-core\lib\site-packages\opentelemetry\util\_decorator.py", line 71, in async_wrapper return await func(*args, **kwargs) # type: ignore File "C:\Users\LucNM\AppData\Local\miniconda3\envs\voice-core\lib\site-packages\livekit\agents\ipc\job_proc_lazy_main.py", line 286, in _traceable_entrypoint await self._job_entrypoint_fnc(job_ctx) File "C:\tmp\02_Voice\02_Research\01_LiveKit\voice_agent.py", line 43, in entrypoint await ctx.connect( TypeError: JobContext.connect() got an unexpected keyword argument 'url' Here code I used await ctx.connect( url=LIVEKIT_URL, api_key=LIVEKIT_API_KEY, api_secret=LIVEKIT_API_SECRET, )
t
Thanks — I see why the TypeError happened. You're passing connection credentials to
ctx.connect()
, but
JobContext.connect()
does not accept
url
,
api_key
, or
api_secret
. The JobContext already contains the connection details when the job is dispatched, so calling
ctx.connect()
with those keywords raises the error you saw. What to do: 1. Replace your call with a no-arg (or only optional) call:
Copy code
await ctx.connect()
or include only supported options such as `auto_subscribe`:
Copy code
from livekit.agents import AutoSubscribe

await ctx.connect(auto_subscribe=AutoSubscribe.AUDIO_ONLY)
1. Keep credentials in your environment (you already have env vars). Do not pass them into
ctx.connect()
. 2. If you need to run the agent locally outside of dispatch (for testing), run the worker/CLI so the JobContext is created correctly (agents.cli.run_app) — then use
await ctx.connect()
as above. If you want, paste your updated entrypoint and I’ll confirm it or suggest the exact
auto_subscribe
setting to use. Sources: Job lifecycle | LiveKit docs | OpenAI and LiveKit partner to turn Advanced Voice into an API | Agent server options | LiveKit docs | An open source stack for real-time multimodal AI