fix: 讯飞语音识别文字覆盖+麦克风未释放

- 使用setChatInput(prev => prev + seg)函数式更新,彻底修复文字覆盖问题
- 提取stopMicrophone函数,在手动停止/VAD自动挂断/报错/关闭时统一释放物理麦克风
This commit is contained in:
2026-04-20 02:10:38 +08:00
parent 75e4e56cb3
commit 9aec836e93

View File

@@ -923,31 +923,43 @@ export default function ReportEditor() {
} }
const toggleListening = async () => { const toggleListening = async () => {
// 专门提取一个彻底关闭物理麦克风的函数
const stopMicrophone = () => {
if (xfAudioContextRef.current) {
try { xfAudioContextRef.current.close(); } catch {}
xfAudioContextRef.current = null;
}
if (xfMediaStreamRef.current) {
xfMediaStreamRef.current.getTracks().forEach(t => t.stop());
xfMediaStreamRef.current = null;
}
};
if (isListening) { if (isListening) {
setIsListening(false); setIsListening(false);
// 1. 发送结束帧告诉服务器录音结束 stopMicrophone();
if (xfWsRef.current && xfWsRef.current.readyState === WebSocket.OPEN) { if (xfWsRef.current && xfWsRef.current.readyState === WebSocket.OPEN) {
try { try {
const endFrame = { data: { status: 2, format: 'audio/L16;rate=16000', encoding: 'raw', audio: '' } }; const endFrame = { data: { status: 2, format: 'audio/L16;rate=16000', encoding: 'raw', audio: '' } };
xfWsRef.current.send(JSON.stringify(endFrame)); xfWsRef.current.send(JSON.stringify(endFrame));
} catch {} } catch {}
} }
// 2. 仅关闭本地麦克风收音,保留 WebSocket 等待服务器返回最终结果
if (xfAudioContextRef.current) { try { xfAudioContextRef.current.close(); } catch {} xfAudioContextRef.current = null; }
if (xfMediaStreamRef.current) { xfMediaStreamRef.current.getTracks().forEach(t => t.stop()); xfMediaStreamRef.current = null; }
return; return;
} }
const xfConfig = storage.get<SystemSettings>('systemSettings', {} as SystemSettings).xfSpeechConfig || DEFAULT_XF_SPEECH; const xfConfig = storage.get<SystemSettings>('systemSettings', {} as SystemSettings).xfSpeechConfig || DEFAULT_XF_SPEECH;
if (!xfConfig?.appId || !xfConfig?.apiKey || !xfConfig?.apiSecret) { if (!xfConfig?.appId || !xfConfig?.apiKey || !xfConfig?.apiSecret) {
alert('请先在系统设置中配置讯飞语音 APPID/APIKey/APISecret'); alert('请先在系统设置中配置讯飞语音 APPID/APIKey/APISecret');
return; return;
} }
try { try {
const authUrl = await getXfAuthUrl(xfConfig.apiKey, xfConfig.apiSecret); const authUrl = await getXfAuthUrl(xfConfig.apiKey, xfConfig.apiSecret);
const ws = new WebSocket(authUrl); const ws = new WebSocket(authUrl);
xfWsRef.current = ws; xfWsRef.current = ws;
let frameStatus = 0; let frameStatus = 0;
let transcript = chatInput;
ws.onopen = async () => { ws.onopen = async () => {
try { try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
@@ -956,8 +968,9 @@ export default function ReportEditor() {
xfAudioContextRef.current = audioContext; xfAudioContextRef.current = audioContext;
const source = audioContext.createMediaStreamSource(stream); const source = audioContext.createMediaStreamSource(stream);
const processor = audioContext.createScriptProcessor(4096, 1, 1); const processor = audioContext.createScriptProcessor(4096, 1, 1);
processor.onaudioprocess = (e) => { processor.onaudioprocess = (e) => {
if (ws.readyState !== WebSocket.OPEN) return; if (ws.readyState !== WebSocket.OPEN || !xfAudioContextRef.current) return;
const inputData = e.inputBuffer.getChannelData(0); const inputData = e.inputBuffer.getChannelData(0);
const pcmBuffer = floatTo16BitPCM(inputData); const pcmBuffer = floatTo16BitPCM(inputData);
const base64Audio = arrayBufferToBase64(pcmBuffer); const base64Audio = arrayBufferToBase64(pcmBuffer);
@@ -969,6 +982,7 @@ export default function ReportEditor() {
ws.send(JSON.stringify(frame)); ws.send(JSON.stringify(frame));
frameStatus = 1; frameStatus = 1;
}; };
source.connect(processor); source.connect(processor);
processor.connect(audioContext.destination); processor.connect(audioContext.destination);
setIsListening(true); setIsListening(true);
@@ -978,31 +992,35 @@ export default function ReportEditor() {
ws.close(); ws.close();
} }
}; };
ws.onmessage = (event) => { ws.onmessage = (event) => {
try { try {
const jsonData = JSON.parse(event.data); const jsonData = JSON.parse(event.data);
if (jsonData.code !== 0 && jsonData.code !== undefined) { if (jsonData.code !== 0 && jsonData.code !== undefined) {
alert(`讯飞语音报错: ${jsonData.message || '未知错误'} (错误码: ${jsonData.code})`); alert(`讯飞语音报错: ${jsonData.message || '未知错误'} (错误码: ${jsonData.code})`);
setIsListening(false); setIsListening(false);
stopMicrophone();
ws.close(); ws.close();
return; return;
} }
if (jsonData.data?.result?.ws) { if (jsonData.data?.result?.ws) {
let seg = ''; let seg = '';
for (const w of jsonData.data.result.ws) { if (w.cw?.[0]?.w) seg += w.cw[0].w; } for (const w of jsonData.data.result.ws) { if (w.cw?.[0]?.w) seg += w.cw[0].w; }
if (jsonData.data.result.ls) { transcript += seg; setChatInput(transcript); } if (seg) {
else { setChatInput(transcript + seg); } setChatInput(prev => prev + seg);
}
} }
// 当接收到服务端的最终状态码 status === 2 时,才彻底断开 websocket
if (jsonData.data?.status === 2) { if (jsonData.data?.status === 2) {
ws.close(); ws.close();
xfWsRef.current = null; xfWsRef.current = null;
setIsListening(false); setIsListening(false);
stopMicrophone();
} }
} catch {} } catch {}
}; };
ws.onerror = () => { alert('讯飞语音连接失败'); setIsListening(false); };
ws.onclose = () => { setIsListening(false); }; ws.onerror = () => { alert('讯飞语音连接失败'); setIsListening(false); stopMicrophone(); };
ws.onclose = () => { setIsListening(false); stopMicrophone(); };
} catch (e: any) { } catch (e: any) {
alert('讯飞语音初始化失败: ' + e.message); alert('讯飞语音初始化失败: ' + e.message);
} }