wooden-smartphone-55787
02/20/2026, 10:35 AMtall-airline-16756
02/20/2026, 10:35 AMtall-airline-16756
02/20/2026, 10:35 AMrtc.AudioStream, but that alone only yields raw audio frames — it does not produce text.
What you can do next (two recommended approaches):
1) Use LiveKit Agents (recommended)
• LiveKit Agents can subscribe to audio, run an STT model (DeepGram, AssemblyAI, Gladia, Whisper, etc.), and emit transcription events you can print.
• High level:
a. Run an Agent that uses an STT plugin (for example deepgram.STT()).
b. The Agent subscribes to room audio and raises transcription events (e.g., user_input_transcribed or similar).
c. Print transcripts from that event handler when the transcription is final.
• This avoids you handling raw frame conversion, encoding, or streaming protocols yourself.
2) Send raw audio frames to an external STT service from your code
• Keep your @room.on("track_subscribed") handler and, when the track is audio, create an async task that reads frames from rtc.AudioStream(track).
• For each audio frame, send frames (or encoded chunks) to an STT API that supports streaming (DeepGram, AssemblyAI, etc.).
• Listen for transcription messages from that STT service and print them when received.
Minimal example outline for option #2 (conceptual):
1. In on_track_subscribed start a background task:
◦ asyncio.create_task(process_audio(track))
1. `process_audio(track)`:
◦ audio_stream = rtc.AudioStream(track)
◦ async for frame in audio_stream:
▪︎ Convert/encode frame as required by your STT provider (PCM, 16-bit little-endian, sample rate, etc.).
▪︎ Push the encoded bytes to the STT service’s streaming API or accumulate and send in chunks.
1. Handle STT responses (websocket or HTTP callbacks) and print final transcripts.
Questions so I can give a concrete snippet you can run
• Do you want to run transcription in a LiveKit Agent, or directly from this Python client/backend?
• Which STT provider would you prefer (DeepGram, AssemblyAI, Gladia, OpenAI/Whisper, or something else)?
• Do you need help with audio frame encoding (sample rate, channel layout) for your chosen STT provider?
If you want the Agent route, I can provide a short working agent example using DeepGram (including the event handler that prints final transcripts). If you prefer direct client-side streaming, tell me which STT API and I’ll show an example that takes rtc.AudioStream frames and sends them to that service.
Sources: Transcriber | LiveKit Documentation | Speech-to-text (STT) models overview | LiveKit Documentationwooden-smartphone-55787
02/20/2026, 10:36 AMtall-airline-16756
02/20/2026, 10:36 AMimport asyncio
import os
from livekit.agents import AgentSession, JobContext, server
from livekit.plugins import deepgram, silero
# Ensure environment variables:
# LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, DEEPGRAM_API_KEY
@server.rtc_session(agent_name="transcriber")
async def entrypoint(ctx: JobContext):
# create an AgentSession that uses Deepgram STT and optional VAD
session = AgentSession(
stt=deepgram.STT(api_key=os.environ.get("DEEPGRAM_API_KEY")),
vad=silero.VAD.load()
)
# Listen for transcriptions from the AgentSession
@session.on("user_input_transcribed")
def on_transcribed(transcript):
# transcript.is_final indicates the final result for an utterance
if getattr(transcript, "is_final", False):
print(f"[Transcription] {transcript.transcript}")
# Start session attached to the same room the agent will join
await session.start(agent=None, room=ctx.room)
await ctx.connect()
if __name__ == "__main__":
# Run the agent server (this process will host the agent and connect to LiveKit)
# The livekit-agents server entrypoint runs automatically; the decorator registers the session.
# Use the agent runner/manager recommended by livekit-agents to run this file.
print("Start your LiveKit Agents runner to host this transcriber session.")
Notes:
• This example uses livekit-agents plugin helpers; the AgentSession will subscribe to user audio automatically and call user_input_transcribed when Deepgram returns text.
• Print statements in on_transcribed output final transcripts.
Option B — Client-side: read rtc.AudioStream frames and stream to Deepgram Realtime
• Installs:
pip install livekit-sdk deepgram-sdk websockets soundfile aiohttp
• This example connects to LiveKit, subscribes to remote audio, converts frames to 16-bit PCM mono 16kHz if needed, and streams to Deepgram Realtime via WebSocket. It prints partial/final transcriptions returned by Deepgram.
• client_transcribe.py:
import asyncio
import json
import logging
import os
import websockets
import struct
from signal import SIGINT, SIGTERM
from typing import Union
from livekit import api, rtc
# Environment variables:
LIVEKIT_URL = os.environ.get("LIVEKIT_URL", "<https://your-livekit-url>")
API_KEY = os.environ.get("LIVEKIT_API_KEY", "YOUR_KEY")
API_SECRET = os.environ.get("LIVEKIT_API_SECRET", "YOUR_SECRET")
DEEPGRAM_API_KEY = os.environ.get("DEEPGRAM_API_KEY", "YOUR_DEEPGRAM_KEY")
logging.basicConfig(level=<http://logging.INFO|logging.INFO>, handlers=[logging.StreamHandler()])
async def deepgram_realtime(ws_url: str):
"""
Opens a Deepgram realtime WebSocket. Returns the websocket.
We will send raw PCM16 little-endian 16k samples to the ws.
"""
headers = [("Authorization", f"Token {DEEPGRAM_API_KEY}")]
ws = await websockets.connect(ws_url, extra_headers=headers)
return ws
async def send_pcm_to_deepgram(ws, pcm_bytes: bytes):
"""
Deepgram realtime expects JSON messages for config and binary for audio.
Here we send raw binary audio frames.
"""
try:
await ws.send(pcm_bytes)
except Exception:
pass
async def deepgram_recv_loop(ws):
async for msg in ws:
try:
data = json.loads(msg)
except Exception:
continue
# Deepgram returns interim and final transcripts as JSON
if "channel" in data and "alternatives" in data["channel"]:
alt = data["channel"]["alternatives"][0]
transcript = alt.get("transcript", "")
is_final = data.get("is_final", False) or data.get("type") == "transcript_final"
print(f"[Deepgram {'FINAL' if is_final else 'PARTIAL'}] {transcript}")
async def process_audio_track(track: rtc.Track):
"""
Reads frames from rtc.AudioStream and forwards to Deepgram WS.
You must ensure the audio format matches Deepgram's expectations (PCM16 16000Hz mono).
This example assumes frames are PCM float32 samples. If not, adapt accordingly.
"""
audio_stream = rtc.AudioStream(track)
# Open a new Deepgram realtime connection per track (or reuse one)
ws = await deepgram_realtime("<wss://api.deepgram.com/v1/listen?encoding=linear16&sample_rate=16000&channels=1>")
# Start a receiving task
asyncio.create_task(deepgram_recv_loop(ws))
async for frame in audio_stream:
# `frame` structure depends on the SDK; convert to 16-bit PCM little-endian.
# Example conversion if frame.samples is a bytes-like PCM16 already:
# pcm_bytes = frame.data
# If frame provides floats, convert float32 -> int16:
# Here we attempt to be generic:
try:
# Try float buffer conversion
samples = frame.samples # may be an ndarray or bytes
# If samples is a bytes buffer of float32:
if isinstance(samples, bytes):
# Interpret as float32 little-endian
count = len(samples) // 4
floats = struct.unpack("<" + "f" * count, samples)
ints = [max(-32768, min(32767, int(s * 32767))) for s in floats]
pcm_bytes = struct.pack("<" + "h" * len(ints), *ints)
else:
# fallback: assume it's already PCM bytes
pcm_bytes = bytes(samples)
except Exception:
# fallback attempt
pcm_bytes = frame.data if hasattr(frame, "data") else b""
if not pcm_bytes:
continue
await send_pcm_to_deepgram(ws, pcm_bytes)
# close ws when done
await ws.close()
async def main(room: rtc.Room):
# Create LiveKit access token
metadata = json.dumps({"is_preview": True, "bot_external_id": "AIBOT-43D15653C7"})
token = (
api.AccessToken(API_KEY, API_SECRET)
.with_identity("python-bot")
.with_name("Python Bot")
.with_grants(api.VideoGrants(room_join=True, room="my-room"))
.with_metadata(metadata)
.to_jwt()
)
await room.connect(LIVEKIT_URL, token)
<http://logging.info|logging.info>(f"connected to room {room.name}")
# Subscribe to audio tracks and send to Deepgram
@room.on("track_subscribed")
def on_track_subscribed(track: rtc.Track, publication, participant):
<http://logging.info|logging.info>("track_subscribed: %s kind=%s from %s", publication.sid, track.kind, participant.identity)
if track.kind == rtc.TrackKind.KIND_AUDIO:
asyncio.create_task(process_audio_track(track))
# keep the client running
await asyncio.Event().wait()
if __name__ == "__main__":
loop = asyncio.get_event_loop()
room = rtc.Room(loop=loop)
async def cleanup():
await room.disconnect()
loop.stop()
asyncio.ensure_future(main(room))
for sig in (SIGINT, SIGTERM):
loop.add_signal_handler(sig, lambda: asyncio.ensure_future(cleanup()))
try:
loop.run_forever()
finally:
loop.close()
Important notes for Option B:
• You must ensure the audio frames provided by rtc.AudioStream are converted to the expected PCM format (16-bit little-endian, 16000Hz, mono) for Deepgram. The example includes a generic conversion attempt but you’ll likely need to adapt it depending on how the SDK exposes frame samples.
• Using Deepgram realtime requires sending initial config query params (shown in the ws URL) and authenticating with your API key.
• Deepgram enforces rate/format limits. For production, reuse sockets or batch frames carefully.
Which option would you like as a final, more detailed runnable snippet?
• I can expand Option A into a fully runnable agent script with exact livekit-agents runner instructions, or
• I can tighten Option B to match the precise frame format produced by your environment (tell me whether frames are float32 arrays, PCM16 bytes, sample rate, and channels).