Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,122 @@
# `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.