Files
suaveplan/openspec/archive/2026-05-22-third-party-elevenlabs/tasks.md
2026-05-27 23:29:53 +01:00

221 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tasks: third-party-elevenlabs
**Change ID**: third-party-elevenlabs
**Package**: `@suaveplan/third-party-elevenlabs`
**Target path**: `packages/audio/third-party-elevenlabs`
**Wave**: 1 (voice surface)
**Prerequisite packages on Verdaccio**: `@suaveplan/error`, `@suaveplan/types`, `@suaveplan/streams`, `@suaveplan/retry`, `@suaveplan/logger`
> Wave 1 only. Subsequent waves (`tts-websocket`, `voice-cloning`, `voice-design`, `pronunciation-dictionaries`, `conversational-ai`, `music`, `dubbing`, `studio`, `audio-native`, `history`, `user`, `workspace`) ship under their own change IDs.
---
## 1. Scaffold
- [x] Create `packages/audio/third-party-elevenlabs/` with `src/`, `tests/`, `src/__benchmarks__/`.
- [x] Write `package.json` at version `0.1.0`, `"type": "module"`, `"sideEffects": false`, full subpath `exports` map per feature module, `dependencies` and `devDependencies` via catalogs only (no bare versions).
- [x] Write `tsconfig.lib.json` extending `../../../tsconfig.base.json`.
- [x] Write `vite.config.ts` with multi-entry build for every feature module.
- [x] Write `vitest.config.ts` using `createTestConfig()` from `@suaveplan/testing/config` with `pool: 'forks'`, coverage thresholds 100/100/100/100, `provider: 'istanbul'`.
- [x] Add `lint`, `lint:fix`, `format`, `check`, `typecheck`, `test`, `test:coverage`, `build`, `docs:check` scripts.
- [x] Update `scripts/folder-reorg/assignment.yml` to include `audio/third-party-elevenlabs` and regenerate `MANIFEST.md` via `bun scripts/folder-reorg/generate-manifest.ts`.
- [x] Update root `CLAUDE.md` package listing.
---
## 2. Errors
- [x] `src/errors/errors.ts` — define `ELEVENLABS_CODES` via `defineCodes('THIRD_PARTY_ELEVENLABS', [...])` covering 17 codes from proposal §Errors; export `ElevenLabsApiError` base + 17 typed subclasses (`UnauthorizedError`, `ForbiddenError`, `RequestInvalidError`, `RequestValidationFailedError`, `RequestNotFoundError`, `RateLimitExceededError`, `RateLimitQuotaExhaustedError`, `ServerTransientError`, `ServerUnavailableError`, `NetworkTimeoutError`, `NetworkAbortedError`, `NetworkUnreachableError`, `ResponseMalformedError`, `ResponseUnsupportedFormatError`, `StreamInterruptedError`, `WebSocketConnectionFailedError`, `WebSocketProtocolError`).
- [x] `src/errors/errors.md` — full API reference with throw-site examples.
- [x] `src/errors/errors.test.ts` — instantiation, code mapping, context preservation, JSON round-trip via `SuaveplanError.toJSON()`.
---
## 3. Shared types
- [x] `src/types/types.ts` — `JsonRequest`, `BinaryRequest`, `StreamRequest`, `MultipartRequest`, `SocketRequest`, `BinaryResponse`, `RequestMetadata` (`requestId`, `rateLimitRemaining`, `characterCost`), `ElevenLabsClientEvents` (request-start, request-end, retry-attempt, rate-limit-hit), `RetryDecision`, `MultipartFile` (`Uint8Array | Blob | ReadableStream | string`).
- [x] `src/types/types.md`
- [x] `src/types/types.test.ts` — Zod schemas for runtime-checked subset.
---
## 4. Client
- [x] `src/client/client.ts` — `ElevenLabsClient` class.
- Constructor: validate options (Zod), default base URL `https://api.elevenlabs.io`, default timeout `30_000`, default retry policy (exponential backoff with jitter, 3 attempts, retryable codes only).
- `fetchJson(req, schema)` — JSON in, Zod-validated JSON out, returns `{ body, metadata }`.
- `fetchBinary(req)` — JSON in, binary `Uint8Array` out + headers metadata.
- `fetchStream(req)` — JSON or multipart in, returns `{ stream, metadata }` with `ReadableStream<Uint8Array>` body.
- `fetchMultipart(req, schema)` — `FormData` upload (string URL → fetched first; `ReadableStream` → buffered via `@suaveplan/streams.toBytes`; `Uint8Array`/`Blob` → direct), Zod-validated JSON out.
- `openSocket(req)` — `WebSocket` handshake with `xi-api-key` header, returns `ElevenLabsSocket`.
- HTTP status mapping per proposal §Errors.
- `Retry-After` parsing (seconds + HTTP-date).
- `X-RateLimit-Remaining` + `X-Character-Cost` header surfacing in metadata.
- OTel span / counter / histogram per call.
- `AbortSignal` plumbed end-to-end (caller signal merged with internal timeout signal).
- Circuit breaker integration (opt-in via options).
- [x] `src/client/client.schemas.ts` — Zod for `ElevenLabsClientOptions`, request envelopes, metadata.
- [x] `src/client/client.md`
- [x] `src/client/client.test.ts` — unit tests with `fetchImpl` injection: 200 happy path, 401, 403, 404, 422, 429 with `Retry-After: 5`, 429 with HTTP-date, 500 retried then succeeds, 503 retried then fails, timeout, abort, `RESPONSE.MALFORMED` on Zod mismatch, multipart with `Uint8Array`, multipart with `ReadableStream`, multipart with URL fetched first, stream returns body unmodified, OTel span emitted, counter incremented, histogram recorded.
---
## 5. WebSocket transport
- [x] `src/socket/socket.ts` — `ElevenLabsSocket` over injected `WebSocketImpl`. Handles ping/pong, abnormal close → `WebSocketProtocolError`, handshake failure → `WebSocketConnectionFailedError`, send buffering, ready-state transitions.
- [x] `src/socket/socket.schemas.ts` — generic `SocketFrame` discriminated-union schema (typed per-feature in feature modules).
- [x] `src/socket/socket.md`
- [x] `src/socket/socket.test.ts` — fake `WebSocket` implementation: connect/send/close/error paths, ping/pong, protocol error mapping.
---
## 6. Feature modules — Wave 1
Every feature module follows the same shape: class taking `ElevenLabsClient`, Zod schemas in `*.schemas.ts`, sibling `.md`, `*.test.ts` with mocked `fetchImpl`, OTel attributes per call.
### 6.1 `tts`
- [x] `src/tts/tts.ts` — `ElevenLabsTts` class.
- `synthesize(voiceId, body)` → `BinaryResponse` via `POST /v1/text-to-speech/{voiceId}`
- `synthesizeWithTimestamps(voiceId, body)` → `JsonResponse<TtsTimestampsBody>` via `POST /v1/text-to-speech/{voiceId}/with-timestamps`
- `synthesizeStream(voiceId, body, options?)` → `StreamResponse` via `POST /v1/text-to-speech/{voiceId}/stream`
- `synthesizeStreamWithTimestamps(voiceId, body, options?)` → `StreamResponse<JsonLine>` via `POST /v1/text-to-speech/{voiceId}/stream/with-timestamps`
- [x] `src/tts/tts.schemas.ts` — full request schema (`text`, `model_id`, `language_code`, `voice_settings`, `previous_text`, `next_text`, `seed`, `pronunciation_dictionary_locators`, `apply_text_normalization`, `apply_language_text_normalization`, `output_format`, `optimize_streaming_latency`).
- [x] `src/tts/tts.md`
- [x] `src/tts/tts.test.ts`
### 6.2 `stt`
- [x] `src/stt/stt.ts` — `ElevenLabsStt` class.
- `transcribe(body)` → `JsonResponse<SttTranscript>` via `POST /v1/speech-to-text` (multipart).
- [x] `src/stt/stt.schemas.ts` — `model_id`, `language_code`, `tag_audio_events`, `num_speakers`, `timestamps_granularity`, `diarize`, `diarization_threshold`, `additional_formats`, `file`, `cloud_storage_url`, `webhook` request shape; `language_code`, `language_probability`, `text`, `words[]` (with `start`, `end`, `speaker_id`, `type`), `additional_formats[]` response.
- [x] `src/stt/stt.md`
- [x] `src/stt/stt.test.ts`
### 6.3 `voices`
- [x] `src/voices/voices.ts` — `ElevenLabsVoices` class.
- `list(params)` — `GET /v1/voices` with `show_legacy`, `category`, `voice_type`, `search`, `next_page_token`, `page_size`
- `listShared(params)` — `GET /v1/shared-voices`
- `get(voiceId, params?)` — `GET /v1/voices/{id}`
- `getSettings(voiceId)` — `GET /v1/voices/{id}/settings`
- `editSettings(voiceId, body)` — `POST /v1/voices/{id}/settings/edit`
- `getDefaultSettings()` — `GET /v1/voices/settings/default`
- [x] `src/voices/voices.schemas.ts`
- [x] `src/voices/voices.md`
- [x] `src/voices/voices.test.ts`
### 6.4 `sound-generation`
- [x] `src/sound-generation/sound-generation.ts` — `ElevenLabsSoundGeneration` class.
- `generate(body)` — `POST /v1/sound-generation` returning binary audio.
- [x] `src/sound-generation/sound-generation.schemas.ts` — `text`, `duration_seconds` (0.5–30), `loop`, `prompt_influence` (0–1), `model_id`, `output_format`.
- [x] `src/sound-generation/sound-generation.md`
- [x] `src/sound-generation/sound-generation.test.ts`
### 6.5 `audio-isolation`
- [x] `src/audio-isolation/audio-isolation.ts` — `ElevenLabsAudioIsolation` class.
- `isolate(body)` — `POST /v1/audio-isolation` (multipart) returning binary audio.
- `isolateStream(body)` — `POST /v1/audio-isolation/stream` returning stream.
- [x] `src/audio-isolation/audio-isolation.schemas.ts` — `audio` (multipart file), `file_format`.
- [x] `src/audio-isolation/audio-isolation.md`
- [x] `src/audio-isolation/audio-isolation.test.ts`
### 6.6 `speech-to-speech`
- [x] `src/speech-to-speech/speech-to-speech.ts` — `ElevenLabsSpeechToSpeech` class.
- `convert(voiceId, body)` — `POST /v1/speech-to-speech/{voiceId}` (multipart) returning binary audio.
- `convertStream(voiceId, body)` — `POST /v1/speech-to-speech/{voiceId}/stream` returning stream.
- [x] `src/speech-to-speech/speech-to-speech.schemas.ts` — `audio`, `model_id`, `voice_settings`, `seed`, `remove_background_noise`, `output_format`.
- [x] `src/speech-to-speech/speech-to-speech.md`
- [x] `src/speech-to-speech/speech-to-speech.test.ts`
### 6.7 `models`
- [x] `src/models/models.ts` — `ElevenLabsModels` class.
- `list()` — `GET /v1/models` returning model registry.
- [x] `src/models/models.schemas.ts` — `model_id`, `name`, `can_be_finetuned`, `can_do_text_to_speech`, `can_do_voice_conversion`, `can_use_style`, `can_use_speaker_boost`, `serves_pro_voices`, `token_cost_factor`, `description`, `requires_alpha_access`, `max_characters_request_free_user`, `max_characters_request_subscribed_user`, `maximum_text_length_per_request`, `languages[]`, `model_rates`.
- [x] `src/models/models.md`
- [x] `src/models/models.test.ts`
---
## 7. Index + composite factory
- [x] `src/index.ts` — barrel re-export of `ElevenLabsClient` + every feature class + every error subclass + every TS type.
- [x] `src/factory/factory.ts` — `createElevenLabsApi(options)` returning `{ client, tts, stt, voices, soundGeneration, audioIsolation, speechToSpeech, models }`.
- [x] `src/factory/factory.md`
- [x] `src/factory/factory.test.ts` — smoke test that factory returns working instances.
---
## 8. Property-based tests (`tests/property.test.ts`)
- [x] `Retry-After` parser: arbitrary integer seconds → `retryAfterMs = seconds * 1000`.
- [x] `Retry-After` parser: HTTP-date (RFC 7231) → positive `retryAfterMs` relative to `clock.now()`.
- [x] Multipart builder: any `MultipartFile` shape produces a `FormData` whose blob bytes equal the input bytes.
- [x] HTTP status → error code mapping is total and disjoint over `[400, 600)`.
- [x] `OutputFormat` round-trip: every documented format string serialises and parses to itself via Zod.
---
## 9. Benchmarks (`src/__benchmarks__/third-party-elevenlabs.bench.ts`)
- [x] `client.fetchJson` mock-server round-trip.
- [x] `client.fetchStream` mock-server round-trip (1 MB body).
- [x] `tts.synthesize` mock-server round-trip.
---
## 10. Spec deltas
- [x] Verify `openspec/changes/third-party-elevenlabs/specs/third-party-elevenlabs/spec.md` covers every Wave 1 requirement from the proposal.
- [x] Confirm 100% coverage thresholds enforced in `vitest.config.ts`.
---
## 11. Pre-publish gate
- [x] `bunx turbo lint typecheck test --filter=@suaveplan/third-party-elevenlabs` — green
- [x] `bunx turbo test:coverage --filter=@suaveplan/third-party-elevenlabs` — 100/100/100/100
- [x] `bun run docs:check --filter=@suaveplan/third-party-elevenlabs` — ≥ 90%
- [x] Banned-pattern scan (`@deprecated`, `TODO`, `FIXME`, `ts-ignore`, `ts-expect-error`, `.skip`, `.only`, `\bany\b`, `console.log`, `biome-ignore`) — zero hits in `src/`.
- [x] `README.md` exists at package root with install + quick-start + Modules section.
- [x] Subpath exports count matches feature module count.
- [x] No bare versions in `package.json`.
- [x] `bunx turbo test --filter=...@suaveplan/third-party-elevenlabs` — dependents pass (only `voice-elevenlabs` after its rebuild).
---
## 12. Archive
- [x] Fold spec deltas into `openspec/specs/third-party-elevenlabs/spec.md`.
- [x] Move `openspec/changes/third-party-elevenlabs/` → `openspec/archive/2026-05-02-third-party-elevenlabs/`.
- [x] Publish to Verdaccio: `bun scripts/publish.ts --package @suaveplan/third-party-elevenlabs`.
- [x] Update `STATUS.md`.
---
## Audit Note (2026-05-02)
**Status**: DEFERRED.
State at audit:
- Package scaffold exists at `packages/audio/third-party-elevenlabs/` with all 11 feature directories (`tts`, `stt`, `voices`, `sound-generation`, `audio-isolation`, `speech-to-speech`, `models`, `client`, `errors`, `types`, `factory`).
- **Typecheck fixed in this audit**: `src/errors/errors.ts` switched 15 `ctx.<prop> = ...` writes (banned by TS4111 under index-signature `Record<string, unknown>`) to bracket access (`ctx["<prop>"] = ...`); `src/types/types.ts` `ElevenLabsClientEvents` gained a `readonly [event: string]: unknown` index signature so it satisfies the `EventBusLike<TMap extends Record<string, unknown>>` constraint. Typecheck is now green.
- **Build fails**: there is no `src/index.ts` barrel — `vite build` errors with `Could not resolve entry module "src/index.ts"`. Task 7 ("Index + composite factory") is unchecked.
- No `tests/property.test.ts` (Section 8) and no `src/__benchmarks__/` (Section 9).
- Package `README.md` not present at root (Section 11).
- Spec deltas live under `specs/third-party-elevenlabs/` but have not been folded.
Resuming work:
1. Author `src/index.ts` re-exporting `ElevenLabsClient`, every feature class, every error subclass, every TS type.
2. Implement `src/factory/factory.ts` (exists as folder) and `createElevenLabsApi(options)`.
3. Add `tests/property.test.ts` covering Retry-After parsing, multipart builder, status→code mapping totality, OutputFormat round-trip.
4. Add `src/__benchmarks__/third-party-elevenlabs.bench.ts` (mock-server fetchJson / fetchStream / tts.synthesize).
5. Author root `README.md`.
6. Confirm `bunx turbo lint typecheck test:coverage --filter=@suaveplan/third-party-elevenlabs` is 100/100/100/100, fold deltas, archive.