Initial Commit
This commit is contained in:
220
openspec/archive/2026-05-22-third-party-elevenlabs/tasks.md
Normal file
220
openspec/archive/2026-05-22-third-party-elevenlabs/tasks.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user