Vue问答助手进阶:录音与语音转文字全流程实现

Vue问答助手进阶:录音与语音转文字全流程实现

一、录音功能实现基础

在Vue3项目中实现录音功能,核心在于使用Web Audio API和MediaRecorder API。这两个浏览器原生API提供了完整的音频采集和处理能力。

1.1 音频采集流程设计

首先需要获取用户麦克风权限,这通过navigator.mediaDevices.getUserMedia()方法实现。建议采用渐进式权限请求策略:

  1. async function requestAudioPermission() {
  2. try {
  3. const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  4. return stream;
  5. } catch (err) {
  6. console.error('权限请求失败:', err);
  7. // 根据错误类型提供不同提示
  8. if (err.name === 'NotAllowedError') {
  9. alert('需要麦克风权限才能使用语音功能');
  10. }
  11. return null;
  12. }
  13. }

1.2 录音状态管理

使用Vue3的Composition API管理录音状态:

  1. import { ref } from 'vue';
  2. export function useRecorder() {
  3. const isRecording = ref(false);
  4. const mediaStream = ref(null);
  5. const audioChunks = ref([]);
  6. const mediaRecorder = ref(null);
  7. const startRecording = async () => {
  8. const stream = await requestAudioPermission();
  9. if (!stream) return;
  10. mediaStream.value = stream;
  11. audioChunks.value = [];
  12. mediaRecorder.value = new MediaRecorder(stream);
  13. mediaRecorder.value.ondataavailable = (event) => {
  14. if (event.data.size > 0) {
  15. audioChunks.value.push(event.data);
  16. }
  17. };
  18. mediaRecorder.value.start(100); // 每100ms收集一次数据
  19. isRecording.value = true;
  20. };
  21. const stopRecording = () => {
  22. if (!mediaRecorder.value) return;
  23. mediaRecorder.value.stop();
  24. mediaRecorder.value.onstop = () => {
  25. const audioBlob = new Blob(audioChunks.value, { type: 'audio/wav' });
  26. // 处理音频Blob
  27. mediaStream.value?.getTracks().forEach(track => track.stop());
  28. isRecording.value = false;
  29. };
  30. };
  31. return { isRecording, startRecording, stopRecording };
  32. }

1.3 录音可视化实现

结合Canvas API实现声波可视化:

  1. function setupVisualization(audioContext, analyser) {
  2. const canvas = document.getElementById('visualizer');
  3. const ctx = canvas.getContext('2d');
  4. const bufferLength = analyser.frequencyBinCount;
  5. const dataArray = new Uint8Array(bufferLength);
  6. function draw() {
  7. requestAnimationFrame(draw);
  8. analyser.getByteFrequencyData(dataArray);
  9. ctx.fillStyle = 'rgb(240, 240, 240)';
  10. ctx.fillRect(0, 0, canvas.width, canvas.height);
  11. const barWidth = (canvas.width / bufferLength) * 2.5;
  12. let x = 0;
  13. for (let i = 0; i < bufferLength; i++) {
  14. const barHeight = dataArray[i] / 2;
  15. ctx.fillStyle = `rgb(${barHeight + 100}, 50, 50)`;
  16. ctx.fillRect(x, canvas.height - barHeight, barWidth, barHeight);
  17. x += barWidth + 1;
  18. }
  19. }
  20. draw();
  21. }

二、语音转文字技术方案

语音转文字(ASR)功能可通过浏览器原生API或第三方服务实现,各有其适用场景。

2.1 Web Speech API实现方案

浏览器原生SpeechRecognition API提供基础语音识别能力:

  1. function setupSpeechRecognition() {
  2. const recognition = new (window.SpeechRecognition ||
  3. window.webkitSpeechRecognition)();
  4. recognition.continuous = false; // 单次识别
  5. recognition.interimResults = true; // 实时返回中间结果
  6. recognition.lang = 'zh-CN'; // 设置中文识别
  7. recognition.onresult = (event) => {
  8. let interimTranscript = '';
  9. let finalTranscript = '';
  10. for (let i = event.resultIndex; i < event.results.length; i++) {
  11. const transcript = event.results[i][0].transcript;
  12. if (event.results[i].isFinal) {
  13. finalTranscript += transcript;
  14. } else {
  15. interimTranscript += transcript;
  16. }
  17. }
  18. // 更新Vue组件中的文本
  19. emit('update-text', finalTranscript || interimTranscript);
  20. };
  21. recognition.onerror = (event) => {
  22. console.error('识别错误:', event.error);
  23. };
  24. return recognition;
  25. }

2.2 第三方ASR服务集成

对于需要更高准确率的场景,可集成专业ASR服务。以下以阿里云语音识别为例:

