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

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

  • 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

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