# `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 ```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` — returns `{ voices, hasMore, nextPageToken? }`. `hasMore` defaults to `false` when the upstream omits it. - `get(voiceId: string): Promise` — `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.