Files
memind/src/voice/speechRecognition.ts
T
John 2e14873f2d Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:04:43 -07:00

130 lines
3.4 KiB
TypeScript

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();
},
};
}