// Web Speech API types (not all browsers have these in their types) export interface SpeechRecognitionResult { readonly length: number; item(index: number): SpeechRecognitionAlternative; [index: number]: SpeechRecognitionAlternative; readonly isFinal: boolean; } export interface SpeechRecognitionAlternative { readonly transcript: string; readonly confidence: number; } export interface SpeechRecognitionResultList { readonly length: number; item(index: number): SpeechRecognitionResult; [index: number]: SpeechRecognitionResult; } export interface SpeechRecognitionEvent extends Event { readonly resultIndex: number; readonly results: SpeechRecognitionResultList; } export interface SpeechRecognitionErrorEvent extends Event { readonly error: string; readonly message: string; } export interface SpeechRecognitionInstance extends EventTarget { continuous: boolean; interimResults: boolean; lang: string; onresult: ((event: SpeechRecognitionEvent) => void) | null; onerror: ((event: SpeechRecognitionErrorEvent) => void) | null; onend: (() => void) | null; start(): void; stop(): void; } interface SpeechRecognitionConstructor { new (): SpeechRecognitionInstance; } declare global { interface Window { SpeechRecognition?: SpeechRecognitionConstructor; webkitSpeechRecognition?: SpeechRecognitionConstructor; } } export interface SpeechDictationOptions { onInterim?: (text: string) => void; onFinal?: (text: string) => void; onError?: (error: string) => void; onStateChange?: (recording: boolean) => void; lang?: string; } export class SpeechDictation { #recognition: SpeechRecognitionInstance | null = null; #recording = false; #opts: SpeechDictationOptions; constructor(opts: SpeechDictationOptions) { this.#opts = opts; this.#init(); } static isSupported(): boolean { return !!(window.SpeechRecognition || window.webkitSpeechRecognition); } get isRecording(): boolean { return this.#recording; } start(): void { if (this.#recording || !this.#recognition) return; try { this.#recognition.start(); this.#recording = true; this.#opts.onStateChange?.(true); } catch (error) { this.#opts.onError?.("Failed to start recording"); } } stop(): void { if (!this.#recording) return; this.#recording = false; this.#recognition?.stop(); this.#opts.onStateChange?.(false); } toggle(): void { if (this.#recording) { this.stop(); } else { this.start(); } } destroy(): void { this.stop(); if (this.#recognition) { this.#recognition.onresult = null; this.#recognition.onerror = null; this.#recognition.onend = null; this.#recognition = null; } } #init(): void { const Impl = window.SpeechRecognition || window.webkitSpeechRecognition; if (!Impl) return; this.#recognition = new Impl(); this.#recognition.continuous = true; this.#recognition.interimResults = true; this.#recognition.lang = this.#opts.lang ?? "en-US"; this.#recognition.onresult = (event) => { for (let i = event.resultIndex; i < event.results.length; i++) { const result = event.results[i]; const text = result[0].transcript; if (result.isFinal) { this.#opts.onFinal?.(text); } else { this.#opts.onInterim?.(text); } } }; this.#recognition.onerror = (event) => { if (event.error !== "no-speech") { this.#opts.onError?.(`Recognition error: ${event.error}`); } }; this.#recognition.onend = () => { // Auto-restart while still recording if (this.#recording && this.#recognition) { this.#recognition.start(); } }; } }