4.3 KiB
tts
ElevenLabs implementation of @suaveplan/voice's VoiceSynthesizer
interface. Posts to /v1/text-to-speech/{voice_id} for buffered
synthesis and the /stream variant for incremental streaming.
(no submodules)
Purpose
Application code that depends on @suaveplan/voice/VoiceSynthesizer
should be portable across providers. This adapter is the seam between
the abstract domain interface and ElevenLabs's REST API: it maps the
SynthesisRequest shape onto the wire body, mirrors voice
settings into ElevenLabs's voice_settings envelope, validates output
formats against the subset ElevenLabs supports, and remaps every
ElevenLabs error onto the corresponding @suaveplan/voice domain error.
Features
- Implements
synthesize(request)for buffered audio andsynthesizeStream(request)for an incrementalReadableStream<Uint8Array>. - Eight ElevenLabs-supported
AudioFormatvalues are passed through with explicit mapping; unsupported formats throwAudioFormatUnsupportedErrorclient-side before the request is sent. - Voice settings (stability, similarity_boost, style, use_speaker_boost) are mapped from the camelCase names to the wire snake_case names with all four optional.
- Latency mode 0–4 is forwarded as
optimize_streaming_latency. - Pronunciation dictionary IDs are wrapped into the
pronunciation_dictionary_locatorsenvelope ElevenLabs expects. - Errors are remapped: 401 →
ProviderNotConfiguredError, 429 →ProviderRateLimitedError(withretryAfterMs),STREAM.INTERRUPTED→TtsStreamInterruptedError, everything else →TtsSynthesisFailedError.
Usage: Basic Example
import {
createElevenLabsAdapters,
} from "@suaveplan/third-party-elevenlabs";
const { synthesizer } = createElevenLabsAdapters({
apiKey: process.env.ELEVENLABS_KEY!,
});
const result = await synthesizer.synthesize({
text: "hello world",
voiceId: "21m00Tcm4TlvDq8ikWAM",
});
await Bun.write("hello.mp3", result.audio);
Usage: Advanced Example
import {
ElevenLabsClient,
ElevenLabsVoiceSynthesizer,
} from "@suaveplan/third-party-elevenlabs";
const client = new ElevenLabsClient({ apiKey: KEY });
const synth = new ElevenLabsVoiceSynthesizer(client);
const stream = synth.synthesizeStream({
text: "long passage…",
voiceId: "abc",
modelId: "eleven_multilingual_v2",
languageCode: "en",
outputFormat: "pcm_44100",
latencyMode: 3,
voiceSettings: {
stability: 0.4,
similarityBoost: 0.6,
style: 0.1,
speakerBoost: true,
},
previousText: "Once upon a time …",
nextText: "and they lived happily.",
seed: 42,
pronunciationDictionaryIds: ["dict-en-1"],
});
const reader = stream.getReader();
while (true) {
const { done, value } = await reader.read();
if (done) break;
sink.write(value);
}
API Reference
class ElevenLabsVoiceSynthesizer implements VoiceSynthesizerproviderName: "elevenlabs"synthesize(request: SynthesisRequest): Promise<SynthesisResult>—result.audiois the raw bytes;formatis the requested format (defaults tomp3_44100_128);requestIdandcharactersUsedare populated from response headers when present.synthesizeStream(request: SynthesisRequest): ReadableStream<Uint8Array>— errors during the stream are surfaced via streamcontroller.error.
Throws (sync from synthesizeStream if format is unsupported, async
otherwise): AudioFormatUnsupportedError, ProviderNotConfiguredError,
ProviderRateLimitedError, TtsSynthesisFailedError,
TtsStreamInterruptedError.
Implementation Notes
- The default
outputFormatfor the result ismp3_44100_128— this is ElevenLabs's server-side default and matches the bytes you'll actually receive when no format is requested. voice_idisencodeURIComponent-escaped because some workspace IDs contain spaces.- Format mapping is a static record rather than a passthrough so that adding a new format requires an explicit code change, not a silent wire-passthrough that ElevenLabs might or might not accept.
- The streaming path constructs the
ReadableStreamitself instead of returning the raw response body so that error remapping can happen insidecontroller.errorrather than as a synchronous throw.