first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

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.