attenlabs-saa 0.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- attenlabs_saa-0.3.0/.gitignore +14 -0
- attenlabs_saa-0.3.0/LICENSE +21 -0
- attenlabs_saa-0.3.0/PKG-INFO +260 -0
- attenlabs_saa-0.3.0/README.md +238 -0
- attenlabs_saa-0.3.0/pyproject.toml +43 -0
- attenlabs_saa-0.3.0/src/saa/__init__.py +36 -0
- attenlabs_saa-0.3.0/src/saa/capture.py +218 -0
- attenlabs_saa-0.3.0/src/saa/client.py +495 -0
- attenlabs_saa-0.3.0/src/saa/events.py +77 -0
- attenlabs_saa-0.3.0/src/saa/ws_protocol.py +8 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Attention Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: attenlabs-saa
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Python SDK for AttentionLabs real-time attention detection.
|
|
5
|
+
Project-URL: Homepage, https://attentionlabs.ai
|
|
6
|
+
Author: Attention Labs
|
|
7
|
+
License: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Keywords: attention,audio,realtime,saa,sd-attention,vad,video,websocket
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: numpy>=1.24
|
|
18
|
+
Requires-Dist: opencv-python>=4.8
|
|
19
|
+
Requires-Dist: sounddevice>=0.4.6
|
|
20
|
+
Requires-Dist: websocket-client>=1.6
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# attenlabs-saa
|
|
24
|
+
|
|
25
|
+
Python SDK for [Attention Labs](https://attentionlabs.ai) real-time selective auditory attention.
|
|
26
|
+
|
|
27
|
+
Every voice pipeline has the same problem: the microphone hears everything, but your ASR should only process speech directed at the device. Wake words solve this with a rigid trigger phrase. SAA solves it without one — classifying every audio frame as **silent**, **human-directed**, or **device-directed** and routing only what matters.
|
|
28
|
+
|
|
29
|
+
`attenlabs-saa` streams mic and webcam data to the SAA inference server over WebSocket and emits typed events: attention predictions, voice activity, conversation state, and ready-to-forward speech audio. LLM routing is left to you.
|
|
30
|
+
|
|
31
|
+
## Sign up
|
|
32
|
+
|
|
33
|
+
Get your API token at [attentionlabs.ai/dashboard](https://attentionlabs.ai/dashboard).
|
|
34
|
+
|
|
35
|
+
**You need your API Key for this project to work**
|
|
36
|
+
|
|
37
|
+
## Install
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install attenlabs-saa
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Requires Python 3.10+. `sounddevice` and `opencv-python` are pulled in automatically for mic and camera access.
|
|
44
|
+
|
|
45
|
+
## Quickstart
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import time
|
|
49
|
+
from saa import AttentionClient
|
|
50
|
+
|
|
51
|
+
client = AttentionClient(token="your-token")
|
|
52
|
+
|
|
53
|
+
@client.on_prediction
|
|
54
|
+
def _(event):
|
|
55
|
+
label = {0: "silent", 1: "human", 2: "device"}.get(event.cls, "?")
|
|
56
|
+
print(f"{label} {event.confidence:.0%} faces={event.num_faces} src={event.source}")
|
|
57
|
+
|
|
58
|
+
@client.on_speech_ready
|
|
59
|
+
def _(event):
|
|
60
|
+
# event.audio_base64 — base64 PCM16 @ 16 kHz mono, ready for OpenAI Realtime / any LLM
|
|
61
|
+
# event.audio_pcm16 — same audio as np.int16 array
|
|
62
|
+
print(f"speech ready ({event.duration_sec:.2f}s)")
|
|
63
|
+
|
|
64
|
+
@client.on_error
|
|
65
|
+
def _(event):
|
|
66
|
+
print(f"ERROR: {event.title}: {event.message}")
|
|
67
|
+
|
|
68
|
+
client.start()
|
|
69
|
+
try:
|
|
70
|
+
while True:
|
|
71
|
+
time.sleep(0.1)
|
|
72
|
+
except KeyboardInterrupt:
|
|
73
|
+
client.stop()
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A full CLI demo wiring SAA + OpenAI Realtime lives at [**saa-py-demo**](https://github.com/attenlabs/saa-py-demo).
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## API
|
|
81
|
+
|
|
82
|
+
### `AttentionClient`
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
from saa import AttentionClient, CameraConfig, MicConfig
|
|
86
|
+
|
|
87
|
+
client = AttentionClient(
|
|
88
|
+
token="...", # Auth token — sent as WS subprotocol
|
|
89
|
+
url=None, # Server URL (default: wss://server.attentionlabs.ai/ws)
|
|
90
|
+
video=CameraConfig(), # Webcam config
|
|
91
|
+
audio=MicConfig(), # Mic config
|
|
92
|
+
initial_threshold=0.7, # Device-class confidence threshold (0..1)
|
|
93
|
+
enable_audio=True, # Set False to skip mic capture
|
|
94
|
+
enable_video=True, # Set False to skip webcam capture
|
|
95
|
+
)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Configuration
|
|
99
|
+
|
|
100
|
+
#### `MicConfig`
|
|
101
|
+
|
|
102
|
+
| field | type | default | notes |
|
|
103
|
+
| ---------- | ----------------------- | ------- | ------------------------------------------ |
|
|
104
|
+
| `device` | `int \| str \| None` | `None` | Device index, name, or `None` for system default |
|
|
105
|
+
| `channels` | `int` | `1` | Number of input channels |
|
|
106
|
+
|
|
107
|
+
#### `CameraConfig`
|
|
108
|
+
|
|
109
|
+
| field | type | default | notes |
|
|
110
|
+
| -------------- | ----- | ------- | ----------------------------- |
|
|
111
|
+
| `device_index` | `int` | `0` | Webcam device index |
|
|
112
|
+
| `width` | `int` | `1920` | Capture width |
|
|
113
|
+
| `height` | `int` | `1080` | Capture height |
|
|
114
|
+
| `jpeg_quality` | `int` | `60` | JPEG compression quality 0–100 |
|
|
115
|
+
|
|
116
|
+
### Methods
|
|
117
|
+
|
|
118
|
+
| method | description |
|
|
119
|
+
| ---------------------------- | ----------- |
|
|
120
|
+
| `start()` | Opens WebSocket, acquires mic + camera, starts capture threads. Non-blocking. Raises on handshake failure. |
|
|
121
|
+
| `stop()` | Tears down capture, joins threads, closes WebSocket. |
|
|
122
|
+
| `mute()` | Pauses upstream audio and signals server to stop VAD. |
|
|
123
|
+
| `unmute()` | Resumes upstream audio. |
|
|
124
|
+
| `mark_responding(bool)` | Tell the server an LLM response is in flight. Server stops emitting predictions while `True`. |
|
|
125
|
+
| `set_threshold(value: float)` | Update device-class confidence threshold (0..1). Server acks via `config` event. |
|
|
126
|
+
|
|
127
|
+
### Events
|
|
128
|
+
|
|
129
|
+
Register handlers with decorators. All callbacks fire on internal threads — keep them fast or hand work off to your own thread.
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
@client.on_prediction
|
|
133
|
+
def handle(event):
|
|
134
|
+
...
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
| decorator | payload | fires when |
|
|
138
|
+
| --------------------- | ------------------------------------------------------------------------ | --------------------------------------- |
|
|
139
|
+
| `@on_connected` | — | WebSocket opens |
|
|
140
|
+
| `@on_started` | — | Server-side warmup complete |
|
|
141
|
+
| `@on_warmup_complete` | — | First non-zero-confidence prediction |
|
|
142
|
+
| `@on_prediction` | `PredictionEvent` | Each attention prediction |
|
|
143
|
+
| `@on_vad` | `VadEvent` | Voice activity update |
|
|
144
|
+
| `@on_state` | `StateEvent` | Conversation state transition |
|
|
145
|
+
| `@on_speech_ready` | `SpeechReadyEvent` | Complete speech segment ready to forward |
|
|
146
|
+
| `@on_config` | `ConfigEvent` | Server acks a threshold change |
|
|
147
|
+
| `@on_stats` | `StatsEvent` | Every ~10s with connection health |
|
|
148
|
+
| `@on_error` | `AttentionErrorEvent` | Connection, auth, or server error |
|
|
149
|
+
| `@on_disconnected` | `DisconnectedEvent` | WebSocket closes |
|
|
150
|
+
|
|
151
|
+
### Event types
|
|
152
|
+
|
|
153
|
+
#### `PredictionEvent`
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
cls: int # 0 = silent, 1 = human-directed, 2 = device-directed
|
|
157
|
+
confidence: float # 0..1
|
|
158
|
+
source: str # "video" or "audio"
|
|
159
|
+
num_faces: int # faces detected in frame
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### `VadEvent`
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
probability: float # VAD probability 0..1
|
|
166
|
+
is_speech: bool # whether speech was detected
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
#### `StateEvent`
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
state: ConversationState # "listening" | "sending" | "cancelled" | "idle"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
#### `SpeechReadyEvent`
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
audio_pcm16: np.ndarray # int16 array @ 16 kHz mono
|
|
179
|
+
audio_base64: str # same audio as base64 — ready for OpenAI Realtime, etc.
|
|
180
|
+
duration_sec: float # duration in seconds
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
#### `ConfigEvent`
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
model_class2_threshold: float # server-confirmed threshold
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### `StatsEvent`
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
rtt_ms: float | None # round-trip latency in ms
|
|
193
|
+
sent_video: int # total video frames sent
|
|
194
|
+
skipped_video: int # total video frames skipped
|
|
195
|
+
sent_audio: int # total audio chunks sent
|
|
196
|
+
uptime_s: float # connection uptime in seconds
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### `AttentionErrorEvent`
|
|
200
|
+
|
|
201
|
+
```python
|
|
202
|
+
title: str # error category ("Auth Failed", "Connection Stalled", etc.)
|
|
203
|
+
message: str # human-readable message
|
|
204
|
+
detail: str | None = None # technical detail
|
|
205
|
+
code: int | None = None # WebSocket close code, if applicable
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### `DisconnectedEvent`
|
|
209
|
+
|
|
210
|
+
```python
|
|
211
|
+
code: int # WebSocket close code
|
|
212
|
+
reason: str # close reason
|
|
213
|
+
was_clean: bool # True if code == 1000
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## LLM integration
|
|
219
|
+
|
|
220
|
+
LLM routing is intentionally **not** part of the SDK. The `speech_ready` event hands you PCM16 audio — both as a NumPy array and as base64 — forward it wherever you like.
|
|
221
|
+
|
|
222
|
+
When your LLM starts generating, call `mute()` + `mark_responding(True)` to suppress predictions during playback. When it finishes, `unmute()` + `mark_responding(False)`.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from saa import AttentionClient
|
|
226
|
+
|
|
227
|
+
client = AttentionClient(token="...")
|
|
228
|
+
|
|
229
|
+
@client.on_speech_ready
|
|
230
|
+
def _(event):
|
|
231
|
+
# Forward to your LLM of choice
|
|
232
|
+
your_llm.send(event.audio_base64)
|
|
233
|
+
|
|
234
|
+
def on_llm_speaking():
|
|
235
|
+
client.mute()
|
|
236
|
+
client.mark_responding(True)
|
|
237
|
+
|
|
238
|
+
def on_llm_done():
|
|
239
|
+
client.unmute()
|
|
240
|
+
client.mark_responding(False)
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
See [**saa-py-demo**](https://github.com/attenlabs/saa-py-demo) for a full working example with OpenAI Realtime.
|
|
244
|
+
|
|
245
|
+
## Threading model
|
|
246
|
+
|
|
247
|
+
The SDK manages four threads internally:
|
|
248
|
+
|
|
249
|
+
| thread | purpose |
|
|
250
|
+
| ---------------- | ---------------------------------- |
|
|
251
|
+
| `saa-ws` | WebSocket send/receive |
|
|
252
|
+
| `saa-heartbeat` | JSON pings every 5s, stats every 10s |
|
|
253
|
+
| `saa-camera` | JPEG capture at 4 fps (250 ms) |
|
|
254
|
+
| *(sounddevice)* | Audio callback at native sample rate, resampled to 16 kHz |
|
|
255
|
+
|
|
256
|
+
All event callbacks fire on `saa-ws` or `saa-heartbeat`. Don't block them — offload heavy work to your own thread.
|
|
257
|
+
|
|
258
|
+
## License
|
|
259
|
+
|
|
260
|
+
MIT
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# attenlabs-saa
|
|
2
|
+
|
|
3
|
+
Python SDK for [Attention Labs](https://attentionlabs.ai) real-time selective auditory attention.
|
|
4
|
+
|
|
5
|
+
Every voice pipeline has the same problem: the microphone hears everything, but your ASR should only process speech directed at the device. Wake words solve this with a rigid trigger phrase. SAA solves it without one — classifying every audio frame as **silent**, **human-directed**, or **device-directed** and routing only what matters.
|
|
6
|
+
|
|
7
|
+
`attenlabs-saa` streams mic and webcam data to the SAA inference server over WebSocket and emits typed events: attention predictions, voice activity, conversation state, and ready-to-forward speech audio. LLM routing is left to you.
|
|
8
|
+
|
|
9
|
+
## Sign up
|
|
10
|
+
|
|
11
|
+
Get your API token at [attentionlabs.ai/dashboard](https://attentionlabs.ai/dashboard).
|
|
12
|
+
|
|
13
|
+
**You need your API Key for this project to work**
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install attenlabs-saa
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires Python 3.10+. `sounddevice` and `opencv-python` are pulled in automatically for mic and camera access.
|
|
22
|
+
|
|
23
|
+
## Quickstart
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import time
|
|
27
|
+
from saa import AttentionClient
|
|
28
|
+
|
|
29
|
+
client = AttentionClient(token="your-token")
|
|
30
|
+
|
|
31
|
+
@client.on_prediction
|
|
32
|
+
def _(event):
|
|
33
|
+
label = {0: "silent", 1: "human", 2: "device"}.get(event.cls, "?")
|
|
34
|
+
print(f"{label} {event.confidence:.0%} faces={event.num_faces} src={event.source}")
|
|
35
|
+
|
|
36
|
+
@client.on_speech_ready
|
|
37
|
+
def _(event):
|
|
38
|
+
# event.audio_base64 — base64 PCM16 @ 16 kHz mono, ready for OpenAI Realtime / any LLM
|
|
39
|
+
# event.audio_pcm16 — same audio as np.int16 array
|
|
40
|
+
print(f"speech ready ({event.duration_sec:.2f}s)")
|
|
41
|
+
|
|
42
|
+
@client.on_error
|
|
43
|
+
def _(event):
|
|
44
|
+
print(f"ERROR: {event.title}: {event.message}")
|
|
45
|
+
|
|
46
|
+
client.start()
|
|
47
|
+
try:
|
|
48
|
+
while True:
|
|
49
|
+
time.sleep(0.1)
|
|
50
|
+
except KeyboardInterrupt:
|
|
51
|
+
client.stop()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A full CLI demo wiring SAA + OpenAI Realtime lives at [**saa-py-demo**](https://github.com/attenlabs/saa-py-demo).
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
### `AttentionClient`
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
from saa import AttentionClient, CameraConfig, MicConfig
|
|
64
|
+
|
|
65
|
+
client = AttentionClient(
|
|
66
|
+
token="...", # Auth token — sent as WS subprotocol
|
|
67
|
+
url=None, # Server URL (default: wss://server.attentionlabs.ai/ws)
|
|
68
|
+
video=CameraConfig(), # Webcam config
|
|
69
|
+
audio=MicConfig(), # Mic config
|
|
70
|
+
initial_threshold=0.7, # Device-class confidence threshold (0..1)
|
|
71
|
+
enable_audio=True, # Set False to skip mic capture
|
|
72
|
+
enable_video=True, # Set False to skip webcam capture
|
|
73
|
+
)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Configuration
|
|
77
|
+
|
|
78
|
+
#### `MicConfig`
|
|
79
|
+
|
|
80
|
+
| field | type | default | notes |
|
|
81
|
+
| ---------- | ----------------------- | ------- | ------------------------------------------ |
|
|
82
|
+
| `device` | `int \| str \| None` | `None` | Device index, name, or `None` for system default |
|
|
83
|
+
| `channels` | `int` | `1` | Number of input channels |
|
|
84
|
+
|
|
85
|
+
#### `CameraConfig`
|
|
86
|
+
|
|
87
|
+
| field | type | default | notes |
|
|
88
|
+
| -------------- | ----- | ------- | ----------------------------- |
|
|
89
|
+
| `device_index` | `int` | `0` | Webcam device index |
|
|
90
|
+
| `width` | `int` | `1920` | Capture width |
|
|
91
|
+
| `height` | `int` | `1080` | Capture height |
|
|
92
|
+
| `jpeg_quality` | `int` | `60` | JPEG compression quality 0–100 |
|
|
93
|
+
|
|
94
|
+
### Methods
|
|
95
|
+
|
|
96
|
+
| method | description |
|
|
97
|
+
| ---------------------------- | ----------- |
|
|
98
|
+
| `start()` | Opens WebSocket, acquires mic + camera, starts capture threads. Non-blocking. Raises on handshake failure. |
|
|
99
|
+
| `stop()` | Tears down capture, joins threads, closes WebSocket. |
|
|
100
|
+
| `mute()` | Pauses upstream audio and signals server to stop VAD. |
|
|
101
|
+
| `unmute()` | Resumes upstream audio. |
|
|
102
|
+
| `mark_responding(bool)` | Tell the server an LLM response is in flight. Server stops emitting predictions while `True`. |
|
|
103
|
+
| `set_threshold(value: float)` | Update device-class confidence threshold (0..1). Server acks via `config` event. |
|
|
104
|
+
|
|
105
|
+
### Events
|
|
106
|
+
|
|
107
|
+
Register handlers with decorators. All callbacks fire on internal threads — keep them fast or hand work off to your own thread.
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
@client.on_prediction
|
|
111
|
+
def handle(event):
|
|
112
|
+
...
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
| decorator | payload | fires when |
|
|
116
|
+
| --------------------- | ------------------------------------------------------------------------ | --------------------------------------- |
|
|
117
|
+
| `@on_connected` | — | WebSocket opens |
|
|
118
|
+
| `@on_started` | — | Server-side warmup complete |
|
|
119
|
+
| `@on_warmup_complete` | — | First non-zero-confidence prediction |
|
|
120
|
+
| `@on_prediction` | `PredictionEvent` | Each attention prediction |
|
|
121
|
+
| `@on_vad` | `VadEvent` | Voice activity update |
|
|
122
|
+
| `@on_state` | `StateEvent` | Conversation state transition |
|
|
123
|
+
| `@on_speech_ready` | `SpeechReadyEvent` | Complete speech segment ready to forward |
|
|
124
|
+
| `@on_config` | `ConfigEvent` | Server acks a threshold change |
|
|
125
|
+
| `@on_stats` | `StatsEvent` | Every ~10s with connection health |
|
|
126
|
+
| `@on_error` | `AttentionErrorEvent` | Connection, auth, or server error |
|
|
127
|
+
| `@on_disconnected` | `DisconnectedEvent` | WebSocket closes |
|
|
128
|
+
|
|
129
|
+
### Event types
|
|
130
|
+
|
|
131
|
+
#### `PredictionEvent`
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
cls: int # 0 = silent, 1 = human-directed, 2 = device-directed
|
|
135
|
+
confidence: float # 0..1
|
|
136
|
+
source: str # "video" or "audio"
|
|
137
|
+
num_faces: int # faces detected in frame
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### `VadEvent`
|
|
141
|
+
|
|
142
|
+
```python
|
|
143
|
+
probability: float # VAD probability 0..1
|
|
144
|
+
is_speech: bool # whether speech was detected
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
#### `StateEvent`
|
|
148
|
+
|
|
149
|
+
```python
|
|
150
|
+
state: ConversationState # "listening" | "sending" | "cancelled" | "idle"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### `SpeechReadyEvent`
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
audio_pcm16: np.ndarray # int16 array @ 16 kHz mono
|
|
157
|
+
audio_base64: str # same audio as base64 — ready for OpenAI Realtime, etc.
|
|
158
|
+
duration_sec: float # duration in seconds
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
#### `ConfigEvent`
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
model_class2_threshold: float # server-confirmed threshold
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
#### `StatsEvent`
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
rtt_ms: float | None # round-trip latency in ms
|
|
171
|
+
sent_video: int # total video frames sent
|
|
172
|
+
skipped_video: int # total video frames skipped
|
|
173
|
+
sent_audio: int # total audio chunks sent
|
|
174
|
+
uptime_s: float # connection uptime in seconds
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### `AttentionErrorEvent`
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
title: str # error category ("Auth Failed", "Connection Stalled", etc.)
|
|
181
|
+
message: str # human-readable message
|
|
182
|
+
detail: str | None = None # technical detail
|
|
183
|
+
code: int | None = None # WebSocket close code, if applicable
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
#### `DisconnectedEvent`
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
code: int # WebSocket close code
|
|
190
|
+
reason: str # close reason
|
|
191
|
+
was_clean: bool # True if code == 1000
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## LLM integration
|
|
197
|
+
|
|
198
|
+
LLM routing is intentionally **not** part of the SDK. The `speech_ready` event hands you PCM16 audio — both as a NumPy array and as base64 — forward it wherever you like.
|
|
199
|
+
|
|
200
|
+
When your LLM starts generating, call `mute()` + `mark_responding(True)` to suppress predictions during playback. When it finishes, `unmute()` + `mark_responding(False)`.
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from saa import AttentionClient
|
|
204
|
+
|
|
205
|
+
client = AttentionClient(token="...")
|
|
206
|
+
|
|
207
|
+
@client.on_speech_ready
|
|
208
|
+
def _(event):
|
|
209
|
+
# Forward to your LLM of choice
|
|
210
|
+
your_llm.send(event.audio_base64)
|
|
211
|
+
|
|
212
|
+
def on_llm_speaking():
|
|
213
|
+
client.mute()
|
|
214
|
+
client.mark_responding(True)
|
|
215
|
+
|
|
216
|
+
def on_llm_done():
|
|
217
|
+
client.unmute()
|
|
218
|
+
client.mark_responding(False)
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
See [**saa-py-demo**](https://github.com/attenlabs/saa-py-demo) for a full working example with OpenAI Realtime.
|
|
222
|
+
|
|
223
|
+
## Threading model
|
|
224
|
+
|
|
225
|
+
The SDK manages four threads internally:
|
|
226
|
+
|
|
227
|
+
| thread | purpose |
|
|
228
|
+
| ---------------- | ---------------------------------- |
|
|
229
|
+
| `saa-ws` | WebSocket send/receive |
|
|
230
|
+
| `saa-heartbeat` | JSON pings every 5s, stats every 10s |
|
|
231
|
+
| `saa-camera` | JPEG capture at 4 fps (250 ms) |
|
|
232
|
+
| *(sounddevice)* | Audio callback at native sample rate, resampled to 16 kHz |
|
|
233
|
+
|
|
234
|
+
All event callbacks fire on `saa-ws` or `saa-heartbeat`. Don't block them — offload heavy work to your own thread.
|
|
235
|
+
|
|
236
|
+
## License
|
|
237
|
+
|
|
238
|
+
MIT
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "attenlabs-saa"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Python SDK for AttentionLabs real-time attention detection."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Attention Labs" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["sd-attention", "attention", "saa", "audio", "video", "vad", "realtime", "websocket"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"numpy>=1.24",
|
|
26
|
+
"websocket-client>=1.6",
|
|
27
|
+
"sounddevice>=0.4.6",
|
|
28
|
+
"opencv-python>=4.8",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://attentionlabs.ai"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
packages = ["src/saa"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.sdist]
|
|
38
|
+
include = [
|
|
39
|
+
"src/saa",
|
|
40
|
+
"README.md",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"pyproject.toml",
|
|
43
|
+
]
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""saa-py — Python SDK for the SD Attention Server (SAA)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .capture import CameraConfig, MicConfig
|
|
6
|
+
from .client import AttentionClient
|
|
7
|
+
from .events import (
|
|
8
|
+
AttentionErrorEvent,
|
|
9
|
+
ConfigEvent,
|
|
10
|
+
ConversationState,
|
|
11
|
+
DisconnectedEvent,
|
|
12
|
+
PredictionEvent,
|
|
13
|
+
StateEvent,
|
|
14
|
+
StatsEvent,
|
|
15
|
+
TurnFrame,
|
|
16
|
+
TurnReadyEvent,
|
|
17
|
+
VadEvent,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"AttentionClient",
|
|
22
|
+
"CameraConfig",
|
|
23
|
+
"MicConfig",
|
|
24
|
+
"PredictionEvent",
|
|
25
|
+
"VadEvent",
|
|
26
|
+
"StateEvent",
|
|
27
|
+
"TurnFrame",
|
|
28
|
+
"TurnReadyEvent",
|
|
29
|
+
"ConfigEvent",
|
|
30
|
+
"StatsEvent",
|
|
31
|
+
"AttentionErrorEvent",
|
|
32
|
+
"DisconnectedEvent",
|
|
33
|
+
"ConversationState",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.3.0"
|