steep-balloon-41261
02/13/2026, 8:30 AMtall-airline-16756
02/13/2026, 8:30 AMtall-airline-16756
02/13/2026, 8:30 AMtall-airline-16756
02/13/2026, 8:32 AMgeneration_created) that signals the model started generating. The timeout means that event never arrived.
• In your session config you set tts=None. For voice agents using a realtime model, omitting a TTS instance can break the realtime audio/output pipeline and prevent the expected generation events from being produced or handled.
• The traceback shows the timeout happens inside _realtime_reply_task at the self._rt_session.generate_reply() call, confirming the problem occurs during the realtime generation handshake.
Recommended next steps (prioritized)
1. Add a TTS plugin to the session (do not keep tts=None)
◦ Replace tts=None with a compatible TTS instance (for example, the Google TTS plugin) so the agent has a full audio output path:
session = AgentSession(
llm=google.realtime.RealtimeModel(voice="Zephyr", temperature=0.8, instructions="You are a helpful assistant"),
tts=google.TTS(), # add an appropriate TTS plugin instead of None
vad=vad,
)
• This aligns the TTS/audio chain with realtime generation and is the most likely fix.
1. Keep your setup/join ordering (you already do this correctly)
◦ Your pattern (setup before ctx.connect(), then start the session after join) is correct. No change needed here.
1. Simplify the first generate call for testing
◦ Try a minimal generate_reply to rule out instruction parsing issues:
await session.generate_reply(instructions="Say hello", allow_interruptions=True)
• If this succeeds, iterate back to your original prompt.
1. Validate API connectivity and credentials
◦ Ensure the Google realtime model credentials/environment variables are correct and reachable from the agent process (invalid creds can cause silent failures).
1. (Optional) Increase the generation timeout temporarily for debugging
◦ Only for debugging: increase any client-side timeout around generate_reply to see if the event is delayed rather than missing. Don’t leave this in production.
Diagnostic checks to run and share if the issue persists
• After adding TTS, share a short excerpt of the logs around the next generate_reply attempt (include timestamps). Look for generation_created or related realtime session events.
• Confirm the exact versions of the livekit agents package and the google realtime client you’re using.
• Confirm environment variables or keys used by google.realtime.RealtimeModel are present in the runtime where the agent runs.
If you want, I can:
• Review a follow-up log excerpt after you add TTS.
• Suggest a minimal reproducible example to run locally to verify realtime generation works end-to-end.
Would you like me to review logs after you try adding google.TTS() (or another TTS) and re-run the failing flow?
Sources: examples/voice_agents | livekit/livekit_composite | DeepWikitall-airline-16756
02/13/2026, 9:09 AMtts=None to google.beta.GeminiTTS(...) — that was the right change to provide a full audio output chain for realtime LLMs.
• You prewarm and reuse a Silero VAD in process userdata — this reduces cold-start cost.
• Setup-before-join and session.start after join are correct and safe for real-time flows.
• You added diagnostic hooks (user_input_transcribed, conversation_item_added, speech_created) which will help detect where latency happens.
Possible causes of perceived delay and mitigations
• LLM/Network latency: realtime models still rely on network/API server response. Some delay may be inherent. Mitigation: test latency from your environment to the Google realtime endpoint and sample end-to-end generation times.
• TTS processing: TTS conversion can add time. GeminiTTS may stream audio (if streaming is supported) or may produce audio after generation finishes. To reduce audio start latency:
◦ Prefer streamed generation (if supported by the google.realtime client/plugin). Confirm whether GeminiTTS yields interim audio frames; if yes, enable streaming option.
◦ Keep replies short for initial acknowledgements so audio is produced faster.
• Preemptive generation: you attempted preemptive triggers on interim transcripts which is good. Avoid simultaneous overlapping generate tasks for the same session if you don’t intend concurrency — race conditions can cause inefficiencies. Use allow_interruptions=True as you already do.
• VAD tuning: your thresholds are appropriate for 8 kHz SIP audio. If you see missed speech starts or extra trailing silence, tune activation_threshold and speech_end_silence.
Concrete checks to run
• Run an isolated end-to-end test: capture timestamps for when user_input_transcribed final=true is fired, when generate_reply is invoked, when generation_created (or similar realtime event) arrives, and when the first audio frame is pushed to LiveKit. That will pinpoint where time is spent.
• Verify that Google realtime credentials and region selection are optimized for your deployment (geographic proximity reduces latency).
• Confirm that GeminiTTS instance in your client is using streaming or chunked output if available. If not, consider a TTS that supports streaming or ask for a config option that enables partial audio emission.
2) Small code suggestions / fixes
• Duplicate @session.on("user_input_transcribed") handlers: you defined this handler twice. Consolidate to a single handler to avoid message duplication or unexpected multiple generate calls.
• Masking log: logging the first 5 chars of the API key is fine for diagnostics, but avoid printing secrets even partially in production logs.
• Error handler: in on_error you call session.say(...) to play a custom audio file — ensure that session is still usable at that point (some unrecoverable errors may require session shutdown).
• Use asyncio.create_task consistently and manage created tasks if you need to cancel them during shutdown.
• Consider adding guard logic to avoid spawning multiple concurrent generate_reply calls for the same final transcript (e.g., set a per-user generation lock or ignore duplicate final events within a short window).
3) Make your Python logs match the SIP-style format Your desired format: [container-name] 2026-02-13T081753.075Z DEBUG <module> path/file:line message {json meta...}
You can approximate this style by configuring Python’s logging to include: container name (static), ISO8601 UTC timestamp with millisecond precision, level, logger name/module, file:line, message, and structured JSON extras. Example logging setup to include in your script (add near top where you configure logging):
import logging
import time
import json
from pythonjsonlogger import jsonlogger # optional, pip install python-json-logger
CONTAINER_NAME = "blen-call-to-mpesa-agent-1" # set dynamically if needed
class ContainerFormatter(logging.Formatter):
def formatTime(self, record, datefmt=None):
# ISO8601 UTC with ms and Z
ts = time.gmtime(record.created)
ms = int(record.msecs)
return time.strftime("%Y-%m-%dT%H:%M:%S", ts) + ".%03dZ" % ms
def format(self, record):
# Base message
base = super().format(record)
# Optional: attach JSON metadata if present in record.extra
extra = {}
for k, v in getattr(record, "extra", {}).items():
extra[k] = v
if extra:
return f"[{CONTAINER_NAME}]\n{self.formatTime(record)}\t{record.levelname}\t{record.name}\t{record.filename}:{record.lineno}\t{base}\t{json.dumps(extra)}"
else:
return f"[{CONTAINER_NAME}]\n{self.formatTime(record)}\t{record.levelname}\t{record.name}\t{record.filename}:{record.lineno}\t{base}"
# Configure root logger
handler = logging.StreamHandler()
fmt = "%(message)s" # message only — we build full line in ContainerFormatter
handler.setFormatter(ContainerFormatter(fmt=fmt))
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.handlers = [handler]
Notes on the formatter:
• This produces two-line logs where the first line is the container name and the second line resembles your SIP log with tab separators. If you prefer a single-line format, remove the newline between container and timestamp.
• If you want fully structured logs, use python-json-logger to emit a single JSON object per log line, and include a container field. That fits many log-aggregation systems better.
How to pass structured metadata (the JSON after the message)
• Use the extra parameter of logging calls, and modify the formatter to pick up record.extra (example above uses record.extra).
• Example logging call:
<http://logger.info|logger.info>("ignoring incoming text stream due to no handler for topic", extra={"extra": {"nodeID": node, "callID": call}})
Putting it together — sample formatted output (single-line JSON variant)
• If you prefer a single-line JSON log similar to many SIP logs but with container included, use python-json-logger:
from pythonjsonlogger import jsonlogger
handler = logging.StreamHandler()
json_fmt = jsonlogger.JsonFormatter('%(asctime)s %(levelname)s %(name)s %(filename)s:%(lineno)s %(message)s %(container)s %(extra)s')
handler.setFormatter(json_fmt)
root = logging.getLogger()
root.setLevel(logging.DEBUG)
root.handlers = [handler]
# set container on each record using a filter
class ContainerFilter(logging.Filter):
def filter(self, record):
record.container = CONTAINER_NAME
record.extra = getattr(record, "extra", {})
return True
root.addFilter(ContainerFilter())
4) Next diagnostic steps I can run through with you
• After you enable the improved logs, re-run and capture logs for a single call. Share the logs around:
◦ VAD detected speech start (user_state_changed)
◦ user_input_transcribed final event
◦ when generate_reply is invoked (we can add a debug log right before the call)
◦ any generation_created or LLM/TTS events
◦ first audio frame emitted by the agent or speech_created event
• If you paste the resulting logs here I’ll point exactly which stage is slower and recommend specific micro-optimizations.
Would you like:
• The single-line JSON logger config instead (for easier ingestion into log systems)?
• Me to review a short log sample after you enable this logging and re-run one call?