Self-hosted service
SaynaAI/sayna avatar
SaynaAI/sayna

Sayna: a Rust voice layer that puts STT and TTS behind one WebSocket

Sayna is a unified Voice Layer for AI Agents with a seemless integration to an existing agentic frameworks

313 stars44 forksRustApache-2.0

At a glance

What is it?
Sayna is an Apache-2.0 Rust server that unifies Deepgram, ElevenLabs, Google Cloud and Azure behind a single REST and WebSocket surface for agent audio. This review covers its provider abstraction, its optional VAD and noise-filter features, and where the design still leaves work to the caller.
Who is it for?
Adopt Sayna if you are already committed to one of its four providers and want a single WebSocket contract instead of four SDKs, and if your team can build the Docker image with --all-features or accept the default build without VAD. Do not adopt it if you need a Python-native library you can call in-process, if your provider is not Deepgram, ElevenLabs, Google Cloud or Azure, or if you need a published crate rather than a container image.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 111 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 18, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem Sayna solves is provider sprawl, not speech recognition

Sayna does not train or host a speech model. It is a coordination layer. The README describes it as "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," and the Cargo.toml description is a shorter version of the same claim.

The audience is narrow and specific: teams building an agent that already exists in some framework and now needs a voice channel. Without a layer like this, that team integrates Deepgram for STT, ElevenLabs for TTS, and then a second provider as a fallback, each with its own authentication, audio format, and streaming semantics. Sayna's value proposition is that the client speaks one protocol to one server, and the provider choice becomes a field in a JSON config message.

The provider list is the concrete evidence for this. Deepgram, ElevenLabs, Google Cloud (WaveNet, Neural2 and Studio voices), and Microsoft Azure (400+ neural voices across 140+ languages) are all exposed behind the same `provider` key. That is a real abstraction, and it is the only reason to pick this over calling a provider SDK directly.

How the VoiceManager, provider traits and WebSocket handler fit together

The architecture section names four core components: VoiceManager as the central coordinator, a trait-based provider system, a WebSocket handler, and LiveKit integration. The trait abstraction is what makes the provider list possible; each provider implements the same STT and TTS interface, and VoiceManager picks the implementation from the config message.

The data flow is documented as two pipelines. For STT: audio goes through an optional noise filter, then to the STT provider, then out as text. For TTS: text goes to the TTS provider, then out as audio to the client. The README renders these as arrows with a corrupted character where the arrow glyphs should be, which is cosmetic but worth knowing if you are reading the raw markdown.

The client-facing contract is a WebSocket at `/ws`. The first message is a config message that selects providers and sets audio parameters. After that, binary frames carry audio in and text messages carry text in. A `text` message type converts text to speech. The config message also carries per-session credentials, which is the part that matters for multi-tenant deployments: `stt_config.auth` and `tts_config.auth` can override the server's own keys. The README is explicit that if the auth object is omitted or sent as `{}`, Sayna falls back to server-configured credentials, and that if it is provided it must be complete for that provider. The shapes differ per provider: `{ "api_key": "..." }` for API-key providers, `{ "credentials": "/path/to/creds.json" }` or an inline service account object for Google Cloud, and `{ "api_key": "...", "region": "eastus" }` for Azure. Getting that shape wrong is a likely first failure.

Installing Sayna with Docker and making the first /speak call

The README's prerequisites are Docker and at least one provider credential source, and it notes credentials are optional in audio-disabled mode. The published image is `saynaai/sayna`, and the quick start runs it on port 3001 with a Deepgram key.

bash
docker run -d \
  -p 3001:3001 \
  -e DEEPGRAM_API_KEY=your-key \
  saynaai/sayna

The README states the server will then be available at `http://localhost:3001`. The root path is a health check, so a GET there is the cheapest way to confirm the container started before you send audio.

For a first real use, the REST surface is simpler than the WebSocket. `POST /speak` takes text plus a `tts_config` and returns generated speech. This is the README's own example, with a bearer token because authentication is enabled in that snippet.

bash
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"
    }
  }'

If `AUTH_REQUIRED` is not set, the README's authentication section implies the header is unnecessary, and it also documents an `?api_key=` query parameter as an alternative. `GET /voices` lists available TTS voices and is gated the same way. For a Docker Compose setup with a persistent cache volume, the README shows `CACHE_PATH: /data/cache` mounted against a named volume, which is the configuration to copy if you are running this beyond a laptop.

The optional features are where the real cost sits

Two capabilities are behind Cargo features rather than in the default build. The `stt-vad` feature brings in Silero-VAD for audio-level silence detection plus ML-based end-of-turn detection, and it pulls in `ort`, `ndarray` and `rustfft`. The `noise-filter` feature brings in DeepFilterNet via `deep_filter` and the `tract-*` crates. Cargo.toml declares `default = []`, so a plain `cargo build` gives you neither.

That matters because the Dockerfile sets `ARG CARGO_BUILD_FEATURES="--all-features"`, meaning the published image is built with both. If you build from source without that argument, you get a server that accepts the same config messages but behaves differently on silence and turn boundaries. The README's development section also lists ONNX Runtime as an optional prerequisite for `stt-vad`, and the Dockerfile downloads an ONNX Runtime release to satisfy it. Anyone building outside Docker should expect to handle that dependency themselves.

The Dockerfile is also heavier than the feature list suggests. It installs `libva-dev`, `libdrm-dev`, `libgbm-dev` and a set of X11 libraries, with a comment that `webrtc-sys` requires many system libraries for video and graphics support. LiveKit integration is why. If you only want the REST `/speak` endpoint, you are paying for a WebRTC stack you will not use.

