useVoiceRecognition.js 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. import { ref, onUnmounted } from 'vue'
  2. import CryptoJS from 'crypto-js'
  3. // 科大讯飞配置(从后端获取)
  4. let XFYUN_CONFIG = {
  5. APPID: '',
  6. ACCESS_KEY_ID: '',
  7. ACCESS_KEY_SECRET: '',
  8. API_URL: 'wss://office-api-ast-dx.iflyaisol.com/ast/communicate/v1'
  9. }
  10. // 从后端获取配置
  11. const loadConfig = async () => {
  12. try {
  13. const token = localStorage.getItem('talk_token')
  14. const response = await fetch('http://localhost:8080/talk/config/xunfei', {
  15. headers: {
  16. 'Authorization': token ? `Bearer ${token}` : '',
  17. 'clientid': 'talk-web'
  18. }
  19. })
  20. const config = await response.json()
  21. XFYUN_CONFIG.APPID = config.appId || ''
  22. XFYUN_CONFIG.ACCESS_KEY_ID = config.apiKey || ''
  23. XFYUN_CONFIG.ACCESS_KEY_SECRET = config.apiSecret || ''
  24. } catch (error) {
  25. console.error('加载讯飞配置失败:', error)
  26. }
  27. }
  28. export function useVoiceRecognition() {
  29. const isRecording = ref(false)
  30. const currentTranscription = ref('')
  31. const tempTranscription = ref('')
  32. let websocket = null
  33. let mediaRecorder = null
  34. let audioContext = null
  35. let sessionId = ''
  36. // 生成 UUID
  37. const generateUUID = () => {
  38. return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
  39. const r = Math.random() * 16 | 0
  40. const v = c === 'x' ? r : (r & 0x3 | 0x8)
  41. return v.toString(16)
  42. })
  43. }
  44. // 生成 WebSocket URL
  45. const getWebSocketUrl = async () => {
  46. // 确保配置已加载
  47. if (!XFYUN_CONFIG.APPID) {
  48. await loadConfig()
  49. }
  50. const appId = XFYUN_CONFIG.APPID
  51. const accessKeyId = XFYUN_CONFIG.ACCESS_KEY_ID
  52. const accessKeySecret = XFYUN_CONFIG.ACCESS_KEY_SECRET
  53. const uuid = generateUUID()
  54. const now = new Date()
  55. const year = now.getFullYear()
  56. const month = String(now.getMonth() + 1).padStart(2, '0')
  57. const day = String(now.getDate()).padStart(2, '0')
  58. const hour = String(now.getHours()).padStart(2, '0')
  59. const minute = String(now.getMinutes()).padStart(2, '0')
  60. const second = String(now.getSeconds()).padStart(2, '0')
  61. const utc = `${year}-${month}-${day}T${hour}:${minute}:${second}+0800`
  62. const params = {
  63. accessKeyId: accessKeyId,
  64. appId: appId,
  65. uuid: uuid,
  66. utc: utc,
  67. audio_encode: 'pcm_s16le',
  68. lang: 'autodialect',
  69. samplerate: '16000'
  70. }
  71. const sortedKeys = Object.keys(params).sort()
  72. const baseString = sortedKeys
  73. .map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
  74. .join('&')
  75. const signatureHash = CryptoJS.HmacSHA1(baseString, accessKeySecret)
  76. const signature = CryptoJS.enc.Base64.stringify(signatureHash)
  77. return `${XFYUN_CONFIG.API_URL}?${baseString}&signature=${encodeURIComponent(signature)}`
  78. }
  79. // 连接 WebSocket
  80. const connectWebSocket = async () => {
  81. return new Promise(async (resolve, reject) => {
  82. const wsUrl = await getWebSocketUrl()
  83. console.log('连接 WebSocket:', wsUrl)
  84. websocket = new WebSocket(wsUrl)
  85. websocket.onopen = () => {
  86. console.log('WebSocket 连接成功')
  87. resolve()
  88. }
  89. websocket.onmessage = (event) => {
  90. try {
  91. const data = JSON.parse(event.data)
  92. console.log('收到消息:', data)
  93. if (data.msg_type === 'action' && data.data && data.data.action === 'started') {
  94. sessionId = data.data.sessionId || generateUUID()
  95. console.log('握手成功, sessionId:', sessionId)
  96. return
  97. }
  98. if (data.msg_type === 'error' || data.action === 'error') {
  99. console.error('转写错误:', data.desc || data.message)
  100. return
  101. }
  102. if (data.msg_type === 'result' && data.res_type === 'asr' && data.data) {
  103. const result = data.data
  104. if (result.cn && result.cn.st && result.cn.st.rt) {
  105. let transcriptText = ''
  106. result.cn.st.rt.forEach(rt => {
  107. if (rt.ws) {
  108. rt.ws.forEach(ws => {
  109. if (ws.cw) {
  110. ws.cw.forEach(cw => {
  111. transcriptText += cw.w
  112. })
  113. }
  114. })
  115. }
  116. })
  117. if (transcriptText) {
  118. // 去掉开头的标点符号
  119. transcriptText = transcriptText.replace(/^[,。!?、;:""''()《》【】…—\s]+/, '')
  120. if (transcriptText) {
  121. const type = result.cn.st.type
  122. if (type === '0') {
  123. currentTranscription.value += transcriptText
  124. tempTranscription.value = ''
  125. console.log('确定性结果:', transcriptText)
  126. } else {
  127. tempTranscription.value = transcriptText
  128. console.log('中间结果:', transcriptText)
  129. }
  130. }
  131. }
  132. }
  133. }
  134. } catch (error) {
  135. console.error('解析消息失败:', error)
  136. }
  137. }
  138. websocket.onerror = (error) => {
  139. console.error('WebSocket 错误:', error)
  140. reject(error)
  141. }
  142. websocket.onclose = () => {
  143. console.log('WebSocket 关闭')
  144. }
  145. })
  146. }
  147. // 发送音频数据
  148. const sendAudioData = (audioData) => {
  149. if (websocket && websocket.readyState === WebSocket.OPEN) {
  150. websocket.send(audioData)
  151. }
  152. }
  153. // 关闭 WebSocket
  154. const closeWebSocket = () => {
  155. if (websocket && websocket.readyState === WebSocket.OPEN) {
  156. const endMessage = {
  157. end: true,
  158. sessionId: sessionId || generateUUID()
  159. }
  160. websocket.send(JSON.stringify(endMessage))
  161. setTimeout(() => {
  162. if (websocket) {
  163. websocket.close()
  164. websocket = null
  165. }
  166. }, 1000)
  167. }
  168. }
  169. // 开始录音
  170. const startRecording = async () => {
  171. try {
  172. const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
  173. // 连接 WebSocket
  174. await connectWebSocket()
  175. // 创建音频上下文
  176. audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000 })
  177. const source = audioContext.createMediaStreamSource(stream)
  178. const processor = audioContext.createScriptProcessor(4096, 1, 1)
  179. source.connect(processor)
  180. processor.connect(audioContext.destination)
  181. processor.onaudioprocess = (e) => {
  182. const inputData = e.inputBuffer.getChannelData(0)
  183. const pcmData = new Int16Array(inputData.length)
  184. for (let i = 0; i < inputData.length; i++) {
  185. pcmData[i] = Math.max(-32768, Math.min(32767, inputData[i] * 32768))
  186. }
  187. sendAudioData(pcmData.buffer)
  188. }
  189. isRecording.value = true
  190. console.log('开始录音')
  191. } catch (error) {
  192. console.error('启动录音失败:', error)
  193. throw error
  194. }
  195. }
  196. // 停止录音
  197. const stopRecording = () => {
  198. if (audioContext) {
  199. audioContext.close()
  200. audioContext = null
  201. }
  202. closeWebSocket()
  203. isRecording.value = false
  204. console.log('停止录音')
  205. }
  206. // 清理资源
  207. onUnmounted(() => {
  208. if (isRecording.value) {
  209. stopRecording()
  210. }
  211. })
  212. return {
  213. isRecording,
  214. currentTranscription,
  215. tempTranscription,
  216. startRecording,
  217. stopRecording
  218. }
  219. }