# `voices/voices` The voice repository interface for `@suaveplan/voice` — read-only listing and lookup of voices available to the active provider. Defines the `VoiceRepository` contract every voice provider adapter must implement, plus the supporting Zod schemas and a runtime type guard. _(no submodules)_ ## Purpose Provider voice catalogues differ in pagination scheme (cursor / page-token / offset), filter taxonomy, and "shared" vs "owned" voice splits. This module locks the canonical surface — `list(options?)` returning a `VoiceList` envelope and `get(voiceId)` returning `Voice | null`. Provider adapters collapse their native responses onto these shapes, so consumers can browse voices without learning each backend's quirks. ## Features - `VoiceRepository` interface with `providerName`, `list`, and `get`. - `list(options?)` returns a `VoiceList` envelope (`voices`, `nextPageToken`, `hasMore`). - `get(voiceId)` resolves to `Voice | null` — adapters MUST map upstream not-found codes to `null` rather than throwing. - Zod schemas re-exporting `ListVoicesOptionsSchema` (as `VoiceRepositoryListOptionsSchema`), `VoiceListSchema` (as `VoiceRepositoryListResultSchema`), and a `Voice | null` union for the `get` result. - Runtime type guard `isVoiceRepository`. ## Usage: Basic Example ```ts import type { VoiceRepository } from "@suaveplan/voice/voices/voices"; async function findVoiceByName( voices: VoiceRepository, name: string, ) { const page = await voices.list({ search: name, pageSize: 25 }); return page.voices[0] ?? null; } ``` ## Usage: Advanced Example Async-iterate every voice across all pages, with a guarded language filter: ```ts import type { ListVoicesOptions, Voice, VoiceRepository, } from "@suaveplan/voice"; async function* allEnglishVoices( voices: VoiceRepository, ): AsyncIterable { let token: string | undefined; while (true) { const opts: ListVoicesOptions = { languageCode: "en", pageSize: 100 }; if (token !== undefined) opts.pageToken = token; const page = await voices.list(opts); for (const v of page.voices) yield v; if (!page.hasMore || page.nextPageToken === undefined) break; token = page.nextPageToken; } } async function lookupOrFail(voices: VoiceRepository, id: string): Promise { const v = await voices.get(id); if (v === null) throw new Error(`voice ${id} not found`); return v; } ``` ## API Reference - `VoiceRepository` — interface with: - `providerName: string` — stable identifier (e.g. `'elevenlabs'`). - `list(options?: ListVoicesOptions): Promise`. - `get(voiceId: string): Promise`. - `VoiceRepositoryListOptionsSchema` — re-export of `ListVoicesOptionsSchema` for ergonomic discovery. - `VoiceRepositoryListResultSchema` — alias of `VoiceListSchema`. - `VoiceRepositoryGetResultSchema` — Zod union of `VoiceSchema` and `z.null()`. - `isVoiceRepository(value): value is VoiceRepository` — duck-type guard. ## Implementation Notes - The `null`-on-not-found contract for `get` is deliberate: callers commonly check existence before falling back to a default voice. Throwing would force a try/catch around every probe. Adapters that receive a 404 from upstream MUST map it to `null`; any other error is propagated as a `VoiceError` subclass. - Voice cloning is intentionally absent. ElevenLabs supports instant / professional cloning; OpenAI does not at all; Azure has its own "Custom Neural Voice" workflow with a multi-week verification process. A future `VoiceCloner` interface will land in a dedicated subpath when the design is settled and ≥ 2 providers are mapped onto it. - The schemas in `voices.schemas.ts` are re-exports / aliases of the authoritative schemas in `types/types.ts`. They exist to give callers ergonomic discovery without forcing them to know that `ListVoicesOptions` and `VoiceList` are shared with non-repository consumers. - The runtime guard checks the three required members but does not enforce argument shapes on the methods. Callers needing stronger validation should call `await voices.list({})` against a fixture and assert the response.