first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,111 @@
# `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.
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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<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.