import { Locale } from '../../providers/shell/I18nProvider'; /** * BCP-47 tag we ask the engine for, per app locale. * * Deliberately identical to `useVoiceInput`'s RECOGNITION_LANG: regional choices * follow where HealthyBowl actually operates (India for ta/hi), because a voice's * pronunciation model is region-specific. Typed `Record` so adding a * locale to fe-libs fails the build here rather than silently falling back to * English. */ export declare const SPEECH_LANG: Record; /** * The subset of `SpeechSynthesisVoice` this module needs. Structural on purpose: * `selectVoiceForLocale` returns the caller's own array element, so the browser * engine gets back the real `SpeechSynthesisVoice` instance it must assign to * `utterance.voice`, while tests pass literals. */ export interface SpeechVoiceLike { readonly name: string; readonly lang: string; /** The engine's default voice (usually the OS UI language). */ readonly default?: boolean; /** On-device rather than server-side — no network, no latency, no vendor. */ readonly localService?: boolean; /** * `'hosted'` for a voice our own TTS proxy speaks (`hostedVoiceCatalogue`). * A platform `SpeechSynthesisVoice` never carries it. * * ★ An explicit marker rather than a name heuristic: a hosted voice id such as * `en-IN-Chirp3-HD-Achernar` matches none of the words `voiceQualityRank` * looks for, so ranking by name alone let Edge "Natural", Chrome "Google …" * and Apple "Enhanced" voices outrank it — and the assistant spoke with the * device voice even where hosted speech was configured and working. */ readonly source?: 'hosted'; } /** A voice our hosted TTS proxy speaks, as opposed to one the device provides. */ export declare function isHostedVoice(voice: SpeechVoiceLike): boolean; /** `voiceQualityRank` for every hosted voice — above any sum of device bonuses. */ export declare const HOSTED_VOICE_RANK = 1000; /** `en_GB`, `EN-gb` and `en-GB` are the same tag as far as matching goes. */ export declare function normalizeLangTag(tag: string): string; /** `ta-IN` → `ta`. The part that decides whether a voice is intelligible at all. */ export declare function primaryLanguageSubtag(tag: string): string; export declare function speechLangForLocale(locale: string): string; /** * Pick the best installed voice for an app locale, or `null` when the device has * no voice for that language at all. * * The ladder, in order: * 1. exact tag (`ta-IN` for Tamil); * 2. same language, any region (`en-GB` when only `en-US` was asked for) — * an accent is a far smaller problem than the wrong language; * 3. within whichever tier matched: the engine's default voice, then an * on-device voice, then engine order. * * There is intentionally NO cross-language fallback. Reading a Tamil reply with * an English voice produces confident nonsense; the caller disables the control * and says why instead (see `AssistantSpeechProvider`). */ /** * Every installed voice that can read this language, best first. * * Exposed so the UI can offer a CHOICE. Quality varies enormously between * voices for the same language — Apple's "Enhanced"/"Premium" downloads and * Chrome's network "Google …" voices are markedly better than the compact * on-device defaults — and none of that is machine-readable, so the honest * answer is to rank by the weak signals available and let the listener decide. */ export declare function voicesForLocale(voices: readonly T[], locale: string): T[]; /** * Best-effort quality ordering. Higher is better. * * Every signal here is a heuristic on a NAME, which is fragile and locale- * dependent — hence the voice picker: this decides the DEFAULT, not the answer. * * Deliberately NOT preferring `localService`. That is an offline-capability * flag, not a quality one, and on desktop Chrome it actively selects the * compact on-device voice over the better-sounding network one. The previous * ladder preferred it and was therefore choosing the worse voice. */ export declare function voiceQualityRank(voice: SpeechVoiceLike): number; /** * The voice to read this language with: the user's explicit pick if it is still * installed, otherwise the best-ranked one. * * `preferredName` is matched by name because that is the only identifier stable * across sessions — voice objects are recreated on every page load, and * `voiceURI` is not reliably unique across platforms. */ export declare function selectVoiceForLocale(voices: readonly T[], locale: string, preferredName?: string | null): T | null; /** * What the UI should do with the speak control, given what the engine reports. * * `pending` matters: `speechSynthesis.getVoices()` famously returns `[]` on the * first call in Chrome and only fills in after `voiceschanged`. Treating an * empty list as "no voice" would disable the button for a moment on every load * and, on engines that never fire the event, forever — so an empty list stays * enabled and we let the engine resolve `utterance.lang` itself. */ export type VoiceAvailability = 'unsupported' | 'pending' | 'ready' | 'missing-voice' | 'no-voices'; /** * `probeSettled` is what separates "the list has not arrived yet" from "this * device genuinely has none". * * Both look like `voices.length === 0`, and the difference is not knowable from * the list alone — only the caller, having waited out `VOICE_LIST_TIMEOUT_MS` * and looked again, can tell them apart. Defaulting to `false` keeps the * optimistic reading (`pending`, control stays enabled) for every caller that * has not probed, which is the pre-existing behaviour. * * Without this distinction the empty list stays `pending` forever, `speechGate` * keeps answering `wait-for-voices`, and the button is enabled but does nothing * on every press — a dead control with no explanation, which is exactly how * this feature looked on any Linux box or bare container. */ export declare function voiceAvailability(isSupported: boolean, voices: readonly SpeechVoiceLike[], locale: string, probeSettled?: boolean): VoiceAvailability; /** * Human name for the language we would have spoken, in the reader's own locale * ("Tamil" for an English UI, "தமிழ்" for a Tamil one). * * Used to make the disabled reason actionable: "No Tamil voice is installed" * tells a user what to go and install; "no voice is installed" does not. * `Intl.DisplayNames` is unavailable in some embedded webviews, so the subtag * itself is the fallback — degraded copy beats a thrown render. */ export declare function languageDisplayName(languageTag: string, inLocale: string): string; /** * What a caller that wants to speak RIGHT NOW should do. * * The one decision every playback path has to make, in one place, because * having it in three (the per-message button, the header toggle, the auto-speak * subscription) is how "there is no cross-language fallback" stopped being true * on two of them: an empty or unmatched voice list means the engine picks its * own default, and a Tamil reply comes out in an English voice. * * - `speak` — a voice for this language exists; go. * - `wait-for-voices` — the list has not arrived yet (Chrome's first * `getVoices()` returns `[]`). Speaking now would gamble on the engine * default, and refusing forever would disable the control on engines that * never fire `voiceschanged`; so the caller waits for the list, briefly, and * asks again. The control stays enabled throughout. * - `block` — unsupported, or no voice for this language. Say nothing. */ export type SpeechGate = 'speak' | 'wait-for-voices' | 'block'; export declare function speechGate(availability: VoiceAvailability): SpeechGate; /** * Should the control be visible but inert? * * `pending` deliberately is NOT disabled — see `speechGate`. `no-voices` is, * because by then we have waited and looked again: staying enabled would leave * the user pressing a button that can never do anything. */ export declare function isSpeechBlocked(availability: VoiceAvailability): boolean; /** How long to wait for a late voice list before giving up on one attempt. */ export declare const VOICE_LIST_TIMEOUT_MS = 2000; //# sourceMappingURL=speechVoices.d.ts.map