【实用教程】在网页里集成语音输入: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 捕获音视频流,然后才能进行录制。

create sound stream
// 因为这里我们只需要录音,所以只要 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; }

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

media handlers
// 录音正式开始时,我们可以在这里处理一些状态,以更新显示状态 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 来减少垃圾评论。了解你的评论数据如何被处理