ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue项目中实现语音文字互转:原生TTS与语音识别实战指南

Vue项目中实现语音文字互转:原生TTS与语音识别实战指南 平时做 Vue 项目总免不了碰到能不能让页面自己说话或者能不能让用户直接说别敲字了这种需求。我最早接到类似需求时第一反应是找云厂商的语音合成接口然后就被工单、鉴权、并发限制折腾了一轮。后来才发现浏览器自带了一套语音能力能够直接完成文字转语音和语音转文字而且 Vue 里接起来非常顺手几乎没有额外依赖。这篇文章就聊一聊我在 Vue 项目里用 Web 自带的 TTS 和语音识别接口实现语音文字互转的完整过程。包括方案选型的思路、核心 API 的使用细节、封装成 Vue 组合式函数的实操代码以及一堆在文档里查不到的浏览器兼容性坑。无论你是刚入门的前端新人还是已经在做管理后台、大屏看板、学习类产品的老手只要页面跑在浏览器里这套方案都值得存一份。1. 整体设计与技术思路解析1.1 浏览器语音能力全景SpeechSynthesis 与 SpeechRecognition先说结论浏览器自带的语音能力分成两半一半是文字转语音另一半是语音转文字。文字转语音的 API 叫 SpeechSynthesis它其实是个非常成熟的接口Chrome、Edge、Firefox、Safari 全部支持。它的原理是调用操作系统或浏览器内置的语音引擎把文本交给合成器然后输出音频。你在页面上调用一句speechSynthesis.speak(utterance)浏览器就能把文字读出来完全不依赖网络也不需要申请任何 API Key。语音转文字的 API 叫 SpeechRecognition标准接口已经定下来了但它的落地情况比 TTS 差不少。目前 Chrome 和 Edge 支持得比较好通过webkitSpeechRecognition前缀也能调通但 Firefox 至今默认关闭Safari 在桌面端支持不稳定移动端 iOS 版本间差异很大。它依赖麦克风权限并且要求页面跑在 HTTPS 环境下才能启用localhost开发环境除外。这两个接口经常被放在一起讨论但我要提醒你它们的出身完全不同。TTS 是合成音频识别率、语速完全由本地引擎决定稳定可靠语音识别是浏览器录制一段音频上传到在线服务做识别速度和准确率依赖网络和提供方的在线服务所以你要在项目里分清哪些功能依赖本地能力哪些功能依赖网络服务否则出了问题排查方向就错了。1.2 方案选型原生 TTS 和云端语音合成怎么选我在做技术方案的时候列过一张对比表把原生方案和云厂商方案放在一起比较这不光是为了说服自己也是为了在评审会上回答为什么要用浏览器的自带功能。对比维度浏览器原生 TTS云厂商语音合成接入成本零依赖几行代码直接调需要注册账号、申请 Key、封装 HTTP 请求网络依赖完全离线可用依赖网络断网就不可用音色质量取决于操作系统和浏览器自带语音适中音色丰富还可以定制发音人费用免费按调用量计费量大了是一笔成本延迟极低几乎实时有网络往返延迟约几百毫秒可定制性语速、音调、音量可以调但不能换音色可以调语气、停顿、说话风格结论其实很清晰如果你的项目是内部工具、大屏播报、数据看板、朗读辅助这类场景对音色要求不高原生 TTS 是最省钱、最省心的方案。反过来如果你要做一个内容创作工具用户需要拿合成的音频去发表那一定要上云厂商的接口因为系统自带音色听起来还是太机器味了。1.3 在 Vue 项目里落地的基本架构我在项目里用的是 Vue 3 Vite Pinia 的技术栈把语音能力单独抽成了一个模块目录放在src/composables/下面。每个语音能力对应一个组合式函数组件里只需要调用函数、拿到状态、绑定事件不需要关心底层 API 怎么操作。整个架构考虑得很简单底层的window.speechSynthesis和window.SpeechRecognition是全局单例我就在它们之上包一层状态管理。组件 A 在朗读的时候组件 B 也能知道现在正在朗读这样 UI 上就不会出现两个按钮同时高亮的奇怪状态。语音识别那边也类似识别结果是异步回来的我把状态放在组合式函数的ref里组件模板上直接绑定就行。2. 文字转语音TTS核心实现与踩坑2.1 SpeechSynthesis 基础封装TTS 这部分代码看起来简单其实就是三件事创建一个SpeechSynthesisUtterance对象配置好文本、语言、语速、音调然后调用speechSynthesis.speak()让它开口。但在 Vue 里我们要多考虑状态同步的问题。我一开始直接写在组件里发现朗读的开始、暂停、继续、结束这些事件都拿到组件外部去了页面一刷新就没法恢复状态。后来我把逻辑全部抽进了组合式函数用一个status的ref来维护当前状态让模板里的按钮文字根据状态切换。// src/composables/useSpeechSynthesis.js import { ref, computed, onBeforeUnmount } from vue export function useSpeechSynthesis() { const status ref(idle) // idle | speaking | paused const isSpeaking computed(() status.value speaking) const isPaused computed(() status.value paused) let currentUtterance null function buildUtterance(text, options {}) { const utterance new SpeechSynthesisUtterance(text) utterance.lang options.lang || zh-CN utterance.rate options.rate || 1 utterance.pitch options.pitch || 1 utterance.volume options.volume || 1 utterance.onstart () { status.value speaking } utterance.onend () { status.value idle currentUtterance null } utterance.onerror (event) { console.error(语音朗读出错, event.error) status.value idle currentUtterance null } utterance.onresume () { status.value speaking } utterance.onpause () { status.value paused } return utterance } function speak(text, options {}) { if (!text) return if (window.speechSynthesis.speaking) { window.speechSynthesis.cancel() } currentUtterance buildUtterance(text, options) window.speechSynthesis.speak(currentUtterance) } function cancel() { window.speechSynthesis.cancel() status.value idle currentUtterance null } function pause() { window.speechSynthesis.pause() } function resume() { window.speechSynthesis.resume() } onBeforeUnmount(() { if (currentUtterance) { window.speechSynthesis.cancel() } }) return { status, isSpeaking, isPaused, speak, cancel, pause, resume, } }这里有几个细节值得注意。第一speak之前一定要先cancel不然连续调用两次speak时第二条会把第一条顶掉但第一条的onend后面还是会触发导致状态错乱。第二onerror里要重置状态用户可能因为各种原因中断朗读状态不重置的话按钮就永远卡在朗读中。第三onBeforeUnmount里必须cancel否则组件销毁了语音还在响这在列表页里尤其明显翻页读着读着就串线了。2.2 中文语音包与 voiceschanged 问题TTS 最大的坑不是 API 用不好而是浏览器找不到合适的中文语音。Chrome 在部分系统上第一次调用speechSynthesis.getVoices()返回的是空数组需要等一个voiceschanged事件之后才能拿到完整列表。很多新手在这里踩坑以为浏览器不支持其实只是列表加载有延迟。我处理这个问题的方法是在页面初始化时就开始监听voiceschanged事件把语音列表存下来并且选好默认的中文语音。同时用localStorage给用户做语音偏好记忆下次进来直接选中同一个音色。// 获取语音列表的封装 function loadVoices() { return new Promise((resolve) { const getVoices () window.speechSynthesis.getVoices() const voices getVoices() if (voices.length) { resolve(voices) return } let resolved false const handler () { if (resolved) return resolved true const latestVoices getVoices() window.speechSynthesis.removeEventListener(voiceschanged, handler) resolve(latestVoices) } window.speechSynthesis.addEventListener(voiceschanged, handler) }) } function pickChineseVoice(voices) { const preferredNames [Microsoft Xiaoxiao, Microsoft Yunxi, Google 普通话, Zira, Huihui, Tingting] const matched voices.find((voice) preferredNames.some((name) voice.name.includes(name))) if (matched) return matched return voices.find((voice) voice.lang.includes(zh)) || null }在 Windows 上Chrome 和 Edge 一般会自带微软的中文语音包比如Microsoft Xiaoxiao Online和Microsoft Yunxi Online听感已经非常自然了。macOS 上一般也有婷婷或Meijia这类中文语音。Linux 桌面版会比较被动很可能一个中文语音都没有这时候你只能手动给系统装上语音包或者放弃原生方案走云 TTS。注意SpeechSynthesisUtterance上可以设置voice如果你不设置浏览器会用默认语音在某些系统上会优先读英文中文文本就被英文语音生硬地读出来。所以拿到语音列表后一定要自己选中文。2.3 长文本分段朗读与进度回调如果你只是朗读一句话原生 TTS 表现可谓完美。但如果你把一整篇五千字的文章丢给speak()你会发现各种诡异的问题读着读着没声音了、只读了一半就中止、延迟越来越大、甚至整个浏览器标签页卡顿。这是因为 Chorme 对单次speak的文本长度和语音合成时长都有隐性限制。我的经验是长文本必须要分段。分段不是无脑按字数切而是应该按语义边界切。我写了一个简单的分段函数优先级从高到低段落换行、句号、感叹号、问号、分号、逗号。每隔一个标点符号就切一刀最后再合并短句保证每段控制在 150~200 个字符左右。function splitLongText(text, maxLength 200) { const segments [] const sentences text.split(/(?[。!?;])/) let buffer for (const sentence of sentences) { if ((buffer sentence).length maxLength) { if (buffer) segments.push(buffer.trim()) buffer sentence } else { buffer sentence } } if (buffer) segments.push(buffer.trim()) return segments }分段之后我再用一个队列机制一段一段地读每段读完触发onend然后自动去读下一段。同时为了给用户展示读到哪里了我利用utterance.onboundary事件获取当前朗读的位置。这个事件并不是所有浏览器都支持Chromium 内核下按字触发配合原文文本可以做高亮效果。// 朗读队列的核心逻辑 async function speakLongText(fullText, options {}) { const segments splitLongText(fullText) for (let i 0; i segments.length; i) { if (stopRequested) break await new Promise((resolve) { const utterance buildUtterance(segments[i], options) utterance.onend resolve utterance.onerror resolve window.speechSynthesis.speak(utterance) }) } }这样处理之后长文本朗读的稳定性大幅提升。还有一个经典 BugChrome 在朗读中途如果调用speechSynthesis.pause()过几分钟再resume()会出现半天没反应的状态这也是因为单次合成队列过长导致的。分段之后这个问题基本消失了。2.4 自动播放限制与用户手势处理浏览器的自动播放策略同样约束了语音合成。虽然没有像视频音频那样严格到必须用户点击一次后才能播放但在 macOS 的 Safari 上如果页面加载完成就直接speak()大概率会被静音拦截。Chrome 在处理上也类似如果你不是在用户手势事件里触发的speak语音合成可能不会发声。在实际的产品设计里这不算大问题因为语音朗读按钮本来就是用户主动去点的。但有一种场景容易踩坑你做好了一个进入页面自动朗读欢迎语的功能结果在线上只有一半用户能听到另一半用户什么都没听到。排查方法就是用浏览器控制台看看speechSynthesis.speaking状态如果显示true但没声音那就是被自动播放策略卡住了。我把这个问题的处理思路整理成了一条规则不要在路由跳转之后的nextTick里直接调用speak一定要让用户先有一次真正的点击交互。如果产品经理强行让你做自动播报那么最稳妥的做法是做一个引导弹窗让用户点击进入按钮后再触发这样既能满足需求又不会踩自动播放的坑。3. 语音转文字ASR核心实现与兼容性适配3.1 SpeechRecognition 基础封装语音转文字这部分原生 API 的体验就明显不如 TTS 顺手了。首先你得先做一个能力检测因为不是所有浏览器都支持SpeechRecognition而且标准接口和带前缀的接口在不同浏览器里名称不一样。// 兼容性检测 function createRecognition() { const SpeechRecognitionClass window.SpeechRecognition || window.webkitSpeechRecognition if (!SpeechRecognitionClass) { throw new Error(当前浏览器不支持语音识别建议使用 Chrome 或 Edge) } return new SpeechRecognitionClass() }SpeechRecognition的核心用法也比较直接配置识别语言、设置continuous和interimResults然后调用start()开始监听。它会通过一个onresult回调把听到的话转换成文本返回给你。你要做的就是把这个回调里的事件数据清洗好提出来放进 Vue 的ref里。我第一次做的时候以为它可以像语音助手一样一边说话一边持续输出一整段文字。后来动手才发现它默认情况下是一次录音、一次识别、一次回调就结束你需要手动处理恢复逻辑才能实现对话式的连续识别。这个差异直接影响了你后续的代码结构。3.2 连续识别与临时结果处理先看一段我封装好的识别核心逻辑// src/composables/useSpeechRecognition.js import { ref, onBeforeUnmount } from vue export function useSpeechRecognition() { const isListening ref(false) const finalText ref() const intermediateText ref() const isError ref(false) const errorMessage ref() let recognition null try { recognition createRecognition() } catch (e) { errorMessage.value e.message } if (recognition) { recognition.lang zh-CN recognition.continuous true recognition.interimResults true recognition.onstart () { isListening.value true isError.value false } recognition.onresult (event) { let interim let final for (let i event.resultIndex; i event.results.length; i) { const transcript event.results[i][0].transcript if (event.results[i].isFinal) { final transcript } else { interim transcript } } if (final) { finalText.value final } intermediateText.value interim } recognition.onerror (event) { isError.value true errorMessage.value 识别出错${event.error} } recognition.onend () { isListening.value false intermediateText.value } } function start() { if (!recognition) return recognition.start() } function stop() { if (!recognition) return recognition.stop() } function reset() { finalText.value intermediateText.value } onBeforeUnmount(() { if (recognition) { recognition.onend null recognition.onresult null recognition.onerror null if (isListening.value) { recognition.stop() } } }) return { isListening, finalText, intermediateText, isError, errorMessage, start, stop, reset, } }这个封装里有几个很关键的信息。continuous true表示持续识别但注意它并不是你按住说话就持续识别而是当一段静音结束之后浏览器会自动重新开启一轮识别期间连续输出。interimResults true用来打开临时结果你可以实时看到识别出来但还没确认的文本。onresult里我用event.results[i].isFinal来判断某一段结果是临时结果还是最终结果最终结果追加到finalText临时结果放到intermediateText页面上的效果就是你一边说文字一边蹦出来停顿一下之后临时文字就会固定上去。我在实际项目里发现一个有意思的问题Chrome 的语音识别在断句之后经常会多出来一些嗯啊之类的语气词这些也会被当成文本记录进去。如果你做的是内容录入工具最好在后处理阶段把语气词过滤掉否则用户后面要手动删一堆嗯嗯嗯。3.3 录音权限与麦克风异常处理语音识别是绕不开麦克风权限的。用户第一次点击开始识别的时候浏览器会弹出权限请求这个没问题后续同一个域名权限会记住。真正麻烦的是权限被拒之后的处理以及用户装了什么麦克风增强软件导致的音频采集异常。我在onerror里会重点区分几类错误码错误码含义处理建议not-allowed用户拒绝了权限提示用户在地址栏重新授权麦克风service-not-allowed浏览器不允许当前页面使用该服务确认是否 HTTPS 环境确认是否有第三方插件拦截no-speech没有检测到语音提示用户说话声音大一点或者调整麦克风audio-capture麦克风设备不可用检查系统输入设备看是否被其他应用占用network识别服务网络异常检查网络语音识别依赖在线服务处理权限拒绝时我的建议是别只弹一个请允许麦克风权限就完事最好把操作路径也给出来。Chrome 地址栏左侧的小锁图标点开下拉菜单里有麦克风权限设置让用户自己去改成允许刷新页面再试。这个提示虽然丑但确实是我实测过最有效的解决方式。3.4 Safari/移动端兼容性适配如果你把项目部署到 iOS 的 Safari 上或者放在某些 App 内置的 WebView 里语音识别这件事就会变得相当不稳定。iOS 13 以后的 Safari 其实内置了webkitSpeechRecognition但识别质量和触发条件跟 Chrome 差距明显而且必须要求 HTTPS。安卓手机上的 Chrome 倒是非常稳定但也要注意不同厂商的定制系统可能屏蔽了部分语音服务。我的经验是语音识别这块不要勉强跨所有浏览器能用原生就用原生不行的直接引导降级。在代码里做一个浏览器能力检测如果当前环境不支持SpeechRecognition就弹出一个提示把输入框切换到普通的打字输入模式不要让用户以为功能坏了。与其在兼容性泥潭里挣扎不如把路径设计成一个选填项能识别就识别识别不了就老老实实打字。如果是双端项目App 里的 WebView 建议直接套原生语音识别桥接通过 JSBridge 调用原生能力再回传给 Web 层识别率和稳定性都会有质的提升。原生 TTS 在 WebView 里通常没有问题语音识别在移动端的坑确实多到值得单开一篇来讲。4. 工程化实践Vue3 组合式封装与组件化4.1 组合式函数封装useSpeechSynthesis封装useSpeechSynthesis的时候我特意加了些产品向的功能。比如朗读状态的枚举idle表示空闲、speaking表示朗读中、paused表示暂停中。有了这个枚举组件里的按钮文字和图标就可以用计算属性自动切换不需要在每个组件里到处if/else。我还给这个组合式函数加了一个getVoices方法方便组件初始化时填充语音选择下拉框。用户选中某个语音之后可以存进 Pinia 或者 localStorage后续每次朗读都用他选的音色。这些细节看起来不复杂但实际做完之后你会发现整个语音功能的代码质量和可维护性高了很多。4.2 组合式函数封装useSpeechRecognition识别相关的组合式函数我额外做了两个设计上的考量。第一个是临时结果和最终结果分离。很多语音识别界面上用户会看到一行灰字实时跟随隔一会儿变成黑字。这是通过intermediateText和finalText两个ref实现的。用户在界面上看到灰字就知道它还在识别中变黑就意味着内容已经稳定了这种反馈非常自然。第二个是手动停止与自动停止的区分。用户点停止按钮和浏览器因为长时间静音自动停止在状态表现上应该有区别。我保留了一个manualStop标志位当用户主动停止时把isListening置为false并且清空临时结果当浏览器自动结束时同样置为false但会保留已有的识别文本让用户判断是否需要继续。4.3 完整组件示例语音朗读加语音输入把两个组合式函数合在一起用其实就是做语音文字互转的完整闭环了。我在项目里做了一个语音交互面板左边是一段文本可以点击朗读右边是一个输入框可以点击麦克风说话识别出的文字直接填入。组件的代码结构非常直观template div classvoice-panel section classtts-section h3文字转语音/h3 textarea v-modelttsText placeholder输入要朗读的文字/textarea div classcontrols button clickhandleSpeak朗读/button button v-iftts.isSpeaking clicktts.pause()暂停/button button v-iftts.isPaused clicktts.resume()继续/button button clicktts.cancel()停止/button /div p状态{{ tts.status }}/p /section section classasr-section h3语音转文字/h3 textarea v-modelasrText placeholder点击右侧麦克风开始说话/textarea div classcontrols button v-if!recognition.isListening.value clickrecognition.start()开始识别/button button v-else clickrecognition.stop()停止识别/button button clickrecognition.reset()清空/button /div p v-ifrecognition.intermediateText.value正在识别{{ recognition.intermediateText.value }}/p /section /div /template script setup import { ref, reactive } from vue import { useSpeechSynthesis } from /composables/useSpeechSynthesis import { useSpeechRecognition } from /composables/useSpeechRecognition const ttsText ref(你好我是项目助手可以帮你朗读这段文字。) const asrText ref() const tts reactive(useSpeechSynthesis()) const recognition reactive(useSpeechRecognition()) async function handleSpeak() { tts.speak(ttsText.value, { lang: zh-CN, rate: 1 }) } // 当识别出最终结果时同步给 asrText watchEffect(() { if (recognition.finalText.value) { asrText.value recognition.finalText.value } }) /script这里有一个地方要特别说明useSpeechRecognition内部暴露的是ref在setup中直接用reactive包一层之后模板里访问recognition.isListening.value会有点别扭。更好的做法是在组合式函数内部就完成解构或者统一暴露成普通对象。我上面的代码偏演示目的真实项目里建议在函数内部把ref转成对象返回模板用起来更清爽。4.4 状态管理与多组件通信注意点在一整个项目里语音状态往往不止一个组件在用。比如左边菜单有一个全局朗读当前页的浮动按钮正文区也有一个朗读本文的按钮如果两个按钮点击冲突页面就要乱套。我这里的处理方案是用 Pinia 建一个voiceStore把isSpeaking、status、finalText全部提升到全局两个组合式函数的内部状态都从 store 读取。这个做法的好处很明显用户在浮动按钮上触发的朗读和正文区的朗读共用同一个SpeechSynthesis单例不会出现两个语音同时响的情况。后面如果要做朗读历史、收藏发音人、语速记忆这些数据也能顺理成章地放进 store 管理不需要改组件层的逻辑。5. 常见问题与排查经验5.1 常见问题速查表把我在不同浏览器、不同系统上碰过的问题整理成了一张表遇到问题先对着查一遍能省很多时间。现象可能原因排查方向点击朗读没有声音但状态是 speaking自动播放策略拦截确认是否在用户手势回调里触发speakchrome 里第一次读不出中文voices 加载延迟监听voiceschanged拿到语音列表再选语音长文本读到一半就停单次合成超长文本触发限制按标点分段用队列连续朗读朗读中断控制台报interrupted新语音打断旧语音检查是否有其他组件调用了cancel()语音识别提示not-allowed麦克风权限被拒引导用户到地址栏小锁图标重新授权语音识别无任何反应当前环境不是 HTTPSlocalhost可用非本机部署必须 HTTPSFirefox 里语音识别不能用Firefox 未支持该标准接口做能力检测降级到手动输入语音识别结果全是乱码语言参数lang设置不对确认langzh-CN已正确设置读中文时声音偏英式未指定 voice采用系统默认英文语音手动pickChineseVoice指定中文音色5.2 部署到 Nginx 后的注意事项我项目开发模式一切正常结果部署到测试环境的 Nginx 之后TTS 还能用语音识别却彻底失效了。排查了半天才想起来测试环境的 Nginx 配的是 HTTP而语音识别这个 API 明确要求 HTTPS 环境。解决方式分两层。第一层如果你公司有域名和证书直接用 HTTPS 就能解决。第二层如果只能通过 IP 访问那语音识别基本没救只能降级到 TTS。这也是我在前文说语音识别不要勉强跨所有环境的原因TTS 是纯本地能力语音识别有明显的安全策略门槛这是硬限制。另外部署到 Nginx 之后建议在响应头里排查一下是否被 CSP 策略拦截了。有些项目的安全配置比较严格会把speech-synthesis相关的connect-src拦掉导致语音识别请求发不出去。我在生产环境就遇到过net::ERR_CONNECTION_REFUSED最后是运维在 CSP 白名单里加上了语音识别的在线服务域名才解决。5.3 移动端低版本 WebView 降级方案在移动端特别是各种 App 的 WebView 里原生语音能力的表现会很分散。安卓 5.0 以下的 WebView 版本太老很多 JavaScript API 根本不存在更别提语音识别。iOS 的 WKWebView 在早期版本对webkitSpeechRecognition支持也有缺失。遇到这种情况我给团队的方案是做一个能力开关后端下发一个配置项voiceEnabled是true才渲染语音按钮否则只展示普通输入框。如果产品要求在老旧机型上也要能用那就只能走原生壳方案在原生代码里调用系统的语音识别再通过 JSBridge 把结果送给 Vue。这种方案虽然要写原生代码但体验和稳定性是最高的。5.4 性能与体验优化技巧最后分享几个我在实际使用中积累的优化经验。关于 TTS 的语速和音调做数据播报的时候建议把rate调到 1.2 到 1.5听起来更有播报感。如果只是给视力障碍用户做朗读辅助rate保持 1.0 最自然。音量其实不建议调默认 1 就行浏览器自己会跟随系统音量。关于语音识别的体验一定要给用户清晰的正在识别反馈。我除了把按钮上的文字从开始识别切换成停止识别之外还会让按钮带一个脉冲动画同时把临时识别结果用灰色展示出来。有了这两个视觉反馈用户才能确认系统确实在听着他说。关于朗读进度的展示如果你有原文展示需求可以基于onboundary事件做文字高亮。Chromium 内核下charIndex给的是 UTF-16 编码索引在做中文字符串切割时建议用Array.from转成数组再按索引取位置否则遇到表情符号或者特殊字符下标会对不上。6. 语音播报的扩展思路与替代方案写到这里TTS 的基础用法已经覆盖得差不多了但实际项目总会有一些特殊要求比如指定某个音色、让声音更像真人主播。如果你做完原生方案后觉得不够用我再补充几条扩展思路。我的建议是按照需求阶梯来选方案。如果只是内部工具、轻量配额、大屏播报原生 TTS 足够。如果用户上传了一篇文章希望生成一段可以保存下载的 MP3 文件那浏览器自带的语音合成是做不到文件导出的SpeechSynthesis只负责播放不提供音频流导出能力。这时就必须过渡到云厂商的 TTS 接口拿到音频文件再交给用户下载。还有一种混合方案我在项目中用过先用浏览器原生 TTS 做即时预览用户觉得语速、文本没问题之后再去调用云端接口生成正式音频文件。这种预览用本地、成品用云端的组合既保证了交互的流畅性又满足了最终的质量要求。如果你后续接入云厂商的语音服务Vue 封装思路是一样的。useSpeechSynthesis这个组合式函数可以提供一个synthesize()方法内部可以是调用浏览器 API也可以是请求云服务接口组件层不需要变。音频播放则统一交给浏览器audio元素把 MP3 的 URL 丢给它就行。这套按需替换的思想能让你的语音模块在项目里始终保持灵活性。我在一次接入云端音色的时候就是直接改动useSpeechSynthesis内部的buildUtterance逻辑把它从SpeechSynthesisUtterance换成了 HTTP 请求组件代码一行没动。这种抽象带来的收益在项目后期真的很明显。7. 写在最后的实操心得做语音文字互转这个功能踩得最深的一个坑其实是需求方口中的能做和浏览器实际能力之间的差距。TTS 这块浏览器比我们想象中靠谱得多语音识别这块则要时刻记住它是互联网服务的一部分有网络、有权限、有 HTTPS 门槛而且不同浏览器体验差异极大。我个人的建议是在 Vue 项目里做 TTS放心大胆用原生接口只要把语音选择、长文本分段、状态管理这三件事处理好体验已经足够好用。语音识别则要看场景管理后台、大屏项目这类偏内部系统可以用原生面向外部用户的 C 端页面优先考虑接可靠的云服务否则用户投诉识别不准的工单会把你埋掉。另外还要提醒一句不管用哪种方案语音功能都一定记得做无痕降级。不是每个用户都有麦克风不是每个用户都愿意开权限页面里保留传统输入方式永远是对的。语音交互是加分项但不是拦路虎。好的产品应该让用户想用语音的时候用得顺手不想用语音的时候完全不打扰这才是语音功能在页面里应该有的样子。
返回列表