damp-toothbrush-11775
12/30/2025, 3:21 PMtall-airline-16756
12/30/2025, 3:21 PMtall-airline-16756
12/30/2025, 3:21 PMdamp-toothbrush-11775
12/30/2025, 3:23 PMtall-airline-16756
12/30/2025, 3:24 PMdamp-toothbrush-11775
12/30/2025, 3:24 PMtall-airline-16756
12/30/2025, 3:25 PMuser_started_speaking events.
b. LLM transcript analyzer: collects the initial seconds of audio (or a short transcript) and asks an LLM if the answer is a human or voicemail greeting.
• Decision logic: silence detection triggers a preliminary “possible voicemail” state after a threshold (recommended 5s). If preliminary triggered, run LLM check on the initial transcript (or first N seconds) to confirm. On confirmation, run voicemail flow (leave message & hang up). If LLM indicates human, continue call.
• Hangup: use room disconnect/delete_room to cleanly end the session.
Recommended thresholds and tradeoffs (real‑time)
• Silence threshold: 5 seconds for initial detection.
◦ Tradeoff: shorter (2–3s) detects faster but increases false positives on humans who pause; longer (8–10s) reduces false positives but delays action and increases cost.
• Transcript window: capture first 3–7 seconds of audio for LLM analysis.
◦ Tradeoff: shorter windows are faster and cheaper but may miss clear greeting cues; longer windows improve LLM accuracy but add latency/cost.
• LLM confidence policy: require both silence + LLM confidence (or specific keyword patterns like “you’ve reached”, “leave a message”) to confirm voicemail.
◦ Tradeoff: stricter policies reduce false positives but increase missed voicemails.
• Retry / grace: allow one short grace period (1–2s) after LLM check to catch late speech before hanging up.
• Cost vs accuracy: LLM checks add compute cost and latency. For high-volume real-time systems, prefer a lightweight text-based classifier or keywords plus LLM only on ambiguous cases.
How to implement in Python (step-by-step)
1. Instrument speech activity
◦ Listen for user_started_speaking (or equivalent event) and update user_last_spoke_time = time.time() when fired.
1. Start a background monitor task
◦ Every 0.5–1s check: if time.time() - user_last_spoke_time >= SILENCE_THRESHOLD and call is in “post-pickup” phase, mark preliminary voicemail candidate and trigger LLM analysis.
1. Capture initial audio / transcript
◦ Either request a short audio buffer (first 3–7s) or obtain real‑time transcription of initial speech. Send that transcript (or a short summary) to the LLM.
1. LLM analysis
◦ Ask a short prompt: “Is this a voicemail greeting or a live human response? Reply: voicemail/human and confidence score. Highlight phrases that indicate voicemail.” Include transcript text and request brief justification.
◦ If LLM returns “voicemail” with sufficient confidence OR contains voicemail keywords, proceed to voicemail flow.
1. Voicemail flow
◦ Optionally generate/record a short callback message via the agent (use TTS or pre-recorded message).
◦ Wait 0.5–2s to ensure message playback completes.
◦ Call your hangup routine.
1. Hangup routine
◦ Use LiveKit API to disconnect participants / delete the room (e.g., ctx.api.room.delete_room(...)) to end the call cleanly.
1. Safety / logging
◦ Log decisions (silence timestamp, transcript, LLM result, final action) for tuning thresholds and auditing false positives.
◦ Implement metrics for false positive/negative rates, average detection time, and LLM cost.
Minimal code pattern (pseudocode outline)
SILENCE_THRESHOLD = 5
TRANSCRIPT_WINDOW = 5 # seconds
user_last_spoke_time = time.time()
voicemail_candidate = False
async def on_user_started_speaking(event):
global user_last_spoke_time, voicemail_candidate
user_last_spoke_time = time.time()
voicemail_candidate = False
async def silence_monitor(ctx):
while ctx.call_active:
if time.time() - user_last_spoke_time >= SILENCE_THRESHOLD and not voicemail_candidate:
voicemail_candidate = True
transcript = await capture_initial_transcript(ctx, seconds=TRANSCRIPT_WINDOW)
llm_result = await analyze_transcript_with_llm(transcript)
if is_voicemail(llm_result):
await leave_message_and_hangup(ctx)
else:
voicemail_candidate = False
await asyncio.sleep(0.5)
Points to tune and test
• Start with SILENCE_THRESHOLD = 5s and TRANSCRIPT_WINDOW = 3–5s. Run A/B tests to measure false positives and detection latency.
• If traffic is high, only call the LLM for transcripts that contain likely voicemail keywords to reduce cost.
• Add a short grace period after LLM-confirmed voicemail detection before hangup to ensure message playback isn't cut off.
I can provide:
• A runnable Python example that hooks into LiveKit events and an LLM prompt template, or
• A ready-to-run snippet showing how to capture audio, get a transcript, call the LLM, and delete the room.
Which would you like next?
Sources: examples/voice_agents | livekit/livekit_composite | DeepWikidamp-toothbrush-11775
12/30/2025, 3:33 PMtall-airline-16756
12/30/2025, 3:33 PMuser_started_speaking events (or equivalent), can capture/stream audio or receive transcripts, and can call an LLM (or an LLM API wrapper).
• You have access to LiveKit server API to delete rooms or disconnect participants.
Runnable example (async Python — replace placeholder functions with your app’s implementations)
import asyncio
import time
from typing import Optional
SILENCE_THRESHOLD = 5.0 # seconds before we consider voicemail candidate
TRANSCRIPT_WINDOW = 5.0 # seconds of initial audio to transcribe/send to LLM
GRACE_PERIOD = 1.0 # wait before hangup to ensure message playback
# --- Replace these with your implementations ---
async def capture_initial_transcript(ctx, seconds: float) -> str:
"""
Collect the first `seconds` seconds of audio or transcript from the remote party.
If you have streaming transcription, grab that buffer; otherwise request a short recording.
"""
# Example placeholder: return existing buffer or call your transcription service
return await ctx.get_transcript(seconds)
async def call_llm_classify(transcript: str) -> dict:
"""
Call your LLM. Return a dict like {"label":"voicemail"/"human", "confidence":0.85, "reason":"matched 'you've reached'"}
"""
# Placeholder: implement actual HTTP/gRPC call to your LLM and parse result
return await your_llm_client.classify_voicemail(transcript)
async def hangup_call(ctx):
"""
Use LiveKit API to end the call / delete room.
"""
# Example placeholder: call LiveKit admin API to delete the room
await ctx.api.room.delete_room(room=ctx.room.name)
# --- End replaceable functions ----------------------------------------
class AMDController:
def __init__(self, ctx):
self.ctx = ctx
self.user_last_spoke = time.time()
self.voicemail_candidate = False
self._monitor_task: Optional[asyncio.Task] = None
self.call_active = True
async def start(self):
# Start monitor loop
self._monitor_task = asyncio.create_task(self._silence_monitor())
async def stop(self):
self.call_active = False
if self._monitor_task:
self._monitor_task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await self._monitor_task
async def on_user_started_speaking(self, _event=None):
# Call this when you receive user_started_speaking event
self.user_last_spoke = time.time()
self.voicemail_candidate = False
async def _silence_monitor(self):
# Loop checks for silence and triggers LLM classification
while self.call_active and self.ctx.call_connected:
elapsed = time.time() - self.user_last_spoke
if elapsed >= SILENCE_THRESHOLD and not self.voicemail_candidate:
self.voicemail_candidate = True
# Capture transcript for the initial window
transcript = await capture_initial_transcript(self.ctx, TRANSCRIPT_WINDOW)
# Optional quick keyword heuristic to avoid LLM costs
if self._keyword_heuristic(transcript):
llm_result = {"label": "voicemail", "confidence": 0.99, "reason": "keywords"}
else:
llm_result = await call_llm_classify(transcript)
if self._is_voicemail(llm_result):
# Optionally leave a short message or TTS here before hangup
await asyncio.sleep(GRACE_PERIOD)
await hangup_call(self.ctx)
self.call_active = False
return
else:
# false alarm — reset and continue monitoring
self.voicemail_candidate = False
self.user_last_spoke = time.time()
await asyncio.sleep(0.5)
def _keyword_heuristic(self, transcript: str) -> bool:
if not transcript:
return False
lowered = transcript.lower()
keywords = ["you've reached", "leave a message", "please leave", "no one is available", "voicemail"]
return any(k in lowered for k in keywords)
def _is_voicemail(self, llm_result: dict) -> bool:
label = llm_result.get("label", "").lower()
confidence = float(llm_result.get("confidence", 0.0))
# Require silence + LLM label "voicemail" with threshold confidence
return label == "voicemail" and confidence >= 0.7
How to integrate this into your existing LiveKit agent
1. Hook event handlers
◦ When your agent connects to a room and call is active, create an AMDController(ctx) instance and call await amd.start().
◦ Wire your existing user_started_speaking (or VAD) event handler to call await amd.on_user_started_speaking(event).
◦ Ensure ctx.get_transcript(seconds) or your equivalent transcription call captures the initial audio buffer.
1. Provide implementations for the placeholder functions
◦ `capture_initial_transcript`: if you have streaming STT (e.g., WebSocket or service callbacks), keep a ring buffer of the first N seconds after pickup. Otherwise request a short recorded snippet from your telephony integration.
◦ `call_llm_classify`: implement a small prompt that asks the LLM to return a simple JSON with label/confidence/reason. Example prompt: "Given this transcript, reply with JSON {label: 'voicemail'|'human', confidence:0-1, reason: '...'}. Transcript: '...'"
◦ `hangup_call`: call the LiveKit admin API to delete the room or use your existing call-end logic.
1. Tune thresholds and fallbacks
◦ Start with the constants shown (SILENCE_THRESHOLD=5, TRANSCRIPT_WINDOW=5, confidence >= 0.7).
◦ Add the keyword heuristic first to reduce LLM calls; only call LLM when keywords absent or ambiguous.
◦ Add logging/metrics for decisions so you can iterate.
1. Safety and UX
◦ Add a short grace period (GRACE_PERIOD) before hangup to ensure any agent playback or TTS completes.
◦ Consider playing a short identification message before hanging up so you capture voicemail intent if needed.
◦ Ensure your hangup routine cleans up resources and records the outcome.
Testing checklist
• Simulate live human (longer back-and-forth) and verify AMD does not trigger.
• Simulate voicemail greeting with keywords and verify hangup after detection.
• Test edge cases: very short human response, delayed greeting, noisy lines.
• Track false-positive and false-negative counts and iterate thresholds.
If you want, I can:
• Provide a concrete LLM prompt template and example response parser.
• Convert the placeholders into a full example using a specific STT or LLM client you use (mention which ones).
Which would you like next?