Speko Docs

SpekoTTS

LiveKit Agents TTS adapter backed by POST /v1/synthesize.

SpekoTTS is a tts.TTS implementation. Each sentence is synthesised via the Speko proxy, decoded into PCM, chunked into AudioFrames at 50 Hz (20 ms frames), and pushed to the LiveKit session.

import { SpekoTTS } from '@spekoai/adapter-livekit';
import { tts as ttsNs, tokenize } from '@livekit/agents';

const spekoTTS = new SpekoTTS({
  speko,
  intent: { language: 'en' },
  voice: 'sonic-english',
  sampleRate: 24_000,
});

const wrapped = new ttsNs.StreamAdapter(spekoTTS, new tokenize.basic.SentenceTokenizer());

Constructor

new SpekoTTS(options: SpekoTTSOptions)

SpekoTTSOptions

FieldTypeRequiredDescription
spekoSpeko@spekoai/sdk client.
intentIntentValidated at construction time.
voicestring?Voice id forwarded to the proxy.
speednumber?Speech-speed multiplier forwarded to the proxy.
sampleRatenumber?Output sample rate advertised to LiveKit. Default 24000. Every emitted frame is at this rate; a provider that emits another rate is resampled to it.
constraintsPipelineConstraints?Allow-list constraints.

Properties

  • label = 'speko.TTS'
  • provider = 'speko'
  • model = 'speko-router'
  • numChannels = 1, streaming = false

Streaming requirement

SpekoTTS.stream() throws because LiveKit's TTS StreamAdapter handles sentence tokenization for this class. /v1/synthesize streams audio bytes for each sentence request. Wrap:

import { tts, tokenize } from '@livekit/agents';

const adapter = new tts.StreamAdapter(spekoTTS, new tokenize.basic.SentenceTokenizer());

Or use createSpekoComponents which does this for you.

.synthesize(text, connOptions?, abortSignal?)

Returns a SpekoTTSChunkedStream (exported for type use). Internally:

  1. Calls speko.synthesize(text, { ...intent, voice, speed, constraints }).
  2. Reads the response's real audio format from X-Speko-Audio-Format (falling back to Content-Type) and decodes via decodeSynthesisResult.
  3. Chunks the PCM into AudioFrames of round(responseRate / 50) samples each via AudioByteStream.
  4. Resamples to the configured sampleRate when the response rate differs, so playback is never pitched. Matching rates are a zero-copy pass-through.
  5. Pushes frames onto the output queue, marking the last one final: true.

Empty provider output throws SpekoTTS: provider returned empty audio.

Audio format support (v1)

decodeSynthesisResult(result) branches on result.contentType:

Content typeBehavior
audio/pcm;rate=NNNNRaw PCM, rate parsed from the MIME. Channels pinned to 1 (Cartesia's contract).
audio/wav / audio/x-wavHeader stripped via parseWav. Stereo WAV throws.
audio/mpegThrows — v1 doesn't include an MP3 decoder. Pin Cartesia or another PCM-capable provider via constraints.
anything elseThrows with provider info for debugging.

Work around MP3 by pinning your TTS pool:

new SpekoTTS({
  speko,
  intent,
  constraints: { allowedProviders: { tts: ['cartesia'] } },
});

Sample rates

The router serves whatever rate the chosen provider produces: 24 kHz for most, 48 kHz for Hume and Gradium, 16 kHz for Amazon Polly. The adapter reads that rate per response from X-Speko-Audio-Format and resamples to the sampleRate this instance advertises, so routing to (or failing over onto) a provider at another rate is handled rather than fatal.

Since v0.1.3 a differing rate is no longer an error. Earlier versions rejected the utterance with SpekoTTS: provider returned audio at 16000 Hz but the TTS was configured for 24000 Hz.

Set sampleRate to the rate your pipeline wants; leaving it at the 24 kHz default is the cheapest option because most providers emit it natively and need no resampling.

decodeSynthesisResult

Exported for unit testing. Given a SynthesizeResult (and optionally the audio-format string to branch on, normally the response's X-Speko-Audio-Format), returns { pcm, sampleRate, channels }. Throws for unsupported content types (see table above). The returned sampleRate is whatever the response declares; the caller normalizes it.

import { decodeSynthesisResult } from '@spekoai/adapter-livekit';

On this page