sayna-client 0.0.9__py3-none-any.whl

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.
@@ -0,0 +1,553 @@
1
+ Metadata-Version: 2.4
2
+ Name: sayna-client
3
+ Version: 0.0.9
4
+ Summary: Python SDK for Sayna server-side WebSocket connections
5
+ Author: Sayna Team
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/SaynaAi/saysdk
8
+ Project-URL: Repository, https://github.com/SaynaAi/saysdk
9
+ Project-URL: Documentation, https://github.com/SaynaAi/saysdk/tree/main/python-sdk
10
+ Project-URL: Issues, https://github.com/SaynaAi/saysdk/issues
11
+ Keywords: sayna,websocket,sdk,server,real-time
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: Communications
24
+ Classifier: Framework :: AsyncIO
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ Requires-Dist: aiohttp>=3.9.0
29
+ Requires-Dist: pydantic>=2.0.0
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=8.0.0; extra == "dev"
32
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == "dev"
33
+ Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
34
+ Requires-Dist: mypy>=1.8.0; extra == "dev"
35
+ Requires-Dist: ruff>=0.6.0; extra == "dev"
36
+
37
+ # Sayna Python SDK
38
+
39
+ Python SDK for Sayna's real-time voice interaction API. Send audio for speech recognition, receive synthesized speech, and manage voice sessions from your Python applications.
40
+
41
+ ## Features
42
+
43
+ - 🎤 **Speech-to-Text**: Real-time transcription with support for multiple providers (Deepgram, Google, etc.)
44
+ - 🔊 **Text-to-Speech**: High-quality voice synthesis with various TTS providers (ElevenLabs, Google, etc.)
45
+ - 🔌 **WebSocket Connection**: Async/await support with aiohttp
46
+ - 🌐 **REST API**: Standalone endpoints for health checks, voice catalog, TTS synthesis, and SIP hooks management
47
+ - 🔐 **Webhook Receiver**: Secure verification and parsing of SIP webhooks with HMAC-SHA256 signatures
48
+ - ✅ **Type Safety**: Full type hints with Pydantic models
49
+ - 🚀 **Easy to Use**: Simple, intuitive API
50
+ - 📦 **Modern Python**: Built for Python 3.9+
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install sayna-client
56
+ ```
57
+
58
+ ## Quick Start
59
+
60
+ ```python
61
+ import asyncio
62
+ from sayna_client import SaynaClient, STTConfig, TTSConfig
63
+
64
+ async def main():
65
+ # Initialize the client with configs
66
+ client = SaynaClient(
67
+ url="https://api.sayna.ai",
68
+ stt_config=STTConfig(
69
+ provider="deepgram",
70
+ model="nova-2"
71
+ ),
72
+ tts_config=TTSConfig(
73
+ provider="cartesia",
74
+ voice_id="example-voice"
75
+ ),
76
+ api_key="your-api-key"
77
+ )
78
+
79
+ # Register callbacks
80
+ client.register_on_stt_result(lambda result: print(f"Transcription: {result.transcript}"))
81
+ client.register_on_tts_audio(lambda audio: print(f"Received {len(audio)} bytes of audio"))
82
+
83
+ # Connect and interact
84
+ await client.connect()
85
+ await client.speak("Hello, world!")
86
+ await client.disconnect()
87
+
88
+ if __name__ == "__main__":
89
+ asyncio.run(main())
90
+ ```
91
+
92
+ ## API
93
+
94
+ ### REST API Methods
95
+
96
+ These methods use HTTP endpoints and don't require an active WebSocket connection:
97
+
98
+ #### `await client.health()`
99
+
100
+ Performs a health check on the Sayna server.
101
+
102
+ **Returns**: `HealthResponse` - Response object with `status` field ("OK" when healthy).
103
+
104
+ **Example**:
105
+ ```python
106
+ health = await client.health()
107
+ print(health.status) # "OK"
108
+ ```
109
+
110
+ ---
111
+
112
+ #### `await client.get_voices()`
113
+
114
+ Retrieves the catalogue of text-to-speech voices grouped by provider.
115
+
116
+ **Returns**: `dict[str, list[VoiceDescriptor]]` - Dictionary where keys are provider names and values are lists of voice descriptors.
117
+
118
+ **Example**:
119
+ ```python
120
+ voices = await client.get_voices()
121
+ for provider, voice_list in voices.items():
122
+ print(f"{provider}:", [v.name for v in voice_list])
123
+ ```
124
+
125
+ ---
126
+
127
+ #### `await client.speak_rest(text, tts_config)`
128
+
129
+ Synthesizes text into audio using the REST API. This is a standalone method that doesn't require an active WebSocket connection.
130
+
131
+ | Parameter | Type | Purpose |
132
+ | --- | --- | --- |
133
+ | `text` | `str` | Text to synthesize (must be non-empty). |
134
+ | `tts_config` | `TTSConfig` | Text-to-speech provider configuration. |
135
+
136
+ **Returns**: `tuple[bytes, dict[str, str]]` - Tuple of (audio_data, response_headers). Headers include: Content-Type, Content-Length, x-audio-format, x-sample-rate.
137
+
138
+ **Example**:
139
+ ```python
140
+ audio_data, headers = await client.speak_rest("Hello, world!", TTSConfig(
141
+ provider="elevenlabs",
142
+ voice_id="21m00Tcm4TlvDq8ikWAM",
143
+ model="eleven_turbo_v2",
144
+ speaking_rate=1.0,
145
+ audio_format="mp3",
146
+ sample_rate=24000,
147
+ connection_timeout=30,
148
+ request_timeout=60,
149
+ pronunciations=[]
150
+ ))
151
+ print(f"Received {len(audio_data)} bytes of {headers['Content-Type']}")
152
+ ```
153
+
154
+ ---
155
+
156
+ #### `await client.get_livekit_token(room_name, participant_name, participant_identity)`
157
+
158
+ Issues a LiveKit access token for a participant.
159
+
160
+ | Parameter | Type | Purpose |
161
+ | --- | --- | --- |
162
+ | `room_name` | `str` | LiveKit room to join or create. |
163
+ | `participant_name` | `str` | Display name for the participant. |
164
+ | `participant_identity` | `str` | Unique identifier for the participant. |
165
+
166
+ **Returns**: `LiveKitTokenResponse` - Object containing token, room name, participant identity, and LiveKit URL.
167
+
168
+ **Example**:
169
+ ```python
170
+ token_info = await client.get_livekit_token(
171
+ "my-room",
172
+ "John Doe",
173
+ "user-123"
174
+ )
175
+ print("Token:", token_info.token)
176
+ print("LiveKit URL:", token_info.livekit_url)
177
+ ```
178
+
179
+ ---
180
+
181
+ #### `await client.get_sip_hooks()`
182
+
183
+ Retrieves all configured SIP webhook hooks from the runtime cache.
184
+
185
+ **Returns**: `SipHooksResponse` - Object containing the list of configured SIP hooks.
186
+
187
+ **Example**:
188
+ ```python
189
+ hooks = await client.get_sip_hooks()
190
+ for hook in hooks.hooks:
191
+ print(f"{hook.host} -> {hook.url}")
192
+ ```
193
+
194
+ ---
195
+
196
+ #### `await client.set_sip_hooks(hooks)`
197
+
198
+ Adds or replaces SIP webhook hooks. Existing hooks with matching hosts (case-insensitive) are replaced.
199
+
200
+ | Parameter | Type | Purpose |
201
+ | --- | --- | --- |
202
+ | `hooks` | `list[SipHook]` | List of SipHook objects to add or replace. |
203
+
204
+ **Returns**: `SipHooksResponse` - Object containing the merged list of all hooks (existing + new).
205
+
206
+ **Example**:
207
+ ```python
208
+ from sayna_client import SipHook
209
+
210
+ hooks = [
211
+ SipHook(host="example.com", url="https://webhook.example.com/events"),
212
+ SipHook(host="another.com", url="https://webhook.another.com/events"),
213
+ ]
214
+ response = await client.set_sip_hooks(hooks)
215
+ print(f"Total hooks: {len(response.hooks)}")
216
+ ```
217
+
218
+ ---
219
+
220
+ ### WebSocket API Methods
221
+
222
+ These methods require an active WebSocket connection:
223
+
224
+ #### `SaynaClient(url, stt_config, tts_config, livekit_config=None, without_audio=False, api_key=None)`
225
+
226
+ Creates a new SaynaClient instance.
227
+
228
+ | Parameter | Type | Default | Purpose |
229
+ | --- | --- | --- | --- |
230
+ | `url` | `str` | - | Sayna server URL (http://, https://, ws://, or wss://). |
231
+ | `stt_config` | `STTConfig` | - | Speech-to-text provider configuration. |
232
+ | `tts_config` | `TTSConfig` | - | Text-to-speech provider configuration. |
233
+ | `livekit_config` | `LiveKitConfig` (optional) | `None` | Optional LiveKit room configuration. |
234
+ | `without_audio` | `bool` | `False` | Disable audio streaming. |
235
+ | `api_key` | `str` (optional) | `None` | API key for authentication. |
236
+
237
+ ---
238
+
239
+ #### `await client.connect()`
240
+
241
+ Establishes WebSocket connection and sends initial configuration. Resolves when server sends ready message.
242
+
243
+ ---
244
+
245
+ #### `client.register_on_stt_result(callback)`
246
+
247
+ Registers a callback for speech-to-text transcription results.
248
+
249
+ **Example**:
250
+ ```python
251
+ def handle_stt(result):
252
+ print(f"Transcription: {result.transcript}")
253
+
254
+ client.register_on_stt_result(handle_stt)
255
+ ```
256
+
257
+ ---
258
+
259
+ #### `client.register_on_tts_audio(callback)`
260
+
261
+ Registers a callback for text-to-speech audio data.
262
+
263
+ **Example**:
264
+ ```python
265
+ def handle_audio(audio_data: bytes):
266
+ print(f"Received {len(audio_data)} bytes of audio")
267
+
268
+ client.register_on_tts_audio(handle_audio)
269
+ ```
270
+
271
+ ---
272
+
273
+ #### `client.register_on_error(callback)`
274
+
275
+ Registers a callback for error messages.
276
+
277
+ ---
278
+
279
+ #### `client.register_on_message(callback)`
280
+
281
+ Registers a callback for participant messages.
282
+
283
+ ---
284
+
285
+ #### `client.register_on_participant_disconnected(callback)`
286
+
287
+ Registers a callback for participant disconnection events.
288
+
289
+ ---
290
+
291
+ #### `client.register_on_tts_playback_complete(callback)`
292
+
293
+ Registers a callback for TTS playback completion events.
294
+
295
+ ---
296
+
297
+ #### `await client.speak(text, flush=True, allow_interruption=True)`
298
+
299
+ Sends text to be synthesized as speech.
300
+
301
+ | Parameter | Type | Default | Purpose |
302
+ | --- | --- | --- | --- |
303
+ | `text` | `str` | - | Text to synthesize. |
304
+ | `flush` | `bool` | `True` | Clear TTS queue before speaking. |
305
+ | `allow_interruption` | `bool` | `True` | Allow speech to be interrupted. |
306
+
307
+ **Example**:
308
+ ```python
309
+ await client.speak("Hello, world!")
310
+ await client.speak("Important message", flush=True, allow_interruption=False)
311
+ ```
312
+
313
+ ---
314
+
315
+ #### `await client.on_audio_input(audio_data)`
316
+
317
+ Sends raw audio data (bytes) to the server for speech recognition.
318
+
319
+ **Example**:
320
+ ```python
321
+ await client.on_audio_input(audio_bytes)
322
+ ```
323
+
324
+ ---
325
+
326
+ #### `await client.send_message(message, role, topic="messages", debug=None)`
327
+
328
+ Sends a message to the Sayna session with role and optional metadata.
329
+
330
+ | Parameter | Type | Default | Purpose |
331
+ | --- | --- | --- | --- |
332
+ | `message` | `str` | - | Message content. |
333
+ | `role` | `str` | - | Sender role (e.g., 'user', 'assistant'). |
334
+ | `topic` | `str` | `"messages"` | LiveKit topic/channel. |
335
+ | `debug` | `dict` (optional) | `None` | Optional debug metadata. |
336
+
337
+ ---
338
+
339
+ #### `await client.clear()`
340
+
341
+ Clears the text-to-speech queue.
342
+
343
+ ---
344
+
345
+ #### `await client.tts_flush(allow_interruption=True)`
346
+
347
+ Flushes the TTS queue by sending an empty speak command.
348
+
349
+ ---
350
+
351
+ #### `await client.disconnect()`
352
+
353
+ Disconnects from the WebSocket server and cleans up resources.
354
+
355
+ ---
356
+
357
+ #### Properties
358
+
359
+ - **`client.ready`**: Boolean indicating whether the connection is ready (received ready message).
360
+ - **`client.connected`**: Boolean indicating whether the WebSocket is connected.
361
+ - **`client.livekit_room_name`**: LiveKit room name (available after ready when LiveKit is enabled).
362
+ - **`client.livekit_url`**: LiveKit URL (available after ready).
363
+ - **`client.sayna_participant_identity`**: Sayna participant identity (available after ready when LiveKit is enabled).
364
+ - **`client.sayna_participant_name`**: Sayna participant name (available after ready when LiveKit is enabled).
365
+
366
+ ---
367
+
368
+ ### Webhook Receiver
369
+
370
+ The `WebhookReceiver` class securely verifies and parses cryptographically signed webhooks from the Sayna SIP service.
371
+
372
+ #### Security Features
373
+
374
+ - **HMAC-SHA256 Signature Verification**: Ensures webhook authenticity
375
+ - **Constant-Time Comparison**: Prevents timing attack vulnerabilities
376
+ - **Replay Protection**: 5-minute timestamp window prevents replay attacks
377
+ - **Strict Validation**: Comprehensive checks on all required fields
378
+
379
+ #### `WebhookReceiver(secret=None)`
380
+
381
+ Creates a new webhook receiver instance.
382
+
383
+ | Parameter | Type | Default | Purpose |
384
+ | --- | --- | --- | --- |
385
+ | `secret` | `str` (optional) | `None` | HMAC signing secret (min 16 chars). Falls back to `SAYNA_WEBHOOK_SECRET` env variable. |
386
+
387
+ **Example**:
388
+ ```python
389
+ from sayna_client import WebhookReceiver
390
+
391
+ # Explicit secret
392
+ receiver = WebhookReceiver("your-secret-key-min-16-chars")
393
+
394
+ # Or from environment variable (SAYNA_WEBHOOK_SECRET)
395
+ receiver = WebhookReceiver()
396
+ ```
397
+
398
+ ---
399
+
400
+ #### `receiver.receive(headers, body)`
401
+
402
+ Verifies and parses an incoming SIP webhook.
403
+
404
+ | Parameter | Type | Purpose |
405
+ | --- | --- | --- |
406
+ | `headers` | `dict` | HTTP request headers (case-insensitive). |
407
+ | `body` | `str` | Raw request body as string (not parsed JSON). |
408
+
409
+ **Returns**: `WebhookSIPOutput` - Validated webhook payload with fields:
410
+ - `participant`: Object with `identity`, `sid`, and optional `name`
411
+ - `room`: Object with `name` and `sid`
412
+ - `from_phone_number`: Caller's phone number (E.164 format)
413
+ - `to_phone_number`: Called phone number (E.164 format)
414
+ - `room_prefix`: Room name prefix configured in Sayna
415
+ - `sip_host`: SIP domain extracted from the To header
416
+
417
+ **Raises**: `SaynaValidationError` if signature verification fails or payload is invalid.
418
+
419
+ ---
420
+
421
+ #### Flask Example
422
+
423
+ ```python
424
+ from flask import Flask, request, jsonify
425
+ from sayna_client import WebhookReceiver, SaynaValidationError
426
+
427
+ app = Flask(__name__)
428
+ receiver = WebhookReceiver("your-secret-key-min-16-chars")
429
+
430
+ @app.route("/webhook", methods=["POST"])
431
+ def webhook():
432
+ try:
433
+ # CRITICAL: Pass raw body, not parsed JSON
434
+ body = request.get_data(as_text=True)
435
+ webhook = receiver.receive(request.headers, body)
436
+
437
+ print(f"From: {webhook.from_phone_number}")
438
+ print(f"To: {webhook.to_phone_number}")
439
+ print(f"Room: {webhook.room.name}")
440
+ print(f"SIP Host: {webhook.sip_host}")
441
+ print(f"Participant: {webhook.participant.identity}")
442
+
443
+ return jsonify({"received": True}), 200
444
+ except SaynaValidationError as error:
445
+ print(f"Webhook verification failed: {error}")
446
+ return jsonify({"error": "Invalid signature"}), 401
447
+ ```
448
+
449
+ ---
450
+
451
+ #### FastAPI Example
452
+
453
+ ```python
454
+ from fastapi import FastAPI, Request, HTTPException
455
+ from sayna_client import WebhookReceiver, SaynaValidationError
456
+
457
+ app = FastAPI()
458
+ receiver = WebhookReceiver() # Uses SAYNA_WEBHOOK_SECRET env variable
459
+
460
+ @app.post("/webhook")
461
+ async def webhook(request: Request):
462
+ try:
463
+ body = await request.body()
464
+ body_str = body.decode("utf-8")
465
+ webhook = receiver.receive(dict(request.headers), body_str)
466
+
467
+ # Process webhook...
468
+ return {"received": True}
469
+ except SaynaValidationError as error:
470
+ raise HTTPException(status_code=401, detail=str(error))
471
+ ```
472
+
473
+ ---
474
+
475
+ #### Important Notes
476
+
477
+ - **Raw Body Required**: You MUST pass the raw request body string, not the parsed JSON object. The signature is computed over the exact bytes received, so any formatting changes will cause verification to fail.
478
+
479
+ - **Case-Insensitive Headers**: Header names are case-insensitive in HTTP. The receiver handles both `X-Sayna-Signature` and `x-sayna-signature` correctly.
480
+
481
+ - **Secret Security**: Never commit secrets to version control. Use environment variables or a secret management system. Generate a secure secret with: `openssl rand -hex 32`
482
+
483
+ ---
484
+
485
+ ## Development
486
+
487
+ ### Setup
488
+
489
+ ```bash
490
+ # Create a virtual environment
491
+ python -m venv .venv
492
+
493
+ # Activate it
494
+ source .venv/bin/activate # On Linux/macOS
495
+ # .venv\Scripts\activate # On Windows
496
+
497
+ # Install development dependencies
498
+ pip install -e ".[dev]"
499
+ ```
500
+
501
+ > **Tip**: For faster dependency installation, you can use [uv](https://github.com/astral-sh/uv):
502
+ > ```bash
503
+ > pip install uv
504
+ > uv pip install -e ".[dev]"
505
+ > ```
506
+
507
+ ### Running Tests
508
+
509
+ ```bash
510
+ # Run all tests
511
+ pytest
512
+
513
+ # Run with coverage
514
+ pytest --cov=sayna_client --cov-report=html
515
+
516
+ # Run specific test file
517
+ pytest tests/test_client.py
518
+ ```
519
+
520
+ ### Type Checking
521
+
522
+ ```bash
523
+ mypy src/sayna_client
524
+ ```
525
+
526
+ ### Linting and Formatting
527
+
528
+ This project uses [Ruff](https://github.com/astral-sh/ruff) for linting and formatting:
529
+
530
+ ```bash
531
+ # Check code
532
+ ruff check .
533
+
534
+ # Format code
535
+ ruff format .
536
+
537
+ # Fix auto-fixable issues
538
+ ruff check --fix .
539
+ ```
540
+
541
+ ## Requirements
542
+
543
+ - Python 3.9 or higher
544
+ - aiohttp >= 3.9.0
545
+ - pydantic >= 2.0.0
546
+
547
+ ## Contributing
548
+
549
+ Contributions are welcome! Please feel free to submit a Pull Request.
550
+
551
+ ## Support
552
+
553
+ For issues and questions, please visit the [GitHub Issues](https://github.com/SaynaAi/saysdk/issues) page.
@@ -0,0 +1,10 @@
1
+ sayna_client/__init__.py,sha256=fhU-y78RJbrCeyqXjjJN-X4wh83sjAGwhC_zq9X_HB8,1899
2
+ sayna_client/client.py,sha256=ZaksHSVzsKzNJaEPd3a5Q5aINrWF9PO2SqcKEdfdwIQ,34429
3
+ sayna_client/errors.py,sha256=_EJuKRZ8cTOR3jp7xf_miEnE6JALXXtKPfFmDcTBJJE,2057
4
+ sayna_client/http_client.py,sha256=9L3_ftSwE3JvKmoF2kt_qrWYdJZuNSwFv5VLv0z6NTc,7847
5
+ sayna_client/types.py,sha256=Ewbd-K6rqEhr6MgMLYCShnslCei3wb6xMQCC7BjG79g,14391
6
+ sayna_client/webhook_receiver.py,sha256=dfcjJMvLrFSrMA3ACL59ToMT95XGsqaBBkcSVTJx1lw,12830
7
+ sayna_client-0.0.9.dist-info/METADATA,sha256=eUTy5cKy0FruwSLP88BNhT-IWB8wIAnShMws7BXweHM,15694
8
+ sayna_client-0.0.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ sayna_client-0.0.9.dist-info/top_level.txt,sha256=-GplaA_LXnxFbFb4Xhzu9hMHUMmoZ2cjgJAjtTVYp-8,13
10
+ sayna_client-0.0.9.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ sayna_client