Files
2026-05-27 23:47:53 +01:00

3.6 KiB

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.

(no submodules)

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

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

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.