【实用教程】在网页里集成语音输入:1. 在浏览器里完成录音

继续写实用教程。接下来准备讲解一下如何在网页里集成语音识别,语音输入的功能。也就是俗称的 STT(Speech to Text)。这个功能可以很好的鼓励用户输入更多的内容,而且考虑到现在大家用移动设备、一体机的很多,麦克风也早已普及,所以我建议大家都了解一下相关操作。不知道要写多长,长的话会分成几篇,慢慢连载。

计划内容:

  1. 基础概念
  2. 使用 MediaRecorder 完成录音
  3. STT 的几个方案:
    • OpenAI Whisper API
    • CloudFlare Workers AI
    • 腾讯云
    • WASM
  4. 细节处理
  5. 总结

基础概念

好的,闲言少叙,我们来看语音输入需要用到哪些技术。

  1. 首先,我们要想办法把用户的声音录下来。
  2. 接下来,就是把录音文件发给某个模型来解析。这一步的选择很多,我会介绍几个我用过的服务。其实浏览器自己也有 SpeechRecognition API,但是我测试的结果并不可用,也不可控,所以就放弃了。
  3. 然后接收识别结果,显示出来,就可以了。

有些系统自带语音输入,不过质量可能不好;而且录音文件中所携带的用户情绪等信息在将来大模型发展后,也有更多的可探索空间。所以我觉得这个功能还是值得学习使用的。

使用 MediaRecorder 完成录音

浏览器提供 MediaRecorder 帮我们完成录音工作。它的用法很简单,相信大家看一眼文档就能学会。我觉得值得注意的点只有几个:

  1. iOS Safari 和基于 Chromium 的 Edge / Chrome 支持的文件格式不同,需要处理
  2. 使用时需要用户许可,所以我们要处理用户拒绝的状态
  3. Safari 需要指定 timeslice

接下来我们看代码(基于 Nuxt3,即 Vue3 + TS)。首先,我们要使用 navigator.mediaDevices 捕获音视频流,然后才能进行录制。

// 因为这里我们只需要录音,所以只要 audio 就可以
const constraints = { audio: true };
// 这里的 stream 最好是全局或者组件内可访问的状态。因为录音结束我们要及时释放它,否则可能会一直都有在录音的提示
let stream: MediaStream | null = null;
// 需要根据浏览器类型确定音频文件 mimeType
const supportedMimeType = import.meta.client && isSafari() ? 'audio/mp4' : 'audio/webm;codecs=opus';
let mediaRecorder: MediaRecorder | null = null;

// 各种初始化,比如声音、画波形图等
isStarting.value = true;

try {    
  stream = await navigator.mediaDevices.getUserMedia(constraints);
  mediaRecorder = new MediaRecorder(stream, {
    mimeType: supportedMimeType,
  });
} catch (e) {
  isStarting.value = false;
  // 判断错误类型,如果是用户主动的 Permission denied,就请用户自己去开开关;否则可能无法处理,就显示不支持好了
  startingError.value = (e as Error).message === 'Permission denied'
    ? 'You have denied access to your microphone. To use this feature, please enable it in your browser settings and try again. Thank you!'
    : 'Sorry, your browser doesn\'t support voice input.';
  return;
}

接下来,如果用户许可我们使用录音设备,就可以开始录音了。这里其实我省掉了选择不同输入设备的选项,大多数时候不重要,用默认即可。感兴趣有需求的同学可以自己摸索。

// 录音正式开始时,我们可以在这里处理一些状态,以更新显示状态
mediaRecorder.addEventListener('start', async function () {
  isRecordingStarted.value = true;
  isRecording.value = true;
  isStarting.value = false;
  startTime = Date.now();
  startCountingTime();
  emit('update:isWorking', true);
});
// 录音数据有变化,记录到 recordedChunks
mediaRecorder.addEventListener('dataavailable', function (event: BlobEvent) {
  if (event.data.size > 0) {
    recordedChunks.push(event.data);
  }
});
// 录音停止,即调用 `mediaRecorder.stop()` 之后,处理语音文字转换,或者用户主动放弃(丢掉录音)等
mediaRecorder.addEventListener('stop', async function () {
  isRecording.value = false;
  // 把录音转换成文件,可以下载
  audioBlob.value = new Blob(recordedChunks, {
    type: supportedMimeType,
  });
  // 释放 stream,这一步很重要,录音提示不消除,很影响用户体验
  stream?.getTracks().forEach(track => track.stop());
  stream = null;
  audioDuration += Date.now() - startTime;

  // 这是我的一个标记,用来标记用户点击的是“取消”还是“转换”
  if (isBreak) {
    isBreak = false;
    return;
  }

  // 考虑到用户可能使用移动设备,移动网络情况不稳定,所以不能只请求一次,要隔一段时间多尝试几次
  retryCount = 0;
  let error = 'init';
  while (retryCount < autoRetry && error) {
    error = (await speechToText(audioDuration)) || '';
    if (error) {
      retryCount++;
      if (retryCount < autoRetry) {
        await sleep(1000 + (retryCount - 1) * 5000); // 1s, 6s, 11s
      }
    }
  }
  if (error) {
    convertingError.value = error;
  }
});

// 启动录音。在 Safari 里我们必须写明 `timeslice` 参数,否则可能在 stop 的时候还没记录数据。我觉得这是个 bug
mediaRecorder.start(1000);

小结

基本上,这样就可以完成录音部分的功能,配合文件下载做一个录音机习作也是可以的。算算字数也不少了,本文就先到这里,下次介绍使用大模型 API 进行文字转换的功能。

如果你对网页开发、AI 产品开发、录音播放等有兴趣或问题,欢迎留言与我讨论。

如果您觉得文章内容对您有用,不妨支持我创作更多有价值的分享:


已发布

分类

来自

评论

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据