A high-performance real-time voice processing server built in Rust that provides unified Speech-to-Text (STT) and Text-to-Speech (TTS) services through WebSocket and REST APIs.

stt-vad feature)noise-filter feature)docker run -d \
-p 3001:3001 \
-e DEEPGRAM_API_KEY=your-key \
saynaai/sayna
The server will be available at http://localhost:3001.
version: "3.9"
services:
sayna:
image: saynaai/sayna
ports:
- "3001:3001"
environment:
DEEPGRAM_API_KEY: ${DEEPGRAM_API_KEY}
ELEVENLABS_API_KEY: ${ELEVENLABS_API_KEY}
CACHE_PATH: /data/cache
volumes:
- sayna-cache:/data/cache
volumes:
sayna-cache: {}
For complete Docker documentation including LiveKit integration, see docs/docker.md.
You can run Sayna without provider credentials by using audio-disabled mode. This is useful when you want to exercise the control plane without STT/TTS initialization. Send a WebSocket configuration message with audio: false:
{
"type": "config",
"audio": false
}
This mode is useful for: - Local development and testing - UI/UX development without audio processing - Testing WebSocket message flows - Debugging non-audio features
Sayna supports customer-based authentication that delegates token validation to an external authentication service. When enabled, protected API endpoints require a valid token provided either as Authorization: Bearer <token> or as ?api_key=<token>.
Add to your .env file:
AUTH_REQUIRED=true
AUTH_SERVICE_URL=https://your-auth-service.com/auth
AUTH_SIGNING_KEY_PATH=/path/to/auth_private_key.pem
AUTH_TIMEOUT_SECONDS=5
Generate signing keys:
# Generate RSA private key
openssl genrsa -out auth_private_key.pem 2048
# Extract public key (share with auth service)
openssl rsa -in auth_private_key.pem -pubout -out auth_public_key.pem
# Authorization header
curl -X POST http://localhost:3001/speak \
-H "Authorization: Bearer your-token-here" \
-H "Content-Type: application/json" \
-d '{
"text": "Hello world",
"tts_config": {
"provider": "deepgram",
"model": "aura-asteria-en"
}
}'
# Query parameter alternative
curl -X POST "http://localhost:3001/speak?api_key=your-token-here" \
-H "Content-Type: application/json" \
-d '{
"text": "Hello world",
"tts_config": {
"provider": "deepgram",
"model": "aura-asteria-en"
}
}'
For complete authentication setup and architecture details, see docs/authentication.md.
/wsConfiguration Message:
{
"type": "config",
"audio": true,
"stt_config": {
"provider": "deepgram",
"language": "en-US",
"sample_rate": 16000,
"channels": 1,
"punctuation": true,
"encoding": "linear16",
"model": "nova-3",
"auth": {
"api_key": "dg-session-key"
}
},
"tts_config": {
"provider": "elevenlabs",
"voice_id": "voice_id_here",
"model": "eleven_flash_v2_5",
"audio_format": "mp3_44100_128",
"sample_rate": 44100,
"auth": {
"api_key": "el-session-key"
}
}
}
stt_config.auth and tts_config.auth are optional. If omitted, or sent as {}, Sayna falls back to the server-configured provider credentials. If an auth object is provided, it must be complete for that provider. Current shapes are:
{ "api_key": "..." }{ "credentials": "/path/to/creds.json" } or { "credentials": { ...service account json... } }{ "api_key": "...", "region": "eastus" }Audio Input: Binary audio data (16kHz, 16-bit PCM)
Text Input:
{
"type": "text",
"text": "Convert this text to speech"
}
GET / - Server health check endpointGET /voices - List available TTS voices (requires auth if AUTH_REQUIRED=true)POST /speak - Generate speech from text (requires auth if AUTH_REQUIRED=true)POST /livekit/token - Generate LiveKit participant token (requires auth if AUTH_REQUIRED=true)POST /livekit/webhook - Webhook endpoint for LiveKit events (unauthenticated, uses LiveKit signature verification)/wsFor local development, you can build and run from source.
stt-vad feature which includes VAD and turn detection)# Development build
cargo build
# Release build (optimized)
cargo build --release
# Run the server
cargo run
# Run with a config file
cargo run -- -c config.yaml
Sayna exposes several Cargo features that gate heavyweight subsystems:
stt-vad: Silero-VAD voice activity detection with integrated ONNX-based turn detection. When enabled, VAD monitors audio for silence and triggers the turn detection model to confirm if the speaker's turn is complete.noise-filter: DeepFilterNet noise suppression pipelineopenapi: OpenAPI 3.1 specification generation# Run with VAD and turn detection
cargo run --features stt-vad
# Run with noise filter
cargo run --features noise-filter
# Run with multiple features
cargo run --features stt-vad,noise-filter,openapi
The Docker image includes stt-vad and noise-filter by default.
# Run all tests
cargo test
# Run specific test
cargo test test_name
# Run with output
cargo test -- --nocapture
# Format code
cargo fmt
# Run linter
cargo clippy
# Check for security vulnerabilities
cargo audit
# Build Docker image
docker build -t sayna .
# Run container
docker run -p 3001:3001 --env-file .env sayna
| Variable | Description | Default | Required |
|---|---|---|---|
DEEPGRAM_API_KEY |
Deepgram API authentication | - | No* |
ELEVENLABS_API_KEY |
ElevenLabs API authentication | - | No* |
GOOGLE_APPLICATION_CREDENTIALS |
Path to Google Cloud service account JSON | - | No* |
AZURE_SPEECH_SUBSCRIPTION_KEY |
Azure Speech Services subscription key | - | No* |
AZURE_SPEECH_REGION |
Azure region (e.g., eastus, westeurope) | eastus |
No* |
LIVEKIT_URL |
LiveKit server WebSocket URL | ws://localhost:7880 |
No |
LIVEKIT_API_KEY |
LiveKit API key (for webhooks and token generation) | - | No*** |
LIVEKIT_API_SECRET |
LiveKit API secret (for webhooks and token generation) | - | No*** |
HOST |
Server bind address | 0.0.0.0 |
No |
PORT |
Server port | 3001 |
No |
AUTH_REQUIRED |
Enable authentication | false |
No |
AUTH_API_SECRETS_JSON |
API secrets JSON array ([{id, secret}]) |
- | Yes** |
AUTH_API_SECRET |
Legacy single API secret | - | No**** |
AUTH_API_SECRET_ID |
Legacy API secret id for AUTH_API_SECRET |
default |
No |
AUTH_SERVICE_URL |
External auth service endpoint | - | Yes** |
AUTH_SIGNING_KEY_PATH |
Path to JWT signing private key | - | Yes** |
AUTH_TIMEOUT_SECONDS |
Auth request timeout | 5 |
No |
Not required when using audio-disabled mode
Required when AUTH_REQUIRED=true for the auth method you choose
Required for LiveKit webhook validation and token generation features
**Legacy single-secret fallback; prefer AUTH_API_SECRETS_JSON
Sayna supports first-class SIP configuration for managing SIP-specific settings. See docs/sip_config.md for detailed documentation.
YAML Configuration:
sip:
room_prefix: "sip-"
allowed_addresses:
- "192.168.1.0/24"
- "10.0.0.1"
hook_secret: "your-signing-secret" # Required if hooks configured
hooks:
- host: "example.com"
url: "https://webhook.example.com/events"
Important: All SIP webhook forwarding requests are signed with HMAC-SHA256. You must configure hook_secret (or per-hook secret overrides) if using sip.hooks. See docs/livekit_webhook.md#webhook-signing for signature verification examples.
Environment Variables:
- SIP_ROOM_PREFIX: Room name prefix for SIP calls (required if SIP enabled)
- SIP_ALLOWED_ADDRESSES: Comma-separated IP addresses/CIDRs
- SIP_HOOK_SECRET: Global signing secret for webhook authentication (min 16 chars)
- SIP_HOOKS_JSON: JSON array of webhook configurations (with optional per-hook secret field)
.cursor/rules/:rust.mdc: Rust best practicescore.mdc: Business logic specificationsaxum.mdc: Framework patternslivekit.mdc: LiveKit integration details
Follow the existing code patterns and conventions
cargo fmt and cargo clippy passFor issues, questions, or contributions, please visit the GitHub repository.
$ claude mcp add sayna \
-- python -m otcore.mcp_server <graph>