ARTICLE DETAIL

资讯详情

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

用浏览器原生语音识别API实现React语音转文字工具

用浏览器原生语音识别API实现React语音转文字工具 简介针对语音转文字场景的React前端应用源码包基于Create React App构建利用浏览器原生Web Speech API实现语音识别转换适合希望在Web端快速集成语音输入功能的JavaScript开发者参考学习。资源共17个文件包含5个JS源码文件组件、识别逻辑、入口、2个CSS样式文件、2个JSON配置及2个Markdown说明文档整体仅117KB结构精简。已有672人学习下载。通过源码可了解React组件拆分、事件绑定、浏览器API调用方式描述中涉及代码分割、样式预处理、环境变量配置、API后端集成等CRA常用功能点README提供了从安装依赖到部署的指引便于按需查阅是入门React语音应用开发的轻量范例。 最近在做一个 voice-to-speech 的小项目说白了就是打开网页点一下按钮对着麦克风说话浏览器就把你说的话实时转成文字落到屏幕上。技术选型很直接前端用 React 管界面语音识别完全靠浏览器内置的 Web Speech API不接任何付费语音服务也不写后端。做语音转文字工具这是个人开发者最省事的一条路没有之一。很多朋友一听到“语音转文字”第一反应就是去注册云厂商的语音识别服务弄 Key、配 SDK、看计费规则一套流程下来还没开始写代码就想放弃了。而这个项目最大的意义在于现代浏览器早就把语音识别能力打包成了一个原生接口你只需要处理几个事件和方法十几行代码就能跑通最核心的功能。我用它做完了从拍脑袋到可用的完整闭环整个过程基本没踩到什么无法绕过的坑所以把整个项目从设计、实现到排错的过程整理成这篇文章。这篇文章适合两类人一是想快速给 React 项目加一个“语音输入”能力的前端开发者二是在第三方语音服务和浏览器原生方案之间纠结、想知道各自边界的产品技术朋友。看完你应该能自己搭出一个可复用的语音转文字组件并且知道哪些地方会出问题、怎么排查。1. 项目思路与整体方案设计1.1 为什么选浏览器内置 API而不是第三方语音服务先回答一个绕不开的问题市面上那么多大厂的语音识别服务准确率高、支持方言、还能定制热词为什么偏要用浏览器自带的 API最核心的原因是成本曲线完全不同。第三方服务比如云厂商的 Speech-to-Text 或 Azure 的语音服务确实做得更重、更准但你需要经历注册账号、绑定支付方式、创建密钥、封装鉴权、处理音频流上传、拉取识别结果。而且计费通常按音频时长或调用次数走一个测试工具如果忘记关跑一晚上第二天账单可能让你肉疼。内置的 Web Speech API 完全免费不需要密钥不需要后端转发麦克风权限一给就能用。对个人工具、内部 Demo、教学项目来说这才是最匹配的“开工方式”。准确率方面内置方案也不是完全不能打。在吐字清楚、环境安静的前提下中文识别结果可用度相当高做会议速记雏形或内容创作辅助完全够用第三方服务更强的地方主要在强噪场景、专业术语、方言理解以及更细粒度的断句和标点控制上。如果你后续确实发现识别质量是瓶颈也可以把架构上预留一个“识别器替换层”现在先快速跑通后面再接付费引擎。我实际做的方案就是把识别逻辑全部封装进一个 Hook 里组件完全不知道底层是浏览器 API 还是云服务这个设计后面详说。1.2 用 React 重新思考这个需求从 Vue 阵营切到 React 来做这个工具我最直观的感受是 React 的声明式状态管理让这类“监听设备 实时反馈”的需求写起来非常舒坦。语音识别的状态抽象出来就四类是否正在识别、最终文字、临时文字、错误信息。这四个状态在 React 里就是四个 hook 返回值UI 层只管根据这些状态渲染按钮样式和文本内容不用操心事件回调里手动操作 DOM。项目目录结构我也简单规划了一下保持小而清晰voice-to-speech/ ├── src/ │ ├── hooks/ │ │ └── useSpeechRecognition.ts │ ├── components/ │ │ ├── ControlPanel.tsx │ │ └── TranscriptView.tsx │ ├── App.tsx │ └── main.tsx组件拆分的思路是useSpeechRecognition负责所有和浏览器 API 打交道的逻辑ControlPanel放开始/停止/清空/复制按钮TranscriptView展示语义完整的最终文本和正在识别的临时文本。这样后面想换识别服务或者想换 UI 库都能在不推倒重来的前提下各自迭代。1.3 功能需求拆解先想清楚边界动工之前我给自己定了五个必须完成的点点击按钮开始/停止语音识别按钮状态要随识别状态切换。识别过程中已确定的文字和当前正在“犹豫”的文字要分开展示视觉上有层次。支持清空重录和复制结果这是最常被忽略但最常用的两个操作。识别结束或出错时有明确提示特别是麦克风被拒绝这种常见情况。最后结果可以导出成 txt 文件方便存稿。这些东西看着基础但真正影响使用的往往是这些细节而不是识别引擎本身。比如临时文字如果不单独展示用户对着屏幕会一直怀疑到底有没有在工作。2. 浏览器语音识别核心细节解析2.1 SpeechRecognition 接口与事件模型理解这套 API 的关键浏览器提供的语音识别接口标准名字叫SpeechRecognition但 Safari 和旧版 Chrome 需要用带前缀的webkitSpeechRecognition。这个东西本质上是一个长连接式的识别器对象不像是 HTTP 那种“请求-响应”模式更像是一个持续收听的麦克风小管家。你把参数配置好调用start()开始听然后它通过事件通知你现在听到什么、听完了没。事件模型是整个 API 最关键的部分我用下来最常用的有四类onresult识别引擎返回了内容无论是临时的还是最终的都会触达这里需要自己判断isFinal。onerror任何错误都会走到这里比如网络问题、无麦克风、权限拒绝、没有语音等。onend识别会话结束不管正常听完了还是中途出错最终一定会触发这个事件。onstart/onspeechstart前者是识别器启动成功后者是检测到人声开始说话可以用来做 UI 反馈。理解这些事件最大的好处是你不会被“按钮点了没反应”这种事困扰。比如 Chrome 里如果网络不通onerror会给出network错误如果用户拒绝权限会给not-allowed如果说了半天没检测到声音会给no-speech。把这些错误状态映射成用户能看懂的中文提示体验会上一个台阶。2.2 几个关键参数配置错了效果天差地别SpeechRecognition实例上有几个属性我不建议直接抄网上的配置就完事最好理解一下再改。continuous这个参数控制识别器是否在用户停顿后持续监听。设为false的话你说一句话停下来识别器就自动结束适合做语音指令我这个应用做的是连续转写所以设为true这样中途喘口气、想一下措辞都不会断。interimResults决定了是否返回识别过程中的临时结果。大家用过输入法的语音输入应该有体验你还在说话时屏幕上有“正在听”的文字等你说完它会自动校准成最终语句。这就是临时结果和最终结果的区别。要做出“实时字幕”效果必须把这个值设为true否则你得到的是用户话说完之后一整段一次性吐出来互动感差很多。lang是语言代码zh-CN是简体中文。这里有个小坑如果你想同时识别中英文混说单个lang参数搞不定浏览器原生 API 目前只能以单一主语言为主中英混说场景下识别率会比较飘这属于能力边界后面扩展章节我再给方案。还有一个容易被忽略的细节maxAlternatives属性可以让引擎返回多个候选结果。默认是 1也就是只给一个最自信的结果如果设成 3你可以在result里拿到多个排序好的候选文本有时第一名不对第二名反而是你想要的。2.3 安全上下文与浏览器兼容性门槛很多新手第一次跑起来发现点了开始没反应控制台报The API is not supported in non-secure contexts这是 Web Speech API 最硬的限制之一只能在安全上下文HTTPS或localhost下运行。也就是说如果你直接把页面部署到一个纯 HTTP 的服务器上这个功能是彻底不可用的。开发阶段用localhost没问题真上线要么走 HTTPS要么用带自动 HTTPS 的托管平台。浏览器兼容性方面桌面端最省心的是 Chrome 和 Edge支持最完整Firefox 需要配置开关才支持而且默认关闭Safari 从 14 开始部分支持新版本已经能跑主要流程但行为细节和 Chrome 有差异。移动端 Android 的 Chrome 表现不错iOS 的 Safari 对continuous的支持不理想很多时候说一句话就自动停了需要我手动重启。所以如果目标以桌面端为主这个方案能覆盖大部分用户如果重度依赖移动端得做更多兼容测试甚至考虑降级方案。注意SpeechRecognition在 Chrome 的实现里音频数据实际会发送到谷歌的服务器做识别并不是完全离线本地计算。对数据隐私有硬性要求的场景不要用这个方案需要回到私有化部署的 ASR 引擎。3. 实操用 React 实现一个完整的语音转文字应用3.1 创建项目并封装 useSpeechRecognition Hook项目脚手架直接用 Vite 搭 React TypeScript干净又快。运行命令npm create vitelatest voice-to-speech -- --template react-ts cd voice-to-speech npm install我不太建议在这类小项目里直接用 CRA启动速度、依赖体积都不如 Vite 舒服。装完基础依赖之后核心动作就是封装那个自定义 Hook。先给 TypeScript 补上 SpeechRecognition 的类型声明因为 TS 默认不认识这些浏览器 API// src/types/speech.d.ts interface SpeechRecognitionAlternative { transcript: string; confidence: number; } interface SpeechRecognitionResult { isFinal: boolean; length: number; [index: number]: SpeechRecognitionAlternative; } interface SpeechRecognitionResultList { length: number; [index: number]: SpeechRecognitionResult; } interface SpeechRecognitionEvent extends Event { resultIndex: number; results: SpeechRecognitionResultList; } interface SpeechRecognitionErrorEvent extends Event { error: string; message: string; } interface SpeechRecognition extends EventTarget { lang: string; continuous: boolean; interimResults: boolean; maxAlternatives: number; start(): void; stop(): void; abort(): void; onresult: ((event: SpeechRecognitionEvent) void) | null; onerror: ((event: SpeechRecognitionErrorEvent) void) | null; onend: (() void) | null; onstart: (() void) | null; } declare var SpeechRecognition: { prototype: SpeechRecognition; new (): SpeechRecognition; }; declare var webkitSpeechRecognition: { prototype: SpeechRecognition; new (): SpeechRecognition; };接下来是 hook 本体我把完整代码贴出来并逐段说// src/hooks/useSpeechRecognition.ts import { useCallback, useEffect, useRef, useState } from react; interface UseSpeechRecognitionOptions { lang?: string; continuous?: boolean; interimResults?: boolean; maxAlternatives?: number; } interface UseSpeechRecognitionReturn { isSupported: boolean; isListening: boolean; transcript: string; interimTranscript: string; error: string | null; startListening: () void; stopListening: () void; resetTranscript: () void; } export function useSpeechRecognition({ lang zh-CN, continuous true, interimResults true, maxAlternatives 3, }: UseSpeechRecognitionOptions {}): UseSpeechRecognitionReturn { const [isSupported] useState(() { if (typeof window undefined) return false; return SpeechRecognition in window || webkitSpeechRecognition in window; }); const [isListening, setIsListening] useState(false); const [transcript, setTranscript] useState(); const [interimTranscript, setInterimTranscript] useState(); const [error, setError] useStatestring | null(null); const recognitionRef useRefSpeechRecognition | null(null); useEffect(() { if (!isSupported) return; const SpeechRecognitionImpl window.SpeechRecognition || window.webkitSpeechRecognition; const recognition new SpeechRecognitionImpl(); recognition.lang lang; recognition.continuous continuous; recognition.interimResults interimResults; recognition.maxAlternatives maxAlternatives; recognition.onresult (event) { let finalText ; let interimText ; for (let i event.resultIndex; i event.results.length; i) { const result event.results[i]; if (result.isFinal) { finalText result[0].transcript; } else { interimText result[0].transcript; } } setTranscript((prev) prev finalText); setInterimTranscript(interimText); }; recognition.onerror (event) { const errorMap: Recordstring, string { not-allowed: 麦克风权限被拒绝请在浏览器设置中允许访问麦克风。, service-not-allowed: 浏览器禁止使用语音识别服务。, no-speech: 没有检测到语音请靠近麦克风再试。, network: 网络连接出错无法访问识别服务。, aborted: 识别被中断。, }; setError(errorMap[event.error] || 识别出错${event.error}); }; recognition.onend () { setIsListening(false); // 单独处理为手动停止时清空临时文字 setInterimTranscript(); }; recognitionRef.current recognition; return () { recognition.abort(); recognitionRef.current null; }; }, [isSupported, lang, continuous, interimResults, maxAlternatives]); const startListening useCallback(() { const recognition recognitionRef.current; if (!recognition) return; setError(null); setTranscript(); setInterimTranscript(); try { recognition.start(); setIsListening(true); } catch (err) { setError(启动识别失败可能正在识别中或浏览器不支持。); } }, []); const stopListening useCallback(() { recognitionRef.current?.stop(); }, []); const resetTranscript useCallback(() { setTranscript(); setInterimTranscript(); setError(null); }, []); return { isSupported, isListening, transcript, interimTranscript, error, startListening, stopListening, resetTranscript, }; }这段代码里最需要留意的是onresult里的循环逻辑。event.resultIndex表示这次事件里新增的结果从哪个下标开始我从这个下标遍历到结尾对每一条结果判断是否最终状态。最终结果追加到正式文本transcript非最终结果覆盖写入interimTranscript。这样设计才能保证在连续识别模式下用户不断说话时页面上的临时文字一直在跟着变而已经确定的历史文本不会被重复拼接。另一个容易踩的坑是recognition.start()不能连续调用两次否则会抛异常。所以我在startListening里包了 try-catch并且通过isListening状态在 UI 层保证按钮在开始之后会被替换成停止按钮从源头避免重复触发。如果你的界面里没有做这种互斥程序里就要对isListening做一次判定否则会有奇怪的行为。3.2 页面组件与交互细节实现控制面板组件我做得非常简单但按钮的禁用逻辑不能省// src/components/ControlPanel.tsx interface ControlPanelProps { isListening: boolean; hasTranscript: boolean; onStart: () void; onStop: () void; onReset: () void; onCopy: () void; onExport: () void; } export function ControlPanel({ isListening, hasTranscript, onStart, onStop, onReset, onCopy, onExport, }: ControlPanelProps) { return ( div classNamecontrol-panel {isListening ? ( button classNamebtn stop onClick{onStop} 停止识别 /button ) : ( button classNamebtn start onClick{onStart} 开始说话 /button )} button classNamebtn onClick{onReset} disabled{!hasTranscript !isListening} 清空 /button button classNamebtn onClick{onCopy} disabled{!hasTranscript} 复制 /button button classNamebtn onClick{onExport} disabled{!hasTranscript} 导出 txt /button /div ); }识别状态提示和文本展示我用一个 status 字段区分四种状态空闲、聆听中、出错、已就绪。聆听中会在界面上出现一个醒目的红点跳动动画这种视觉反馈非常重要不然用户会对麦克风到底有没有开产生怀疑// src/components/TranscriptView.tsx interface TranscriptViewProps { transcript: string; interimTranscript: string; isListening: boolean; } export function TranscriptView({ transcript, interimTranscript, isListening }: TranscriptViewProps) { return ( div classNametranscript-view p classNamefinal-text{transcript}/p {interimTranscript p classNameinterim-text{interimTranscript}/p} {!transcript !interimTranscript ( p classNameplaceholder {isListening ? 正在聆听请开始说话…… : 点击按钮开始语音输入} /p )} /div ); }这里有一个体验细节值得强调临时文字和最终文字的样式必须明显不同。最终文字用深色正常字号临时文字用浅色或带底部虚线让用户知道“这句话还没定稿可能会被引擎修正”。我最初没有做这种区分用户反馈说“为什么我说完那些字还在变”以为是 bug其实就是把两类结果混在一起展示造成的误解。在App.tsx里我把复制和导出的逻辑也加上了。复制用navigator.clipboard.writeText注意它只能在 HTTPS 或 localhost 下使用这和语音识别的安全上下文要求刚好一致导出则是生成一个 Blob 对象然后触发一次模拟点击下载// src/App.tsx import { useSpeechRecognition } from ./hooks/useSpeechRecognition; import { ControlPanel } from ./components/ControlPanel; import { TranscriptView } from ./components/TranscriptView; function App() { const { isSupported, isListening, transcript, interimTranscript, error, startListening, stopListening, resetTranscript, } useSpeechRecognition({ lang: zh-CN }); if (!isSupported) { return ( div classNameunsupported h2当前浏览器不支持语音识别/h2 p请使用最新版 Chrome、Edge 或 Safari 访问本页面。/p /div ); } const handleCopy async () { try { await navigator.clipboard.writeText(transcript); alert(已复制到剪贴板); } catch { alert(复制失败请手动选择文本复制); } }; const handleExport () { const blob new Blob([transcript], { type: text/plain;charsetutf-8 }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download voice-transcript-${Date.now()}.txt; a.click(); URL.revokeObjectURL(url); }; return ( main classNameapp h1Voice-to-Speech 语音转文字/h1 ControlPanel isListening{isListening} hasTranscript{transcript.length 0} onStart{startListening} onStop{stopListening} onReset{resetTranscript} onCopy{handleCopy} onExport{handleExport} / {error div classNameerror-banner{error}/div} TranscriptView transcript{transcript} interimTranscript{interimTranscript} isListening{isListening} / /main ); } export default App;3.3 样式与体验优化做一个能看的界面样式方面我没有什么炫技的地方就是一个居中的卡片式布局但有几个点对使用体验帮助很大状态红点动画、错误提示的浅红底、最终文字和临时文字的字体区分。核心 CSS 大概是这样.app { max-width: 720px; margin: 60px auto; padding: 24px; font-family: system-ui, -apple-system, sans-serif; } .control-panel { display: flex; gap: 12px; margin: 24px 0; flex-wrap: wrap; } .btn { padding: 10px 20px; border: none; border-radius: 8px; cursor: pointer; font-size: 15px; } .btn.start { background: #2563eb; color: #fff; } .btn.stop { background: #dc2626; color: #fff; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } .status-dot { width: 10px; height: 10px; border-radius: 50%; background: #dc2626; display: inline-block; margin-right: 8px; animation: pulse 1.2s infinite; } keyframes pulse { 0% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(1.4); } 100% { opacity: 1; transform: scale(1); } } .transcript-view { min-height: 260px; border: 1px solid #e5e7eb; border-radius: 12px; padding: 20px; line-height: 1.8; background: #fafafa; } .final-text { font-size: 18px; color: #1f2937; margin: 0; } .interim-text { font-size: 18px; color: #9ca3af; margin: 8px 0 0 0; border-bottom: 1px dashed #d1d5db; } .error-banner { background: #fef2f2; color: #991b1b; padding: 12px 16px; border-radius: 8px; margin: 16px 0; }样式的核心逻辑就一个让用户一眼看出“当前哪个区域是已经确定的内容哪个区域还没定稿整个工具是不是正在运行”。4. 常见问题与排查技巧实录4.1 识别器启动失败、麦克风权限被拒这一类问题我实际开发中遇到的第一大类问题就是点了“开始说话”没任何反应。排查顺序固定在下面这条路线上页面是不是localhost或 HTTPS不是的话SpeechRecognition 会直接不可用。浏览器是不是 Chrome/EdgeFirefox 默认关掉这个功能别在 Firefox 上调试。麦克风权限是不是被拒绝了页面地址栏右侧能看到麦克风图标如果是打叉状态需要手动改回允许。系统层面麦克风是不是被别的软件占用比如你正开着会议软件浏览器可能拿不到采集通道。not-allowed这个错误最常出现在用户第一次点击时手滑点了“阻止”或者之前在浏览器设置里全局禁用了麦克风。这时页面需要给一个清晰的引导文案而不是一个干巴巴的 “ERROR: not-allowed”。我上面代码里已经做了错误映射基本能帮用户定位到具体原因。4.2 识别中断、识别结果为空、准确率飘忽不定第二个高发问题也是被问最多的说几句话之后识别器自己停了。这个现象在 Chrome 里大多数是因为continuous被设成了false另一种可能是长时间没有说话引擎超时自动结束。解决办法是continuous: true并在onend里判断如果当前还处于“应该继续听”的状态就自动重启识别。不过自动重启有风险需要把握好时机否则会陷入无限循环我的做法是加一个“是否用户手动停止”的标记只有非手动停止时才重启。识别结果为空也很常见。一种情况是interimResults设了false临时结果不返回而最终结果可能因为断句一直没到导致看起来“什么都没识别到”。另一种情况是距离麦克风太远尤其笔记本内置麦克风收音本来就一般需要靠近一点说话。准确率飘忽最典型的中文场景是中英混说前面说过lang只能设一种主语言纯中文环境没问题一混入英文单词就经常把英文也变成中文谐音。一个勉强能用的技巧是把lang设为zh-CN然后在文本里做后处理正则清洗把明显的英文谐音纠正回英文但这只是权宜之计效果没法跟专门的双语模型比。4.3 部署到线上后的环境差异问题本地一切正常部署到线上出问题的案例我见过不少主要坑有两类。第一类是 HTTPS。如果部署平台没有启用 HTTPS语音识别和剪贴板写入都会静默失败。现在主流托管平台都默认带 HTTPS但如果你内网自建服务器或者用某个不规范的静态托管这就是第一个检查点。第二类是浏览器限制差异。Chrome 对连续识别的支持最好但长时间使用会累积内存占用我建议在onend里如果检测到已经持续识别超过一定分钟数就提示用户手动停止再重新开始。Safari 对continuous的支持不够稳定同一个页面在 Mac 的 Safari 和 Chrome 上行为可能不一样需要在不同浏览器上各测一遍。提示上线前一定要做一次“拒绝麦克风权限”的完整流程测试确保错误提示对用户是可见且可理解的而不是变成控制台里的一行红色日志。5. 项目扩展方向与进阶思考5.1 用热词和文本后处理提升中文识别效果使用过程中我明显感觉到通用引擎对专有名词、人名、产品名的识别非常不稳定。比如我说“React 和 Vue 的区别”它可能把“React”识别成“瑞艾克特”“Vue”识别成“V”。既然底层引擎不提供热词注册能力我的做法是在结果文本上做一层后处理维护一个常见误转的替换字典在onresult拿到最终文本后执行正则替换。const DICT: [RegExp, string][] [ [/瑞艾克特|艾克特|瑞可特/g, React], [/由衣|维修|惟/g, Vue], [/泰普斯克利普特/g, TypeScript], [/圣克吉|圣克/g, Socket], ]; function normalizeText(text: string): string { let result text; for (const [pattern, replacement] of DICT) { result result.replace(pattern, replacement); } return result; }这个方案没法和真正的热词机制比但成本极低对固定场景下的专业术语纠错很有效果。词典的数据来源可以直接收集用户实际识别错误的样例一段时间后效果会越来越好。5.2 多语言切换、标点回填与更丰富的导出能力语言切换做起来很简单把lang参数开放成可配置的下拉选择项切换后重新创建识别器实例即可。但中文识别和英文识别在标点习惯上差别很大英文文本没有中文那么多顿号而中文长句如果不自动加标点阅读体验很差。一种务实方案是接一个轻量的本地标点预测模型在拿到完整文本后做标点回填但这会增加项目复杂度。更简单的做法是结合后端大模型处理文本内容已经是文字直接调用文本大模型做格式化加标点、去掉口癖、整理成结构化内容。这种“语音识别 大模型整理”的组合在当前开发成本下非常划算。导出能力也可以继续做深目前是纯文本 .txt后续可以支持 Markdown、SRT 字幕、或直接同步到剪贴板和本地存储。做字幕方向的话需要把每句话的时间戳信息保留下来不过当前 Web Speech API 对时间戳的暴露很有限可能需要配合录音音频自行做 VAD 切片复杂度会上升不少。5.3 把 Hook 抽象成通用能力沉淀成团队工具最后一件事如果团队里多个项目都需要语音输入建议把你封装好的useSpeechRecognition抽成独立 npm 包或内部组件库用前做浏览器能力检测用的时候提供统一的回调接口。识别服务本身可以做成可插拔的底层默认浏览器 API但通过适配器接口可以切换到付费云服务。我在项目里预留了这个设计实际切换的成本比想象中低得多因为组件层只认isListening、transcript这些抽象状态不关心底层实现。做下来我最大的感受是浏览器内置 API 虽然不完美但它在“零成本原型验证”这个阶段是完全够用的你唯一需要确保的是理解它的边界在哪里以及错误路径要对用户足够友好。这个项目从零到能用的全部代码量不到两百行如果你也想做一个自己专属的语音转文字工具现在就可以打开终端开始搭建了。本文还有配套的精品资源点击获取
返回列表