Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
# @suaveplan/voice-elevenlabs - ElevenLabs domain adapter for @suaveplan/voice: TTS, STT, voice repository, sound effects, and voice isolation engines implemented by delegating to @suaveplan/third-party-elevenlabs.
|
||||
|
||||
ElevenLabs domain adapter for the `@suaveplan/voice` interface family.
|
||||
|
||||
This package implements every `@suaveplan/voice` engine contract —
|
||||
`VoiceSynthesizer`, `TranscriptionEngine`, `VoiceRepository`,
|
||||
`SoundEffectEngine`, `VoiceIsolationEngine` — by delegating to the typed
|
||||
HTTP/WebSocket transport in `@suaveplan/third-party-elevenlabs`. It owns
|
||||
DTO ↔ domain mapping, ElevenLabs-specific error remapping into the
|
||||
`VOICE.*` namespace, multipart/stream plumbing for the voice surface,
|
||||
and a composite factory that wires every engine against a shared
|
||||
transport.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [factory](./src/factory/create-elevenlabs-voice.md) — Composition root for the `@suaveplan/voice` engine adapters that target ElevenLabs. One call, one shared transport, fiv…
|
||||
- [create-elevenlabs-voice](./src/factory/create-elevenlabs-voice.md) — Composition root for the `@suaveplan/voice` engine adapters that target ElevenLabs. One call, one shared transport, fiv…
|
||||
- [models](./src/models/models.md) — ElevenLabs model catalog. Lists every model the API key has access to, along with capability flags (TTS / voice convers…
|
||||
- [models](./src/models/models.md) — ElevenLabs model catalog. Lists every model the API key has access to, along with capability flags (TTS / voice convers…
|
||||
- [sound-effects](./src/sound-effects/sound-effects.md) — ElevenLabs implementation of `@suaveplan/voice`'s `SoundEffectEngine` interface. Posts a JSON body to `POST /v1/sound-g…
|
||||
- [sound-effects](./src/sound-effects/sound-effects.md) — ElevenLabs implementation of `@suaveplan/voice`'s `SoundEffectEngine` interface. Posts a JSON body to `POST /v1/sound-g…
|
||||
- [speech-to-speech](./src/speech-to-speech/speech-to-speech.md) — ElevenLabs Speech-to-Speech (voice conversion) adapter. Given source audio and a target `voiceId`, produces audio that…
|
||||
- [speech-to-speech](./src/speech-to-speech/speech-to-speech.md) — ElevenLabs Speech-to-Speech (voice conversion) adapter. Given source audio and a target `voiceId`, produces audio that…
|
||||
- [stt](./src/stt/stt.md) — ElevenLabs implementation of `@suaveplan/voice`'s `TranscriptionEngine` interface. Posts the audio as multipart form-da…
|
||||
- [stt](./src/stt/stt.md) — ElevenLabs implementation of `@suaveplan/voice`'s `TranscriptionEngine` interface. Posts the audio as multipart form-da…
|
||||
- [tts](./src/tts/tts.md) — ElevenLabs implementation of `@suaveplan/voice`'s `VoiceSynthesizer` interface. Posts to `/v1/text-to-speech/{voice_id}…
|
||||
- [tts](./src/tts/tts.md) — ElevenLabs implementation of `@suaveplan/voice`'s `VoiceSynthesizer` interface. Posts to `/v1/text-to-speech/{voice_id}…
|
||||
- [voice-isolation](./src/voice-isolation/voice-isolation.md) — ElevenLabs implementation of `@suaveplan/voice`'s `VoiceIsolationEngine` interface. Posts the audio as multipart form-d…
|
||||
- [voice-isolation](./src/voice-isolation/voice-isolation.md) — ElevenLabs implementation of `@suaveplan/voice`'s `VoiceIsolationEngine` interface. Posts the audio as multipart form-d…
|
||||
- [voices](./src/voices/voices.md) — ElevenLabs implementation of `@suaveplan/voice`'s `VoiceRepository` interface. Lists voices via `GET /v1/voices` and lo…
|
||||
- [voices](./src/voices/voices.md) — ElevenLabs implementation of `@suaveplan/voice`'s `VoiceRepository` interface. Lists voices via `GET /v1/voices` and lo…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
bun add @suaveplan/voice-elevenlabs @suaveplan/voice
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import { createElevenLabsVoice } from "@suaveplan/voice-elevenlabs";
|
||||
|
||||
const { tts, voices } = createElevenLabsVoice({
|
||||
apiKey: process.env.ELEVENLABS_KEY!,
|
||||
});
|
||||
|
||||
const all = await voices.list();
|
||||
const result = await tts.synthesize({
|
||||
text: "hello",
|
||||
voiceId: all.voices[0]!.id,
|
||||
});
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
| Subpath | Engine | `@suaveplan/voice` contract |
|
||||
| --- | --- | --- |
|
||||
| `./tts/tts` | `ElevenLabsVoiceSynthesizer` | `VoiceSynthesizer` |
|
||||
| `./stt/stt` | `ElevenLabsTranscriptionEngine` | `TranscriptionEngine` |
|
||||
| `./voices/voices` | `ElevenLabsVoiceRepository` | `VoiceRepository` |
|
||||
| `./sound-effects/sound-effects` | `ElevenLabsSoundEffectEngine` | `SoundEffectEngine` |
|
||||
| `./voice-isolation/voice-isolation` | `ElevenLabsVoiceIsolationEngine` | `VoiceIsolationEngine` |
|
||||
| `./factory/create-elevenlabs-voice` | `createElevenLabsVoice` | bundle factory |
|
||||
|
||||
## Composing with other ElevenLabs surfaces
|
||||
|
||||
The factory accepts a pre-constructed `ElevenLabsClient` from
|
||||
`@suaveplan/third-party-elevenlabs` so the same transport (with its
|
||||
retry, rate-limit, OTel, base URL, and event-bus configuration) can be
|
||||
shared with ElevenLabs-specific surfaces that are *not* part of the
|
||||
voice domain — for example the model catalogue or the
|
||||
speech-to-speech (voice-conversion) endpoint:
|
||||
|
||||
```ts
|
||||
import {
|
||||
ElevenLabsClient,
|
||||
ElevenLabsModelCatalog,
|
||||
ElevenLabsSpeechToSpeech,
|
||||
} from "@suaveplan/third-party-elevenlabs";
|
||||
import { createElevenLabsVoice } from "@suaveplan/voice-elevenlabs";
|
||||
|
||||
const client = new ElevenLabsClient({
|
||||
apiKey: process.env.ELEVENLABS_KEY!,
|
||||
baseUrl: "https://eu.api.elevenlabs.io",
|
||||
});
|
||||
const voice = createElevenLabsVoice({ client });
|
||||
const models = new ElevenLabsModelCatalog(client);
|
||||
const s2s = new ElevenLabsSpeechToSpeech(client);
|
||||
```
|
||||
|
||||
## Error remapping
|
||||
|
||||
Every `ElevenLabsApiError` subclass raised by the underlying transport
|
||||
is remapped into a `VoiceError` from `@suaveplan/voice`, preserving the
|
||||
original via `.cause`. The full table is documented inline at the call
|
||||
sites; a representative subset:
|
||||
|
||||
| Third-party error | Voice domain error |
|
||||
| --- | --- |
|
||||
| `UnauthorizedError`, `ForbiddenError` | `ProviderNotConfiguredError` |
|
||||
| `RequestNotFoundError` (from `voices.get`) | resolves to `null` (no throw) |
|
||||
| `RequestValidationFailedError`, `RequestInvalidError` | `VoiceValidationError` |
|
||||
| `RateLimitExceededError`, `RateLimitQuotaExhaustedError` | `ProviderRateLimitedError` (with `retryAfterMs`) |
|
||||
| `ServerTransientError` / `ServerUnavailableError` (TTS) | `TtsSynthesisFailedError` |
|
||||
| `ServerTransientError` (STT) | `SttTranscriptionFailedError` |
|
||||
| `StreamInterruptedError` | `TtsStreamInterruptedError` |
|
||||
| `ResponseUnsupportedFormatError` | `AudioFormatUnsupportedError` |
|
||||
|
||||
`voices.get(voiceId)` follows the `VoiceRepository` null-on-not-found
|
||||
contract: any upstream 404 is mapped to `null` rather than thrown.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
@suaveplan/voice abstract engine interfaces
|
||||
▲
|
||||
│ implements
|
||||
│
|
||||
@suaveplan/voice-elevenlabs domain adapter (this package)
|
||||
│
|
||||
│ delegates HTTP / WebSocket / multipart
|
||||
▼
|
||||
@suaveplan/third-party-elevenlabs typed transport, retry, OTel,
|
||||
rate-limit headers, raw DTOs
|
||||
```
|
||||
|
||||
Future ElevenLabs surfaces (music, dubbing, conversational AI) compose
|
||||
against the same `@suaveplan/third-party-elevenlabs` client; they do not
|
||||
fan out from this package. This keeps each surface's domain-mapping
|
||||
concerns isolated.
|
||||
|
||||
## Verification
|
||||
|
||||
```sh
|
||||
bunx turbo lint typecheck test --filter=@suaveplan/voice-elevenlabs
|
||||
bunx turbo test:coverage --filter=@suaveplan/voice-elevenlabs
|
||||
```
|
||||
|
||||
Coverage gate: 100/100/100/100 statements/branches/functions/lines.
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# `factory/create-elevenlabs-voice`
|
||||
|
||||
Composition root for the `@suaveplan/voice` engine adapters that target
|
||||
ElevenLabs. One call, one shared transport, five voice engines wired up.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`@suaveplan/voice-elevenlabs` exposes five domain adapters (TTS, STT,
|
||||
voices, sound-effects, voice-isolation). Each needs an
|
||||
`ElevenLabsClient` from `@suaveplan/third-party-elevenlabs`. Without a
|
||||
factory, every consumer would manually construct each adapter and
|
||||
remember to share the same client between them — duplicating the same
|
||||
boilerplate over and over and leaving room for subtle bugs (one adapter
|
||||
using a different timeout, one created on every request and leaking
|
||||
event listeners). `createElevenLabsVoice` makes the right thing easy:
|
||||
either supply a pre-constructed client (so the same transport can be
|
||||
shared with `ElevenLabsModelCatalog` or `ElevenLabsSpeechToSpeech` from
|
||||
`@suaveplan/third-party-elevenlabs`), or pass the same
|
||||
`ElevenLabsClientOptions` a fresh client would accept, and get all five
|
||||
voice engines back as a typed bundle.
|
||||
|
||||
## Features
|
||||
|
||||
- Single function: `createElevenLabsVoice(options)`.
|
||||
- Returns `ElevenLabsVoiceBundle` — `{ client, tts, stt, voices, effects,
|
||||
isolation }`.
|
||||
- Every engine shares the same `client`, which means they share the same
|
||||
retry policy, timeout, event bus, logger, and observability context.
|
||||
- The `client` field is exposed so consumers can issue ad-hoc requests
|
||||
(e.g. account-info endpoints not covered by the engines) against the
|
||||
same transport, or wire it into a sibling provider catalogue.
|
||||
- `options.client` overrides every transport-construction field; pass a
|
||||
client explicitly to compose with surfaces outside the voice domain.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createElevenLabsVoice } from "@suaveplan/voice-elevenlabs";
|
||||
|
||||
const { tts, voices } = createElevenLabsVoice({
|
||||
apiKey: process.env.ELEVENLABS_KEY!,
|
||||
});
|
||||
|
||||
const all = await voices.list();
|
||||
const result = await tts.synthesize({
|
||||
text: "hello",
|
||||
voiceId: all.voices[0]!.id,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ElevenLabsClient,
|
||||
ElevenLabsModelCatalog,
|
||||
} from "@suaveplan/third-party-elevenlabs";
|
||||
import { createElevenLabsVoice } from "@suaveplan/voice-elevenlabs";
|
||||
|
||||
// One transport shared by both the voice engines AND the model catalogue.
|
||||
const client = new ElevenLabsClient({
|
||||
apiKey: process.env.ELEVENLABS_KEY!,
|
||||
baseUrl: "https://eu.api.elevenlabs.io",
|
||||
timeoutMs: 60_000,
|
||||
maxRetries: 5,
|
||||
initialRetryDelayMs: 250,
|
||||
fetchImpl: instrumentedFetch,
|
||||
webSocketImpl: WS, // 'ws' on Node
|
||||
events: appEventBus,
|
||||
logger: appLogger,
|
||||
userAgent: "my-app/1.4.2",
|
||||
defaultHeaders: { "x-tenant": "acme" },
|
||||
});
|
||||
|
||||
const voice = createElevenLabsVoice({ client });
|
||||
const models = new ElevenLabsModelCatalog(client);
|
||||
|
||||
const [voiceList, modelList] = await Promise.all([
|
||||
voice.voices.list(),
|
||||
models.list(),
|
||||
]);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface ElevenLabsVoiceBundle { client, tts, stt, voices, effects, isolation }`
|
||||
- `type CreateElevenLabsVoiceOptions = { client: ElevenLabsClient } | (ElevenLabsClientOptions & { client?: undefined })`
|
||||
- `function createElevenLabsVoice(options: CreateElevenLabsVoiceOptions): ElevenLabsVoiceBundle`
|
||||
|
||||
Each field is the corresponding `@suaveplan/voice` engine implementation
|
||||
instantiated against the shared `client`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- When `options.client` is present it is reused as-is; every other field
|
||||
on `options` is ignored. This is by design so consumers can wire their
|
||||
own transport (e.g. an `ElevenLabsClient` already configured for an EU
|
||||
shard, a private base URL, or a shared event bus) and the voice
|
||||
adapters drop straight in.
|
||||
- When `options.client` is absent the factory delegates to
|
||||
`new ElevenLabsClient(options)` so every `ElevenLabsClientOptions`
|
||||
field is honoured. Invalid options surface as the same construction
|
||||
errors a direct `new ElevenLabsClient(opts)` would raise.
|
||||
- The factory does not validate `apiKey` — passing an empty string
|
||||
surfaces as a 401 / `VOICE.PROVIDER.NOT_CONFIGURED` on the first call
|
||||
rather than a synchronous throw.
|
||||
- The bundle is a plain object literal; consumers can destructure freely
|
||||
(`const { tts } = createElevenLabsVoice(...)`) without losing the
|
||||
shared client reference.
|
||||
- Stateful adapters (none currently) would still be instantiated per
|
||||
call. Call `createElevenLabsVoice` once per process / per long-lived
|
||||
consumer, not once per request.
|
||||
@@ -0,0 +1,89 @@
|
||||
# `models`
|
||||
|
||||
ElevenLabs model catalog. Lists every model the API key has access to,
|
||||
along with capability flags (TTS / voice conversion / fine-tuning) and
|
||||
the language matrix. Calls `GET /v1/models`.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Most ElevenLabs adapters take an optional `modelId` parameter — but
|
||||
which model is appropriate depends on the operation, the language, the
|
||||
account tier, and whether the model serves Pro voices or requires Alpha
|
||||
access. This catalog gives application code a typed view of the
|
||||
account's available models so it can pick the right one at runtime
|
||||
instead of hard-coding strings, and degrade gracefully if a previously
|
||||
chosen model is no longer accessible.
|
||||
|
||||
## Features
|
||||
|
||||
- One method, `list()`, returning `readonly ElevenLabsModel[]`.
|
||||
- Each `ElevenLabsModel` carries the model id, display name, capability
|
||||
flags (`canDoTextToSpeech`, `canDoVoiceConversion`, `canBeFinetuned`),
|
||||
language matrix, and account-tier limits (`maxCharactersFree`,
|
||||
`maxCharactersSubscribed`).
|
||||
- Optional metadata (`description`, `requiresAlphaAccess`,
|
||||
`servesProVoices`, `tokenCostFactor`) is included only when present in
|
||||
the upstream response.
|
||||
- Boolean capability flags default to `false` when missing, and the
|
||||
language list defaults to `[]`.
|
||||
- Errors are remapped onto the `@suaveplan/voice` domain:
|
||||
`ProviderNotConfiguredError`, `ProviderRateLimitedError`, and a
|
||||
generic `VoiceError` for everything else.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createElevenLabsAdapters } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const { models } = createElevenLabsAdapters({ apiKey: KEY });
|
||||
const all = await models.list();
|
||||
for (const m of all) console.log(m.modelId, m.name);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createElevenLabsAdapters } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const { models } = createElevenLabsAdapters({ apiKey: KEY });
|
||||
|
||||
const all = await models.list();
|
||||
const ttsModelsForEnglish = all.filter(
|
||||
(m) =>
|
||||
m.canDoTextToSpeech &&
|
||||
m.languages.some((l) => l.languageId === "en") &&
|
||||
!m.requiresAlphaAccess
|
||||
);
|
||||
const cheapest = ttsModelsForEnglish.reduce((best, m) =>
|
||||
(m.tokenCostFactor ?? 1) < (best.tokenCostFactor ?? 1) ? m : best
|
||||
);
|
||||
console.log("picked:", cheapest.modelId);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface ElevenLabsLanguage { languageId: string; name: string }`
|
||||
- `interface ElevenLabsModel { modelId, name, canDoTextToSpeech, canDoVoiceConversion, canBeFinetuned, languages, description?, maxCharactersFree?, maxCharactersSubscribed?, requiresAlphaAccess?, servesProVoices?, tokenCostFactor? }`
|
||||
- `class ElevenLabsModelCatalog`
|
||||
- `providerName: "elevenlabs"`
|
||||
- `list(): Promise<readonly ElevenLabsModel[]>`
|
||||
|
||||
Throws: `ProviderNotConfiguredError`, `ProviderRateLimitedError`, and
|
||||
generic VoiceError-shaped failures.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The endpoint returns an array, not a paged envelope. There is no
|
||||
`pageToken` to follow.
|
||||
- Language entries with missing `language_id` or `name` are still
|
||||
emitted, but with empty strings — caller code should guard against
|
||||
this rather than crash.
|
||||
- The catalog is not cached. Wrap it in an async-cache if you call it
|
||||
frequently — model lists change rarely (days / weeks).
|
||||
- `tokenCostFactor` is ElevenLabs's relative pricing multiplier vs. the
|
||||
base model. A value of `1` means 1× character billing; `0.5` means
|
||||
half-cost (cheaper, often lower fidelity).
|
||||
@@ -0,0 +1,93 @@
|
||||
# `sound-generation`
|
||||
|
||||
ElevenLabs implementation of `@suaveplan/voice`'s `SoundEffectEngine`
|
||||
interface. Posts a JSON body to `POST /v1/sound-generation` and returns
|
||||
the generated audio as raw bytes.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Generative sound effects are an ElevenLabs strength but the
|
||||
`/v1/sound-generation` endpoint has its own request shape with
|
||||
snake_case fields and ElevenLabs-specific knobs (`prompt_influence`,
|
||||
`loop`). This adapter maps the `SoundEffectRequest` shape
|
||||
(camelCase, with optional duration / prompt-influence / loop / model id /
|
||||
output format) onto the wire body and remaps every error onto the
|
||||
`@suaveplan/voice` domain. Application code stays portable across
|
||||
providers; only this file knows the upstream conventions.
|
||||
|
||||
## Features
|
||||
|
||||
- One method, `generate(request)`, returning the raw audio bytes plus
|
||||
the requested format (defaulting to `mp3_44100_128`).
|
||||
- Optional `durationSeconds` (0.5–30s typical), `loop`,
|
||||
`promptInfluence` (0..1), `modelId`, and `outputFormat` are forwarded
|
||||
when defined and omitted otherwise.
|
||||
- Surfaces the upstream `x-request-id` on the result.
|
||||
- Errors are remapped: 401 → `ProviderNotConfiguredError`, 429 →
|
||||
`ProviderRateLimitedError` (with `retryAfterMs`), 5xx →
|
||||
`SoundEffectGenerationFailedError`.
|
||||
- Built-in retry / backoff inherited from the shared
|
||||
`ElevenLabsClient`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createElevenLabsAdapters } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const { soundEffects } = createElevenLabsAdapters({ apiKey: KEY });
|
||||
const { audio } = await soundEffects.generate({ text: "ocean waves at dusk" });
|
||||
await Bun.write("waves.mp3", audio);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ElevenLabsClient,
|
||||
ElevenLabsSoundEffectEngine,
|
||||
} from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const eng = new ElevenLabsSoundEffectEngine(
|
||||
new ElevenLabsClient({ apiKey: KEY, timeoutMs: 60_000 })
|
||||
);
|
||||
|
||||
const ctrl = new AbortController();
|
||||
const result = await eng.generate({
|
||||
text: "thunderclap followed by rain",
|
||||
durationSeconds: 8,
|
||||
loop: false,
|
||||
promptInfluence: 0.7,
|
||||
modelId: "sound_v2",
|
||||
outputFormat: "wav_44100",
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
console.log(`got ${result.audio.byteLength} bytes`, result.requestId);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class ElevenLabsSoundEffectEngine implements SoundEffectEngine`
|
||||
- `providerName: "elevenlabs"`
|
||||
- `generate(request: SoundEffectRequest): Promise<SoundEffectResult>`
|
||||
- returns `{ audio, format, requestId? }`.
|
||||
|
||||
Throws: `ProviderNotConfiguredError`, `ProviderRateLimitedError`,
|
||||
`SoundEffectGenerationFailedError`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `format` on the result reflects the **requested** format, not
|
||||
necessarily the encoded output — ElevenLabs may return a different
|
||||
format if it rejects the request. Inspect the response bytes / use a
|
||||
metadata reader if you need ground truth.
|
||||
- `prompt_influence` defaults to ElevenLabs's server-side default when
|
||||
omitted (commonly 0.3); set explicitly to 0–1 for deterministic
|
||||
control.
|
||||
- `text` is sent verbatim — there is no client-side validation. Empty
|
||||
text will be rejected upstream as a 422.
|
||||
- The adapter inherits retry, timeout, and observability events from the
|
||||
shared `ElevenLabsClient`. Configure them on the client, not here.
|
||||
@@ -0,0 +1,104 @@
|
||||
# `speech-to-speech`
|
||||
|
||||
ElevenLabs Speech-to-Speech (voice conversion) adapter. Given source
|
||||
audio and a target `voiceId`, produces audio that preserves the source
|
||||
cadence and prosody but adopts the target voice's timbre. Posts to
|
||||
`POST /v1/speech-to-speech/{voice_id}` as multipart form-data.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Voice conversion is an ElevenLabs-specific capability that hasn't been
|
||||
abstracted into the `@suaveplan/voice` core (the abstraction surface for
|
||||
"voice" is still settling — different providers expose conversion with
|
||||
very different envelopes). The adapter exposes a clean
|
||||
`VoiceConversionRequest` / `VoiceConversionResult` shape that mirrors
|
||||
the voice domain conventions (camelCase, AbortSignal,
|
||||
`AudioSource` union) so it composes naturally with the rest of the
|
||||
package even though no abstract interface backs it yet.
|
||||
|
||||
## Features
|
||||
|
||||
- Accepts `Uint8Array`, `ReadableStream<Uint8Array>`, or a URL string as
|
||||
the source audio.
|
||||
- Optional `voiceSettings`, `modelId`, and `outputFormat` are forwarded
|
||||
when defined; output format is validated client-side against the eight
|
||||
ElevenLabs-supported values and rejected with `TtsSynthesisFailedError`
|
||||
if unsupported.
|
||||
- Returns the converted audio as raw bytes with the requested format
|
||||
label and the upstream `x-request-id` when present.
|
||||
- Errors are remapped onto the `@suaveplan/voice` domain:
|
||||
`ProviderNotConfiguredError`, `ProviderRateLimitedError`,
|
||||
`TtsSynthesisFailedError`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createElevenLabsAdapters } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const { speechToSpeech } = createElevenLabsAdapters({ apiKey: KEY });
|
||||
|
||||
const source = new Uint8Array(
|
||||
await Bun.file("acting-take.wav").arrayBuffer()
|
||||
);
|
||||
const { audio } = await speechToSpeech.convert({
|
||||
audio: source,
|
||||
voiceId: "21m00Tcm4TlvDq8ikWAM",
|
||||
});
|
||||
await Bun.write("converted.mp3", audio);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ElevenLabsClient,
|
||||
ElevenLabsSpeechToSpeech,
|
||||
} from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const sts = new ElevenLabsSpeechToSpeech(
|
||||
new ElevenLabsClient({ apiKey: KEY, timeoutMs: 120_000 })
|
||||
);
|
||||
|
||||
const result = await sts.convert({
|
||||
audio: "https://example.com/raw-take.wav",
|
||||
voiceId: "abc",
|
||||
modelId: "eleven_multilingual_sts_v2",
|
||||
outputFormat: "pcm_44100",
|
||||
voiceSettings: {
|
||||
stability: 0.5,
|
||||
similarityBoost: 0.6,
|
||||
style: 0.1,
|
||||
speakerBoost: false,
|
||||
},
|
||||
});
|
||||
console.log("rid:", result.requestId);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `interface VoiceConversionRequest { audio, voiceId, outputFormat?, voiceSettings?, modelId?, signal? }`
|
||||
- `interface VoiceConversionResult { audio: Uint8Array, format: AudioFormat, requestId?: string }`
|
||||
- `class ElevenLabsSpeechToSpeech`
|
||||
- `providerName: "elevenlabs"`
|
||||
- `convert(request: VoiceConversionRequest): Promise<VoiceConversionResult>`
|
||||
|
||||
Throws: `ProviderNotConfiguredError`, `ProviderRateLimitedError`,
|
||||
`TtsSynthesisFailedError`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `voice_settings` is sent as a JSON-string scalar field inside the
|
||||
multipart envelope (not as a separate JSON part) because that's the
|
||||
format ElevenLabs's `/v1/speech-to-speech` endpoint accepts.
|
||||
- Output formats that aren't in ElevenLabs's supported set
|
||||
(`mp3_22050_32`, `mp3_44100_128`, `mp3_44100_192`, `pcm_16000`,
|
||||
`pcm_22050`, `pcm_44100`, `ulaw_8000`) are rejected client-side rather
|
||||
than letting the request reach ElevenLabs and come back as a 422.
|
||||
- The default `outputFormat` label on the result is `mp3_44100_128`,
|
||||
matching ElevenLabs's server default.
|
||||
- Long source audio (> 30s) needs a bumped `timeoutMs` on the underlying
|
||||
client.
|
||||
@@ -0,0 +1,105 @@
|
||||
# `stt`
|
||||
|
||||
ElevenLabs implementation of `@suaveplan/voice`'s `TranscriptionEngine`
|
||||
interface. Posts the audio as multipart form-data to
|
||||
`POST /v1/speech-to-text` and maps the response into the voice
|
||||
domain shape.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Speech-to-text providers vary wildly in their wire formats — some accept
|
||||
raw bytes, some require a URL, some return word timings as
|
||||
`{start, end}` floats and others as integer-millisecond ranges. This
|
||||
adapter handles ElevenLabs's idiosyncrasies (multipart upload required,
|
||||
seconds-based word timings, `request_id` returned in the body sometimes
|
||||
and the response header other times) and presents the canonical
|
||||
`@suaveplan/voice/TranscriptionResult` shape. Application code stays
|
||||
provider-agnostic; only this file knows ElevenLabs's quirks.
|
||||
|
||||
## Features
|
||||
|
||||
- Accepts `Uint8Array`, `ReadableStream<Uint8Array>`, or a URL string as
|
||||
the audio source — streams are buffered and URLs are fetched first
|
||||
because ElevenLabs requires a complete multipart body.
|
||||
- Maps every optional `TranscriptionRequest` field onto the corresponding
|
||||
wire field, including `tag_audio_events`, `num_speakers`,
|
||||
`timestamps_granularity`, `diarize`, `diarization_threshold`,
|
||||
`use_multi_channel`, `entity_detection`, `entity_redaction`,
|
||||
`keyterms`, `temperature`, and `seed`.
|
||||
- Word timestamps are converted from seconds (ElevenLabs) to milliseconds
|
||||
and `spacing` / `audio_event` entries are filtered out.
|
||||
- Word `logprob` is converted to a normalised confidence in `[0, 1]`
|
||||
via `exp(logprob)`.
|
||||
- Errors are remapped onto `@suaveplan/voice` domain errors:
|
||||
`ProviderNotConfiguredError`, `ProviderRateLimitedError` (with
|
||||
`retryAfterMs`), and `SttTranscriptionFailedError`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createElevenLabsAdapters } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const { transcriber } = createElevenLabsAdapters({ apiKey: KEY });
|
||||
const audio = new Uint8Array(await Bun.file("clip.wav").arrayBuffer());
|
||||
const result = await transcriber.transcribe({ audio });
|
||||
console.log(result.text);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ElevenLabsClient,
|
||||
ElevenLabsTranscriptionEngine,
|
||||
} from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const stt = new ElevenLabsTranscriptionEngine(
|
||||
new ElevenLabsClient({ apiKey: KEY })
|
||||
);
|
||||
|
||||
const result = await stt.transcribe({
|
||||
audio: "https://example.com/podcast.mp3",
|
||||
modelId: "scribe_v1",
|
||||
languageCode: "en",
|
||||
timestampsGranularity: "word",
|
||||
diarize: true,
|
||||
numSpeakers: 3,
|
||||
diarizationThreshold: 0.4,
|
||||
entityDetection: ["person", "location"],
|
||||
entityRedaction: "all",
|
||||
keyterms: ["Suaveplan", "suaveplan"],
|
||||
tagAudioEvents: true,
|
||||
temperature: 0,
|
||||
seed: 42,
|
||||
});
|
||||
|
||||
for (const w of result.words ?? []) {
|
||||
console.log(`[${w.speakerId}] ${w.startMs}–${w.endMs}: ${w.word}`);
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class ElevenLabsTranscriptionEngine implements TranscriptionEngine`
|
||||
- `providerName: "elevenlabs"`
|
||||
- `transcribe(request: TranscriptionRequest): Promise<TranscriptionResult>`
|
||||
- returns `{ text, words?, language?, requestId? }`.
|
||||
- `words` is omitted entirely when the response contains only spacing /
|
||||
audio-event entries.
|
||||
- `requestId` prefers the response header value; falls back to the
|
||||
body's `request_id` field.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Default `model_id` is `scribe_v1`. Override with `request.modelId`.
|
||||
- `entityDetection` and `entityRedaction` accept either an array or the
|
||||
literal `"all"` per the upstream contract.
|
||||
- `keyterms` is sent as a comma-joined string (ElevenLabs's wire format),
|
||||
so commas embedded in keyterms will be split on the server side. If
|
||||
you need commas inside keyterms, escape them before passing in.
|
||||
- `audio_event` and `spacing` word entries are dropped because the
|
||||
`@suaveplan/voice/WordTimestamp` schema requires `word: string().min(1)`.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,92 @@
|
||||
# `audio-isolation`
|
||||
|
||||
ElevenLabs implementation of `@suaveplan/voice`'s `VoiceIsolationEngine`
|
||||
interface. Posts the audio as multipart form-data to
|
||||
`POST /v1/audio-isolation` and returns the isolated voice as raw bytes.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
Voice isolation is one of those capabilities every audio platform
|
||||
exposes differently — some take a URL, some take a base64 blob, some
|
||||
return WAV, some return MP3. This adapter handles ElevenLabs's
|
||||
multipart-only contract and presents the canonical
|
||||
`@suaveplan/voice/IsolationResult` shape so callers can swap providers
|
||||
without touching their code path. Buffered upload, error remapping, and
|
||||
default-format selection all live here.
|
||||
|
||||
## Features
|
||||
|
||||
- Accepts `Uint8Array`, `ReadableStream<Uint8Array>`, or a URL string as
|
||||
the audio source — streams are drained and URLs are fetched into
|
||||
memory first because ElevenLabs requires a complete body.
|
||||
- Returns the isolated audio as `Uint8Array` plus the format the caller
|
||||
asked for (defaulting to `mp3_44100_128`).
|
||||
- Surfaces the upstream `x-request-id` on the result when present.
|
||||
- Errors are remapped: 401 → `ProviderNotConfiguredError`, 429 →
|
||||
`ProviderRateLimitedError` (with `retryAfterMs`), URL-fetch failures
|
||||
and 5xx → `VoiceIsolationFailedError`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createElevenLabsAdapters } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const { isolation } = createElevenLabsAdapters({ apiKey: KEY });
|
||||
|
||||
const noisy = new Uint8Array(await Bun.file("noisy.wav").arrayBuffer());
|
||||
const { audio } = await isolation.isolate({ audio: noisy });
|
||||
await Bun.write("clean.mp3", audio);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ElevenLabsClient,
|
||||
ElevenLabsVoiceIsolationEngine,
|
||||
} from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const ctrl = new AbortController();
|
||||
setTimeout(() => ctrl.abort(), 60_000);
|
||||
|
||||
const eng = new ElevenLabsVoiceIsolationEngine(
|
||||
new ElevenLabsClient({
|
||||
apiKey: KEY,
|
||||
timeoutMs: 120_000,
|
||||
maxRetries: 5,
|
||||
})
|
||||
);
|
||||
|
||||
const result = await eng.isolate({
|
||||
audio: "https://example.com/raw-recording.wav",
|
||||
outputFormat: "wav_44100",
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
console.log(`isolated ${result.audio.byteLength} bytes`, result.requestId);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class ElevenLabsVoiceIsolationEngine implements VoiceIsolationEngine`
|
||||
- `providerName: "elevenlabs"`
|
||||
- `isolate(request: IsolationRequest): Promise<IsolationResult>` —
|
||||
returns `{ audio, format, requestId? }`.
|
||||
|
||||
Throws: `ProviderNotConfiguredError`, `ProviderRateLimitedError`,
|
||||
`VoiceIsolationFailedError`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `outputFormat` on the result is purely a label — ElevenLabs's
|
||||
`/v1/audio-isolation` endpoint doesn't currently accept a format
|
||||
parameter, so the bytes are whatever ElevenLabs sends (typically MP3).
|
||||
Callers that need a specific format should re-encode after isolation.
|
||||
- URL audio sources are fetched with the global `fetch` (not the typed
|
||||
client) because they're external URLs that don't accept the ElevenLabs
|
||||
API key.
|
||||
- Long uploads (> 30s) require bumping `timeoutMs` on the underlying
|
||||
client. The default is 30s.
|
||||
@@ -0,0 +1,100 @@
|
||||
# `voices`
|
||||
|
||||
ElevenLabs implementation of `@suaveplan/voice`'s `VoiceRepository`
|
||||
interface. Lists voices via `GET /v1/voices` and looks them up via
|
||||
`GET /v1/voices/{voice_id}`. 404 on lookup is normalised to `null` per
|
||||
the contract.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
ElevenLabs returns voice records with a labels bag (`accent`,
|
||||
`description`, `age`, `gender`, `use_case`) and a settings bag
|
||||
(`stability`, `similarity_boost`, `style`, `use_speaker_boost`) that
|
||||
look nothing like the `Voice` shape. This adapter maps the
|
||||
upstream record onto the package schema, normalises gender / age
|
||||
strings, copies labels into a `metadata` bag for round-tripping, and
|
||||
honours the `VoiceRepository.get()` contract that "not found" must
|
||||
resolve to `null` rather than throw.
|
||||
|
||||
## Features
|
||||
|
||||
- `list(options?)` — paged listing with `category`, `languageCode`,
|
||||
`search`, `pageSize`, and `pageToken` filters.
|
||||
- `get(voiceId)` — returns the mapped voice or `null` on 404.
|
||||
- Maps unknown / non-canonical category strings to `"premade"`.
|
||||
- Tolerates missing `gender` / `age` fields and only emits values that
|
||||
match the canonical enums; unknown values are dropped silently.
|
||||
- Pulls language code from `fine_tuning.language` into a single-element
|
||||
`languageCodes` array.
|
||||
- Settings mapping converts each ElevenLabs snake_case field into the
|
||||
camelCase equivalent, omitting unspecified entries.
|
||||
- Description falls back to `labels.description` when the top-level
|
||||
field is absent.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { createElevenLabsAdapters } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const { voices } = createElevenLabsAdapters({ apiKey: KEY });
|
||||
|
||||
const page = await voices.list({ category: "premade" });
|
||||
for (const v of page.voices) console.log(v.id, v.name);
|
||||
|
||||
const adam = await voices.get("21m00Tcm4TlvDq8ikWAM");
|
||||
if (adam === null) throw new Error("voice missing");
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { createElevenLabsAdapters } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const { voices } = createElevenLabsAdapters({ apiKey: KEY });
|
||||
|
||||
let pageToken: string | undefined;
|
||||
const all: Voice[] = [];
|
||||
do {
|
||||
const page = await voices.list({
|
||||
category: "professional",
|
||||
languageCode: "en",
|
||||
search: "narrator",
|
||||
pageSize: 50,
|
||||
...(pageToken !== undefined ? { pageToken } : {}),
|
||||
});
|
||||
all.push(...page.voices);
|
||||
pageToken = page.hasMore ? page.nextPageToken : undefined;
|
||||
} while (pageToken !== undefined);
|
||||
|
||||
console.log(`${all.length} voices retrieved`);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `class ElevenLabsVoiceRepository implements VoiceRepository`
|
||||
- `providerName: "elevenlabs"`
|
||||
- `list(options?: ListVoicesOptions): Promise<VoiceList>` — returns
|
||||
`{ voices, hasMore, nextPageToken? }`. `hasMore` defaults to `false`
|
||||
when the upstream omits it.
|
||||
- `get(voiceId: string): Promise<Voice | null>` — `null` on 404, throws
|
||||
on any other error.
|
||||
|
||||
Throws: `ProviderNotConfiguredError`, `ProviderRateLimitedError`, and
|
||||
generic VoiceError-shaped failures.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Age detection uses substring matching on the ElevenLabs label
|
||||
(`"young adult"` → `"young"`, `"middle aged"` → `"middle"`, `"old man"`
|
||||
→ `"old"`) because ElevenLabs has no canonical enum.
|
||||
- Gender is matched exactly against `male` / `female` / `neutral`; other
|
||||
values (e.g. ElevenLabs's occasional `"non-binary"`) are dropped — the
|
||||
raw label remains accessible via `metadata`.
|
||||
- `voice_id` is `encodeURIComponent`-escaped before being interpolated
|
||||
into the URL so that workspace IDs with spaces work correctly.
|
||||
- A 404 on `get` is the **only** non-2xx response that's swallowed; every
|
||||
other error (401 / 422 / 429 / 5xx) throws.
|
||||
Reference in New Issue
Block a user