One interface for text-to-speech and speech-to-text
A TypeScript SDK for Cartesia, Deepgram, and ElevenLabs. Batch and realtime, with the same types for every provider.
Portability
Write once, run on any provider
The provider is chosen in the constructor. Everything after it uses the same methods, the same inputs, and the same return types.
import { Voice } from "@swungstudent/voice";import { DeepgramProvider } from "@swungstudent/deepgram";const voice = new Voice({ provider: new DeepgramProvider() });const { audio, format } = await voice.speak({ text: "Hello there.", format: { container: "wav", sampleRate: 24000 },});const { text } = await voice.transcribe({ audio });Voice
speak · transcribe · sessions
Cartesia
Deepgram
ElevenLabs
Providers declare the core package as a peer dependency, so an application resolves a single copy of the types. Compare the providers
Surface
Four capabilities
Each provider declares which capabilities it supports. Calling a method a provider does not implement throws a CapabilityError with the provider name.
tts
speak() · speakStream()
Convert text to audio, buffered or streamed.
stt
transcribe()
Convert a complete recording to a transcript.
realtimeTTS
openTTSSession()
Stream text into a session and receive audio back.
realtimeSTT
openSTTSession()
Stream audio into a session and receive transcripts back.
| Provider | tts | stt | realtimeTTS | realtimeSTT | listVoices | Timings |
|---|---|---|---|---|---|---|
| Cartesia | Word | |||||
| Deepgram | — | None | ||||
| ElevenLabs | Character |
Deepgram does not expose a voice listing endpoint, so the provider does not implement listVoices().
Realtime
Duplex sessions in both directions
A session keeps one connection open and streams in both directions. Push text and receive audio, or push audio and receive transcripts.
Text pushed a token at a time
session.push(token)
Audio frames
Audio frames pushed live
session.push(frame)
Transcript events
Speaking
const session = await voice.openTTSSession({ format: { container: "raw", sampleRate: 24000 },});// push() does not return a promise, so the loop is not blocked per token.for await (const token of llm) session.push(token);await session.flush();for await (const event of session.output) { if (event.type === "audio") speaker.write(event.data); if (event.type === "done") break;}await session.close();Listening
import { turns } from "@swungstudent/voice";const session = await voice.openSTTSession({ turnDetection: { mode: "vad", silence: 1 },});microphone.on("data", (frame) => session.push(frame));// turns() filters the event stream down to completed turns.for await (const turn of turns(session.output)) { console.log(turn.text);}Transcript events carry one of three finality levels: partial for text that is still being revised, final for a stable segment, and turn_end when the speaker stops. Realtime transcription
Audio
Formats resolve to a known value
Every field in a format request is optional. Every field in the format you get back is set, so downstream code always knows how to play the bytes.
const { audio, format } = await voice.speak({ text: "Hello there.", format: { container: "wav" },});format;// {// container: "wav",// encoding: "pcm_s16le",// sampleRate: 24000,// channels: 1,// }ElevenLabs
mp3_44100_128container, sample rate and bitrate in one token
Cartesia
pcm_mulawcodec names carry a prefix
Deepgram
?encoding=linear16&sample_rate=24000split across query parameters
You write one AudioFormat. The provider package translates it.
Errors
Invalid requests fail before the network call
Format support differs the most between providers, so requests are validated locally. Errors name the field instead of returning a 400 from the API.
await voice.speak({ text: "Hello there.", format: { container: "wav", channels: 2 },});// ValidationError: Provider "deepgram" rejected "channels":// only mono is supported.CapabilityError
The provider does not implement the method you called.
ConfigError
A required option is missing, such as an API key.
ValidationError
A request field is not supported by the provider.
All three extend VoiceError, so a single catch covers the SDK.
Start with a working example
Six runnable programs covering synthesis, transcription, realtime sessions, and switching providers.