import { defineWebComponent } from './define'; import { VoiceOutput, type VoiceOutputController } from '../components/voice-output'; interface Props extends Record { /** The utterance to read aloud. */ text?: string; /** Speak automatically when `text` is set/changed. */ autoplay?: boolean; /** * TTS model seam the host supplies — given text, returns an audio `Blob` to * play. This is a **function-valued property** (`el.synthesize = async text => * blob`); when set, the native `speechSynthesis` path is bypassed. Mirrors * ``'s `transcribe`. A value-returning callback can't be * modelled as a fire-and-forget event, hence a property. */ synthesize?: (text: string) => Promise; /** Disable the button (non-interactive). */ disabled?: boolean; } /** Events fired by ``. */ interface Events { /** Playback started or stopped — drive your own UI in sync. Fires on real * transitions only (manual click and programmatic speak()/stop()), never on mount. */ 'kai-speaking-change': { speaking: boolean }; /** The model path (`synthesize`) resolved audio — the raw `Blob` before playback. */ 'kai-synthesized': { blob: Blob }; } /** * `` — a speaker button that reads `text` aloud. Native by * default (`speechSynthesis`); set `el.synthesize` to route through your TTS * model instead. The output sibling of ``. Emits * `kai-speaking-change` and (model path) `kai-synthesized`. */ defineWebComponent('kai-voice-output', { text: '', autoplay: false, synthesize: undefined, disabled: false, }, (props, { dispatch, flag, expose }) => { // Pattern C: the VoiceOutput component owns playback; it hands up a // speak/pause/resume/stop controller. The facade captures it and exposes // delegating methods (manual + programmatic run the same path, so both emit // the same kai-speaking-change / kai-synthesized events). let controller: VoiceOutputController | undefined; expose({ /** Speak the current `text` (native, or via `synthesize` if set). */ speak: () => controller?.speak(), /** Pause playback (resumable). */ pause: () => controller?.pause(), /** Resume paused playback. */ resume: () => controller?.resume(), /** Stop playback and reset. */ stop: () => controller?.stop(), }); return ( props.synthesize!(text) : undefined} onSpeakingChange={(speaking) => dispatch('kai-speaking-change', { speaking })} onSynthesized={(blob) => dispatch('kai-synthesized', { blob })} controllerRef={(c) => (controller = c)} /> ); });