import { FC, ReactNode } from 'react'; import { SpeechEngine, SpeechVoiceLike } from '../speech'; import { VoiceAvailability } from '../voice/speechVoices'; export interface AssistantSpeechContextValue { /** False when the platform has no speech output — hide the control entirely. */ isSupported: boolean; availability: VoiceAvailability; /** * True when nothing can be spoken in the app language, so every control that * starts playback — the per-message button AND the header toggle — must be * inert. `pending` is not blocked: see `speechGate`. */ isBlocked: boolean; /** Explains a disabled control (no installed voice for the app language). */ disabledReason: string | null; /** * Why THIS message cannot be spoken, when the app language is fine but the * reply is not in it — a Tamil answer in an English UI on a device with no * Tamil voice. `isBlocked` cannot see that: it is computed before any reply * exists. Without this the control looks enabled and silently refuses, which * is the dead-button bug all over again (BOFF-7106). */ blockedFor: (messageId: string) => string | null; /** * Every installed voice that can read the language the user speaks, best * first. Empty when there is no choice to offer — the picker hides itself * rather than showing a one-item dropdown. */ voiceChoices: readonly SpeechVoiceLike[]; /** The voice that will actually be used, resolved from the pick + ranking. */ /** * The reply as it is being SPOKEN: every chunk, plus the one sounding now. * * `null` when nothing is playing. `index: -1` means chunked but not yet * audible. Consumers reveal `chunks[0..index]` — anything beyond that is the * caption running ahead of the voice. */ spokenChunks: { chunks: string[]; index: number; } | null; activeVoiceName: string | null; /** * The RAW stored pick: `null` means "use the ranked default". * * The picker must be driven by this rather than by `activeVoiceName`, or the * two states collapse — with nothing stored, the ranked winner is already the * shown option, so choosing it fires no change event and the pick can never * be made. The user's voice would then silently move the next time the device * installs a higher-ranked one. Same trap as the language selector. */ preferredVoiceName: string | null; /** `null` restores the ranked default for the current language. */ setVoice: (voiceName: string | null) => void; /** The message currently being spoken, if any. */ speakingMessageId: string | null; isSpeaking: (messageId: string) => boolean; /** * Speak this message, or stop if it is already the one speaking. * * `turnLocale` is the language the reply was GENERATED under, from the * message's own metadata. It matters because script detection cannot separate * Latin-script languages, so without it an old French reply would be read * with whatever voice the CURRENT preference selects. */ toggle: (messageId: string, markdown: string, turnLocale?: string) => void; stop: () => void; autoSpeakEnabled: boolean; setAutoSpeak: (enabled: boolean) => void; /** * Speak replies for the duration of a live voice session, WITHOUT touching the * user's saved preference. * * ★ Live Voice used to force `autoSpeakEnabled` true and restore it on exit. * That field is PERSISTED (assistantStore partialize), and the restore only * ran on a clean `end()` — so a reload, a closed tab or a backgrounded webview * during a session left auto-speak switched ON for the user permanently, in * normal typed chat, with the toggle showing a state they never chose. The * default was never wrong; the session was overwriting it. * * Session-scoped React state instead: it cannot outlive the page, so there is * nothing to restore and nothing to leak. */ sessionSpeaking: boolean; setSessionSpeaking: (enabled: boolean) => void; /** * Is the live session's MICROPHONE open right now? * * ★ Reported by `useVoiceSession` so the auto-speak subscription below can * refuse to play audio into an open microphone. `sessionSpeaking` cannot * answer this: it is session-scoped — set once on start and cleared on end — * so it stays true for the whole conversation, including the `listening` * phases between turns. * * Write-only by design, and deliberately NOT React state: nothing renders it, * and making it state would re-render every speech consumer on each phase * change AND re-subscribe the store listener mid-turn, which can drop the * in-flight → complete edge the subscription exists to catch. */ setSessionMicOpen: (open: boolean) => void; /** Play a short sample in a named voice. No model call, no message, no tokens. */ previewVoice: (voiceName: string) => void; } export declare function useAssistantSpeech(): AssistantSpeechContextValue; interface Props { children: ReactNode; /** Panel visibility. Hiding the panel stops the voice. */ visible?: boolean; /** * Injectable for tests and local experiments. The default routes between the * device engine and the hosted one; with hosted TTS unconfigured it IS the * device engine, unchanged. */ engine?: SpeechEngine; } export declare const AssistantSpeechProvider: FC; export {}; //# sourceMappingURL=AssistantSpeechProvider.d.ts.map