UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

134 lines (133 loc) • 6.14 kB
/** * ElevenLabs Text-to-Speech Handler * * Implementation of TTS using ElevenLabs API. * * @module voice/providers/ElevenLabsTTS */ import type { TTSHandler, TTSOptions, TTSResult, TTSVoice } from "../../types/index.js"; /** * ElevenLabs Text-to-Speech Handler * * Supports high-quality multilingual TTS with voice cloning. * * @see https://elevenlabs.io/docs/api-reference */ export declare class ElevenLabsTTS implements TTSHandler { private readonly apiKey; private readonly baseUrl; /** `ELEVENLABS_VOICE_ID`, else Rachel; a per-request `voice` still wins. */ private readonly defaultVoiceId; /** `ELEVENLABS_MODEL`, else `eleven_multilingual_v2`; a per-request `model` still wins. */ private readonly defaultModel; private voicesCache; private static readonly CACHE_TTL_MS; /** * Maximum text length (5000 characters) */ readonly maxTextLength = 5000; constructor(apiKey?: string); /** A trimmed, non-empty environment value, else `undefined`. */ private static envValue; isConfigured(): boolean; getVoices(languageCode?: string): Promise<TTSVoice[]>; synthesize(text: string, options?: TTSOptions): Promise<TTSResult>; /** * Issue the synthesis request, retrying on 429 / 5xx / network failure / * per-attempt timeout up to `retries` more times. A `Retry-After` header * sets the pause before the next attempt (capped); otherwise exponential * backoff from 500 ms. Any other failure — 4xx, or a body that is not audio * — is thrown on the first attempt. * * Total wall-clock is bounded by `timeoutMs × (1 + retries)` plus the pauses * between attempts; `BaseProvider`'s outer synthesis timeout is raised to at * least that figure (see `resolveTTSHandlerBudgetMs`). * * `signal` is the caller's cancellation: once it fires, the attempt in * flight is aborted and no further attempt is made. Without it, a caller * that had already given up (its own timeout, an aborted request) left the * loop to pause and issue a second, billable request nobody consumed. */ private fetchWithRetry; /** * A backoff pause that ends as soon as `signal` fires, so a cancelled * caller never waits it out. The timer is owned here and cleared on the * way out: racing a bare `delay()` against the signal settled the promise * at the abort but left the backoff timer armed for up to `RETRY_AFTER_CAP_MS`, * holding the event loop open after the caller had already been answered. */ private static pause; /** The non-retriable error a caller-cancelled synthesis surfaces as. */ private static cancelledError; /** * One attempt, end to end: the per-attempt timer covers the response * headers AND the audio body, so a stalled or truncated body is a timed-out * attempt the retry loop can resend rather than a hang that only the outer * provider budget ends. The caller's `signal` aborts the same controller, * and an abort it caused is reported as a cancellation, not a timeout. */ private fetchOnce; /** Map an abort from the per-attempt timer to a retriable timeout error. */ private static asAttemptError; private static isRetriableStatus; /** * 429 / 5xx, a per-attempt timeout or broken body (already a retriable * `TTSError`), or a transport failure. Any other throw — a `TypeError` for * an invalid header value or URL, thrown before a request exists — is * permanent and is not retried; it used to be, on the grounds that it was * an `Error`. */ private static isRetriableFailure; /** * A socket-level failure: undici wraps one as `TypeError("fetch failed")` * with the system error on `cause`, and the codes are checked on both. */ private static isTransportError; private static errorCode; /** A BCP-47 tag reduced to its ISO 639-1 primary subtag: `en-US` → `en`, `pt_BR` → `pt`. */ private static toIso639Primary; /** One of `allowed`, else `fallback` with a warning — never an unlisted value in `output_format`. */ private static resolveBitrate; /** `Retry-After` as delta-seconds or an HTTP-date, in milliseconds. */ private static parseRetryAfter; /** A positive finite `timeoutMs`, clamped to the largest delay a timer honours; otherwise the default. */ private static resolveTimeoutMs; private static resolveRetries; private static normalizeBaseUrl; /** * Assemble `voice_settings`. The camelCase options are the documented * surface; `voiceSettings` passes raw fields through and wins on conflict. * `speed` comes from the generic `TTSOptions.speed` (0.25–4.0) clamped into * ElevenLabs' accepted 0.7–1.2 — clamped with a warning rather than * rejected, because the generic validator has already accepted the value. */ private buildVoiceSettings; /** * Map gender string to standard type */ private mapGender; /** * Map TTSAudioFormat to an ElevenLabs `output_format`. * * ElevenLabs has no `ogg_*` family: Ogg/Opus is `opus_48000_<kbps>`. Both * `ogg` and `opus` map there. `wav` maps to raw `pcm_44100` (headerless — * see `effectiveFormat`). Anything else falls back to `mp3_44100_128`. */ private mapFormat; /** * Get sample rate from format string */ private getSampleRate; /** * Map the ElevenLabs `output_format` string back to a canonical * TTSAudioFormat. mapFormat() falls back to mp3_44100_128 for unsupported * inputs, so this is needed to keep TTSResult.format honest. * * NOTE: ElevenLabs `pcm_*` outputs are RAW 16-bit signed-LE PCM samples * with no RIFF/WAV header. We surface that as `pcm16` (which exists in the * `TTSAudioFormat` union exactly for this case) — labeling it as `wav` * would cause consumers writing the buffer to a `.wav` file or feeding it * to a WAV parser to produce unplayable output (CodeRabbit review). */ private effectiveFormat; }