Files
skills/docs/media/audio/voice-elevenlabs/src/factory/create-elevenlabs-voice.md
2026-05-29 08:20:56 +01:00

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

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