JavaScript SDK

@attenlabs/saa-js streams your mic and webcam from the browser and classifies every frame as silent, human-directed, or device-directed. No wake word. You decide what reaches your language model; the SDK handles capture, streaming, and event typing.

Overview

your audio
SAA
attention prediction
device-directed speech

Wraps the inference server for the browser: grabs mic and camera via getUserMedia, captures audio through an AudioWorklet and video through canvas, and manages the WebSocket. The model is versioned and hosted for you.

Install

npm install @attenlabs/saa-js

View the SDK on GitHub

Requires a browser: getUserMedia, AudioContext, AudioWorklet, Canvas, and WebSocket. Sign up for the dashboard to get a key now and pass it as the token option. Keep tokens secret; load from an environment variable rather than hardcoding.

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 });

Calling start() resolves your connection in two steps:

Each reconnect re-runs allocate for a fresh backend. Pass a ws:// or wss:// URL as url to skip the broker and connect directly.

Quickstart

Add a <video> element for camera preview, then initialize the client and start streaming. Listen for prediction and error at minimum: cls is 0 (silent), 1 (human-directed), or 2 (device-directed). Only forward class 2 to your language model.

import { AttentionClient } from "@attenlabs/saa-js";

const client = new AttentionClient({ token: "your-token-here" });

const CLASS_LABELS = { 0: "silent", 1: "human", 2: "device" };
const log = document.getElementById("log");

client.on("connected", () => { log.textContent += "connected\n"; });
client.on("started", () => { log.textContent += "warmup complete, streaming live\n"; });

client.on("prediction", (e) => {
  const label = CLASS_LABELS[e.cls] ?? "?";
  log.textContent = `${label}  ${(e.confidence * 100).toFixed(0)}%  faces=${e.numFaces}  src=${e.source}\n`;
});

client.on("turnReady", (e) => {
  log.textContent += `turn ready (${e.durationSec.toFixed(2)}s)\n`;
  // Forward e.audioBase64 to your LLM here
});

client.on("error", (e) => {
  log.textContent += `ERROR: ${e.title}: ${e.message}\n`;
});

const video = document.getElementById("preview");
await client.start({ videoElement: video });

Call mute() and markResponding(true) while your LLM is speaking, then unmute() and markResponding(false) when it's done. This stops the SDK from mistaking playback for a new prediction.

function onLlmSpeaking() {
  client.mute();
  client.markResponding(true);
}

function onLlmDone() {
  client.unmute();
  client.markResponding(false);
}

AttentionClient

The constructor accepts a token plus optional capture and threshold settings.

const client = new AttentionClient({
  token: "...",                  // auth token (Bearer to broker + WS subprotocol)
  url: undefined,                // broker base URL (default https://broker.attentionlabs.ai)
  video: {},                     // video capture config (VideoCaptureOptions)
  audio: {},                     // audio capture config (AudioCaptureOptions)
  workletUrl: undefined,         // override the AudioWorklet module URL
  initialThreshold: 0.7,         // device-class confidence threshold (0..1)
  enableAudio: true,             // set false to skip mic capture
  enableVideo: true,             // set false to skip webcam capture
  serverProfile: undefined,      // override server profile; auto "audio_only" when video off, "default" forces the full processor
});

VideoCaptureOptions: width (default 1920), height (default 1080), jpegQuality (0..1, default 0.5). AudioCaptureOptions: targetSampleRate (default 16000; audio always captures at 16 kHz mono).

Call client.start({ videoElement }); videoElement is required when video is enabled. Already have a stream? Pass start({ mediaStream }) to skip the SDK's own getUserMedia call.

Properties: isConnected (boolean, true when the WebSocket is open) and currentThreshold (number, the current confidence threshold).

Methods

JavaScript method names are camelCase.

