Documentation
attention labs SDKs stream your mic and webcam to the hosted server and return typed events: silent, human-directed, or device-directed. No wake word. You choose what reaches your language model.
JavaScript
@attenlabs/saa-js for the browser. Captures mic and webcam through getUserMedia, an AudioWorklet, and a canvas; delivers typed events. Full constructor, methods, events, and event-type reference.
Python
attenlabs-saa for server and embedded applications. Synchronous API with decorator callbacks; same event surface as the browser client. Full constructor, methods, events, and event-type reference.
Overview
Two clients share the same surface, @attenlabs/saa-js for the browser and attenlabs-saa for Python. Both stream to a managed, versioned model and return typed events; routing to your language model is up to you.
Install
# JavaScript
npm install @attenlabs/saa-js
# Python
pip install attenlabs-saa
The JavaScript client needs a browser (getUserMedia, AudioContext, AudioWorklet, Canvas, WebSocket). The Python client needs 3.10+ and installs sounddevice and opencv-python automatically.
Authentication and connect
Sign up for the dashboard, create an API key, and pass it to the client as the token option. Keys are self-serve and available immediately, scoped to your account. Keep yours secret: load it from an environment variable rather than hardcoding it.
// JavaScript
const client = new AttentionClient({ token: "your-token" });
# Python
client = AttentionClient(token=os.environ["SAA_API_KEY"])
When you call start(), the SDK resolves a connection in two steps:
- Allocate. The client sends a POST to the broker's
/allocateendpoint (the broker base defaults tohttps://broker.attentionlabs.ai; override it with theurloption). The token is sent in theAuthorization: Bearer <token>header. The broker picks a backend and responds with JSON containing aurlfield, the WebSocket URL for your session. - Connect. The client opens a WebSocket to that returned
url, passing the token again as the WebSocket subprotocol. Mic and webcam media stream over this socket and typed events come back on it.
The allocate step runs once per connect, so each reconnect requests a fresh backend. If you pass a ws:// or wss:// URL as the url option, the SDK treats it as a direct backend and skips the broker step.
Core concepts
Four ideas explain everything else in these docs.
Hosted, not local
Both clients are thin. They capture mic and webcam, stream frames to the hosted server through the broker, and hand you typed events back. No model runs in your process, and there's nothing to version or update yourself.
Three classes, one threshold
Every prediction resolves to a class: 0 silent, 1 human-directed, 2 device-directed. initialThreshold (default 0.7) sets how confident the model must be before it commits to class 2. Call setThreshold() (JavaScript) or set_threshold() (Python) to tune it while the client is running.
Prediction and turn events
prediction fires on every frame while the model classifies live audio and video; it's what you gate a live stream on. turnReady fires once per completed utterance, with the assembled clip ready to send. Most integrations use both: prediction to hold a gate open or closed, turnReady to grab the audio that goes to the LLM.
Fail closed
Only class 2 should ever reach your language model. If the socket drops, an error fires, or a prediction is inconclusive, treat it the same as human-directed: keep the gate closed rather than forward audio you're not sure about. Missing an occasional device-directed turn is safer than leaking a side conversation to your model.
Quickstart
Create a client, listen for prediction and turnReady, and start streaming. cls tells you who is speaking: 0 silent, 1 human-directed, 2 device-directed. Only 2 should reach your model.
// JavaScript
import { AttentionClient } from "@attenlabs/saa-js";
const client = new AttentionClient({ token: "your-token" });
client.on("prediction", (e) => {
const label = { 0: "silent", 1: "human", 2: "device" }[e.cls] ?? "?";
console.log(`${label} ${(e.confidence * 100).toFixed(0)}% faces=${e.numFaces}`);
});
client.on("turnReady", (e) => {
console.log(`turn ready (${e.durationSec.toFixed(2)}s)`);
// e.audioBase64 is PCM16 @ 16 kHz, ready to forward to your LLM
});
const video = document.getElementById("preview");
await client.start({ videoElement: video });
# Python
import os, time
from saa import AttentionClient
client = AttentionClient(token=os.environ["SAA_API_KEY"])
@client.on_prediction
def _(event):
label = {0: "silent", 1: "human", 2: "device"}.get(event.cls, "?")
print(f"{label} {event.confidence:.0%} faces={event.num_faces}")
@client.on_turn_ready
def _(event):
print(f"turn ready ({event.duration_sec:.2f}s)")
# event.audio_base64 is ready to forward to your LLM
client.start()
try:
while True:
time.sleep(0.1)
except KeyboardInterrupt:
client.stop()
From here, follow the full references for the complete constructor options, the shared method surface, every event and its payload type, and the LLM mute pattern: JavaScript SDK and Python SDK.
Integrations
The Cloud SDK hands each device-directed turn to common voice-agent frameworks through an adapter, in one of two shapes:
- Existing pipeline. The adapter gates it in place: device-directed speech passes through, side conversation is held back.
- New agent. The adapter hands each completed turn to your
onTurncallback as aTurnReadyEvent.
LiveKit Agents
Joins your room and gates the pipeline in place: it enables the agent's audio input only while the speaker is device-directed. A greenfield agent can use the entrypoint's onTurn callback instead.
engine.on("prediction", (p) => {
// enable input only for device-directed speech (class 2)
session.input.setAudioEnabled(p.alignedClass === 2);
});
engine.on("interrupt", () => session.interrupt());
Pipecat
Classifies your media; no model runs in your process. An addressee gate before speech-to-text suppresses person-directed frames, so transcription only sees device-directed speech. A greenfield runner offers onTurn instead.
engine.on("prediction", (p) => {
// hold back person-directed speech before it reaches STT
addresseeGate.suppressed = p.alignedClass === 1;
});
ElevenLabs
ElevenLabs owns its own session, so this adapter taps the audio interface directly instead of joining a room. Only device-directed frames reach the agent; barge-in stays with ElevenLabs' voice-activity logic.
const attn = new SAAFeedAudioInterface(baseInterface, saa, { gate: true });
saa.on("prediction", (e) => {
attn.setGateOpen(e.cls === 2); // only device-directed speech reaches the agent
});
OpenAI Realtime
A realtime model has no voice-activity slot to gate, so the LiveKit adapter enables and disables audio input before the model sees it, and maps barge-in to the model's own cancel. No model surgery required. You can also forward turns to the Realtime API yourself: turnReady audio is ready to send as input to a realtime session.
engine.on("prediction", (p) => {
// gate input before it reaches the realtime model
session.input.setAudioEnabled(p.alignedClass === 2);
});
Twilio
Twilio Media Streams carries call audio over a plain WebSocket, so the same pattern applies without a room or session to join. Feed the inbound track to the client with feedAudio() (JavaScript) or feed_audio() (Python) instead of using the SDK's own mic capture, then gate your call-handling logic on prediction the same way. There's no packaged Twilio adapter yet; contact us for a reference implementation.
Every adapter reports the active speaker's aligned class the same way the core SDK does: 0 silent, 1 person-directed, 2 device-directed. Only class 2 should reach your language model.
Errors and limits
Both clients emit a typed error event, error in JavaScript and @client.on_error in Python, carrying a title, message, and the WebSocket close code when applicable. The most common title is "Auth Failed"; generate a fresh key in the dashboard if yours is missing, expired, or revoked.
- Audio: 16 kHz mono, streamed in 100 ms (1600-sample) chunks.
- Video: JPEG frames at about 4 fps; frames drop under backpressure (WebSocket send buffer over 1 MB) rather than blocking audio.
- Reconnects: every reconnect re-runs the broker's allocate step, so a dropped connection always lands on a fresh backend, never a stale one.
Full detail lives on the client references: JavaScript errors and the shared event-type reference.
Need help integrating? Write to [email protected].