Pipecat + Selective Auditory Attention (SAA) integration guide
How to add addressee detection to a Pipecat voice agent
SAA is the Cloud SDK's Pipecat adapter: one class, five calls, no pipeline rewrite.
L5 audio stack
The missing layer in your voice pipeline: addressee gating.
Noise suppression, VAD, diarization, and turn detection all pass audio through. None asks is this speech addressed to the agent?
SAA (Selective Auditory Attention) adds addressee gating as a fifth layer, upstream of ASR. The gate fails closed: a missed command beats a ghost response.
| Layer | What it decides | When it runs | Blocks non-addressed audio? |
|---|---|---|---|
| L1: audio cleanup / AEC | Is the audio cleaner? | Pre-ASR | No |
| L2: VAD | Is someone speaking? | Pre-ASR | No |
| L3: diarization | Who spoke (after the fact)? | Post-ASR | No |
| L4: turn detection / endpointing | Has the turn ended? | Pre-ASR | No |
| L5: SAA addressee gate | Is this speech addressed to the agent? | Pre-ASR | Yes, fails closed |
Evaluation results on held-out multi-party sessions are published at arXiv:2604.08412.
Integration architecture
SAA attaches alongside your DailyTransport. Your pipeline code stays the same.
start_attention_session() provisions a hosted bot that joins your Daily room as a hidden participant. AttentionEngine hooks DailyTransport's on_app_message handler and fires typed Python callbacks on the 'saa' topic.
Getting started
Install saa-pipecat-client and wire AttentionEngine in five calls.
Requires Python 3.11+ alongside pipecat-ai[daily]. New pipelines can skip to step 4, build_attention_runner, which wraps all five calls into one coroutine.
Install the package
pip install saa-pipecat-client pipecat-ai[daily]
Also on PyPI as attenlabs-saa and npm as @attenlabs/saa-js. This page covers the Cloud SDK's Pipecat adapter.
Set environment variables
export SAA_API_KEY="your-saa-api-key"
export DAILY_API_KEY="your-daily-api-key"
Get SAA_API_KEY self-serve from the dashboard. DAILY_API_KEY mints the hosted bot's meeting token via attention_agent_token().
Wire AttentionEngine into your pipeline
import asyncio
from saa_pipecat_client import (
attention_agent_token,
start_attention_session,
AttentionEngine,
AttentionStartupError,
)
async def main(transport, task):
# Step 1: mint a Daily meeting token for the hosted bot.
token = await attention_agent_token()
# Step 2: provision a session; the hosted bot joins the room.
session = await start_attention_session(room_url=DAILY_ROOM_URL)
# Step 3: create the engine, passing the transport and the
# agent identity returned by start_attention_session().
engine = AttentionEngine(
transport,
agent_identity=session.agent_identity,
)
# Step 4: bind the PipelineTask so upstream actions can be
# queued via DailyOutputTransportMessageUrgentFrame.
engine.bind_task(task)
# Step 5: start. Raises AttentionStartupError if the bot
# publishes an error, or asyncio.TimeoutError if it never
# publishes 'started' within ready_timeout (default 30 s).
await engine.start()
Greenfield alternative: build_attention_runner
from saa_pipecat_client import build_attention_runner
# build_attention_runner composes all three provisioning steps
# (attention_agent_token + start_attention_session +
# AttentionEngine.start) into a single run() coroutine.
runner = build_attention_runner(room_url=DAILY_ROOM_URL)
await runner.run()
Use for new pipelines. For existing pipelines, the five-call pattern above gives full control over session lifecycle.
Event API
PredictionEvent fires at 4 Hz. TurnReadyEvent delivers each utterance as PCM.
Suppress audio forwarding on aligned_class, not raw_class: a 5-tick correction cancels flips right after AI playback ends.
PredictionEvent (4 Hz)
Gate your STT on aligned_class
Carries aligned_class (0/1/2), raw_class, responding (bool), and source ('ai_responding' during playback).
TurnReadyEvent (edge)
Assembled utterance as audio_pcm16
audio_pcm16 (int16 mono 16 kHz), optional JPEG frames, and context (e.g. 'interjection_follow_up'). AttentionEngine reassembles chunks before your callback fires.
InterruptEvent (edge)
Class-2 streak detected during AI playback
Ring-buffer audio is already pre-rolled, so the next TurnReadyEvent carries the barge-in fully assembled. Use this event to cancel TTS.
InterjectionEvent (edge)
Humans-went-quiet pattern detected
Signals an opening for the agent to speak proactively. audio_pcm16 carries the preceding conversation, not silence; any reply arrives as a TurnReadyEvent with context='interjection_follow_up'.
from saa_pipecat_client import (
PredictionEvent,
TurnReadyEvent,
InterruptEvent,
InterjectionEvent,
)
@engine.on("prediction")
async def on_prediction(event: PredictionEvent) -> None:
# Use aligned_class (5-tick AI-aware correction), not raw_class.
# 0 = silent, 1 = human-to-human, 2 = human-to-device.
if event.aligned_class != 2:
# Suppress audio: do not forward to STT or LLM.
return
# Forward audio to your STT pipeline here.
@engine.on("turn_ready")
async def on_turn_ready(event: TurnReadyEvent) -> None:
# event.audio_pcm16: int16 mono PCM at 16 kHz, assembled utterance.
# event.context == 'interjection_follow_up' flags a proactive turn;
# consider using a different LLM prompt for these.
await send_to_stt(event.audio_pcm16)
@engine.on("interrupt")
async def on_interrupt(event: InterruptEvent) -> None:
# Class-2 streak detected during AI playback.
# Ring-buffer audio is pre-rolled; next TurnReadyEvent has the barge-in.
await cancel_tts_playback()
@engine.on("interjection")
async def on_interjection(event: InterjectionEvent) -> None:
# event.audio_pcm16: the preceding conversation audio, not silence.
await handle_interjection(event.audio_pcm16)
Upstream actions
Signal AI playback to arm the interrupt detector. Adjust the threshold anytime.
responding_start / responding_stop arm the interrupt detector. set_threshold changes the gate live, no restart needed.
BotSpeakingObserver: arm the interrupt detector
from pipecat.processors.frame_processor import FrameProcessor
from pipecat.frames.frames import (
TTSStartedFrame,
TTSStoppedFrame,
)
class BotSpeakingObserver(FrameProcessor):
"""Thin FrameProcessor that signals AI playback state to SAA.
Passes all frames through unchanged. Watches TTSStartedFrame
and TTSStoppedFrame as side effects and calls
engine.responding_start() / engine.responding_stop().
Without this signal, InterruptEvent and InterjectionEvent
cannot fire correctly.
"""
def __init__(self, engine: AttentionEngine):
super().__init__()
self._engine = engine
async def process_frame(self, frame, direction):
await super().process_frame(frame, direction)
if isinstance(frame, TTSStartedFrame):
await self._engine.responding_start()
elif isinstance(frame, TTSStoppedFrame):
await self._engine.responding_stop()
The responding state is also reflected in PredictionEvent.responding and PredictionEvent.source so your UI can track it.
set_threshold: tune addressee confidence at runtime
# Adjust the class-2 confidence threshold at runtime.
# Accepts a float in [0.0, 1.0]. Takes effect immediately;
# no session restart required.
#
# Suggested comparison points:
# 0.6: passes borderline utterances; higher recall
# 0.77: balanced default for most shared-space deployments
# 0.88: high-confidence only; fewer false triggers in noisy rooms
#
# Run each under realistic room conditions and keep the value
# with the best command-catch to false-trigger ratio.
await engine.set_threshold(0.77)
OpenAI Realtime
Disable OpenAI's server-side VAD and hand turn ownership to SAA.
Set turn_detection=False and start_audio_paused=True. In on_turn_ready, resample SAA's 16 kHz PCM to 24 kHz and inject it via ConversationItemCreateEvent plus ResponseCreateEvent.
import base64
from pipecat.services.openai_realtime_beta import (
OpenAIRealtimeLLMService,
SessionProperties,
ConversationItemCreateEvent,
ResponseCreateEvent,
)
# Disable OpenAI server-side VAD; SAA owns turn boundaries.
llm = OpenAIRealtimeLLMService(
session_properties=SessionProperties(
audio={"input": {"turn_detection": False}}
),
# No mic audio reaches OpenAI until SAA opens the gate.
start_audio_paused=True,
)
@engine.on("turn_ready")
async def on_turn_ready(event: TurnReadyEvent) -> None:
# SAA delivers int16 mono PCM at 16 kHz.
# OpenAI Realtime expects 24 kHz; resample before injecting.
pcm_24k = resample_16k_to_24k(event.audio_pcm16)
encoded = base64.b64encode(pcm_24k.tobytes()).decode()
# Inject the assembled utterance as a conversation item,
# then request a response.
await llm.push_frame(ConversationItemCreateEvent(audio=encoded))
await llm.push_frame(ResponseCreateEvent())
turn_detection=False
OpenAI never sees side conversations
Every turn OpenAI receives has already been classified class 2. Background speech is filtered before it reaches the model.
start_audio_paused=True
Mic audio stays paused until a turn is ready
The stream opens per utterance, not continuously, when TurnReadyEvent fires. OpenAI's context stays free of audio it was never meant to hear.
Deployment
Any host that runs pipecat-ai[daily] runs saa-pipecat-client identically.
It's a pure Python package with no platform-specific extension modules, so behavior is identical across hosts.
Requirements
Python 3.11+, pipecat-ai[daily] >= 1.0.0
daily-python >= 0.19.0 is a direct dependency. macOS, Linux, and WSL2 are supported; daily-python does not publish Windows native wheels.
Daily Bots + Pipecat Cloud
requirements.txt is enough
No build configuration needed on Daily Bots, Pipecat Cloud, Modal, Kubernetes, or any VM.
Multi-user rooms
One session per target participant
Call start_attention_session once per participant. Each gets its own AttentionEngine instance and event stream.
Network
Daily room URL must be publicly reachable
The hosted bot joins over standard WebRTC; Daily Cloud rooms satisfy this automatically, but self-hosted deployments must allow inbound connections from SAA infrastructure.
For the LiveKit equivalent, see the LiveKit integration guide. For OpenAI Realtime, see the OpenAI Realtime integration guide. For Twilio and ElevenLabs transport adapters, contact us.
Pipecat agents deployed anywhere background speech reaches the mic. SAA gates non-addressed audio before STT or LLM ever run.
Common questions
What Pipecat developers ask before integrating.
Does SAA replace Pipecat's built-in VAD or work alongside it?
SAA works alongside Pipecat's built-in VAD as a separate, higher-level gate: VAD tells you someone is speaking, SAA tells you whether that speech is addressed to your agent (class 2) or someone else (class 1). With OpenAI Realtime, SAA replaces OpenAI's server-side VAD entirely: set turn_detection=False and let on_turn_ready deliver each addressed utterance as PCM.
What happens when speech in the room is not addressed to the agent?
SAA is fail-closed: audio is not forwarded unless the hosted model confidently predicts class 2 (human-to-device). Silence (0) or human-to-human speech (1) is suppressed before inference runs, so you get a missed command rather than a ghost response. A 5-tick AI-aware correction on aligned_class stops transient flips during AI playback from opening the gate.
What is the SAA app-message topic and how are events structured?
All SAA events arrive as JSON on Daily's app-message channel under the topic 'saa', each with a type field. Prediction and VAD envelopes fire at 4 Hz. Edge events fire once: turn_ready at the end of an addressed utterance (PCM and JPEG frames are reassembled automatically), interrupt on a class-2 streak during AI playback, interjection when all humans go quiet, and error for out-of-band faults.
How does SAA know when my agent is speaking, and why does it matter?
Call responding_start() when TTS begins and responding_stop() when it ends, typically from a BotSpeakingObserver FrameProcessor watching TTSStartedFrame and TTSStoppedFrame. Without this signal, the hosted model cannot tell your agent's playback from a user speaking, and the interrupt detector cannot arm correctly.
Does saa-pipecat-client run on Daily Bots or Pipecat Cloud?
Yes. saa-pipecat-client is a pure Python package with no custom extension modules beyond pipecat-ai[daily], so it runs identically on Daily Bots, Pipecat Cloud, Modal, or any container host. See the deployment section above for network and multi-user room requirements.
Can I adjust the addressee confidence threshold at runtime without restarting the session?
Yes. Call set_threshold(value) with a float in [0.0, 1.0] any time after the engine starts. It takes effect immediately, no session restart required. See the upstream actions section above for suggested threshold values.
Get access
Sign up for API keys and start gating addressee audio today.
Self-serve, no approval step. Includes saa-pipecat-client and full docs. Production licensing is per device.
Last updated June 29, 2026