narrow-chef-71655
12/29/2025, 5:50 AMtall-airline-16756
12/29/2025, 5:51 AMtall-airline-16756
12/29/2025, 5:51 AMuv add "livekit-agents[baseten]~=1.3"
Replace baseten with the provider you choose.
2) Add credentials
• Put the provider API key in your environment, e.g.:
BASETEN_API_KEY=<your-api-key>
DEEPGRAM_API_KEY=<your-api-key>
ASSEMBLYAI_API_KEY=<your-api-key>
3) Use the STT plugin in an AgentSession (WebSocket endpoint required for Baseten)
• Example (Baseten via WebSocket):
from livekit.plugins import baseten
from livekit.agents import AgentSession
session = AgentSession(
stt=baseten.STT(
model_endpoint="wss://<your-model-id>.<http://api.baseten.co/v1/websocket|api.baseten.co/v1/websocket>",
),
# other params: llm, tts, etc.
)
• The model_endpoint must be a wss:// URL for real-time streaming.
4) Standalone streaming pattern (works for Deepgram, AssemblyAI, etc.)
• Example pattern to pipe audio frames to an STT stream and read transcripts:
import asyncio
from livekit import rtc
from livekit.plugins import deepgram # substitute provider plugin
async def process_track(track: rtc.RemoteTrack):
stt = deepgram.STT(model="your_model_name")
stt_stream = stt.stream()
audio_stream = rtc.AudioStream(track)
async def handle_transcript():
async for event in stt_stream:
if event.is_final:
print("Transcript:", event.alternatives[0].text)
asyncio.create_task(handle_transcript())
async for audio_event in audio_stream:
stt_stream.push_frame(audio_event.frame)
stt_stream.end_input()
5) Real-time features and tuning
• Interim (partial) and final transcripts are supported by providers like Deepgram and AssemblyAI.
• Turn detection and silence thresholds can be configured (example with AssemblyAI in an AgentSession):
session = AgentSession(
turn_detection="stt",
stt=assemblyai.STT(
end_of_turn_confidence_threshold=0.4,
min_end_of_turn_silence_when_confident=400,
max_turn_silence=1280,
),
)
If you tell me which STT provider you want to use (Baseten, Deepgram, AssemblyAI, Google, Azure, etc.) and whether you’ll run STT inside an AgentSession or as a standalone stream, I’ll provide a tailored code example and the exact install/auth steps.
Sources: Baseten STT plugin guide | LiveKit docs | Speech-to-text (STT) models | LiveKit Docsnarrow-chef-71655
12/29/2025, 5:58 AMasync with websockets.connect(ws_url) as ws:
print("Connected to WebSocket")
# Send handshake
handshake = {
"type": "handshake",
"api_key": api_key,
"options": {
"silence_timeout": 30.0,
"sample_rate": sample_rate,
"min_silence_duration_ms": 50,
"min_speech_ms": 50,
"vad_threshold": 0.6,
"eos_enabled": True,
"eos_threshold": 0.6,
"client_logging": True
}
}
await ws.send(json.dumps(handshake))
ack = await ws.recv()
ack_data = json.loads(ack)
if ack_data.get("type") != "handshake_ack":
print(f"Auth failed: {ack_data.get('error')}")
return
print("✓ Connected and authenticated")
print("Streaming audio...")
# Run send and receive concurrently
send_task = asyncio.create_task(
send_wav_chunks(ws, wf, frames_per_chunk, sample_rate)
)
receive_task = asyncio.create_task(receive_results(ws))
await asyncio.gather(send_task, receive_task)
async def send_wav_chunks(ws, wf, frames_per_chunk, sample_rate):
"""Read and send WAV chunks to the server"""
try:
while True:
frames = wf.readframes(frames_per_chunk)
if not frames:
print("\nFinished streaming audio.")
await ws.send(json.dumps({"type": "end_of_stream"}))
break
await ws.send(frames)
await asyncio.sleep(frames_per_chunk / sample_rate) # simulate real-time pacing
except Exception as e:
print(f"Send error: {e}")
async def receive_results(ws):
"""Receive transcription/diarization results"""
try:
while True:
msg = await ws.recv()
data = json.loads(msg)
if data.get("type") == "transcription":
result = data.get("data", {})
print(f"\n data: {data}")
print(f"\n✓ {result.get('transcription')}")
print(f" Gender: {result.get('gender')} | EOS: {result.get('eos', {}).get('prediction'):.2f}")
elif data.get("type") == "log":
print(f" [Log] {data.get('data')}")
elif data.get("type") == "error":
print(f"\n✗ Error: {data.get('error')}")
break
except Exception as e:
print(f"Receive error: {e}")tall-airline-16756
12/29/2025, 5:59 AMTryHamsaSTT) that implements LiveKit’s `SpeechToTextEngine`/`SpeechStream` interface.
• Reuse your handshake/send/receive logic inside that stream class.
• Implement push_frame(frame) to send audio bytes to TryHamsa and make the stream yield LiveKit `SpeechEvent`s for interim/final transcripts.
• Use the class in AgentSession(stt=TryHamsaSTT(...)) or call stream() directly for standalone usage.
Minimal template (adapt your sample into this)
import asyncio, json, websockets, logging
from livekit.agents import stt
from livekit import rtc
class TryHamsaSTT(stt.SpeechToTextEngine):
def __init__(self, api_key: str, ws_url: str, sample_rate: int = 16000):
self.api_key = api_key
self.ws_url = ws_url
self.sample_rate = sample_rate
super().__init__()
def stream(self, *, language: str = "", cancellation_event: asyncio.Event = None):
return TryHamsaStream(self.api_key, self.ws_url, self.sample_rate, cancellation_event)
class TryHamsaStream(stt.SpeechStream):
def __init__(self, api_key, ws_url, sample_rate, cancellation_event=None):
self.api_key = api_key
self.ws_url = ws_url
self.sample_rate = sample_rate
self.cancellation_event = cancellation_event or asyncio.Event()
self._ws = None
self._queue = asyncio.Queue()
self._closed = False
async def _connect(self):
self._ws = await websockets.connect(self.ws_url)
handshake = {
"type": "handshake",
"api_key": self.api_key,
"options": {"sample_rate": self.sample_rate}
}
await self._ws.send(json.dumps(handshake))
ack = json.loads(await self._ws.recv())
if ack.get("type") != "handshake_ack":
raise Exception(f"Auth failed: {ack.get('error')}")
asyncio.create_task(self._receive_results())
async def _receive_results(self):
try:
async for msg in self._ws:
if self.cancellation_event.is_set():
break
data = json.loads(msg)
if data.get("type") == "transcription":
result = data.get("data", {})
text = result.get("transcription", "")
is_final = result.get("eos", {}).get("final", False)
event_type = stt.SpeechEventType.FINAL_TRANSCRIPT if is_final else stt.SpeechEventType.INTERIM_TRANSCRIPT
await self._queue.put(stt.SpeechEvent(
type=event_type,
alternatives=[stt.SpeechEventAlternative(text=text, confidence=1.0)]
))
elif data.get("type") == "end_of_speech":
await self._queue.put(stt.SpeechEvent(type=stt.SpeechEventType.END_OF_SPEECH))
except Exception as e:
await self._queue.put(stt.SpeechEvent(type=stt.SpeechEventType.ERROR, error=e))
finally:
await self.close()
async def push_frame(self, frame: rtc.AudioFrame):
if not self._ws:
await self._connect()
try:
# LiveKit AudioFrame data is LPCM16; send raw bytes as your sample did
await self._ws.send(frame.data)
except Exception as e:
logging.error("Send error: %s", e)
async def flush(self):
if self._ws and not self._closed:
await self._ws.send(json.dumps({"type": "end_of_stream"}))
async def close(self):
self._closed = True
if self._ws:
await self._ws.close()
async def __aiter__(self):
if not self._ws:
await self._connect()
while not self._closed:
try:
item = await asyncio.wait_for(self._queue.get(), timeout=1.0)
yield item
except asyncio.TimeoutError:
if self.cancellation_event.is_set():
break
continue
How to use
• In AgentSession:
from livekit.agents import AgentSession
stt_engine = TryHamsaSTT(api_key="KEY", ws_url="wss://...")
session = AgentSession(stt=stt_engine, llm="openai/gpt-4o")
• Standalone (pipe audio from a LiveKit track):
◦ Create the stream: stream = TryHamsaSTT(...).stream()
◦ For each rtc.AudioFrame call await stream.push_frame(frame)
◦ Iterate async for ev in stream: to read transcripts.
Notes and tips
• Ensure frames are in the format TryHamsa expects (mono, 16-bit little-endian PCM). Convert if needed.
• Map TryHamsa’s interim/final flags to SpeechEventType.INTERIM_TRANSCRIPT and FINAL_TRANSCRIPT.
• Implement reconnect/error handling and optional flush() semantics per your service.
• If TryHamsa returns speaker IDs, emit diarization info via custom metadata or additional event fields.
If you want, I can:
• Convert your exact sample file-reading code (send_wav_chunks/receive_results) into a full working example using LiveKit rtc.AudioFrame emulation, or
• Scaffold a packaged plugin module named livekit.plugins.tryhamsa.
Which would you prefer?
Sources: Speech-to-text (STT) models | LiveKit Docs | Pipeline nodes and hooks | LiveKit docs | Baseten STT plugin guide | LiveKit docsnarrow-chef-71655
12/29/2025, 8:17 AMtall-airline-16756
12/29/2025, 8:17 AMturn_detection="stt").
• Skip VAD only if you have confidence in STT endpointing and also handle interruptions and edge cases.
Why combining VAD + STT endpointing is better
• Faster interruption handling (detect new speaker starting while TTS/LLM runs).
• Fallback if STT endpointing lags or misdetects silence.
• Cleaner decision points for the agent (when to call the LLM).
Practical tips for TryHamsa WebSocket
• Keep streaming audio while START_OF_SPEECH is active; on VAD END_OF_SPEECH send your end_of_stream (or the provider’s end marker) so TryHamsa emits a final transcription.
• Tune VAD parameters (min_silence_duration_ms, vad_threshold) to match your environment.
• Use interim transcripts for live feedback; rely on final transcripts for LLM prompts.
• Implement short timeout fallback: if neither VAD nor STT EOS appear after N ms of silence, finalize the segment anyway.
LiveKit settings examples
• VAD-driven turns (recommended):
session = AgentSession( stt=TryHamsaSTT(...), vad=silero.VAD.load(), turn_detection="vad", ... )
• STT-driven turns (if provider has endpointing):
session = AgentSession( stt=TryHamsaSTT(...), vad=silero.VAD.load(), # still useful as fallback turn_detection="stt", ... )
If you want, I can:
• Suggest specific VAD parameter values for TryHamsa based on your expected mic/sample_rate and noise level; or
• Show how to wire Silero VAD into the TryHamsa stream class (code snippet) so END_OF_SPEECH triggers sending {"type":"end_of_stream"}.
Which would you like?
Sources: Agents Framework | livekit/livekit_composite | DeepWiki | examples/voice_agents | livekit/livekit_composite | DeepWiki | Turns overview | LiveKit docs