Vue实现AI问答小助手(3):录音与语音转文字全流程解析

Vue实现AI问答小助手(3):录音与语音转文字全流程解析

一、录音功能实现基础

1.1 浏览器录音API原理

现代浏览器通过MediaRecorder API实现音频采集,该接口基于WebRTC技术栈,支持PCM、Opus等编码格式。录音流程分为三个阶段:

  • 权限申请:通过navigator.mediaDevices.getUserMedia({ audio: true })获取麦克风访问权限
  • 媒体流处理:创建MediaStream对象并绑定到MediaRecorder实例
  • 数据采集:监听dataavailable事件获取音频Blob数据
  1. // 基础录音控制器
  2. class AudioRecorder {
  3. constructor() {
  4. this.mediaRecorder = null
  5. this.audioChunks = []
  6. this.isRecording = false
  7. }
  8. async startRecording() {
  9. try {
  10. const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
  11. this.mediaRecorder = new MediaRecorder(stream, {
  12. mimeType: 'audio/webm',
  13. audioBitsPerSecond: 128000
  14. })
  15. this.mediaRecorder.ondataavailable = (e) => {
  16. this.audioChunks.push(e.data)
  17. }
  18. this.mediaRecorder.start(100) // 每100ms触发一次dataavailable
  19. this.isRecording = true
  20. } catch (err) {
  21. console.error('录音启动失败:', err)
  22. throw err
  23. }
  24. }
  25. }

1.2 权限管理最佳实践

  • 动态权限申请:在用户点击录音按钮时触发权限请求,避免页面加载时弹出干扰
  • 权限状态检测:通过navigator.permissions.query({ name: 'microphone' })预检权限状态
  • 错误降级方案:当权限被拒绝时,提供文本输入的替代方案
  1. <template>
  2. <div>
  3. <button @click="handleRecordClick" :disabled="isProcessing">
  4. {{ isRecording ? '停止录音' : '开始录音' }}
  5. </button>
  6. <div v-if="permissionDenied" class="error-tip">
  7. 麦克风访问已被拒绝,请通过浏览器设置重新授权
  8. </div>
  9. </div>
  10. </template>
  11. <script setup>
  12. import { ref } from 'vue'
  13. const isRecording = ref(false)
  14. const permissionDenied = ref(false)
  15. const isProcessing = ref(false)
  16. const handleRecordClick = async () => {
  17. if (isRecording.value) {
  18. // 停止录音逻辑
  19. return
  20. }
  21. try {
  22. const permissionStatus = await navigator.permissions.query({ name: 'microphone' })
  23. if (permissionStatus.state === 'denied') {
  24. permissionDenied.value = true
  25. return
  26. }
  27. // 继续录音流程
  28. } catch (err) {
  29. console.warn('权限检测失败:', err)
  30. }
  31. }
  32. </script>

二、语音转文字服务集成

2.1 服务选型对比

技术方案 优势 局限性
Web Speech API 浏览器原生支持,无需后端 仅支持部分语言,准确率有限
云端ASR服务 高准确率,支持多语言 需处理网络延迟与费用问题
本地模型部署 离线可用,数据隐私可控 硬件要求高,模型体积大

2.2 云端ASR服务实现(以WebSocket为例)

  1. // 语音转文字服务封装
  2. class SpeechToText {
  3. constructor(apiKey, endpoint) {
  4. this.apiKey = apiKey
  5. this.endpoint = endpoint
  6. this.socket = null
  7. }
  8. async connect(audioChunks) {
  9. return new Promise((resolve, reject) => {
  10. this.socket = new WebSocket(this.endpoint)
  11. this.socket.onopen = () => {
  12. const authHeader = `Bearer ${this.apiKey}`
  13. this.socket.send(JSON.stringify({
  14. type: 'auth',
  15. data: { authorization: authHeader }
  16. }))
  17. // 分块发送音频数据
  18. audioChunks.forEach(chunk => {
  19. this.socket.send(chunk)
  20. })
  21. }
  22. let fullText = ''
  23. this.socket.onmessage = (event) => {
  24. const data = JSON.parse(event.data)
  25. if (data.type === 'final_result') {
  26. fullText += data.text
  27. resolve(fullText)
  28. } else if (data.type === 'partial') {
  29. // 实时显示中间结果(可选)
  30. }
  31. }
  32. this.socket.onerror = (err) => reject(err)
  33. })
  34. }
  35. }

2.3 错误处理机制

  • 网络异常:设置重试次数与超时时间
  • 音频格式错误:验证Blob的mimeType是否符合服务要求
  • 服务端错误:解析错误响应并显示友好提示
  1. async function transcribeAudio(audioBlob) {
  2. const MAX_RETRIES = 3
  3. let retryCount = 0
  4. while (retryCount < MAX_RETRIES) {
  5. try {
  6. const formData = new FormData()
  7. formData.append('audio', audioBlob, 'recording.webm')
  8. const response = await fetch('/api/asr', {
  9. method: 'POST',
  10. body: formData,
  11. headers: {
  12. 'Authorization': `Bearer ${API_KEY}`
  13. }
  14. })
  15. if (!response.ok) throw new Error(`HTTP错误: ${response.status}`)
  16. return await response.json()
  17. } catch (err) {
  18. retryCount++
  19. if (retryCount === MAX_RETRIES) {
  20. throw new Error('语音识别服务多次尝试失败')
  21. }
  22. await new Promise(res => setTimeout(res, 1000 * retryCount))
  23. }
  24. }
  25. }

