Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,16 @@ ElevenLabs runs its agent inside its own sealed WebRTC room, so this sample uses

Needs **attenlabs-saa >= 0.6.0** and **elevenlabs >= 2.45**.

## Vapi WebSocket transport

Vapi exposes a raw PCM16 WebSocket seam, so this sample uses the **streaming SDK's `feed_audio` ingestion**: it captures the local mic with `sounddevice`, feeds every frame to SAA, and streams gated audio to Vapi's WebSocket transport.

| Sample | What it shows | Run |
|---|---|---|
| [`vapi/voice_agent/`](./vapi/voice_agent) | SAA gating a Vapi assistant over the WebSocket transport so only device-directed speech reaches the model, via `attenlabs-saa`'s `feed_audio`. | `python agent.py` |

Needs **attenlabs-saa >= 0.6.0**, **sounddevice**, and **websockets**. See [`vapi/README.md`](./vapi/README.md) for setup.

## Twilio Media Streams

Inbound or outbound PSTN phone calls, gated by SAA before any audio reaches STT or LLM. The adapter in [`twilio/media_streams/server.py`](./twilio/media_streams/server.py) transcodes μ-law 8 kHz Twilio frames to PCM16 16 kHz and feeds them to SAA via `feed_audio`. Only device-directed caller speech reaches the bridge; side talk and the agent's own TTS echo are filtered out. Three reference bridges are included: `LoggingBridge` (no keys, good for smoke-testing), `OpenAIRealtimeBridge`, and `DeepgramOpenAIElevenLabsBridge`.
Expand Down
6 changes: 6 additions & 0 deletions examples/twilio/media_streams/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@
from dataclasses import dataclass, field
from typing import Any, Awaitable, Callable, Optional, Union

from pathlib import Path

from dotenv import load_dotenv

load_dotenv(Path(__file__).resolve().parents[1] / ".env")

try:
from fastapi import FastAPI, HTTPException, Request, WebSocket, WebSocketDisconnect
from fastapi.responses import JSONResponse, PlainTextResponse, Response
Expand Down
14 changes: 14 additions & 0 deletions examples/vapi/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Shared environment for the examples/vapi sample.
# Copy to .env (cp .env.example .env) and fill in. The sample auto-loads it.

SAA_API_KEY=

# --- Vapi WebSocket transport -----------------------------------------------
# Private API key from https://dashboard.vapi.ai
VAPI_API_KEY=
# Assistant to run (create one in the Vapi dashboard)
VAPI_ASSISTANT_ID=

# --- optional ---------------------------------------------------------------
# Class-2 (device-directed) confidence threshold, 0..1. Higher = stricter gate.
# SAA_CLASS2_THRESHOLD=0.7
66 changes: 66 additions & 0 deletions examples/vapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# SAA + Vapi WebSocket transport

