| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- 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
- }
- }
|