2.2.1 服务端配置

  1. // Node.js服务端示例
  2. const express = require('express');
  3. const router = express.Router();
  4. const AliyunSDK = require('aliyun-sdk');
  5. router.post('/asr', async (req, res) => {
  6. const client = new AliyunSDK({
  7. accessKeyId: 'YOUR_ACCESS_KEY',
  8. accessKeySecret: 'YOUR_SECRET_KEY',
  9. endpoint: 'nls-meta.cn-shanghai.aliyuncs.com',
  10. apiVersion: '2019-02-28'
  11. });
  12. const params = {
  13. AppKey: 'YOUR_APP_KEY',
  14. Format: 'wav',
  15. SampleRate: '16000',
  16. FileLink: req.body.audioUrl // 或直接上传音频文件
  17. };
  18. try {
  19. const result = await client.request('CreateToken', params);
  20. res.json({ token: result.Token });
  21. } catch (err) {
  22. res.status(500).json({ error: err.message });
  23. }
  24. });

2.2.2 前端集成

  1. async function uploadAndRecognize(audioBlob) {
  2. // 1. 上传音频到服务器
  3. const formData = new FormData();
  4. formData.append('audio', audioBlob, 'recording.wav');
  5. const uploadRes = await fetch('/api/upload', {
  6. method: 'POST',
  7. body: formData
  8. });
  9. const { audioUrl } = await uploadRes.json();
  10. // 2. 获取ASR服务token
  11. const tokenRes = await fetch('/api/asr/token');
  12. const { token } = await tokenRes.json();
  13. // 3. 初始化WebSocket连接
  14. const ws = new WebSocket('wss://nls-gateway.cn-shanghai.aliyuncs.com/ws/v1');
  15. ws.onopen = () => {
  16. const startReq = {
  17. header: {
  18. app_key: 'YOUR_APP_KEY',
  19. token: token
  20. },
  21. payload: {
  22. audio: {
  23. audio_url: audioUrl,
  24. format: 'wav',
  25. sample_rate: 16000
  26. },
  27. service_type: 'asr',
  28. language: 'zh_cn',
  29. enable_punctuation_prediction: true,
  30. enable_words: false
  31. }
  32. };
  33. ws.send(JSON.stringify(startReq));
  34. };
  35. let fullText = '';
  36. ws.onmessage = (event) => {
  37. const data = JSON.parse(event.data);
  38. if (data.header.status === 20000) {
  39. if (data.payload.result) {
  40. fullText += data.payload.result.sentences.map(s => s.text).join('');
  41. // 更新Vue组件显示
  42. emit('update-text', fullText);
  43. }
  44. }
  45. };
  46. }

三、完整交互流程设计

3.1 组件化实现

  1. <template>
  2. <div class="voice-assistant">
  3. <div class="controls">
  4. <button @click="toggleRecording" :disabled="isProcessing">
  5. {{ isRecording ? '停止录音' : '开始录音' }}
  6. </button>
  7. <button @click="startSpeechRecognition" v-if="!isRecording">
  8. 语音转文字
  9. </button>
  10. </div>
  11. <div class="visualization">
  12. <canvas id="visualizer" width="600" height="200"></canvas>
  13. </div>
  14. <div class="transcript">
  15. <div class="interim" v-if="interimText">{{ interimText }}</div>
  16. <div class="final" v-else>{{ finalText }}</div>
  17. </div>
  18. <div class="question-area">
  19. <textarea v-model="question" placeholder="输入问题或通过语音提问"></textarea>
  20. <button @click="submitQuestion" :disabled="!question.trim()">
  21. 提交问题
  22. </button>
  23. </div>
  24. </div>
  25. </template>
  26. <script setup>
  27. import { ref } from 'vue';
  28. import { useRecorder } from './composables/recorder';
  29. const { isRecording, startRecording, stopRecording } = useRecorder();
  30. const interimText = ref('');
  31. const finalText = ref('');
  32. const question = ref('');
  33. const isProcessing = ref(false);
  34. // 语音转文字逻辑
  35. const startSpeechRecognition = () => {
  36. const recognition = setupSpeechRecognition();
  37. recognition.start();
  38. recognition.onend = () => {
  39. question.value = finalText.value;
  40. interimText.value = '';
  41. };
  42. };
  43. // 录音完成处理
  44. const onRecordingStop = (audioBlob) => {
  45. isProcessing.value = true;
  46. uploadAndRecognize(audioBlob)
  47. .then(text => {
  48. question.value = text;
  49. isProcessing.value = false;
  50. })
  51. .catch(() => isProcessing.value = false);
  52. };
  53. const toggleRecording = () => {
  54. if (isRecording.value) {
  55. stopRecording();
  56. // 这里需要修改recorder逻辑,在stop时调用onRecordingStop
  57. } else {
  58. startRecording();
  59. }
  60. };
  61. </script>

3.2 性能优化策略

  1. 音频预处理

    • 采样率统一转换为16kHz(ASR服务标准)
    • 使用Web Audio API进行降噪处理

      1. function createAudioContext(stream) {
      2. const audioContext = new (window.AudioContext ||
      3. window.webkitAudioContext)();
      4. const source = audioContext.createMediaStreamSource(stream);
      5. // 创建降噪节点
      6. const processor = audioContext.createScriptProcessor(4096, 1, 1);
      7. processor.onaudioprocess = (audioProcessingEvent) => {
      8. const input = audioProcessingEvent.inputBuffer.getChannelData(0);
      9. // 实现简单的降噪算法
      10. const output = input.map(sample => {
      11. return Math.abs(sample) < 0.01 ? 0 : sample;
      12. });
      13. // 将处理后的数据传递给ASR
      14. };
      15. source.connect(processor);
      16. return { audioContext, processor };
      17. }
  2. 分块传输优化

    • 对于长音频,采用分块上传策略
    • 实现断点续传机制
  3. 错误恢复机制

    • 录音失败时自动重试(最多3次)
    • 网络中断时缓存音频数据,网络恢复后继续传输