三、Vue组件实现方案

3.1 完整组件示例

  1. <template>
  2. <div class="voice-assistant">
  3. <div class="control-panel">
  4. <button
  5. @click="toggleRecording"
  6. :class="{ 'recording': isRecording }"
  7. >
  8. <icon-mic v-if="!isRecording" />
  9. <icon-stop v-else />
  10. </button>
  11. <div class="status">{{ statusText }}</div>
  12. </div>
  13. <div class="transcript-area">
  14. <div v-for="(line, index) in transcriptLines" :key="index" class="transcript-line">
  15. {{ line }}
  16. </div>
  17. </div>
  18. <div v-if="error" class="error-message">
  19. {{ error }}
  20. </div>
  21. </div>
  22. </template>
  23. <script setup>
  24. import { ref, onBeforeUnmount } from 'vue'
  25. import { useToast } from 'vue-toastification'
  26. const isRecording = ref(false)
  27. const statusText = ref('准备就绪')
  28. const transcriptLines = ref([])
  29. const error = ref(null)
  30. let mediaRecorder = null
  31. let audioChunks = []
  32. let sttService = null
  33. // 初始化语音转文字服务
  34. onMounted(() => {
  35. sttService = new SpeechToText(
  36. import.meta.env.VITE_ASR_API_KEY,
  37. import.meta.env.VITE_ASR_WS_ENDPOINT
  38. )
  39. })
  40. const toggleRecording = async () => {
  41. if (isRecording.value) {
  42. await stopRecording()
  43. } else {
  44. await startRecording()
  45. }
  46. }
  47. const startRecording = async () => {
  48. try {
  49. const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
  50. mediaRecorder = new MediaRecorder(stream, {
  51. mimeType: 'audio/webm',
  52. audioBitsPerSecond: 16000
  53. })
  54. audioChunks = []
  55. mediaRecorder.ondataavailable = (e) => {
  56. audioChunks.push(e.data)
  57. }
  58. mediaRecorder.onstop = async () => {
  59. statusText.value = '语音识别中...'
  60. const audioBlob = new Blob(audioChunks, { type: 'audio/webm' })
  61. try {
  62. const result = await sttService.recognize(audioBlob)
  63. transcriptLines.value.push(result.text)
  64. statusText.value = '识别完成'
  65. } catch (err) {
  66. error.value = `识别失败: ${err.message}`
  67. useToast().error('语音识别服务异常')
  68. }
  69. }
  70. mediaRecorder.start(100)
  71. isRecording.value = true
  72. statusText.value = '录音中...'
  73. } catch (err) {
  74. error.value = `录音启动失败: ${err.message}`
  75. useToast().error('无法访问麦克风')
  76. }
  77. }
  78. const stopRecording = () => {
  79. if (mediaRecorder && isRecording.value) {
  80. mediaRecorder.stop()
  81. mediaRecorder.stream.getTracks().forEach(track => track.stop())
  82. isRecording.value = false
  83. }
  84. }
  85. onBeforeUnmount(() => {
  86. stopRecording()
  87. })
  88. </script>
  89. <style scoped>
  90. .voice-assistant {
  91. max-width: 600px;
  92. margin: 0 auto;
  93. padding: 20px;
  94. }
  95. .control-panel {
  96. display: flex;
  97. align-items: center;
  98. gap: 15px;
  99. margin-bottom: 20px;
  100. }
  101. button {
  102. width: 60px;
  103. height: 60px;
  104. border-radius: 50%;
  105. border: none;
  106. background: #4CAF50;
  107. color: white;
  108. cursor: pointer;
  109. }
  110. button.recording {
  111. background: #F44336;
  112. }
  113. .transcript-area {
  114. min-height: 200px;
  115. border: 1px solid #eee;
  116. padding: 15px;
  117. border-radius: 8px;
  118. }
  119. .transcript-line {
  120. margin-bottom: 10px;
  121. padding-bottom: 10px;
  122. border-bottom: 1px dashed #eee;
  123. }
  124. </style>

四、性能优化策略

4.1 音频处理优化

  • 采样率标准化:统一转换为16kHz采样率(多数ASR服务推荐)
  • 音频压缩:使用Opus编码减少数据量
  • 分块传输:控制每个数据包大小在50-100KB范围内

4.2 用户体验优化

  • 实时反馈:显示音量波形图增强交互感
  • 断点续传:网络中断后恢复连接时继续传输
  • 多语言支持:根据用户设置自动切换识别语言

五、安全与隐私考虑

  1. 数据加密:传输过程使用TLS 1.2+加密
  2. 本地处理选项:提供WebAssembly实现的本地识别方案
  3. 隐私政策声明:明确告知用户音频数据的处理方式
  4. 临时存储:录音文件在识别完成后自动删除

六、部署与监控

  1. 服务监控:设置ASR服务的调用成功率、响应时间等指标
  2. 降级策略:当云端服务不可用时自动切换到备用方案
  3. 日志记录:记录关键错误信息用于问题排查
  4. A/B测试:对比不同ASR服务的准确率和用户体验

通过以上实现方案,开发者可以构建一个完整的语音交互模块,该方案兼顾了功能实现与用户体验,同时考虑了实际部署中的各种边界情况。在实际开发中,建议先实现核心录音功能,再逐步集成语音转文字服务,最后进行性能优化和错误处理完善。