type BrowserSpeechRecognition = SpeechRecognition & { onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => void) | null; onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => void) | null; onend: ((this: SpeechRecognition, ev: Event) => void) | null; }; type SpeechRecognitionCtor = new () => BrowserSpeechRecognition; declare global { interface Window { webkitSpeechRecognition?: SpeechRecognitionCtor; } } export function isSpeechRecognitionSupported() { return typeof window !== 'undefined' && !!(window.SpeechRecognition || window.webkitSpeechRecognition); } function getSpeechRecognitionCtor(): SpeechRecognitionCtor | null { if (typeof window === 'undefined') return null; return window.SpeechRecognition ?? window.webkitSpeechRecognition ?? null; } export function createLiveSpeechRecognition({ onInterim, onFinal, onError, }: { onInterim: (text: string) => void; onFinal: (text: string) => void; onError: (message: string) => void; }) { const Ctor = getSpeechRecognitionCtor(); if (!Ctor) return null; const recognition = new Ctor(); recognition.lang = 'zh-CN'; recognition.interimResults = true; recognition.continuous = true; recognition.maxAlternatives = 1; let active = false; let restarting = false; let restartTimer: number | null = null; const clearRestartTimer = () => { if (restartTimer != null) { window.clearTimeout(restartTimer); restartTimer = null; } restarting = false; }; recognition.onresult = (event) => { let interim = ''; let finals = ''; for (let i = event.resultIndex; i < event.results.length; i += 1) { const result = event.results[i]; const chunk = result[0]?.transcript ?? ''; if (result.isFinal) finals += chunk; else interim += chunk; } if (finals) onFinal(finals); onInterim(interim); }; recognition.onerror = (event) => { if (event.error === 'aborted' || event.error === 'no-speech') return; if (event.error === 'not-allowed') { onError('请在浏览器设置中允许麦克风'); return; } if (event.error === 'network') { onError('语音识别网络异常,请检查连接'); return; } onError(`语音识别失败:${event.error}`); }; recognition.onend = () => { if (!active || restarting) return; restarting = true; restartTimer = window.setTimeout(() => { restartTimer = null; restarting = false; if (!active) return; try { recognition.start(); } catch { // ignore restart races } }, 120); }; return { start() { clearRestartTimer(); active = true; try { recognition.start(); } catch (err) { active = false; const message = err instanceof Error ? err.message : String(err); if (/already started/i.test(message)) { window.setTimeout(() => { if (!active) return; try { recognition.start(); } catch { onError('无法启动语音识别,请关闭后重试'); } }, 150); return; } onError('无法启动语音识别,请重试'); } }, stop() { active = false; clearRestartTimer(); recognition.stop(); }, abort() { active = false; clearRestartTimer(); recognition.abort(); }, }; }