继续写实用教程。接下来准备讲解一下如何在网页里集成语音识别,语音输入的功能。也就是俗称的 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
捕获音视频流,然后才能进行录制。
// 因为这里我们只需要录音,所以只要 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 产品开发、录音播放等有兴趣或问题,欢迎留言与我讨论。
发表回复