/** Text-to-Speech abstraction — browser SpeechSynthesis or OpenAI TTS */ /** * How much of a reply is spoken aloud (#179). Was a bare `1500` inside `cleanForSpeech`, * so a user who wanted the whole of a long answer read out had no way to ask for it, and * a user who only ever wants the gist had no way to cut it short. * * The bounds are not taste, they're physics: * - **Floor 200.** Below roughly two sentences the cap truncates a normal reply mid-thought, * which reads as "the voice is broken" rather than "I chose a short cap". Anyone wanting * less than that wants TTS off, not a 20-character cap. * - **Ceiling 4096.** OpenAI's `/v1/audio/speech` REJECTS input longer than 4096 characters, * so a higher setting would silently 400 the request (or, on the browser voice, queue * minutes of speech nobody listens to). Uncapped is the one value that must not exist. */ export declare const DEFAULT_TTS_MAX_CHARS = 1500; export declare const MIN_TTS_MAX_CHARS = 200; export declare const MAX_TTS_MAX_CHARS = 4096; /** * Prepare text for TTS. Two modes: * * - **consumer** (default): strip ALL technical noise — paths, URLs, filenames, code, * hashes — so a plain-speech agent reads a clean human summary. * - **technical** (`opts.technical`): a code explainer's answer is ABOUT the code, so * gutting it to "a file … a file" makes the spoken reply useless. Keep identifiers and * file basenames (drop only the long directory chain), and condense only what is * genuinely unspeakable aloud (fenced code, URLs, git hashes). * * `opts.maxChars` caps the spoken length (default {@link DEFAULT_TTS_MAX_CHARS}); it is * clamped here as well as server-side, because this is the last line before the text * reaches a provider that will reject an over-long body. */ export declare function cleanForSpeech(raw: string, opts?: { technical?: boolean; maxChars?: number; }): string; export interface TtsOptions { apiKey?: string; voice?: string; speed?: number; /** BCP-47 language for the browser voice (Settings → Voice → Language). Without it * SpeechSynthesis picks the system-default (usually English) voice, which mangles * replies in any other language. OpenAI TTS is multilingual and ignores it. */ language?: string; /** Technical agent (code explainer / coding): keep identifiers + file basenames in * spoken output instead of gutting them to "a file". Default false (plain speech). */ technical?: boolean; /** How much of a reply to speak, in characters (#179). Default * {@link DEFAULT_TTS_MAX_CHARS}; clamped to 200–4096 by `cleanForSpeech`. */ maxChars?: number; } /** * Exported for unit tests only — do not call from outside tts.ts. * * Wraps an `AudioBufferSourceNode` playback in a promise that ALWAYS resolves, even when: * - `onerror` fires instead of `onended` (the node failed to play back the audio), or * - the node stalls and neither callback ever fires (the "speaking · tap to stop" hang). * * The watchdog fires after `audioDurationS * 1000 + PLAYBACK_WATCHDOG_SLACK_MS`, which gives a * generous buffer beyond the clip's own duration without letting a stalled node wedge hands-free * indefinitely. * * `AudioBufferSourceNode.onerror` is not in the TypeScript DOM types (it is a runtime-only * property on some implementations), so the parameter is typed as a minimal interface rather * than `Pick`. */ export declare const PLAYBACK_WATCHDOG_SLACK_MS = 5000; export interface AudioSourceLike { onended: ((ev: Event) => void) | null; onerror: ((ev: Event) => void) | null; start(): void; } export declare function playAudioBufferSource(source: AudioSourceLike, audioDurationS: number): Promise; export declare class VoiceTts { provider: string; apiKey: string; voice: string; speed: number; language: string; technical: boolean; maxChars: number; speaking: boolean; private _queue; private _processing; private _currentSource; private _gen; constructor(provider: string, opts?: TtsOptions); /** Stop every OTHER live instance so this one is the single audible voice (#127). */ private _silenceOthers; /** * Queue a message to speak. A message that arrives while one is already * playing WAITS its turn — speech never overlaps. cancel() stops the current * utterance and drops anything queued behind it. */ speak(text: string, opts?: { lang?: string; }): Promise; /** * Prime audio output from INSIDE a user gesture (mic toggle, double-tap) so a * LATER async reply can actually play. iOS/Safari refuse to start a Web Audio * context or the SpeechSynthesis queue outside a gesture — without this the * OpenAI TTS context stays "suspended" (silent) and the browser voice never * un-pauses, which is the "hands-free makes no sound" symptom. Best-effort. */ unlock(): Promise; cancel(): void; /** Teardown (unmount): stop this instance's speech and leave the roster. The Web Audio * context is deliberately NOT closed — it is shared page-wide (see sharedAudioCtx), so * there is nothing per-instance left to leak, and closing it here is what used to throw * away the iOS gesture priming on every navigation. */ dispose(): void; private _speakBrowser; private _speakOpenAI; } //# sourceMappingURL=tts.d.ts.map