四、安全与隐私考虑

4.1 权限管理最佳实践

  1. 采用”按需请求”策略,只在用户点击录音按钮时请求权限
  2. 提供清晰的权限用途说明
  3. 实现权限状态持久化,避免重复请求

4.2 数据安全措施

  1. 音频数据传输使用HTTPS/WSS加密
  2. 敏感操作(如ASR服务调用)需要用户确认
  3. 实现自动数据清理机制,录音完成后删除本地缓存

4.3 隐私政策集成

  1. 在应用启动时显示隐私政策摘要
  2. 提供完整的隐私政策链接
  3. 记录所有语音数据处理操作日志

五、测试与调试方案

5.1 单元测试示例

  1. import { mount } from '@vue/test-utils';
  2. import VoiceAssistant from '@/components/VoiceAssistant.vue';
  3. describe('VoiceAssistant.vue', () => {
  4. it('正确显示录音状态', async () => {
  5. const wrapper = mount(VoiceAssistant);
  6. expect(wrapper.find('.controls button').text()).toBe('开始录音');
  7. await wrapper.find('.controls button').trigger('click');
  8. expect(wrapper.vm.isRecording).toBe(true);
  9. expect(wrapper.find('.controls button').text()).toBe('停止录音');
  10. });
  11. it('语音转文字结果正确显示', async () => {
  12. const wrapper = mount(VoiceAssistant, {
  13. global: {
  14. mocks: {
  15. setupSpeechRecognition: () => ({
  16. start: () => {
  17. setTimeout(() => {
  18. wrapper.vm.finalText = '测试语音内容';
  19. }, 100);
  20. }
  21. })
  22. }
  23. }
  24. });
  25. await wrapper.find('button:contains("语音转文字")').trigger('click');
  26. await new Promise(resolve => setTimeout(resolve, 150));
  27. expect(wrapper.find('.final').text()).toBe('测试语音内容');
  28. });
  29. });

5.2 跨浏览器兼容性处理

  1. 检测API可用性:

    1. function checkBrowserSupport() {
    2. const support = {
    3. getUserMedia: !!navigator.mediaDevices?.getUserMedia,
    4. speechRecognition: !!(window.SpeechRecognition || window.webkitSpeechRecognition),
    5. mediaRecorder: !!window.MediaRecorder
    6. };
    7. if (!support.getUserMedia) {
    8. alert('您的浏览器不支持麦克风访问,请使用Chrome/Firefox/Edge最新版');
    9. }
    10. return support;
    11. }
  2. 提供降级方案:

    • 不支持录音时显示文本输入框
    • 不支持语音识别时隐藏相关按钮

六、部署与监控

6.1 服务端监控指标

  1. ASR请求成功率
  2. 平均响应时间
  3. 错误率按错误类型分类

6.2 前端性能监控

  1. // 使用Performance API监控录音性能
  2. function monitorPerformance() {
  3. const observer = new PerformanceObserver((list) => {
  4. for (const entry of list.getEntries()) {
  5. if (entry.name === 'audio-process') {
  6. console.log(`音频处理耗时: ${entry.duration}ms`);
  7. // 上报到监控系统
  8. }
  9. }
  10. });
  11. observer.observe({ entryTypes: ['measure'] });
  12. // 在关键代码段前后添加测量
  13. performance.mark('audio-start');
  14. // ...音频处理代码...
  15. performance.mark('audio-end');
  16. performance.measure('audio-process', 'audio-start', 'audio-end');
  17. }

6.3 日志收集方案

  1. 前端错误日志:

    1. window.addEventListener('error', (event) => {
    2. const log = {
    3. type: 'frontend-error',
    4. message: event.message,
    5. filename: event.filename,
    6. lineno: event.lineno,
    7. stack: event.error?.stack,
    8. timestamp: new Date().toISOString()
    9. };
    10. fetch('/api/logs', {
    11. method: 'POST',
    12. headers: { 'Content-Type': 'application/json' },
    13. body: JSON.stringify(log)
    14. });
    15. });
  2. 服务端ASR日志:

    • 记录每个请求的音频时长、识别结果、处理时间
    • 关联用户ID(需匿名化处理)

本实现方案提供了从录音采集到语音转文字的完整技术路径,结合了浏览器原生API和第三方专业服务,既保证了基础功能的可用性,又提供了高准确率的升级方案。开发者可根据实际需求选择适合的实现方式,并通过组件化设计提高代码复用性。实际开发中还需注意权限管理、错误处理和性能优化等关键环节,确保应用的稳定性和用户体验。