Open source · MIT

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 });
Your application

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.

ProviderttssttrealtimeTTSrealtimeSTTlistVoicesTimings
CartesiaWord
Deepgram—None
ElevenLabsCharacter

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)

openTTSSession

Audio frames

Audio frames pushed live

session.push(frame)

openSTTSession

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_128

container, sample rate and bitrate in one token

Cartesia

pcm_mulaw

codec names carry a prefix

Deepgram

?encoding=linear16&sample_rate=24000

split 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.