MethodDescription
start({ videoElement, mediaStream? })Acquires mic and camera, opens the WebSocket, starts capture. Pass videoElement (required when video is enabled); pass mediaStream to reuse a stream instead of calling getUserMedia.
stop()Tears down capture, releases media, closes the WebSocket.
mute()Pauses upstream audio and signals the server to stop VAD.
unmute()Resumes upstream audio.
markResponding(bool)Tell the server an LLM response is in flight. The server stops emitting predictions while it is true.
setThreshold(value)Update the device-class confidence threshold (0..1). The server acknowledges via a config event.
on(event, listener)Subscribe a listener. Returns an unsubscribe function.
off(event, listener)Unsubscribe a previously registered listener.
feedAudio(audio, sampleRate?)Feed externally captured audio instead of using the SDK's mic capture.
feedVideo(jpeg)Feed an externally captured JPEG frame instead of using the SDK's webcam capture.

Events

Register handlers with client.on("prediction", cb); the returned function unsubscribes the listener, or call client.off("prediction", cb).

EventPayloadFires when
connectednoneThe WebSocket opens.
startednoneThe server has loaded the model.
warmupCompletenoneThe model is warmed up and producing predictions.
predictionPredictionEventEach attention prediction.
vadVadEventA voice-activity update.
stateStateEventA conversation-state transition.
turnReadyTurnReadyEventA complete user turn is ready to forward.
configConfigEventThe server acknowledges a threshold change.
statsStatsEventAbout every 10s, with connection health.
interruptInterruptEventThe user is barging in mid-response.
interjectionInterjectionEventA proactive volunteer after people go quiet.
errorAttentionErrorEventA connection, auth, or server error.
disconnectedDisconnectedEventThe WebSocket closes.

Event types

TypeFields
PredictionEventcls (int: 0 silent, 1 human-directed, 2 device-directed), rawCls (int or null, the unaligned model class), confidence (0..1), source ("video" or "audio"), numFaces (faces detected in frame), responding (boolean, true while the AI is mid-playback).
VadEventprobability (VAD probability 0..1), isSpeech (boolean).
StateEventstate: one of "listening", "sending", "cancelled", "idle".
TurnReadyEventaudioBase64 (base64 PCM16 @ 16 kHz mono), audioPcm16 (Int16Array), durationSec (seconds), serverTurnReadyTsMs (number or null, server timestamp when the turn was ready), frames (array of TurnFrame, empty unless the server sends per-turn stills), context (string or null, for example "interjection_follow_up").
TurnFrametsOffsetS (seconds from listening-start), imageBase64 (base64 JPEG, no data: prefix).
ConfigEventmodelClass2Threshold (server-confirmed threshold).
StatsEventrttMs (round-trip latency in ms or null), bufferedAmount (bytes queued on the WebSocket), sentVideo, skippedVideo, sentAudio, uptimeMs (connection uptime in ms).
InterruptEventfadeMs (suggested fade duration in ms before stopping playback), confidence (raw confidence of the firing device-directed prediction).
InterjectionEventreason (why the volunteer fired), audioBase64 (base64 PCM16 @ 16 kHz mono), audioPcm16 (Int16Array), durationSec (seconds).
AttentionErrorEventtitle (error category, for example "Auth Failed", "Connection Stalled"), message (human-readable), detail (technical detail or null), code (WebSocket close code, if applicable).
DisconnectedEventcode (WebSocket close code), reason (close reason), wasClean (true if code is 1000).

Audio and video

Audio streams at 16 kHz mono in 100 ms chunks (1600 samples). Video frames capture as JPEG at about 4 fps (every 250 ms) and drop if the WebSocket send buffer exceeds 1 MB.

// The SDK binds the camera stream to your video element and
// captures both mic and webcam once you call start().
const video = document.getElementById("preview");
await client.start({ videoElement: video });

// Disable the camera with enableVideo: false when constructing the client.

Errors

Errors arrive as an error event: title, message, detail, and code (WebSocket close code). Auth failures show title "Auth Failed"; create a fresh token in the dashboard if yours is missing, expired, or revoked.

client.on("error", (e) => {
  console.log(`${e.title}: ${e.message}`, e.detail, e.code);
});

Looking for the shared connection model, integrations, or the Python client? See the documentation home and the Python SDK.