import { ref, onUnmounted } from 'vue' import CryptoJS from 'crypto-js' // 科大讯飞配置(从后端获取) let XFYUN_CONFIG = { APPID: '', ACCESS_KEY_ID: '', ACCESS_KEY_SECRET: '', API_URL: 'wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1' } // 从后端获取配置 const loadConfig = async () => { try { const token = localStorage.getItem('talk_token') const response = await fetch('http://localhost:8080/talk/config/xunfei', { headers: { 'Authorization': token ? `Bearer ${token}` : '', 'clientid': 'talk-web' } }) const config = await response.json() XFYUN_CONFIG.APPID = config.appId || '' XFYUN_CONFIG.ACCESS_KEY_ID = config.apiKey || '' XFYUN_CONFIG.ACCESS_KEY_SECRET = config.apiSecret || '' } catch (error) { console.error('加载讯飞配置失败:', error) } } export function useVoiceRecognition() { const isRecording = ref(false) const currentTranscription = ref('') const tempTranscription = ref('') let websocket = null let mediaRecorder = null let audioContext = null let sessionId = '' // 生成 UUID const generateUUID = () => { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { const r = Math.random() * 16 | 0 const v = c === 'x' ? r : (r & 0x3 | 0x8) return v.toString(16) }) } // 生成 WebSocket URL const getWebSocketUrl = async () => { // 确保配置已加载 if (!XFYUN_CONFIG.APPID) { await loadConfig() } const appId = XFYUN_CONFIG.APPID const accessKeyId = XFYUN_CONFIG.ACCESS_KEY_ID const accessKeySecret = XFYUN_CONFIG.ACCESS_KEY_SECRET const uuid = generateUUID() const now = new Date() const year = now.getFullYear() const month = String(now.getMonth() + 1).padStart(2, '0') const day = String(now.getDate()).padStart(2, '0') const hour = String(now.getHours()).padStart(2, '0') const minute = String(now.getMinutes()).padStart(2, '0') const second = String(now.getSeconds()).padStart(2, '0') const utc = `${year}-${month}-${day}T${hour}:${minute}:${second}+0800` const params = { accessKeyId: accessKeyId, appId: appId, uuid: uuid, utc: utc, audio_encode: 'pcm_s16le', lang: 'autodialect', samplerate: '16000' } const sortedKeys = Object.keys(params).sort() const baseString = sortedKeys .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`) .join('&') const signatureHash = CryptoJS.HmacSHA1(baseString, accessKeySecret) const signature = CryptoJS.enc.Base64.stringify(signatureHash) return `${XFYUN_CONFIG.API_URL}?${baseString}&signature=${encodeURIComponent(signature)}` } // 连接 WebSocket const connectWebSocket = async () => { return new Promise(async (resolve, reject) => { const wsUrl = await getWebSocketUrl() console.log('连接 WebSocket:', wsUrl) websocket = new WebSocket(wsUrl) websocket.onopen = () => { console.log('WebSocket 连接成功') resolve() } websocket.onmessage = (event) => { try { const data = JSON.parse(event.data) console.log('收到消息:', data) if (data.msg_type === 'action' && data.data && data.data.action === 'started') { sessionId = data.data.sessionId || generateUUID() console.log('握手成功, sessionId:', sessionId) return } if (data.msg_type === 'error' || data.action === 'error') { console.error('转写错误:', data.desc || data.message) return } if (data.msg_type === 'result' && data.res_type === 'asr' && data.data) { const result = data.data if (result.cn && result.cn.st && result.cn.st.rt) { let transcriptText = '' result.cn.st.rt.forEach(rt => { if (rt.ws) { rt.ws.forEach(ws => { if (ws.cw) { ws.cw.forEach(cw => { transcriptText += cw.w }) } }) } }) if (transcriptText) { // 去掉开头的标点符号 transcriptText = transcriptText.replace(/^[,。!?、;:""''()《》【】…—\s]+/, '') if (transcriptText) { const type = result.cn.st.type if (type === '0') { currentTranscription.value += transcriptText tempTranscription.value = '' console.log('确定性结果:', transcriptText) } else { tempTranscription.value = transcriptText console.log('中间结果:', transcriptText) } } } } } } catch (error) { console.error('解析消息失败:', error) } } websocket.onerror = (error) => { console.error('WebSocket 错误:', error) reject(error) } websocket.onclose = () => { console.log('WebSocket 关闭') } }) } // 发送音频数据 const sendAudioData = (audioData) => { if (websocket && websocket.readyState === WebSocket.OPEN) { websocket.send(audioData) } } // 关闭 WebSocket const closeWebSocket = () => { if (websocket && websocket.readyState === WebSocket.OPEN) { const endMessage = { end: true, sessionId: sessionId || generateUUID() } websocket.send(JSON.stringify(endMessage)) setTimeout(() => { if (websocket) { websocket.close() websocket = null } }, 1000) } } // 开始录音 const startRecording = async () => { try { const stream = await navigator.mediaDevices.getUserMedia({ audio: true }) // 连接 WebSocket await connectWebSocket() // 创建音频上下文 audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 }) const source = audioContext.createMediaStreamSource(stream) const processor = audioContext.createScriptProcessor(4096, 1, 1) source.connect(processor) processor.connect(audioContext.destination) processor.onaudioprocess = (e) => { const inputData = e.inputBuffer.getChannelData(0) const pcmData = new Int16Array(inputData.length) for (let i = 0; i < inputData.length; i++) { pcmData[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32768)) } sendAudioData(pcmData.buffer) } isRecording.value = true console.log('开始录音') } catch (error) { console.error('启动录音失败:', error) throw error } } // 停止录音 const stopRecording = () => { if (audioContext) { audioContext.close() audioContext = null } closeWebSocket() isRecording.value = false console.log('停止录音') } // 清理资源 onUnmounted(() => { if (isRecording.value) { stopRecording() } }) return { isRecording, currentTranscription, tempTranscription, startRecording, stopRecording } }