Files
2026-05-27 23:29:53 +01:00

14 KiB
Raw Permalink Blame History

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

  • Create packages/audio/third-party-elevenlabs/ with src/, tests/, src/__benchmarks__/.
  • 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).
  • Write tsconfig.lib.json extending ../../../tsconfig.base.json.
  • Write vite.config.ts with multi-entry build for every feature module.
  • Write vitest.config.ts using createTestConfig() from @suaveplan/testing/config with pool: 'forks', coverage thresholds 100/100/100/100, provider: 'istanbul'.
  • Add lint, lint:fix, format, check, typecheck, test, test:coverage, build, docs:check scripts.
  • Update scripts/folder-reorg/assignment.yml to include audio/third-party-elevenlabs and regenerate MANIFEST.md via bun scripts/folder-reorg/generate-manifest.ts.
  • Update root CLAUDE.md package listing.

2. Errors

  • 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).
  • src/errors/errors.md — full API reference with throw-site examples.
  • src/errors/errors.test.ts — instantiation, code mapping, context preservation, JSON round-trip via SuaveplanError.toJSON().

3. Shared types

  • 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).
  • src/types/types.md
  • src/types/types.test.ts — Zod schemas for runtime-checked subset.

4. Client

  • 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).
  • src/client/client.schemas.ts — Zod for ElevenLabsClientOptions, request envelopes, metadata.
  • src/client/client.md
  • 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

  • src/socket/socket.ts — ElevenLabsSocket over injected WebSocketImpl. Handles ping/pong, abnormal close → WebSocketProtocolError, handshake failure → WebSocketConnectionFailedError, send buffering, ready-state transitions.
  • src/socket/socket.schemas.ts — generic SocketFrame discriminated-union schema (typed per-feature in feature modules).
  • src/socket/socket.md
  • 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

  • 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
  • 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).
  • src/tts/tts.md
  • src/tts/tts.test.ts

6.2 stt

  • src/stt/stt.ts — ElevenLabsStt class.
    • transcribe(body) → JsonResponse<SttTranscript> via POST /v1/speech-to-text (multipart).
  • 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.
  • src/stt/stt.md
  • src/stt/stt.test.ts

6.3 voices

  • 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
  • src/voices/voices.schemas.ts
  • src/voices/voices.md
  • src/voices/voices.test.ts

6.4 sound-generation

  • src/sound-generation/sound-generation.ts — ElevenLabsSoundGeneration class.
    • generate(body) — POST /v1/sound-generation returning binary audio.
  • src/sound-generation/sound-generation.schemas.ts — text, duration_seconds (0.5–30), loop, prompt_influence (0–1), model_id, output_format.
  • src/sound-generation/sound-generation.md
  • src/sound-generation/sound-generation.test.ts

6.5 audio-isolation

  • 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.
  • src/audio-isolation/audio-isolation.schemas.ts — audio (multipart file), file_format.
  • src/audio-isolation/audio-isolation.md
  • src/audio-isolation/audio-isolation.test.ts

6.6 speech-to-speech

  • 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.
  • src/speech-to-speech/speech-to-speech.schemas.ts — audio, model_id, voice_settings, seed, remove_background_noise, output_format.
  • src/speech-to-speech/speech-to-speech.md
  • src/speech-to-speech/speech-to-speech.test.ts

6.7 models

  • src/models/models.ts — ElevenLabsModels class.
    • list() — GET /v1/models returning model registry.
  • 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.
  • src/models/models.md
  • src/models/models.test.ts

7. Index + composite factory

  • src/index.ts — barrel re-export of ElevenLabsClient + every feature class + every error subclass + every TS type.
  • src/factory/factory.ts — createElevenLabsApi(options) returning { client, tts, stt, voices, soundGeneration, audioIsolation, speechToSpeech, models }.
  • src/factory/factory.md
  • src/factory/factory.test.ts — smoke test that factory returns working instances.

8. Property-based tests (tests/property.test.ts)

  • Retry-After parser: arbitrary integer seconds → retryAfterMs = seconds * 1000.
  • Retry-After parser: HTTP-date (RFC 7231) → positive retryAfterMs relative to clock.now().
  • Multipart builder: any MultipartFile shape produces a FormData whose blob bytes equal the input bytes.
  • HTTP status → error code mapping is total and disjoint over [400, 600).
  • OutputFormat round-trip: every documented format string serialises and parses to itself via Zod.

9. Benchmarks (src/__benchmarks__/third-party-elevenlabs.bench.ts)

  • client.fetchJson mock-server round-trip.
  • client.fetchStream mock-server round-trip (1 MB body).
  • tts.synthesize mock-server round-trip.

10. Spec deltas

  • Verify openspec/changes/third-party-elevenlabs/specs/third-party-elevenlabs/spec.md covers every Wave 1 requirement from the proposal.
  • Confirm 100% coverage thresholds enforced in vitest.config.ts.

11. Pre-publish gate

  • bunx turbo lint typecheck test --filter=@suaveplan/third-party-elevenlabs — green
  • bunx turbo test:coverage --filter=@suaveplan/third-party-elevenlabs — 100/100/100/100
  • bun run docs:check --filter=@suaveplan/third-party-elevenlabs — ≥ 90%
  • Banned-pattern scan (@deprecated, TODO, FIXME, ts-ignore, ts-expect-error, .skip, .only, \bany\b, console.log, biome-ignore) — zero hits in src/.
  • README.md exists at package root with install + quick-start + Modules section.
  • Subpath exports count matches feature module count.
  • No bare versions in package.json.
  • bunx turbo test --filter=...@suaveplan/third-party-elevenlabs — dependents pass (only voice-elevenlabs after its rebuild).

12. Archive

  • Fold spec deltas into openspec/specs/third-party-elevenlabs/spec.md.
  • Move openspec/changes/third-party-elevenlabs/ → openspec/archive/2026-05-02-third-party-elevenlabs/.
  • Publish to Verdaccio: bun scripts/publish.ts --package @suaveplan/third-party-elevenlabs.
  • 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.