4.4 KiB
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.
(no submodules)
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
clientfield 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.clientoverrides every transport-construction field; pass a client explicitly to compose with surfaces outside the voice domain.
Usage: Basic Example
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
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.clientis present it is reused as-is; every other field onoptionsis ignored. This is by design so consumers can wire their own transport (e.g. anElevenLabsClientalready configured for an EU shard, a private base URL, or a shared event bus) and the voice adapters drop straight in. - When
options.clientis absent the factory delegates tonew ElevenLabsClient(options)so everyElevenLabsClientOptionsfield is honoured. Invalid options surface as the same construction errors a directnew ElevenLabsClient(opts)would raise. - The factory does not validate
apiKey— passing an empty string surfaces as a 401 /VOICE.PROVIDER.NOT_CONFIGUREDon 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
createElevenLabsVoiceonce per process / per long-lived consumer, not once per request.