import { SpeechErrorReason } from './types'; /** What the client sends per chunk. */ export interface HostedTtsRequest { text: string; /** BCP-47, e.g. `ta-IN`. The vendor needs the region, not a bare subtag. */ languageCode: string; /** Vendor voice id, e.g. `ta-IN-Chirp3-HD-Kore`. Omitted = server default. */ voiceName?: string; /** Vendor voice tier, so the backend need not infer it from the name. */ tier: string; /** * The same knob the device engine calls `rate`. * * ★ The accepted range depends on which vendor ENDPOINT the proxy calls, not * on the voice tier — see `SPEAKING_RATE_MAX_SYNC` below. Values in use are * 0.9, and 0.85 for Indic scripts and Arabic, comfortably inside every * variant, so this has never affected playback. */ speakingRate?: number; } /** * Google's synthesize response is `{ audioContent: }`. We accept * that shape directly so the proxy can be a thin pass-through, but the proxy is * also free to return raw audio bytes — see `decodeAudioResponse`. */ export interface HostedTtsJsonResponse { audioContent?: string; error?: { message?: string; }; } /** * The accepted speaking-rate bounds, for any caller that clamps. * * ★ The ceiling is a property of the ENDPOINT, not of the voice tier, and the * vendor's own rejection message says both numbers out loud: * * "Unable to adjust speaking rate. Please ensure that speaking_rate is in the * range [0.25, 4.0] for sync synthesis and in the range [0.25, 2.0] for * streaming synthesis." * * Measured against the live API on 2026-08-31 with `en-IN-Chirp3-HD-Puck` and a * fixed sentence, reading the returned LINEAR16 duration rather than trusting * the HTTP status — 4.0 synthesises and is genuinely ~4x faster (21.93s at 0.25 * down to 1.37s at 4.0); 4.1, 5.0 and 0.2 are all rejected with the message * above. * * This corrects a previous edit of this file which narrowed the ceiling to 2.0 * and attributed 4.0 to "the older Standard/WaveNet tiers". That reading was * wrong: 2.0 is the STREAMING limit, and the tier has nothing to do with it. * * `SPEAKING_RATE_MAX` is the sync ceiling because the proxy calls * `v1/text:synthesize`. Moving it to `v1/text:streamingSynthesize` — tempting, * since that is how you cut time-to-first-audio — halves the ceiling, and rates * the client had treated as valid would start returning 400. That switch has to * change this alias in the same commit, which is why both bounds are named * rather than one being a magic number. */ export declare const SPEAKING_RATE_MIN = 0.25; export declare const SPEAKING_RATE_MAX_SYNC = 4; export declare const SPEAKING_RATE_MAX_STREAMING = 2; export declare const SPEAKING_RATE_MAX = 4; /** * Should a failure stop the engine claiming support — for `disableDurationMs`, * not for the rest of the page? * * ★ `not-allowed` has TWO producers and only one of them is terminal: * * - `classifyHttpFailure` maps HTTP 401/403 to it — the proxy refuses us, and * every later request will be refused identically. Latching is right: it * makes `routingSpeechEngine` fall back to device voices instead of * repeating a request that cannot succeed. * - the engine maps the browser's `NotAllowedError` to it — the autoplay * policy declining to play audio that was not started by a user gesture. * That is PER-GESTURE and entirely transient; the very next tap works. * * Latching on the second silently deleted the hosted voice for the whole page * after a single refusal — and auto-speak is precisely the path that triggers * it, because an arriving reply is not a gesture. The user would hear Chirp 3 * once, be refused once, and get device voices for the rest of the session with * nothing explaining why. * * Pure and exported because the engine itself cannot be unit-tested here (no * DOM, no Audio), so the decision is kept where it CAN be. */ export declare function shouldDisableEngine(reason: string | undefined, transient: boolean): boolean; /** * How long a disabled hosted engine stays off before it is tried again. * * ★★ Bounded, not for the rest of the page (BOFF-7283). "Every later request * will be refused identically" is true of a genuinely wrong credential and false * of everything else that surfaces as the same status: * * - the proxy answers 401 when ITS auth check cannot reach the gateway and gets * a JSON error back (a gateway 503 included) — an outage, not a refusal; * - a token that was stale at the moment of the request is fresh a minute * later, because `getHeaders` is read per request; * - a proxy redeploy that briefly 404s comes back on its own. * * A page-lifetime latch turned any of those into device voices (or, before the * fallback was fixed, silence) until the user happened to reload — with nothing * on screen explaining why. The cost of a bound is one failed request per * window while a refusal is real, which is negligible. * * `unsupported` (404/501: no such route) is held longer than `not-allowed`, * because it only recovers on a deploy. */ export declare const HOSTED_DISABLE_MS: Readonly>; export declare function disableDurationMs(reason: string | undefined): number; /** Vendor limit is 5000 bytes per request; stay well inside it. */ export declare const HOSTED_TTS_MAX_CHARS = 2000; export declare function buildRequest(text: string, languageCode: string, tier: string, voiceName?: string | null, speakingRate?: number): HostedTtsRequest; /** * Map a transport failure onto the reasons the UI already knows how to explain. * * `cancelled` is deliberately NOT produced here — an aborted request is the * caller's own doing and the engine reports it, exactly as the browser engine * swallows its own `canceled` utterance errors. */ export declare function classifyHttpFailure(status: number): SpeechErrorReason; /** Base64 → bytes, without assuming a DOM (`atob` is not in node). */ export declare function base64ToBytes(base64: string): Uint8Array; /** * Accept either shape the proxy may return: a JSON body carrying base64 * (the vendor's own shape, so the proxy can pass it straight through) or raw * audio bytes. * * Returns null when the payload carries no audio at all — which must be treated * as a failure, not as silence. A zero-byte "success" that plays nothing is the * dead-button bug this feature already fixed once. */ export declare function decodeAudioResponse(contentType: string | null, body: ArrayBuffer | HostedTtsJsonResponse): Uint8Array | null; /** * Split text the hosted vendor would reject for length. * * `chunkForSpeech` already splits for the browser engine's ~15s utterance * ceiling, and those chunks are normally far under the vendor limit — this is a * backstop for a single pathological sentence, and it splits on whitespace so a * word is never cut in half. */ export declare function splitForVendor(text: string, limit?: number): string[]; //# sourceMappingURL=hostedTtsProtocol.d.ts.map