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:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -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.

View File

@@ -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.

View File

@@ -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).

View File

@@ -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.

View File

@@ -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.

View File

@@ -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)`.

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.

View File

@@ -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.

View File

@@ -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.