Authentication delegates token validation to a service you have to run

Sayna does not issue tokens. The README describes authentication as delegating validation to an external authentication service, configured through `AUTH_REQUIRED`, `AUTH_SERVICE_URL`, `AUTH_SIGNING_KEY_PATH` and `AUTH_TIMEOUT_SECONDS`. You generate an RSA key pair with `openssl genrsa -out auth_private_key.pem 2048`, keep the private key where Sayna can read it, and share the extracted public key with the auth service.

This is a deliberate boundary and it is worth being clear about the consequence: enabling authentication adds a second service to your deployment and a network hop to every protected request, bounded by `AUTH_TIMEOUT_SECONDS`. The README gives no failure behaviour for a timeout, no caching story, and no rollback instructions for rotating the signing key. If you are deploying this for a single internal team, running it with `AUTH_REQUIRED` unset and putting it behind your own gateway is the smaller system, and the README supports that path by treating authentication as optional.

One endpoint is deliberately outside this scheme: `POST /livekit/webhook` is unauthenticated and validates requests using LiveKit's JWT signature mechanism instead. The README notes it logs SIP-related attributes for phone call troubleshooting. That endpoint is reachable without a bearer token by design, so it is the one to firewall at the network layer if the server is exposed.

Where Sayna is the wrong tool, and what to use instead

Sayna is the wrong choice if you want an in-process library. It is a server. Cargo.toml sets `publish = false` with the comment "Binary-only distribution via GitHub releases," so there is no crate on crates.io to depend on, and no library API to call from your own Rust process. Your integration is HTTP and WebSocket, whether you like it or not.

The closest alternative in approach is a Python framework such as Pipecat, which also abstracts STT and TTS providers behind a pipeline but lives in your process as a library. The difference is not quality, it is topology. Pipecat composes frames inside a Python event loop you own; Sayna runs as a separate binary you talk to over the network. Choose Pipecat when you want to debug the pipeline in the same process as your agent logic and you are already in Python. Choose Sayna when your agent is written in something else, when you want one language-agnostic endpoint that several clients can share, or when you specifically want the LiveKit room integration, which is the part Pipecat does not replicate in the same shape.

There is also the simpler option of calling the provider SDK directly. If you only ever use Deepgram and never plan to switch, Sayna's abstraction is overhead: an extra container, an extra hop, and a config message format to learn, in exchange for flexibility you are not using.

Maintenance, licensing and what upgrading costs

The repository is not archived, and the last push was on 2026-05-31. That is roughly three and a half months before today, so it sits inside the six-month window but is not recent enough to describe as active daily work. The release cadence is one release per month through the spring: v0.1.14 on 2026-03-30, v0.1.15 on 2026-04-30, and v0.1.16 on 2026-05-25. Note that Cargo.toml still declares `version = "0.1.15"` while the newest release is v0.1.16, so the manifest lags the tag. If you build from source, your binary will report 0.1.15.

Upgrade cost is dominated by the config message schema, not by the Rust code. Provider auth shapes and the `stt_config` and `tts_config` keys are the contract your clients depend on, and the README gives no versioning or compatibility policy for that contract. A version bump could change a field without a migration path, and the README documents no rollback procedure. Pin the image tag rather than tracking a floating tag, and keep the config message you send under test.

The licence is Apache-2.0, which is permissive and includes an explicit patent grant. That is the whole of what the repository supports: the LICENSE file is present at the repository root and Cargo.toml declares `license = "Apache-2.0"`. Whether Apache-2.0 satisfies your organisation's policy on patent clauses, attribution and NOTICE files is a question for your own counsel, not something this review can settle. The `publish = false` setting means there is no crates.io artefact to audit; your supply chain is the container image and the GitHub release binaries.

Editorial conclusion

Adopt Sayna if you are already committed to one of its four providers and want a single WebSocket contract instead of four SDKs, and if your team can build the Docker image with --all-features or accept the default build without VAD. Do not adopt it if you need a Python-native library you can call in-process, if your provider is not Deepgram, ElevenLabs, Google Cloud or Azure, or if you need a published crate rather than a container image. Verify first that the /speak endpoint returns audio with your own provider key, that the config message's stt_config.auth and tts_config.auth shapes match your provider, and that the stt-vad and noise-filter features compile in your environment before you plan around them.

Frequently asked questions

How do you pronounce Sayna?

The README and repository files contain no phonetic guidance for the project name. Only the spelling "Sayna" appears, including in the image name saynaai/sayna and the domain sayna.ai.

Who is Sayna Sheikh?

This is outside the scope of the Sayna repository, which is a Rust voice processing server published under the Apache-2.0 licence. The repository contains nothing about a person by that name.

Can I run Sayna without any provider API keys?

Yes. The README documents an audio-disabled mode where you send a WebSocket configuration message with audio set to false, which exercises the control plane without initialising STT or TTS. It is intended for local development, UI work, and testing WebSocket message flows.

Does Sayna ship as a crate I can add to Cargo.toml?

No. Cargo.toml sets publish to false with the comment that distribution is binary-only via GitHub releases, so the project is consumed as a server, not as a library dependency.

Which TTS and STT providers does Sayna support?

The README lists Deepgram, ElevenLabs, Google Cloud (including WaveNet, Neural2 and Studio voices) and Microsoft Azure, which it describes as offering over 400 neural voices across more than 140 languages. Each is selected through the provider key in the config message.

Official sources

  1. License: Apache-2.0
  2. Project website
  3. README
  4. Releases
  5. SaynaAI/sayna on GitHub
Community notes

Community notes