useVoiceRecognition.js 7.5 KB

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