Files
2026-05-27 23:47:53 +01:00

123 lines
4.3 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# `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.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 and
`synthesizeStream(request)` for an incremental `ReadableStream<Uint8Array>`.
- Eight ElevenLabs-supported `AudioFormat` values are passed through
with explicit mapping; unsupported formats throw
`AudioFormatUnsupportedError` client-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_locators` envelope ElevenLabs expects.
- Errors are remapped: 401 → `ProviderNotConfiguredError`, 429 →
`ProviderRateLimitedError` (with `retryAfterMs`), `STREAM.INTERRUPTED`
→ `TtsStreamInterruptedError`, everything else →
`TtsSynthesisFailedError`.
## Usage: Basic Example
```ts
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
```ts
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 VoiceSynthesizer`
- `providerName: "elevenlabs"`
- `synthesize(request: SynthesisRequest): Promise<SynthesisResult>` —
`result.audio` is the raw bytes; `format` is the requested format
(defaults to `mp3_44100_128`); `requestId` and `charactersUsed` are
populated from response headers when present.
- `synthesizeStream(request: SynthesisRequest): ReadableStream<Uint8Array>`
— errors during the stream are surfaced via stream `controller.error`.
Throws (sync from `synthesizeStream` if format is unsupported, async
otherwise): `AudioFormatUnsupportedError`, `ProviderNotConfiguredError`,
`ProviderRateLimitedError`, `TtsSynthesisFailedError`,
`TtsStreamInterruptedError`.
## Implementation Notes
- The default `outputFormat` for the result is `mp3_44100_128` — this is
ElevenLabs's server-side default and matches the bytes you'll actually
receive when no format is requested.
- `voice_id` is `encodeURIComponent`-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 `ReadableStream` itself instead of
returning the raw response body so that error remapping can happen
inside `controller.error` rather than as a synchronous throw.