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:
John
2026-06-15 15:04:43 -07:00
commit 2e14873f2d
272 changed files with 64133 additions and 0 deletions
+188
View File
@@ -0,0 +1,188 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { transcribeOneShot } from '../voice/asrTransport';
import { mapMicError } from '../voice/audioAnalyser';
import { MicCapture } from '../voice/micCapture';
import { waitForMicRelease } from '../voice/micSession';
import { createLiveSpeechRecognition, isSpeechRecognitionSupported } from '../voice/speechRecognition';
import type { VoiceSessionPhase } from '../voice/types';
const MIN_RECORD_MS = 500;
export function useVoiceSession({
active,
onError,
}: {
active: boolean;
onError?: (message: string) => void;
}) {
const [phase, setPhase] = useState<VoiceSessionPhase>('idle');
const [text, setText] = useState('');
const [analyser, setAnalyser] = useState<AnalyserNode | null>(null);
const [liveRecognition, setLiveRecognition] = useState(isSpeechRecognitionSupported());
const committedRef = useRef('');
const interimRef = useRef('');
const userEditedRef = useRef(false);
const captureRef = useRef<MicCapture | null>(null);
const recognitionRef = useRef<ReturnType<typeof createLiveSpeechRecognition> | null>(null);
const startedAtRef = useRef(0);
const sessionRef = useRef(0);
const syncDisplayText = useCallback(() => {
if (userEditedRef.current) return;
setText(`${committedRef.current}${interimRef.current}`);
}, []);
const resetSession = useCallback(() => {
committedRef.current = '';
interimRef.current = '';
userEditedRef.current = false;
setText('');
setAnalyser(null);
setPhase('idle');
}, []);
const cleanup = useCallback(async () => {
recognitionRef.current?.abort();
recognitionRef.current = null;
const capture = captureRef.current;
captureRef.current = null;
if (capture) {
await capture.cancel();
}
setAnalyser(null);
}, []);
const stopListening = useCallback(() => {
recognitionRef.current?.stop();
recognitionRef.current = null;
const capture = captureRef.current;
captureRef.current = null;
if (capture) {
void capture.cancel();
}
setAnalyser(null);
setPhase((current) => (current === 'transcribing' ? current : 'ready'));
}, []);
const finishFallbackRecording = useCallback(async () => {
const duration = Date.now() - startedAtRef.current;
const capture = captureRef.current;
captureRef.current = null;
setAnalyser(null);
if (duration < MIN_RECORD_MS) {
if (capture) await capture.cancel();
setPhase('ready');
onError?.('说话时间太短');
return;
}
setPhase('transcribing');
try {
if (!capture) throw new Error('未录制到音频');
const blob = await capture.stop();
if (!blob.size) throw new Error('未录制到音频');
const transcript = await transcribeOneShot(blob);
if (!transcript) {
onError?.('未识别到语音,请重试');
} else {
committedRef.current = transcript;
userEditedRef.current = false;
setText(transcript);
}
} catch (err) {
onError?.(err instanceof Error ? err.message : '识别失败');
} finally {
setPhase('ready');
}
}, [onError]);
useEffect(() => {
if (!active) {
void cleanup().then(resetSession);
return undefined;
}
const sessionId = sessionRef.current + 1;
sessionRef.current = sessionId;
resetSession();
setPhase('requesting');
const start = async () => {
try {
await waitForMicRelease();
if (sessionRef.current !== sessionId) return;
startedAtRef.current = Date.now();
const useLiveSpeech = isSpeechRecognitionSupported();
if (useLiveSpeech) {
const recognition = createLiveSpeechRecognition({
onInterim: (chunk) => {
interimRef.current = chunk;
syncDisplayText();
},
onFinal: (chunk) => {
committedRef.current += chunk;
interimRef.current = '';
syncDisplayText();
},
onError: (message) => onError?.(message),
});
if (!recognition) throw new Error('当前浏览器不支持实时语音识别');
recognitionRef.current = recognition;
recognition.start();
if (sessionRef.current !== sessionId) {
recognition.abort();
return;
}
setLiveRecognition(true);
setAnalyser(null);
setPhase('listening');
return;
}
const capture = new MicCapture();
captureRef.current = capture;
await capture.start();
if (sessionRef.current !== sessionId) {
await capture.cancel();
return;
}
setAnalyser(capture.levelAnalyser);
setLiveRecognition(false);
setPhase('listening');
} catch (err) {
if (sessionRef.current !== sessionId) return;
await cleanup();
setPhase('error');
onError?.(mapMicError(err));
}
};
void start();
return () => {
void cleanup();
};
}, [active, cleanup, onError, resetSession, syncDisplayText]);
const updateText = useCallback((value: string) => {
userEditedRef.current = true;
committedRef.current = value;
interimRef.current = '';
setText(value);
}, []);
return {
phase,
text,
analyser,
liveRecognition,
updateText,
stopListening,
finishFallbackRecording,
resetSession,
};
}