# 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` 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` 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` 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` 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. = ...` writes (banned by TS4111 under index-signature `Record`) to bracket access (`ctx[""] = ...`); `src/types/types.ts` `ElevenLabsClientEvents` gained a `readonly [event: string]: unknown` index signature so it satisfies the `EventBusLike>` 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.