Python SDK
The Python client streams mic and webcam to the hosted server, tagging each frame silent, human-directed, or device-directed, and forwards only device-directed audio.
Overview
The Python client mirrors the browser client for server and embedded apps, and requires Python 3.10+.
- Install
attenlabs-saa, but import fromsaa. - Pulls in
sounddeviceandopencv-pythonautomatically for mic and camera access.
# Install
pip install attenlabs-saa
Pass your token to the constructor. start() authenticates to the broker, which allocates a backend and returns a per-session WebSocket URL for mic, webcam, and events.
- Default broker:
https://broker.attentionlabs.ai, called with anAuthorization: Bearer <token>header. - Keep tokens secret. Pass them from an environment variable, not hardcoded.
- The model is versioned and managed for you.
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()
Quickstart
Install the SDK, export your token, initialize the client, register decorator callbacks, then keep the main thread alive while events stream.
By default the client captures both mic and webcam; pass enable_video=False to disable the camera.
cls0: silence.cls1: human talking to another human.cls2: human talking to the device. Only this should reach your language model.
import os
import time
from saa import AttentionClient
client = AttentionClient(token=os.environ["SAA_API_KEY"])
CLASS_LABELS = {0: "silent", 1: "human", 2: "device"}
@client.on_connected
def _():
print("connected")
@client.on_started
def _():
print("warmup complete, streaming live")
@client.on_prediction
def _(event):
label = CLASS_LABELS.get(event.cls, "?")
print(f"{label:7} {event.confidence:.0%} faces={event.num_faces} src={event.source}")
@client.on_turn_ready
def _(event):
print(f"turn ready ({event.duration_sec:.2f}s)")
# Forward event.audio_base64 to your LLM here
@client.on_error
def _(event):
print(f"ERROR: {event.title}: {event.message}")
client.start()
try:
while True:
time.sleep(0.1)
except KeyboardInterrupt:
client.stop()
on_turn_ready returns the turn's audio as event.audio_base64 (base64 PCM16, 16 kHz mono) or event.audio_pcm16 (NumPy int16 array), ready for your language model.
LLM integration
Call mute() and mark_responding(True) when your LLM starts speaking, then unmute() and mark_responding(False) when it finishes, so the SDK ignores its own audio.
def on_llm_speaking():
client.mute()
client.mark_responding(True)
def on_llm_done():
client.unmute()
client.mark_responding(False)
AttentionClient (Python)
The constructor accepts a token plus optional config objects and capture toggles. The device-class confidence threshold defaults to 0.7.
from saa import AttentionClient, CameraConfig, MicConfig
client = AttentionClient(
token="...", # auth token (Bearer to broker + WS subprotocol)
url=None, # broker base URL (default https://broker.attentionlabs.ai)
video=CameraConfig(), # webcam config
audio=MicConfig(), # mic config
initial_threshold=0.7, # device-class confidence threshold (0..1)
enable_audio=True, # set False to skip mic capture
enable_video=True, # set False to skip webcam capture
server_profile=None, # override server profile; auto "audio_only" when video off
)
If you pass a ws:// or wss:// URL as url, the SDK treats it as a direct backend and skips the broker step.
MicConfig and CameraConfig
| Type | Fields |
|---|---|
MicConfig | device (int | str | None, default None for system default), channels (int, default 1). |
CameraConfig | device_index (int, default 0), width (int, default 1920), height (int, default 1080), jpeg_quality (int 0-100, default 50). |
Methods
Python method names are snake_case; the Python forms below summarize the full method surface documented on the JavaScript SDK reference.
| Method | Description |
|---|---|
start() | Acquires mic and camera, opens the WebSocket, starts capture. Non-blocking; raises on handshake failure. |
stop() | Tears down capture, releases media, closes the WebSocket. |
mute() | Pauses upstream audio and signals the server to stop VAD. |
unmute() | Resumes upstream audio. |
mark_responding(bool) | Tell the server an LLM response is in flight. The server stops emitting predictions while it is True. |
set_threshold(value) | Update the device-class confidence threshold (0..1). The server acknowledges via a config event. |
feed_audio(audio, *, sample_rate=16000) | Feed externally captured audio instead of using the built-in mic. See External capture below. |
feed_video(frame) | Feed an externally captured video frame instead of using the built-in camera. See External capture below. |
The current threshold is available on the threshold attribute. Python registers events with decorators, not on()/off(); see below.
Event decorators
Register handlers with decorators on the client. The event set matches the browser client; payloads arrive as snake_case dataclasses, detailed in the table below.
| Decorator | Payload | Fires when |
|---|---|---|
@client.on_connected | none | The WebSocket opens. |
@client.on_started | none | The server has loaded the model. |
@client.on_warmup_complete | none | The model is warmed up and producing predictions. |
@client.on_prediction | PredictionEvent | Each attention prediction. |
@client.on_vad | VadEvent | A voice-activity update. |
@client.on_state | StateEvent | A conversation-state transition. |
@client.on_turn_ready | TurnReadyEvent | A complete user turn is ready to forward. |
@client.on_config | ConfigEvent | The server acknowledges a threshold change. |
@client.on_stats | StatsEvent | About every 10s, with connection health. |
@client.on_interrupt | InterruptEvent | The user is barging in mid-response. |
@client.on_interjection | InterjectionEvent | A proactive volunteer after people go quiet. |
@client.on_error | AttentionErrorEvent | A connection, auth, or server error. |
@client.on_disconnected | DisconnectedEvent | The WebSocket closes. |
Event payloads (Python)
Each callback receives a snake_case dataclass, matching the shared event-types reference. StatsEvent.uptime_s is in seconds; raw_cls and server_turn_ready_ts_ms are JavaScript-only.
| Type | Fields |
|---|---|
PredictionEvent | cls (int: 0 silent, 1 human-directed, 2 device-directed), confidence (0..1), source ("video" or "audio"), num_faces (faces detected in frame), responding (bool, default False, True while the AI is mid-playback). |
VadEvent | probability (VAD probability 0..1), is_speech (bool). |
StateEvent | state: one of "listening", "sending", "cancelled", "idle". |
TurnReadyEvent | audio_pcm16 (NumPy int16 array, 16 kHz mono), audio_base64 (base64 PCM16 @ 16 kHz mono), duration_sec (seconds), frames (list of TurnFrame, empty unless the server sends per-turn stills), context (str or None, for example "interjection_follow_up"). |
TurnFrame | ts_offset_s (seconds from listening-start), image_base64 (base64 JPEG, no data: prefix). |
ConfigEvent | model_class2_threshold (server-confirmed threshold). |
StatsEvent | rtt_ms (round-trip latency in ms or None), sent_video, skipped_video, sent_audio, uptime_s (seconds). |
InterruptEvent | fade_ms (suggested fade duration in ms before stopping playback), confidence (raw confidence of the firing device-directed prediction). |
InterjectionEvent | reason (why the volunteer fired), audio_pcm16 (NumPy int16 array), audio_base64 (base64 PCM16 @ 16 kHz mono), duration_sec (seconds). |
AttentionErrorEvent | title (error category, for example "Auth Failed", "Connection Stalled"), message (human-readable), detail (technical detail or None, default None), code (WebSocket close code, default None). |
DisconnectedEvent | code (WebSocket close code), reason (close reason), was_clean (True if code is 1000). |
Threading
The SDK runs four internal threads. Callbacks fire on saa-ws or saa-heartbeat, so keep them fast and offload heavy work to your own thread.
saa-ws: WebSocket send and receive.saa-heartbeat: JSON pings every 5s, stats every 10s.saa-camera: JPEG capture at 4 fps.sounddevicecallback: audio at native sample rate, resampled to 16 kHz.
External capture
Already capturing audio or video elsewhere? Pass enable_audio=False or enable_video=False, then feed frames with feed_audio() or feed_video().
- Audio: PCM16 at 16 kHz mono by default; pass
sample_rateif yours differs and the SDK resamples it. - Video: raw captured frames; the SDK encodes them to JPEG.
from saa import AttentionClient
# Disable the built-in mic and feed your own audio.
client = AttentionClient(token=os.environ["SAA_API_KEY"], enable_audio=False)
client.start()
# later, from your own capture loop:
client.feed_audio(chunk, sample_rate=16000)
Shared reference
The method surface, events, event types, audio and video framing, errors, and integrations are all shared with the browser client on the JavaScript SDK reference.
Need help integrating? Write to [email protected].