继续写实用教程。接下来准备讲解一下如何在网页里集成语音识别,语音输入的功能。也就是俗称的 STT(Speech to Text)。这个功能可以很好的鼓励用户输入更多的内容,而且考虑到现在大家用移动设备、一体机的很多,麦克风也早已普及,所以我建议大家都了解一下相关操作。不知道要写多长,长的话会分成几篇,慢慢连载。
计划内容:
- 基础概念
- 使用 MediaRecorder 完成录音
- STT 的几个方案:
- OpenAI Whisper API
- CloudFlare Workers AI
- 腾讯云
- WASM
- 细节处理
- 总结
基础概念
好的,闲言少叙,我们来看语音输入需要用到哪些技术。
- 首先,我们要想办法把用户的声音录下来。
- 接下来,就是把录音文件发给某个模型来解析。这一步的选择很多,我会介绍几个我用过的服务。其实浏览器自己也有 SpeechRecognition API,但是我测试的结果并不可用,也不可控,所以就放弃了。
- 然后接收识别结果,显示出来,就可以了。
有些系统自带语音输入,不过质量可能不好;而且录音文件中所携带的用户情绪等信息在将来大模型发展后,也有更多的可探索空间。所以我觉得这个功能还是值得学习使用的。
使用 MediaRecorder 完成录音
浏览器提供 MediaRecorder
帮我们完成录音工作。它的用法很简单,相信大家看一眼文档就能学会。我觉得值得注意的点只有几个:
- iOS Safari 和基于 Chromium 的 Edge / Chrome 支持的文件格式不同,需要处理
- 使用时需要用户许可,所以我们要处理用户拒绝的状态
- 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 产品开发、录音播放等有兴趣或问题,欢迎留言与我讨论。
欢迎吐槽,共同进步