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>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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();
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user