A sample that enables your [Vapi](https://docs.vapi.ai) assistant to only respond when people are talking to it, and stay silent to side conversations or background voices.

## How SAA integrates

Vapi's WebSocket transport exposes a raw PCM16 seam you own end to end, so this sample uses the streaming SDK's `feed_audio` ingestion:

1. `sounddevice` captures 16 kHz mono PCM from the local mic.
2. Every frame is teed into SAA via [`attenlabs-saa`](../../packages/saa-py)'s `feed_audio()` (feed mode: `enable_audio=False`, the SDK captures nothing itself).
3. SAA classifies each frame and emits `prediction` / `turn_ready` / `interrupt` events.
4. The sample gates what reaches Vapi: real audio while device-directed, silence otherwise. Assistant playback drives `mark_responding()` so SAA knows when the agent is speaking.

## Samples

| Sample | Stack | Run |
|---|---|---|
| [`voice_agent/`](./voice_agent) | Vapi WebSocket transport + local mic/speaker, SAA-gated via `feed_audio` | `python agent.py` |

## Quick start

Needs **Python 3.10+**, a Vapi private API key, and an assistant ID from the [Vapi dashboard](https://dashboard.vapi.ai).

```bash
git clone https://github.com/attenlabs/saa-sdk.git
cd saa-sdk/examples/vapi
cp .env.example .env # fill SAA_API_KEY, VAPI_API_KEY, VAPI_ASSISTANT_ID

cd voice_agent
python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ../../../packages/saa-py # local dev against this repo
pip install -r requirements.txt
python agent.py
```

The sample **auto-loads** `examples/vapi/.env`.

## The lines that integrate SAA

```python
saa = AttentionClient(token=SAA_API_KEY, enable_audio=False, enable_video=False)

@saa.on_prediction
def _(ev): gate.update_gate(ev.cls) # only device-directed audio reaches Vapi

saa.start()
# mic tee -> saa.feed_audio() + gated PCM over the Vapi WebSocket
```

See [`voice_agent/README.md`](./voice_agent/README.md) for the full walk-through.

## Shared environment

One env file, [`.env`](./.env.example) in this directory:

| Key | Purpose |
|---|---|
| `SAA_API_KEY` | Your attention labs API key. Get one at [attentionlabs.ai/dashboard](https://attentionlabs.ai/dashboard). |
| `VAPI_API_KEY` | Vapi private API key |
| `VAPI_ASSISTANT_ID` | The assistant to run |

## Requirements & limitations

- **Audio-only** — no video track on the WebSocket transport.
- Turn boundaries are Vapi's STT/VAD on the gated stream (same tradeoff as the ElevenLabs sample).
- Requires PortAudio for local mic + speaker (`sounddevice`).
48 changes: 48 additions & 0 deletions examples/vapi/voice_agent/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# voice_agent: SAA-gated Vapi WebSocket assistant

A [Vapi](https://docs.vapi.ai) assistant reached over the **WebSocket transport**, with **attention labs SAA** device-directed gating wired on top via the streaming SDK's `feed_audio` ingestion.

## The integration

Single file ([`agent.py`](./agent.py)). The moving parts:

- `AttentionClient(token=..., enable_audio=False, enable_video=False)` → streaming SDK in **feed mode**: it opens the cloud WebSocket but captures nothing itself.
- `sounddevice` captures 16 kHz mono PCM from the local mic; every frame is teed into `saa.feed_audio()`.
- `@saa.on_prediction` → `gate.update(ev.cls)` → **the gate**: opens on class-2 (device-directed), closes immediately on class-1 (human-directed). Vapi receives the user's real audio while the gate is open and silence otherwise, so side talk never reaches the assistant's STT.
- Vapi assistant audio on the return WebSocket drives `saa.mark_responding(True/False)` so SAA knows when the agent is speaking (echo is not fed back as user speech).

### Warmup-gated session start

SAA's model isn't classifying for real until its inference buffer fills (~10–15 s of audio). The mic starts feeding SAA before the Vapi call is created; `start_session()` (REST + WebSocket dial) is held until `@saa.on_warmup_complete` fires, with a 20 s timeout fallback.

## Quickstart

```bash
git clone https://github.com/attenlabs/saa-sdk.git
cd saa-sdk/examples/vapi/voice_agent
cp ../.env.example ../.env # fill SAA_API_KEY, VAPI_API_KEY, VAPI_ASSISTANT_ID

python -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ../../../packages/saa-py # local dev against this repo
pip install -r requirements.txt
python agent.py
```

Talk to it. The assistant answers only when you're addressing it; speech directed at someone else in the room is replaced with silence before it reaches Vapi.

## Logs

One line per SAA prediction so the gating is observable:

```
PRED cls=2 conf=0.91 src=model | resp=False gate=open send=real
```

`cls` is the prediction (0 silent / 1 human-directed / 2 device-directed), `gate` the debounced gate, and `send` whether real audio or silence (`muted`) is reaching Vapi that tick.

## Requirements & limitations

- **Audio-only** — Vapi WebSocket transport carries no video, so class-1 ("talking to a human") is weaker than on the multimodal LiveKit / Pipecat paths.
- Needs a Vapi **assistant** configured in the dashboard; this sample does not create one inline.
- Turn boundaries are Vapi's own STT/VAD on the gated audio stream (same tradeoff as the ElevenLabs sample).
- Requires PortAudio (`sounddevice`) for local mic + speaker access.
Loading