4.2 KiB
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
VoiceRepositoryinterface withproviderName,list, andget.list(options?)returns aVoiceListenvelope (voices,nextPageToken,hasMore).get(voiceId)resolves toVoice | null— adapters MUST map upstream not-found codes tonullrather than throwing.- Zod schemas re-exporting
ListVoicesOptionsSchema(asVoiceRepositoryListOptionsSchema),VoiceListSchema(asVoiceRepositoryListResultSchema), and aVoice | nullunion for thegetresult. - Runtime type guard
isVoiceRepository.
Usage: Basic Example
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:
import type {
ListVoicesOptions,
Voice,
VoiceRepository,
} from "@suaveplan/voice";
async function* allEnglishVoices(
voices: VoiceRepository,
): AsyncIterable<Voice> {
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<Voice> {
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<VoiceList>.get(voiceId: string): Promise<Voice | null>.
VoiceRepositoryListOptionsSchema— re-export ofListVoicesOptionsSchemafor ergonomic discovery.VoiceRepositoryListResultSchema— alias ofVoiceListSchema.VoiceRepositoryGetResultSchema— Zod union ofVoiceSchemaandz.null().isVoiceRepository(value): value is VoiceRepository— duck-type guard.
Implementation Notes
- The
null-on-not-found contract forgetis 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 tonull; any other error is propagated as aVoiceErrorsubclass. - 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
VoiceClonerinterface will land in a dedicated subpath when the design is settled and ≥ 2 providers are mapped onto it. - The schemas in
voices.schemas.tsare re-exports / aliases of the authoritative schemas intypes/types.ts. They exist to give callers ergonomic discovery without forcing them to know thatListVoicesOptionsandVoiceListare 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.