first commit
This commit is contained in:
17
docs/media/audio/third-party-elevenlabs/README.md
Normal file
17
docs/media/audio/third-party-elevenlabs/README.md
Normal file
@@ -0,0 +1,17 @@
|
||||
# @suaveplan/third-party-elevenlabs - Typed thin client for the ElevenLabs platform API: REST + WebSocket transport, retry, rate-limit-header parsing, OTel instrumentation, Zod-validated DTOs. Domain adapters (voice, conversational AI, music, dubbing) compose this client.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [client](./src/client/client.md) — REST + WebSocket transport for the ElevenLabs platform. `ElevenLabsClient` exposes typed `fetchJson` / `fetchBinary` /…
|
||||
- [client](./src/client/client.md) — REST + WebSocket transport for the ElevenLabs platform. `ElevenLabsClient` exposes typed `fetchJson` / `fetchBinary` /…
|
||||
- [headers](./src/client/headers.md) — HTTP header helpers for the ElevenLabs transport. Pure functions for parsing the `Retry-After` header, extracting per-r…
|
||||
- [internals](./src/client/internals.md) — Pure helper functions used by `ElevenLabsClient`. Everything here is side-effect-free, so the helpers are unit-testable…
|
||||
- [socket](./src/client/socket.md) — Wrapper around any `WebSocketLike` (the browser global, `ws` from Node, or a fake from tests). Exposes a typed frame-le…
|
||||
- [url](./src/client/url.md) — Tiny URL composition helper for the ElevenLabs transport. One function: `buildUrl(baseUrl, path, query?)`.
|
||||
- [errors](./src/errors/errors.md) — Stable error class hierarchy and code registry for `@suaveplan/third-party-elevenlabs`. Every non-2xx HTTP response and…
|
||||
- [errors](./src/errors/errors.md) — Stable error class hierarchy and code registry for `@suaveplan/third-party-elevenlabs`. Every non-2xx HTTP response and…
|
||||
- [types](./src/types/types.md) — Shared types for `@suaveplan/third-party-elevenlabs`. Defines the request / response envelope shapes consumed by the tr…
|
||||
- [types](./src/types/types.md) — Shared types for `@suaveplan/third-party-elevenlabs`. Defines the request / response envelope shapes consumed by the tr…
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
109
docs/media/audio/third-party-elevenlabs/src/client/client.md
Normal file
109
docs/media/audio/third-party-elevenlabs/src/client/client.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# `client`
|
||||
|
||||
REST + WebSocket transport for the ElevenLabs platform. `ElevenLabsClient`
|
||||
exposes typed `fetchJson` / `fetchBinary` / `fetchStream` /
|
||||
`fetchMultipartJson` / `fetchMultipartBinary` / `openSocket` methods, plus
|
||||
internal retry-with-exponential-backoff that honours the `Retry-After`
|
||||
header. Every adapter in the package (`tts`, `stt`, `voices`, …) composes
|
||||
this single client so authentication, timeouts, retries, and event
|
||||
emissions live in exactly one place.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
ElevenLabs offers a sprawling REST + WebSocket API surface. Without a
|
||||
shared transport, every adapter would duplicate the same error mapping,
|
||||
header injection, retry policy, and timeout handling. `ElevenLabsClient`
|
||||
solves that: it's the only piece of code in the package that calls
|
||||
`fetch`, and the only piece that knows how to map HTTP status codes onto
|
||||
typed errors. Adapters become thin domain remappers.
|
||||
|
||||
## Features
|
||||
|
||||
- Five typed request shapes — JSON in/out, JSON in / binary out, JSON in /
|
||||
streaming out, multipart in / JSON out, multipart in / binary out.
|
||||
- WebSocket support via `openSocket()` for ElevenLabs's streaming-input
|
||||
TTS endpoints.
|
||||
- Built-in retry: exponential backoff (`initialDelay * 2^attempt`) with
|
||||
`Retry-After` honouring on 429 / 503 / 504 / network errors.
|
||||
- Per-request `AbortSignal` plus a per-call `timeoutMs` race.
|
||||
- Optional `LoggerLike` and `EventBusLike` for observability — emits
|
||||
`request-start`, `request-end`, `retry-attempt`, `rate-limit-hit`.
|
||||
- Caller-supplied `fetchImpl` and `webSocketImpl` for tests / edge runtimes.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ElevenLabsClient } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const client = new ElevenLabsClient({ apiKey: process.env.ELEVENLABS_KEY! });
|
||||
|
||||
const res = await client.fetchJson<{ voice_id: string }>({
|
||||
method: "GET",
|
||||
path: "/v1/voices/abc",
|
||||
feature: "voices",
|
||||
operation: "get",
|
||||
});
|
||||
console.log(res.body.voice_id, res.metadata.requestId);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ElevenLabsClient } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const client = new ElevenLabsClient({
|
||||
apiKey: process.env.ELEVENLABS_KEY!,
|
||||
baseUrl: "https://eu.api.elevenlabs.io",
|
||||
timeoutMs: 60_000,
|
||||
maxRetries: 5,
|
||||
initialRetryDelayMs: 250,
|
||||
fetchImpl: customFetch,
|
||||
events: myBus,
|
||||
logger: myLogger,
|
||||
});
|
||||
|
||||
const ctrl = new AbortController();
|
||||
const stream = await client.fetchStream({
|
||||
method: "POST",
|
||||
path: "/v1/text-to-speech/voice-id/stream",
|
||||
feature: "tts",
|
||||
operation: "synthesize_stream",
|
||||
body: { text: "hello" },
|
||||
accept: "audio/mpeg",
|
||||
signal: ctrl.signal,
|
||||
});
|
||||
for await (const chunk of stream.body) {
|
||||
/* … */
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new ElevenLabsClient(options: ElevenLabsClientOptions)` — constructor.
|
||||
- `fetchJson<TBody>(req: JsonRequest): Promise<JsonResponse<TBody>>`
|
||||
- `fetchBinary(req: BinaryRequest): Promise<BinaryResponse>`
|
||||
- `fetchStream(req: StreamRequest): Promise<StreamResponse>`
|
||||
- `fetchMultipartJson<TBody>(req: MultipartRequest): Promise<JsonResponse<TBody>>`
|
||||
- `fetchMultipartBinary(req: MultipartRequest): Promise<BinaryResponse>`
|
||||
- `openSocket(req: SocketRequest): ElevenLabsSocket` — wss connection with
|
||||
`xi-api-key` injected as a query parameter.
|
||||
|
||||
Throws typed `ElevenLabsApiError` subclasses on non-2xx responses.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `xi-api-key` is sent as a request header for HTTP, but as a query
|
||||
parameter for WebSocket (browsers can't set headers on the WS upgrade).
|
||||
- Multipart values may be `Uint8Array | Blob | ReadableStream | URL`;
|
||||
streams are buffered and URLs are fetched before upload — ElevenLabs
|
||||
multipart endpoints require complete bodies.
|
||||
- Retries fire only for `RATE_LIMIT.EXCEEDED`, `SERVER.TRANSIENT`,
|
||||
`NETWORK.TIMEOUT`, and `NETWORK.UNREACHABLE` codes. Auth and validation
|
||||
failures are not retried.
|
||||
- The class delegates error mapping, multipart materialisation, retry
|
||||
decision, and backoff math to `internals.ts` so the class itself stays
|
||||
focused on transport orchestration.
|
||||
@@ -0,0 +1,89 @@
|
||||
# `headers`
|
||||
|
||||
HTTP header helpers for the ElevenLabs transport. Pure functions for
|
||||
parsing the `Retry-After` header, extracting per-response metadata, and
|
||||
building the canonical outbound header set.
|
||||
|
||||
## Purpose
|
||||
|
||||
ElevenLabs's REST API uses a small but specific set of conventions:
|
||||
authentication via `xi-api-key`, request correlation via `x-request-id`,
|
||||
rate-limit observability via `x-ratelimit-remaining`, and billing
|
||||
observability via `character-cost`. The functions here normalise those
|
||||
conventions into typed values that the rest of the client can safely
|
||||
work with — so no other module has to remember header names or worry
|
||||
about case sensitivity, missing headers, or malformed `Retry-After`
|
||||
values.
|
||||
|
||||
## Features
|
||||
|
||||
- `parseRetryAfter()` — accepts the two RFC 7231 forms (delta-seconds or
|
||||
HTTP-date) and returns milliseconds, with `undefined` on malformed
|
||||
input rather than throwing.
|
||||
- `extractMetadata()` — pulls every observability header into a typed
|
||||
`RequestMetadata` shape, returning a partial object with only the
|
||||
fields that were actually present.
|
||||
- `buildHeaders()` — composes API key + user-agent + caller-supplied
|
||||
defaults + per-request overrides + content-type into the final outbound
|
||||
`Record<string, string>`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { buildHeaders, extractMetadata, parseRetryAfter } from "./headers.js";
|
||||
|
||||
const headers = buildHeaders(
|
||||
"secret-key",
|
||||
"my-app/1.0",
|
||||
undefined,
|
||||
undefined,
|
||||
"application/json",
|
||||
"application/json"
|
||||
);
|
||||
|
||||
const ms = parseRetryAfter("5", () => Date.now()); // 5000
|
||||
const meta = extractMetadata(response.headers);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { buildHeaders, parseRetryAfter } from "./headers.js";
|
||||
|
||||
// Tenant-tagged outbound headers with a per-request override.
|
||||
const headers = buildHeaders(
|
||||
apiKey,
|
||||
"tenant-A/2.3",
|
||||
{ "x-tenant": "acme", "x-feature-flag": "stream" },
|
||||
{ "x-trace-id": traceId },
|
||||
undefined,
|
||||
"audio/mpeg"
|
||||
);
|
||||
|
||||
// Honour an HTTP-date Retry-After against an injectable clock.
|
||||
const fakeClock = () => 1_700_000_000_000;
|
||||
const wait = parseRetryAfter("Wed, 14 Nov 2023 22:13:25 GMT", fakeClock);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `parseRetryAfter(header: string | null | undefined, clock: Clock): number | undefined`
|
||||
— milliseconds until retry, or `undefined` if the header is missing /
|
||||
unparseable.
|
||||
- `extractMetadata(headers: Headers): RequestMetadata` — populates
|
||||
`requestId`, `rateLimitRemaining`, and `characterCost` only when the
|
||||
underlying header is present.
|
||||
- `buildHeaders(apiKey, userAgent, defaults?, perRequest?, contentType?, accept): Record<string, string>`
|
||||
— final header bag. Per-request entries override defaults.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `parseRetryAfter` strictly rejects fractional or negative numerics —
|
||||
V8's `Date.parse` is permissive enough that `"1.5"` becomes a real
|
||||
timestamp; that footgun lives here, not in the caller.
|
||||
- `extractMetadata` checks both `x-request-id` and `request-id` because
|
||||
ElevenLabs has used both spellings historically.
|
||||
- `buildHeaders` is order-sensitive: caller-supplied defaults are layered
|
||||
on top of the canonical headers, then per-request overrides win on top
|
||||
of defaults — but `xi-api-key` and `accept` always reflect the
|
||||
caller-provided values because they're set last in the canonical set.
|
||||
@@ -0,0 +1,96 @@
|
||||
# `internals`
|
||||
|
||||
Pure helper functions used by `ElevenLabsClient`. Everything here is
|
||||
side-effect-free, so the helpers are unit-testable without spinning up
|
||||
the full client machinery, and the `ElevenLabsClient` class can stay
|
||||
focused on transport orchestration.
|
||||
|
||||
## Purpose
|
||||
|
||||
The transport layer has many concerns that are independent of any
|
||||
particular request: mapping HTTP status codes to typed errors, deciding
|
||||
whether an error is retryable, computing the next backoff delay,
|
||||
materialising multipart bodies, and parsing response bodies safely. Each
|
||||
of these is a pure function of its inputs. Exposing them here keeps
|
||||
`client.ts` short, lets each helper be tested in isolation, and makes the
|
||||
HTTP-status-to-error mapping easy to audit at a glance.
|
||||
|
||||
## Features
|
||||
|
||||
- `mapHttpStatus()` — total mapping over `[400, 600)` to typed
|
||||
`ElevenLabsApiError` subclasses, preserving body on 422 and
|
||||
`retryAfterMs` on 429.
|
||||
- `mapNetworkError()` — translates DOMException AbortError / generic
|
||||
thrown errors into typed network errors.
|
||||
- `shouldRetry()` — checks code-and-budget for retryability.
|
||||
- `backoffDelay()` — exponential backoff that honours `Retry-After` when
|
||||
present.
|
||||
- `readJsonBody()` — safe response-body parser that returns `undefined`
|
||||
for empty bodies and a `string` when JSON parsing fails.
|
||||
- `buildMultipartBody()` — turns typed `MultipartField[]` into `FormData`.
|
||||
- `materializeMultipart()` — drains `ReadableStream` and fetches URL
|
||||
values into in-memory `Uint8Array` before upload.
|
||||
- `endpointFor()` / `buildCtx()` — context-builder helpers used in error
|
||||
payloads.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { mapHttpStatus, shouldRetry } from "./internals.js";
|
||||
|
||||
const err = mapHttpStatus(429, "", "GET /v1/x", "rid", 5000, undefined);
|
||||
if (shouldRetry(err, 0, 3)) {
|
||||
/* retry */
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
backoffDelay,
|
||||
buildMultipartBody,
|
||||
materializeMultipart,
|
||||
readJsonBody,
|
||||
} from "./internals.js";
|
||||
|
||||
// 503 retry with no Retry-After → exponential.
|
||||
const wait = backoffDelay(2, 250, undefined); // 1000
|
||||
|
||||
// Multipart upload from a streamed file.
|
||||
const fields = await materializeMultipart([
|
||||
{ kind: "file", name: "audio", filename: "a.wav", value: incomingStream },
|
||||
{ kind: "scalar", name: "model_id", value: "scribe_v1" },
|
||||
]);
|
||||
const form = buildMultipartBody(fields);
|
||||
|
||||
// Tolerant JSON parse.
|
||||
const body = await readJsonBody(response);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `mapHttpStatus(status, statusText, endpoint, requestId?, retryAfterMs?, body?): ElevenLabsApiError`
|
||||
- `mapNetworkError(err: unknown, endpoint: string): ElevenLabsApiError`
|
||||
- `shouldRetry(err, attempt, max): boolean`
|
||||
- `backoffDelay(attempt, initialMs, retryAfterMs?): number`
|
||||
- `readJsonBody(response: Response): Promise<unknown>`
|
||||
- `buildMultipartBody(fields: MultipartField[]): FormData`
|
||||
- `materializeMultipart(fields): Promise<MultipartField[]>`
|
||||
- `endpointFor(env: RequestEnvelope): string`
|
||||
- `buildCtx(endpoint, status, requestId?): { endpoint, statusCode, requestId? }`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The HTTP-status mapping is **total** over `[400, 600)`: every code maps
|
||||
to exactly one `ElevenLabsApiError` subclass. 5xx codes 502 / 503 /
|
||||
504 map to `ServerTransientError` (retryable); other 5xx map to
|
||||
`ServerUnavailableError` (terminal).
|
||||
- 429 uses a heuristic on `statusText` to distinguish quota exhaustion
|
||||
from transient throttling — quota-exhausted is non-retryable.
|
||||
- `materializeMultipart` is the only place in the package that calls the
|
||||
global `fetch` directly (to resolve URL-typed multipart values); every
|
||||
other call goes through `client.#fetchOnce` which adds auth, timeouts,
|
||||
and retries.
|
||||
- `readJsonBody` never throws: empty body → `undefined`, JSON-parse
|
||||
failure → raw string. Callers don't need a try/catch.
|
||||
108
docs/media/audio/third-party-elevenlabs/src/client/socket.md
Normal file
108
docs/media/audio/third-party-elevenlabs/src/client/socket.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# `socket`
|
||||
|
||||
Wrapper around any `WebSocketLike` (the browser global, `ws` from Node,
|
||||
or a fake from tests). Exposes a typed frame-level API and a
|
||||
`Promise<void>` for the open handshake.
|
||||
|
||||
## Purpose
|
||||
|
||||
Raw WebSocket APIs are awkward — open / message / close / error are
|
||||
delivered as untyped events, binary frames arrive as `Blob` /
|
||||
`ArrayBuffer` / typed-array views depending on platform, and there's no
|
||||
built-in way to await the open handshake. `ElevenLabsSocket` papers over
|
||||
all of that so adapters that need WebSocket transport (e.g. the
|
||||
streaming-input TTS endpoint) can `await sock.waitOpen()` and then send
|
||||
typed `SocketFrame` values without touching the raw event surface.
|
||||
|
||||
## Features
|
||||
|
||||
- `waitOpen()` — promise that resolves on `open` and rejects on `error`
|
||||
or caller-signal abort.
|
||||
- `send(frame: SocketFrame)` — typed text or binary send; binary frames
|
||||
are copied into a fresh `ArrayBuffer` so the underlying WS impl never
|
||||
sees a SharedArrayBuffer-backed view by accident.
|
||||
- `onMessage` / `onClose` / `onError` — listeners returning unsubscribe
|
||||
functions.
|
||||
- Accepts `Uint8Array`, `ArrayBuffer`, or any `ArrayBufferView` on the
|
||||
wire and normalises them to `Uint8Array` before dispatching.
|
||||
- Sends on a closed socket throw `WebSocketProtocolError` instead of
|
||||
silently dropping the frame.
|
||||
- Pluggable `WebSocketConstructor` so tests inject a fake.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { ElevenLabsClient } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const client = new ElevenLabsClient({ apiKey: process.env.KEY! });
|
||||
const sock = client.openSocket({
|
||||
path: "/v1/text-to-speech/voice-id/stream-input",
|
||||
feature: "tts",
|
||||
operation: "stream_input",
|
||||
query: { model_id: "eleven_flash_v2" },
|
||||
});
|
||||
await sock.waitOpen();
|
||||
sock.onMessage((f) => process.stdout.write(f.data));
|
||||
sock.send({ kind: "text", data: JSON.stringify({ text: "hello" }) });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { ElevenLabsClient } from "@suaveplan/third-party-elevenlabs";
|
||||
|
||||
const ctrl = new AbortController();
|
||||
const client = new ElevenLabsClient({
|
||||
apiKey: KEY,
|
||||
webSocketImpl: WS, // 'ws' on Node
|
||||
});
|
||||
const sock = client.openSocket({
|
||||
path: "/v1/text-to-speech/abc/stream-input",
|
||||
feature: "tts",
|
||||
operation: "stream_input",
|
||||
});
|
||||
const unsubMsg = sock.onMessage((f) => sink.write(f.data));
|
||||
const unsubClose = sock.onClose((e) =>
|
||||
log.info("closed", { code: e.code, clean: e.wasClean })
|
||||
);
|
||||
sock.onError((err) => log.warn("ws error", { err }));
|
||||
try {
|
||||
await sock.waitOpen();
|
||||
for (const chunk of corpus) {
|
||||
sock.send({ kind: "text", data: JSON.stringify({ text: chunk }) });
|
||||
}
|
||||
} finally {
|
||||
unsubMsg();
|
||||
unsubClose();
|
||||
sock.close(1000, "done");
|
||||
}
|
||||
ctrl.abort();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `new ElevenLabsSocket(ws: WebSocketLike, endpoint: string, signal?: AbortSignal)`
|
||||
- `waitOpen(): Promise<void>`
|
||||
- `readyState: number` — 0 connecting / 1 open / 2 closing / 3 closed.
|
||||
- `send(frame: SocketFrame): void` — `{ kind: "text" | "binary", data }`
|
||||
- `close(code?: number, reason?: string): void`
|
||||
- `onMessage(listener): () => void`
|
||||
- `onClose(listener): () => void`
|
||||
- `onError(listener): () => void`
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The `xi-api-key` is sent as a query parameter (browsers can't set
|
||||
headers on the WS upgrade) — that's done in `ElevenLabsClient.openSocket`,
|
||||
not here.
|
||||
- Binary `send` always copies into a fresh `ArrayBuffer` to defend
|
||||
against SharedArrayBuffer leakage; the cost is one heap allocation per
|
||||
outbound binary frame.
|
||||
- The internal `#wireOpenPromise` removes its `error` listener once the
|
||||
promise settles, so post-open transport errors flow only into the
|
||||
user-installed `onError` listeners — they don't reject `waitOpen()`
|
||||
twice.
|
||||
- `bytesFrom` rejects unsupported binary payload types with
|
||||
`WebSocketProtocolError`. The error is dispatched to `onError` rather
|
||||
than thrown synchronously, since it originates inside an event handler
|
||||
callback.
|
||||
74
docs/media/audio/third-party-elevenlabs/src/client/url.md
Normal file
74
docs/media/audio/third-party-elevenlabs/src/client/url.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# `url`
|
||||
|
||||
Tiny URL composition helper for the ElevenLabs transport. One function:
|
||||
`buildUrl(baseUrl, path, query?)`.
|
||||
|
||||
## Purpose
|
||||
|
||||
The platform exposes around fifty REST endpoints, all rooted at the same
|
||||
base URL, all reached by joining a slash-prefixed path and (sometimes) a
|
||||
query string. Doing that with template literals at every call site
|
||||
inevitably accumulates subtle bugs — double slashes, missing leading
|
||||
slashes, `undefined` values serialised as the literal string
|
||||
`"undefined"`. `buildUrl` normalises the construction so adapters never
|
||||
have to think about it.
|
||||
|
||||
## Features
|
||||
|
||||
- Strips a trailing slash from the base URL, preserves a leading slash on
|
||||
the path, and inserts one if missing.
|
||||
- Accepts `string | number | boolean | undefined` query values and
|
||||
silently drops `undefined` entries before serialisation.
|
||||
- Builds the query string with `URLSearchParams` so values are
|
||||
percent-encoded according to the WHATWG URL spec.
|
||||
- Pure and synchronous — no I/O, no allocations beyond the returned
|
||||
string.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { buildUrl } from "./url.js";
|
||||
|
||||
buildUrl("https://api.elevenlabs.io", "/v1/voices");
|
||||
// → "https://api.elevenlabs.io/v1/voices"
|
||||
|
||||
buildUrl("https://api.elevenlabs.io/", "v1/voices/abc");
|
||||
// → "https://api.elevenlabs.io/v1/voices/abc"
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { buildUrl } from "./url.js";
|
||||
|
||||
const url = buildUrl(
|
||||
"https://api.elevenlabs.io",
|
||||
"/v1/text-to-speech/voice-123/stream",
|
||||
{
|
||||
output_format: "mp3_44100_128",
|
||||
optimize_streaming_latency: 2,
|
||||
enable_logging: false,
|
||||
next_text: undefined, // dropped
|
||||
}
|
||||
);
|
||||
// → "https://api.elevenlabs.io/v1/text-to-speech/voice-123/stream?output_format=mp3_44100_128&optimize_streaming_latency=2&enable_logging=false"
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `buildUrl(baseUrl: string, path: string, query?: Readonly<Record<string, string | number | boolean | undefined>>): string`
|
||||
- `baseUrl` — typically `https://api.elevenlabs.io`; trailing slash
|
||||
is normalised away.
|
||||
- `path` — leading slash is preserved or added.
|
||||
- `query` — optional record. `undefined` values are dropped; non-string
|
||||
values are stringified via `String(value)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The function returns the input as-is if `query` is omitted — no
|
||||
`URLSearchParams` allocation when there are no parameters.
|
||||
- All parameters become string-typed entries on the wire because that's
|
||||
what `URLSearchParams.append` does, regardless of the input type.
|
||||
- `buildUrl` is also reused by `ElevenLabsClient.openSocket` to build
|
||||
WSS URLs after rewriting the scheme — the path / query semantics are
|
||||
the same.
|
||||
148
docs/media/audio/third-party-elevenlabs/src/errors/errors.md
Normal file
148
docs/media/audio/third-party-elevenlabs/src/errors/errors.md
Normal file
@@ -0,0 +1,148 @@
|
||||
# `errors`
|
||||
|
||||
Stable error class hierarchy and code registry for
|
||||
`@suaveplan/third-party-elevenlabs`. Every non-2xx HTTP response and every
|
||||
transport-level failure maps to one of 17 typed `ElevenLabsApiError`
|
||||
subclasses under namespace `THIRD_PARTY_ELEVENLABS.*`.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The ElevenLabs API surface is large and surfaces failures across HTTP status
|
||||
codes, malformed JSON bodies, transport timeouts, and WebSocket protocol
|
||||
errors. Without a stable error hierarchy, every consumer of this client
|
||||
would have to learn the upstream's quirks. This module defines the
|
||||
canonical 17-code surface so callers can branch on `error.code` or
|
||||
`error instanceof RateLimitExceededError` without touching the wire.
|
||||
|
||||
The mapping is total and disjoint over the `[400, 600)` HTTP range, every
|
||||
documented network failure mode, every JSON / Zod parse failure, and the
|
||||
two WebSocket failure modes (handshake / protocol). Every error preserves
|
||||
the original cause via the cause-chain on `SuaveplanError`.
|
||||
|
||||
## Features
|
||||
|
||||
- 17 error codes registered via `defineCodes('THIRD_PARTY_ELEVENLABS', ...)`.
|
||||
- 17 typed subclasses, one per code, with sensible default messages.
|
||||
- Convenience options surface common transport context:
|
||||
- `RateLimitExceededError({ retryAfterMs })` — retries planning.
|
||||
- `RequestValidationFailedError({ body })` — preserved 422 body.
|
||||
- `WebSocketConnectionFailedError({ code })` — close code.
|
||||
- `NetworkTimeoutError({ timeoutMs })` — observed limit.
|
||||
- Every HTTP error accepts `{ endpoint, statusCode, requestId }`.
|
||||
- Caller-supplied `context` is shallow-merged on top of derived context so
|
||||
adapter packages can layer their own attribution (e.g. `tenantId`).
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
ElevenLabsApiError,
|
||||
RateLimitExceededError,
|
||||
} from "@suaveplan/third-party-elevenlabs/errors/errors";
|
||||
|
||||
try {
|
||||
await client.fetchJson(req, schema);
|
||||
} catch (err) {
|
||||
if (err instanceof RateLimitExceededError) {
|
||||
console.log(`retry after ${err.context?.retryAfterMs ?? "unknown"} ms`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Branch on every documented failure mode and re-throw with adapter context:
|
||||
|
||||
```ts
|
||||
import {
|
||||
ElevenLabsApiError,
|
||||
ELEVENLABS_CODES,
|
||||
} from "@suaveplan/third-party-elevenlabs/errors/errors";
|
||||
|
||||
function handleFailure(err: unknown): never {
|
||||
if (!(err instanceof ElevenLabsApiError)) throw err;
|
||||
switch (err.code) {
|
||||
case ELEVENLABS_CODES["AUTH.UNAUTHORIZED"]:
|
||||
case ELEVENLABS_CODES["AUTH.FORBIDDEN"]:
|
||||
throw new ProviderNotConfiguredError({ cause: err });
|
||||
case ELEVENLABS_CODES["RATE_LIMIT.EXCEEDED"]:
|
||||
case ELEVENLABS_CODES["RATE_LIMIT.QUOTA_EXHAUSTED"]:
|
||||
throw new ProviderRateLimitedError({
|
||||
retryAfterMs: err.context?.retryAfterMs as number | undefined,
|
||||
cause: err,
|
||||
});
|
||||
default:
|
||||
throw new TtsSynthesisFailedError({ cause: err });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `ELEVENLABS_CODES` — frozen record `{ "<reason>":
|
||||
"THIRD_PARTY_ELEVENLABS.<reason>" }`.
|
||||
- `ElevenLabsErrorCode` — TypeScript union of every code value.
|
||||
- `ElevenLabsApiError` — abstract base extending `SuaveplanError`.
|
||||
|
||||
Auth + request errors:
|
||||
|
||||
- `UnauthorizedError({ endpoint?, statusCode?, requestId?, ...base })` —
|
||||
`AUTH.UNAUTHORIZED` (HTTP 401).
|
||||
- `ForbiddenError(...)` — `AUTH.FORBIDDEN` (HTTP 403).
|
||||
- `RequestInvalidError(...)` — `REQUEST.INVALID` (other 4xx).
|
||||
- `RequestValidationFailedError({ body?, ...request })` —
|
||||
`REQUEST.VALIDATION_FAILED` (HTTP 422); body preserved verbatim.
|
||||
- `RequestNotFoundError(...)` — `REQUEST.NOT_FOUND` (HTTP 404).
|
||||
|
||||
Rate limit:
|
||||
|
||||
- `RateLimitExceededError({ retryAfterMs?, ...request })` —
|
||||
`RATE_LIMIT.EXCEEDED` (HTTP 429).
|
||||
- `RateLimitQuotaExhaustedError(...)` — `RATE_LIMIT.QUOTA_EXHAUSTED`.
|
||||
|
||||
Server:
|
||||
|
||||
- `ServerTransientError(...)` — `SERVER.TRANSIENT` (retryable 5xx).
|
||||
- `ServerUnavailableError(...)` — `SERVER.UNAVAILABLE` (non-retryable / circuit-open).
|
||||
|
||||
Network:
|
||||
|
||||
- `NetworkTimeoutError({ endpoint?, timeoutMs?, ...base })` —
|
||||
`NETWORK.TIMEOUT`.
|
||||
- `NetworkAbortedError({ endpoint?, ...base })` — `NETWORK.ABORTED`.
|
||||
- `NetworkUnreachableError({ endpoint?, ...base })` — `NETWORK.UNREACHABLE`.
|
||||
|
||||
Response / stream:
|
||||
|
||||
- `ResponseMalformedError({ endpoint?, bodyPreview?, ...base })` —
|
||||
`RESPONSE.MALFORMED`.
|
||||
- `ResponseUnsupportedFormatError({ contentType?, ...base })` —
|
||||
`RESPONSE.UNSUPPORTED_FORMAT`.
|
||||
- `StreamInterruptedError(...)` — `STREAM.INTERRUPTED`.
|
||||
|
||||
WebSocket:
|
||||
|
||||
- `WebSocketConnectionFailedError({ code?, ...base })` —
|
||||
`WEBSOCKET.CONNECTION_FAILED`.
|
||||
- `WebSocketProtocolError({ code?, ...base })` — `WEBSOCKET.PROTOCOL`.
|
||||
|
||||
Every constructor accepts `{ message?, cause?, context?, traceId? }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The 17 codes partition `[400, 600)` HTTP statuses plus three networking
|
||||
modes plus three response / stream modes plus two WebSocket modes. The
|
||||
client never throws a generic `ElevenLabsApiError` — every thrown error
|
||||
is one of the typed subclasses.
|
||||
- `RequestValidationFailedError.body` should be the **parsed** JSON body
|
||||
(truncated to a reasonable bound by the client — currently 4 KB; see
|
||||
`client.ts`). Never put raw network buffers here.
|
||||
- `bodyPreview` on `ResponseMalformedError` is a short string (≤ 256
|
||||
bytes) suitable for logs. Adapters should not re-serialise the full body
|
||||
into context.
|
||||
- All subclasses default to a stable English message. Localisation flows
|
||||
through `MessageResolver` on `@suaveplan/error`.
|
||||
153
docs/media/audio/third-party-elevenlabs/src/types/types.md
Normal file
153
docs/media/audio/third-party-elevenlabs/src/types/types.md
Normal file
@@ -0,0 +1,153 @@
|
||||
# `types`
|
||||
|
||||
Shared types for `@suaveplan/third-party-elevenlabs`. Defines the request /
|
||||
response envelope shapes consumed by the transport layer (`ElevenLabsClient`)
|
||||
and the feature modules built on top of it.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
The transport layer needs a small, stable surface that every feature module
|
||||
agrees on: how to describe a JSON request, how to describe a multipart
|
||||
upload with mixed scalar / file fields, what the response metadata envelope
|
||||
looks like, what events the client emits over an optional event bus. This
|
||||
module locks those types in one place so any new feature module slots into
|
||||
the existing transport without a new envelope shape.
|
||||
|
||||
## Features
|
||||
|
||||
- `RequestEnvelope` shared by every transport call (`method`, `path`,
|
||||
`query`, `headers`, `signal`, `feature`, `operation`).
|
||||
- `JsonRequest`, `BinaryRequest`, `StreamRequest`, `MultipartRequest`,
|
||||
`SocketRequest` — one per transport mode, all derived from the shared
|
||||
envelope.
|
||||
- `MultipartFile` union of `Uint8Array | Blob | ReadableStream | string`
|
||||
(URL).
|
||||
- `MultipartField` discriminated union over `'scalar'` and `'file'` parts.
|
||||
- `JsonResponse<TBody>`, `BinaryResponse`, `StreamResponse` — each carrying
|
||||
the body and a `RequestMetadata` envelope (`requestId`,
|
||||
`rateLimitRemaining`, `characterCost`).
|
||||
- `ElevenLabsClientEvents` — typed event-bus map covering
|
||||
`request-start`, `request-end`, `retry-attempt`, `rate-limit-hit`.
|
||||
- `ElevenLabsClientOptions` — full constructor options including auth, base
|
||||
URL, timeout, retry config, `fetchImpl`, `webSocketImpl`, `clock`,
|
||||
`logger`, `events`, default headers.
|
||||
- `WebSocketConstructor` / `WebSocketLike` — minimal duck types so
|
||||
consumers can inject a fake WebSocket in tests.
|
||||
- `SocketFrame` — `'text' | 'binary'` discriminated union.
|
||||
- `SocketCloseEvent` — close-event surface.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { JsonRequest } from "@suaveplan/third-party-elevenlabs/types/types";
|
||||
|
||||
const req: JsonRequest = {
|
||||
method: "GET",
|
||||
path: "/v1/voices",
|
||||
feature: "voices",
|
||||
operation: "list",
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
Build a multipart upload from a URL, raw bytes, and scalar fields:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
MultipartField,
|
||||
MultipartRequest,
|
||||
} from "@suaveplan/third-party-elevenlabs/types/types";
|
||||
|
||||
const fields: MultipartField[] = [
|
||||
{ kind: "scalar", name: "model_id", value: "scribe_v2" },
|
||||
{
|
||||
kind: "file",
|
||||
name: "file",
|
||||
filename: "audio.wav",
|
||||
contentType: "audio/wav",
|
||||
value: new Uint8Array(audioBytes),
|
||||
},
|
||||
{
|
||||
kind: "file",
|
||||
name: "additional",
|
||||
filename: "extra.wav",
|
||||
contentType: "audio/wav",
|
||||
value: "https://cdn.example.com/extra.wav",
|
||||
},
|
||||
];
|
||||
|
||||
const req: MultipartRequest = {
|
||||
method: "POST",
|
||||
path: "/v1/speech-to-text",
|
||||
fields,
|
||||
feature: "stt",
|
||||
operation: "transcribe",
|
||||
};
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
Request envelopes:
|
||||
|
||||
- `RequestEnvelope` — `{ method, path, query?, headers?, signal?, feature,
|
||||
operation }`.
|
||||
- `JsonRequest extends RequestEnvelope` — `body?: unknown`.
|
||||
- `BinaryRequest extends RequestEnvelope` — `body?: unknown; accept?: string`.
|
||||
- `StreamRequest extends RequestEnvelope` — `body?: unknown; accept?: string`.
|
||||
- `MultipartRequest extends RequestEnvelope` —
|
||||
`fields: readonly MultipartField[]; accept?: string`.
|
||||
- `SocketRequest` — `{ path, query?, headers?, feature, operation }`.
|
||||
|
||||
Multipart:
|
||||
|
||||
- `MultipartFile = Uint8Array | Blob | ReadableStream<Uint8Array> | string`.
|
||||
- `MultipartFieldFile { kind: 'file'; name, filename, contentType?, value }`.
|
||||
- `MultipartFieldScalar { kind: 'scalar'; name, value }`.
|
||||
- `MultipartField = MultipartFieldFile | MultipartFieldScalar`.
|
||||
|
||||
Response envelopes:
|
||||
|
||||
- `RequestMetadata` — `{ requestId?, rateLimitRemaining?, characterCost? }`.
|
||||
- `JsonResponse<TBody>` — `{ body: TBody, metadata: RequestMetadata }`.
|
||||
- `BinaryResponse` — `{ body: Uint8Array, contentType, metadata }`.
|
||||
- `StreamResponse` —
|
||||
`{ body: ReadableStream<Uint8Array>, contentType, metadata }`.
|
||||
|
||||
Events:
|
||||
|
||||
- `ElevenLabsClientEvents` — typed event-bus map. See source for the four
|
||||
event payload shapes.
|
||||
|
||||
Options:
|
||||
|
||||
- `ElevenLabsClientOptions` — see source for the full set of constructor
|
||||
options.
|
||||
|
||||
WebSocket:
|
||||
|
||||
- `WebSocketConstructor` — `new (url, protocols?) => WebSocketLike`.
|
||||
- `WebSocketLike` — minimal duck type covering `readyState`, `url`, `send`,
|
||||
`close`, `addEventListener`, `removeEventListener`.
|
||||
- `SocketFrame` — `'text' | 'binary'` discriminated union.
|
||||
- `SocketCloseEvent` — `{ code, reason, wasClean }`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `feature` and `operation` are intentionally part of every request
|
||||
envelope so the transport layer can label OTel spans and counters
|
||||
consistently without per-call wiring.
|
||||
- `MultipartFile` accepts URLs as strings; the client fetches them via
|
||||
the injected `fetchImpl` before uploading. This is the only place in
|
||||
the transport that issues a *secondary* network call from a single
|
||||
caller invocation.
|
||||
- `RequestMetadata` keeps every field optional because some endpoints
|
||||
do not surface `X-RateLimit-Remaining` or `X-Character-Cost`.
|
||||
- `WebSocketConstructor` mirrors the WHATWG `WebSocket` constructor
|
||||
signature so the global `WebSocket` is a valid value at runtime.
|
||||
- The two-mode `SocketFrame` discriminated union keeps the wire shape
|
||||
explicit; feature modules layer their own typed schemas on top.
|
||||
Reference in New Issue
Block a user