steep-balloon-41261
03/23/2026, 7:10 PMtall-airline-16756
03/23/2026, 7:10 PMtall-airline-16756
03/23/2026, 7:10 PMlk.transcription text stream topic.
• Listen for the user_input_transcribed event inside your AgentSession to receive transcribed text. Final transcripts are indicated by transcript.is_final so you can decide when to persist or display them.
• In client SDKs, register a text stream handler (e.g., registerTextStreamHandler) and check the lk.transcription_final flag to tell interim vs final results.
Frontend/react convenience
• If you use React, use the useTranscriptions hook to receive and render transcriptions in the UI as they arrive.
Saving transcripts for later
• Log final transcripts when you receive transcript.is_final to a file, database, or any storage you use.
• If a user disconnects mid-turn, call await session.aclose() to flush incomplete turns so you don’t lose partial transcripts.
TTS-aligned transcripts with timing (word/sentence timing)
• Enable word/sentence timing by setting use_tts_aligned_transcript=True in AgentSession.
• Implement the transcription_node method to receive TimedString objects that include start_time and end_time.
• Note: word-level timing is supported by specific TTS providers (e.g., Cartesia, ElevenLabs); other providers provide sentence-level alignment.
Observability dashboard (session review and downloads)
• Enable Agent Observability in the LiveKit Cloud Dashboard for sessions you want recorded.
• The dashboard provides synchronized audio playback, full transcripts, turn-by-turn traces, and logs.
• You can download audio, transcripts, and logs directly from a session in the dashboard.
Quick implementation checklist
1. Decide if you need real-time display or post-call storage.
2. Enable STT in your AgentSession and register user_input_transcribed or a text stream handler.
3. Persist transcripts on transcript.is_final to your storage.
4. If you need timing alignment, set use_tts_aligned_transcript=True and use transcription_node.
5. For recorded searchable sessions, enable Agent Observability in the dashboard.
If you want, tell me:
• Which SDK or framework you’re using (browser JS, React, Python agent, etc.), and
• Whether you want realtime display, long-term storage, or time-aligned transcripts.
I’ll give a short code example tailored to your stack.
Sources: Text and transcriptions | LiveKit Documentation | Transcriber | LiveKit Documentation | Pi Zero Transcriber | LiveKit Documentation | How to capture real-time agent transcriptions during conversations | Repeater | LiveKit Documentation, with interactive elementstall-airline-16756
03/23/2026, 7:20 PMuser_input_transcribed events in the AgentSession or register a text stream handler in the client to receive lk.transcription and lk.transcription_final flags. Use interim results for live display and finalize on is_final.
• Persisting transcripts: In the agent, on receiving user_input_transcribed with transcript.is_final, append the final transcript to your conversation_log and write to a file or DB.
• Time-aligned transcripts: Enable use_tts_aligned_transcript=True in your AgentSession and implement transcription_node to receive TimedString objects with `start_time`/`end_time`.
• Flushing incomplete turns: If users disconnect mid-turn, call await session.aclose() to flush partial transcripts before the session ends.
• Dashboard review: Enable Agent Observability/recording in your project to get full session transcripts and downloads from the LiveKit Cloud Dashboard.
Quick Python example (capture + persist final transcripts)
• Add an event handler for transcriptions and save final transcripts into your conversation_log.
# inside your Agent class or entrypoint scope
async def on_user_input_transcribed(session, transcript):
# transcript.is_final indicates final vs interim
text = transcript.transcript
is_final = getattr(transcript, "is_final", False)
# Store interim or final as desired
if is_final:
conversation_log.append({"role": "user", "text": text, "timestamp": time.time()})
# persist to disk (append)
with open("/var/logs/session_transcripts.txt", "a") as f:
f.write(f"{time.time()} USER: {text}\n")
else:
# optional: update live UI/state with interim text
pass
# Register handler after session created
session.register_event_handler("user_input_transcribed", on_user_input_transcribed)
Notes for your current code
• You already create conversation_log = []. Use that list to accumulate final transcript entries and then persist as needed.
• If you need time alignment, set use_tts_aligned_transcript=True when creating AgentSession and add a transcription_node method to receive timed words/sentences.
• Make sure session.start(...) and session.generate_reply(...) are awaited appropriately and the handlers are registered before starting the session so you don’t miss early transcriptions.
Next step Tell me which option you want (real-time display, persistent storage, time-aligned transcripts, or dashboard downloads) and which SDK/framework you’re using for the client (plain Python agent, browser JS, React). I’ll give a short, tailored snippet you can drop into your code.
, with interactive elementstall-airline-16756
03/24/2026, 6:49 AMuser_input_transcribed / transcript.is_final) from the AgentSession.
• Appends final transcript entries to an in-memory list (conversation_log).
• Writes the full conversation to a JSON file when the session ends (or periodically if you prefer).
Example (adapt to your agent/session code)
import json
import time
import os
from pathlib import Path
# where to save transcripts
TRANSCRIPT_DIR = Path("/var/logs/livekit_transcripts")
TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)
def make_session_filename(session_id: str) -> Path:
# create a unique file per session; change naming if you prefer
timestamp = int(time.time())
return TRANSCRIPT_DIR / f"transcript_{session_id}_{timestamp}.json"
# in your agent/session scope:
conversation_log = [] # accumulate final transcript entries
async def on_user_input_transcribed(session, transcript):
"""
Handler for user transcription events.
Called by the AgentSession when user speech is transcribed.
"""
try:
text = transcript.transcript
is_final = getattr(transcript, "is_final", False)
if not text:
return
if is_final:
conversation_log.append({
"role": "user",
"text": text,
"timestamp": time.time()
})
# optional: immediately persist incremental updates to disk
# persist_conversation(session.session_id, conversation_log)
except Exception:
# keep handler robust; don't let exceptions break the session
return
async def on_conversation_item_added(session, item):
"""
Optional: capture when conversation items are committed to history,
including agent replies. Use this if agent outputs are not produced via transcript events.
"""
try:
# example shape — adjust depending on item attributes in your setup
text = getattr(item, "text", None) or getattr(item, "transcript", None)
role = getattr(item, "role", "agent")
if text:
conversation_log.append({
"role": role,
"text": text,
"timestamp": time.time()
})
except Exception:
return
def persist_conversation(session_id: str, log: list):
"""
Write conversation log to JSON file atomically.
"""
filepath = make_session_filename(session_id)
# write to a temp file then rename for atomicity
tmp_path = filepath.with_suffix(".json.tmp")
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump({"session_id": session_id, "items": log}, f, ensure_ascii=False, indent=2)
os.replace(tmp_path, filepath) # atomic on most OSes
# When session ends or periodically:
async def on_session_end(session):
# flush any partial turns before closing
await session.aclose()
# persist the collected final transcripts
persist_conversation(session.session_id, conversation_log)
Integration notes & tips
• Register handlers before starting the session so you don’t miss early events:
◦ session.register_event_handler("user_input_transcribed", on_user_input_transcribed)
◦ session.register_event_handler("conversation_item_added", on_conversation_item_added) (optional)
• Use transcript.is_final to store only finalized text. Interim transcripts are useful for live UI but usually not persisted.
• Call await session.aclose() before persisting to ensure any in-flight speech is flushed to transcription events.
• If multiple sessions run concurrently, use unique filenames (session id + timestamp) as shown. For very high throughput, consider writing to a database or S3 instead of local disk.
• If you want to include time-aligned words/sentences, enable use_tts_aligned_transcript=True and capture TimedString objects in `transcription_node`; you can serialize those into JSON the same way.
If you want, I can:
• Provide a shorter copy-paste-ready snippet tuned to your exact agent framework (show me how you create the AgentSession and I’ll match the handler registration).
• Show an example that flushes and persists to the file after each final transcript (for lower memory usage).
• Show how to store multiple sessions in one rolling JSON file or upload to S3.
Which would you like next?
, with interactive elementstall-airline-16756
03/24/2026, 7:10 AMAgentSession with the actual constructor and arguments your project uses.
• Make sure the transcription and conversation events names match how your AgentSession emits them; this example uses user_input_transcribed and conversation_item_added as described.
• Ensure the process has write permission to the TRANSCRIPT_DIR path.
Complete example
import asyncio
import json
import os
import time
from pathlib import Path
from typing import Any
# === CONFIG ===
TRANSCRIPT_DIR = Path("./livekit_transcripts")
TRANSCRIPT_DIR.mkdir(parents=True, exist_ok=True)
# === Utilities ===
def make_session_filename(session_id: str) -> Path:
timestamp = int(time.time())
safe_id = session_id.replace("/", "_")
return TRANSCRIPT_DIR / f"transcript_{safe_id}_{timestamp}.json"
def persist_conversation(session_id: str, log: list[dict[str, Any]]):
filepath = make_session_filename(session_id)
tmp_path = filepath.with_suffix(".json.tmp")
with open(tmp_path, "w", encoding="utf-8") as f:
json.dump({"session_id": session_id, "items": log}, f, ensure_ascii=False, indent=2)
os.replace(tmp_path, filepath)
print(f"Transcript saved to {filepath}")
# === Transcript capture logic ===
class TranscriptCollector:
def __init__(self, session_id: str):
self.session_id = session_id
self.log: list[dict[str, Any]] = []
async def handle_user_input_transcribed(self, session, transcript):
"""
Expected: transcript has .transcript and .is_final attributes.
Adjust attribute names if your transcript object differs.
"""
try:
text = getattr(transcript, "transcript", None)
is_final = getattr(transcript, "is_final", False)
if not text:
return
if is_final:
entry = {
"role": "user",
"text": text,
"timestamp": time.time()
}
self.log.append(entry)
print(f"[final user] {text}")
else:
# Optional: handle interim text (not persisted here)
interim = text
print(f"[interim user] {interim}")
except Exception as e:
# Keep handler robust; log and continue
print(f"Error in user_input_transcribed handler: {e}")
async def handle_conversation_item_added(self, session, item):
"""
Expected: item may contain agent reply text and role.
Adjust attribute access if your item object structure differs.
"""
try:
# Try common attribute names
text = getattr(item, "text", None) or getattr(item, "transcript", None)
role = getattr(item, "role", None) or "agent"
if text:
entry = {
"role": role,
"text": text,
"timestamp": time.time()
}
self.log.append(entry)
print(f"[{role}] {text}")
except Exception as e:
print(f"Error in conversation_item_added handler: {e}")
async def flush_and_persist(self, session):
# Attempt to flush incomplete turns before persisting
try:
await session.aclose()
except AttributeError:
# If session.aclose not present in your API, skip
pass
except Exception as e:
print(f"Warning: error while closing session: {e}")
persist_conversation(self.session_id, self.log)
# === Example main flow ===
async def main():
# --- Placeholder: create your AgentSession here ---
# Replace the block below with how you create and start your AgentSession.
# The example assumes:
# - `session` is your AgentSession instance
# - it supports register_event_handler(event_name, coroutine_fn)
# - it exposes session.session_id (or similar) for naming files
#
# Example (pseudocode):
# session = AgentSession(agent=MyAgent(), use_tts_aligned_transcript=False, ...)
# await session.start()
#
# For this runnable template, we'll simulate a session object.
class FakeTranscript:
def __init__(self, transcript, is_final):
self.transcript = transcript
self.is_final = is_final
class FakeItem:
def __init__(self, text, role="agent"):
self.text = text
self.role = role
class FakeSession:
def __init__(self, session_id):
self.session_id = session_id
self._handlers = {}
def register_event_handler(self, name, handler):
self._handlers[name] = handler
async def emit(self, name, *args):
h = self._handlers.get(name)
if h:
await h(self, *args)
async def aclose(self):
# simulate flushing
await asyncio.sleep(0.01)
# Create a fake session for demonstration; replace with real session creation
session = FakeSession(session_id="demo-session-123")
# Create collector and register handlers
collector = TranscriptCollector(session.session_id)
session.register_event_handler("user_input_transcribed", collector.handle_user_input_transcribed)
session.register_event_handler("conversation_item_added", collector.handle_conversation_item_added)
# --- In real code you'd start the session and let it run while events come in ---
# For demo: emit some fake events to show how it works
await session.emit("user_input_transcribed", FakeTranscript("hello", is_final=False))
await asyncio.sleep(0.05)
await session.emit("user_input_transcribed", FakeTranscript("hello world", is_final=True))
await session.emit("conversation_item_added", FakeItem("Hi there, how can I help?", role="agent"))
await session.emit("user_input_transcribed", FakeTranscript("I need help with billing", is_final=True))
await session.emit("conversation_item_added", FakeItem("I can help with billing questions.", role="agent"))
# End of session: flush and persist to JSON
await collector.flush_and_persist(session)
if __name__ == "__main__":
asyncio.run(main())
How to adapt this to your real AgentSession
• Replace the FakeSession with the actual AgentSession creation and ensure you register handlers before calling session.start() so you don’t miss early transcriptions.
• If your transcript object uses different field names, adjust getattr(transcript, "...") accordingly.
• This example persists one file per session. If you prefer a single rolling file or uploading to S3/DB, I can provide that variant.
Would you like a version that:
• Immediately writes each final transcript entry to disk (append) to minimize memory use?
• Uses use_tts_aligned_transcript=True and includes timing details in the JSON?
• Uploads transcripts to S3 instead of local disk?
Tell me which and I’ll give that exact variant.
, with interactive elements