Pre-STT addressee gating for LiveKit voice agents
Your LiveKit agent sends every turn to STT, addressed or not.
Selective Auditory Attention (SAA) gates audio before STT so only device-directed turns reach your pipeline.
The audio pipeline
Five pipeline layers; only layer 5 answers who's being addressed.
A user turns to talk to someone nearby: VAD and STT keep running anyway. SAA gates before STT, deciding addressee every 250 ms.
| Pipeline layer | What it answers | Timing | Answers "is this turn addressed to the device?" |
|---|---|---|---|
| L1: Noise cleanup / AEC | Is the audio clean? | Pre-STT | No |
| L2: VAD / wake-word | Is someone speaking? Did the trigger phrase fire? | Pre-STT | No |
| L3: Speaker diarization | Which speaker said it, after the fact? | Post-STT | No |
| L4: Turn detection / endpointing | Has the turn ended? | Pre-STT / in-flight | No |
| L5: SAA addressee gating | Is this turn addressed to the device, right now? | Pre-STT, every 250 ms | Yes |
How SAA joins a LiveKit room
A hidden SAA participant subscribes to your tracks and publishes decisions.
Calling start_attention_session adds that participant to your room. It can subscribe to tracks and publish data, never media, and emits a decision every 250 ms.
aligned_class schema
Three classes; gate on class 2 only
aligned_class is 0 (silence), 1 (human to human), or 2 (device-directed). Gate: voice.input.set_audio_enabled(p.aligned_class == 2).
Use aligned_class, not raw_class: it smooths transient class-2 flips in the frames right after AI playback ends.
minimum-grant JWT
Subscribe and publish-data only; never media publish
attention_agent_token issues a JWT with room_join, hidden, can_subscribe, can_publish_data, and agent grants. can_publish is always False.
Integration: existing AgentSession
One callback expression to gate an AgentSession you already have running.
Add the gate without restructuring your entrypoint. Construct an AttentionEngine and wire three callbacks, shown below.
from saa_livekit import start_attention_session, AttentionEngine
async def entrypoint(ctx: JobContext):
await ctx.connect()
# Summon the hosted SAA participant into this room
saa = await start_attention_session(
room=ctx.room,
participant_identity=ctx.room.local_participant.identity,
)
# Construct the engine that routes PredictionEvents to your callbacks
engine = AttentionEngine(
room=ctx.room,
agent_identity=ctx.room.local_participant.identity,
)
# Gate: the entire addressee decision is one expression
@engine.on_prediction
async def _(p):
voice.input.set_audio_enabled(p.aligned_class == 2)
# Interrupt: stop TTS when a confident class-2 streak fires during playback
@engine.on_interrupt
async def _(ev):
voice.interrupt()
# Responding state: arm the interrupt detector during AI playback
@session.on("agent_state_changed")
def _(old_state, new_state):
if new_state == "speaking":
engine.responding_start()
elif old_state == "speaking":
engine.responding_stop()
ctx.add_shutdown_callback(saa.stop)
ctx.add_shutdown_callback(engine.stop)
Integration: greenfield agent
build_attention_entrypoint: a factory helper for teams starting from scratch.
build_attention_entrypoint returns a coroutine you can pass to your agent server. Give it your async on_turn handler; SAA handles the rest.
from livekit.agents import JobContext, AgentServer
from saa_livekit import build_attention_entrypoint, TurnReadyEvent
async def on_turn(ev: TurnReadyEvent, ctx: JobContext):
# ev.audio_pcm16: int16 mono PCM at 16 kHz, ready for any LLM audio API
if ev.context == "interjection_follow_up":
# Turn was captured after a successful interjection;
# route to a different LLM prompt (e.g. "briefly offer to help")
...
return
# Forward ev.audio_pcm16 to your STT pipeline or realtime model
# Plug the returned coroutine into your agent server
entrypoint = build_attention_entrypoint(
on_turn=on_turn,
# on_interrupt=handle_interrupt, # optional
# on_interjection=handle_interject, # optional
)
server = AgentServer()
server.rtc_session(entrypoint)
# or: WorkerOptions(entrypoint_fnc=entrypoint)
TurnReadyEvent
int16 mono PCM at 16 kHz, ready to forward
audio_pcm16 is int16 mono PCM at 16 kHz, ready for any LLM or STT API. context is None for a fresh command, or 'interjection_follow_up' after one.
environment variables
No runtime configuration required
build_attention_entrypoint reads your SAA and LiveKit credentials from environment variables automatically; no constructor arguments needed. Full list in Setup and requirements below.
Speech-to-speech agents
Disable LiveKit VAD; let SAA own turn boundaries for realtime models.
Set turn_detection=None so the model hears only SAA-injected turns. attention_config can disable frame capture for audio-only pipelines.
from livekit.plugins import openai
from saa_livekit import build_attention_entrypoint, inject_realtime_turn, TurnReadyEvent
async def on_turn(ev: TurnReadyEvent, ctx):
# inject_realtime_turn chunks the PCM at 100 ms intervals,
# calls rt.push_audio for each chunk, rt.commit_audio,
# then calls session.generate_reply()
rt = ctx.proc.userdata["rt_session"]
await inject_realtime_turn(rt, ev)
# Disable server VAD: SAA owns turn boundaries
model = openai.realtime.RealtimeModel(
turn_detection=None,
)
session = model.session()
session.input.set_audio_enabled(False) # model hears only SAA-injected audio
# Disable per-turn frame capture for pure audio pipelines
entrypoint = build_attention_entrypoint(
on_turn=on_turn,
attention_config={"frames_per_turn": 0},
)
Barge-in and proactive interjection
Two typed events: barge-in during playback and proactive interjection.
Call engine.responding_start() when playback begins and responding_stop() when it ends. This arms the interrupt branch.
InterruptEvent: barge-in while the agent is speaking
A confident class-2 streak during playback fires on_interrupt. Call session.interrupt() to stop TTS; SAA has already pre-rolled the audio, so the next turn carries the question.
InterjectionEvent: humans went quiet while still in frame
InterjectionEvent fires when people go quiet mid-conversation while still in frame (reason='stuck_after_question'). Route audio_pcm16 to your LLM with a brief offer-to-help prompt.
Setup and requirements
One package, four environment variables, Python 3.10 or later.
Install saa-livekit-client and set the four environment variables below. SAA supports audio-only and performs best with video. Methodology: arXiv:2604.08412.
pip install saa-livekit-client
Required environment variables
SAA_API_KEY: your attention labs API key, self-serve from the dashboard, no approval needed.
LIVEKIT_URL: your LiveKit server URL, publicly reachable from the SAA cloud.
LIVEKIT_API_KEY and LIVEKIT_API_SECRET: your LiveKit project credentials, used by the broker to issue the hidden participant's minimum-grant JWT.
Networking and runtime
Python 3.10 or later. saa-livekit-client runs inside your worker process, no inbound changes. Outbound only, to broker.attentionlabs.ai. See the networking FAQ below.
Threshold tuning
set_threshold(v) takes a float from 0.0 to 1.0. Raise it in noisy environments to cut false triggers; lower it for quiet or distant speakers. Default is tuned for one participant in view of the camera.
Multi-user rooms
One call to start_attention_session per target participant. saa-livekit-client is the Cloud SDK's LiveKit adapter, built on the base transport packages @attenlabs/saa-js (npm) and attenlabs-saa (PyPI).
Common questions
What LiveKit agent developers ask before integrating.
What does SAA actually add to a LiveKit room?
Calling start_attention_session summons a hidden participant (identity saa-agent by default) into your LiveKit room via broker.attentionlabs.ai. It subscribes to the user's audio and video tracks and publishes decisions on the saa data channel every 250 ms. Your AttentionEngine reads those decisions and fires typed callbacks. The participant holds minimum-necessary grants: subscribe and publish-data only, never media.
How does the addressee gate actually suppress non-addressed audio?
Every 250 ms, AttentionEngine fires an on_prediction callback with a PredictionEvent. Read aligned_class: 0 is silence, 1 is human-to-human speech, 2 means the user is addressing the device. Gating is one expression: voice.input.set_audio_enabled(p.aligned_class == 2). Use aligned_class rather than raw_class; it applies a 5-tick AI-aware correction that suppresses transient class-2 flips in the frames immediately after AI playback ends.
Can SAA replace LiveKit server VAD for speech-to-speech agents?
Yes. Set turn_detection=None on the RealtimeModel and session.input.set_audio_enabled(False) so the model hears only SAA-injected audio. When a complete addressed turn ends, on_turn_ready fires with a TurnReadyEvent carrying audio_pcm16: int16 mono PCM at 16 kHz. The inject_realtime_turn helper chunks the PCM at 100 ms steps, calls push_audio for each chunk, commit_audio, then generate_reply. SAA owns turn boundaries instead of the model's own VAD.
What happens when a user interrupts while the agent is speaking?
Call engine.responding_start() when your agent begins audio playback and engine.responding_stop() when it finishes. If SAA observes a confident class-2 speech streak during playback, on_interrupt fires with an InterruptEvent. The hosted agent has already pre-rolled ring-buffer audio into the chunk accumulator; the next on_turn_ready carries the user's barge-in question. Call session.interrupt() in the on_interrupt callback to stop TTS.
What networking does the integration require?
Your LiveKit URL must be publicly reachable from broker.attentionlabs.ai. The saa-livekit-client library runs inside your existing agent worker process with no inbound networking changes on your side. It communicates outbound only: one POST to start the session and one DELETE to stop it, both to broker.attentionlabs.ai using your SAA_API_KEY.
What is the fail-closed behavior and why does it matter?
SAA errs toward suppression rather than forwarding. Audio is not sent onward unless the model is confident the user is addressing the device (class 2). A turn that shifts mid-sentence from device-directed speech to a side conversation drops to class 1, the gate closes immediately, and nothing reaches STT or the LLM. A low-confidence utterance produces a missed command rather than a ghost response from an AI that was not being spoken to.
Get started
Get your API key and start gating your LiveKit agent today.
Sign up for a free SAA_API_KEY and install saa-livekit-client. Need help with a nonstandard pipeline? Talk to us.
Related: how addressee detection works, Pipecat addressee detection integration, OpenAI Realtime addressee detection integration, voice infrastructure pipeline, engagement-control layer.
Last updated June 30, 2026