Next.js + Web Speech API 实现浏览器内实时语音转文字 简介这是一份面向前端开发者与Web全栈学习者的Next.js实战项目资源聚焦实时语音转文本这一典型人机交互场景解决浏览器端语音采集、流式识别与即时文本呈现的技术落地问题。资源包共20个文件涵盖5个JSON配置与数据文件、3个TSX页面组件、3个JS工具脚本、2个SVG图标及CSS样式、TS类型定义、MD文档与DOCX说明等结构清晰体现Next.js App Router规范含app/layout.tsx、page.tsx、globals.css与工程化配置next.config.js、tailwind.config.js、tsconfig.json压缩包仅132KB轻量易读。已有71人学习下载适合中初级开发者通过可运行代码快速掌握Web Speech API集成、麦克风权限控制、实时识别状态管理及Next.js SSR适配要点。读者可直接启动项目体验语音录制→实时转录全流程并基于源码理解音频流处理逻辑、错误降级策略与响应式UI实现细节。1. 这不是“调个 API 就完事”的语音转写 DemoNext.js Web Speech API 实现真·浏览器内实时语音转文本零后端依赖、无模型部署、麦克风直连识别——适合快速验证语音交互原型、教育类听写工具、无障碍辅助输入场景的轻量级落地方案你可能已经试过用 Python 调 Whisper 模型、也跑通过 WebSocket 接语音流、甚至在 React 里封装过 SpeechRecognition 对象……但真正卡住多数人的从来不是“怎么识别”而是“怎么让识别结果在用户说话时就一行行冒出来且不卡顿、不丢字、不乱序同时还能随时暂停/重录/导出”。这个 Next.js 项目不碰 PyTorch、不搭 FastAPI、不申请云语音服务密钥——它把整套流程压进浏览器从navigator.mediaDevices.getUserMedia({ audio: true })拿到原始音频流到SpeechRecognition实例监听result事件再到用 React State useEffectuseRef做增量拼接与防抖更新最后用 Next.js App Router 的 Server Components 做静态资源托管与 SSR 友好渲染。它不是为生产级高并发设计的但它是目前我见过最干净、最易调试、最贴近“所见即所得”语音交互体验的前端实现。如果你正要给某高校语言实验室做课堂听写小工具、为某跨平台系统加语音笔记入口、或需要快速验证一个语音指令原型是否成立——这个 ZIP 包里的代码就是你该先 unzip 并npm run dev的起点。2. 为什么选 Web Speech API 而不是 Whisper.js 或 WebAssembly 模型技术选型背后的三个硬约束与一次血泪经验2.1 浏览器原生能力优先Web Speech API 的不可替代性Web Speech API特别是SpeechRecognition接口是 W3C 标准Chrome、Edge、Opera 原生支持无需加载百 MB 模型权重、不触发 CORS 跨域限制、不消耗 GPU 显存。它的识别引擎直接调用操作系统级语音服务Windows 下走 Windows Speech RecognitionmacOS 下走 Siri 后端响应延迟稳定在 300–800ms远低于前端加载 Whisper.js约 12MB WASM 200MB 模型后首次 warm-up 的 2–5 秒冷启动时间。更重要的是它天然支持连续识别continuous: true和实时流式输出interimResults: true——这意味着用户每说一个词event.results[i][0].transcript就会返回当前置信度最高的片段而event.results[i][0].isFinal false的 interim 结果可实时渲染为灰色暂态文字isFinal true时再转为黑色定稿。这种“边说边显”的体验是任何一次性上传音频文件再等待回调的方案无法模拟的。提示Web Speech API 不是“免费午餐”。它依赖设备本地语音服务因此中文识别质量受系统语言设置、麦克风硬件、环境噪音影响极大它不提供 speaker diarization说话人分离它不返回时间戳或音素对齐信息。但它完美匹配本项目的定位轻量、即时、免部署、可调试。2.2 Next.js 的角色不是为了“用 Next.js”而是为了“绕过 React 的 hydration 炸弹”很多开发者尝试在纯 React 中实现语音识别却在首次npm run build serve -s build后发现页面白屏、控制台报错SpeechRecognition is not defined。原因在于SpeechRecognition是 window 全局对象只存在于浏览器环境而 React 的 SSR服务端渲染阶段在 Node.js 中执行window未定义。若你在useEffect外直接 new SpeechRecognition()SSR 会直接 crash。Next.js 的解法很务实它默认对 Client Components 做 hydration 隔离。本项目将核心识别逻辑封装在components/VoiceRecorder.tsx中并明确标注use client强制该组件仅在浏览器端挂载。同时利用 Next.js App Router 的loading.tsx和error.tsx文件优雅降级——当用户用 Safari不支持 Web Speech API访问时自动显示提示而非白屏崩溃。2.3 为什么不用react-speech-recognition这类封装库我们实测了react-speech-recognition4.4.0在 Next.js 14 App Router 下的兼容性其内部useEffect依赖SpeechRecognition实例但在app/目录下由于 Server Components 默认无状态该 Hook 会在服务端执行并报错。社区 workaround 是包裹ClientOnly组件但增加了抽象层一旦识别失败调试链路拉长React Hook → 库内部状态 → Web Speech Event → DOM Ref。本项目选择裸写 Web Speech API 调用所有关键逻辑集中在 120 行以内的useVoiceRecognition.ts自定义 Hook 中// hooks/useVoiceRecognition.ts use client; import { useState, useEffect, useRef } from react; export const useVoiceRecognition () { const [isListening, setIsListening] useState(false); const [transcript, setTranscript] useState(); const [interimTranscript, setInterimTranscript] useState(); const recognitionRef useRefSpeechRecognition | null(null); useEffect(() { // 仅在浏览器环境初始化 if (typeof window ! undefined SpeechRecognition in window) { const SpeechRecognition (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition; recognitionRef.current new SpeechRecognition(); const rec recognitionRef.current; rec.continuous true; // 关键开启连续识别 rec.interimResults true; // 关键开启暂态结果 rec.lang zh-CN; // 中文识别可动态切换 rec.maxAlternatives 1; rec.onresult (event: SpeechRecognitionEvent) { let finalTranscript ; let interimTranscript ; for (let i event.resultIndex; i event.results.length; i) { const transcript event.results[i][0].transcript; if (event.results[i][0].isFinal) { finalTranscript transcript ; } else { interimTranscript transcript ; } } setTranscript(prev prev finalTranscript); setInterimTranscript(interimTranscript); }; rec.onerror (event: SpeechRecognitionErrorEvent) { console.error(Speech recognition error, event.error, event.message); setIsListening(false); }; rec.onend () { if (isListening) { // 自动重启维持连续监听需用户授权一次后持续有效 rec.start(); } }; } return () { if (recognitionRef.current) { recognitionRef.current.stop(); } }; }, []); const startListening () { if (recognitionRef.current) { try { recognitionRef.current.start(); setIsListening(true); } catch (err) { console.error(Failed to start speech recognition, err); } } }; const stopListening () { if (recognitionRef.current) { recognitionRef.current.stop(); setIsListening(false); } }; const reset () { setTranscript(); setInterimTranscript(); }; return { isListening, transcript, interimTranscript, startListening, stopListening, reset, }; };这段代码的关键参数说明rec.continuous true识别结束后自动重启监听避免用户每说一句都要点一次按钮rec.interimResults true必须开启否则event.results[i][0].isFinal永远为 true失去“边说边显”能力rec.lang zh-CN中文识别语言码若需支持英文混合可设为zh-CN中文为主兼容英文词或en-USrec.maxAlternatives 1只返回最高置信度结果减少冗余计算useEffect cleanup确保组件卸载时停止识别防止内存泄漏和后台持续录音。3. 从 ZIP 解压到浏览器看到“正在聆听…”五步完成本地开发环境搭建与功能验证3.1 解压与依赖安装确认 Node.js 版本与包管理器一致性下载 ZIP 后解压到任意目录如~/projects/nextjs-voice-transcribe打开终端进入该目录cd nextjs-voice-transcribe本项目基于 Next.js 14.2App Router、React 18.3、TypeScript 5.4 构建。请确认本地 Node.js 版本 ≥ 18.17.0node -v查看node -v # 输出应为 v18.17.0 或更高若版本过低请使用 nvm 升级非必需但避免潜在兼容问题# macOS/Linux curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 18.17.0 nvm use 18.17.0安装依赖推荐使用 npm因 package.json 中 scripts 为 npm 语法npm install注意不要运行yarn install或pnpm install除非你手动修改了package.json中的 scripts。本项目未锁死包管理器但npm run dev是唯一被验证的启动命令。3.2 启动开发服务器绕过 Chrome 的 HTTPS 限制Next.js 开发服务器默认运行在http://localhost:3000但 Web Speech API 要求getUserMedia()必须在安全上下文secure context中调用。Chrome 对http://localhost是放行的但对http://127.0.0.1或自定义 host 则可能拒绝麦克风权限。启动服务npm run dev等待控制台输出✓ Ready in 1243ms ○ Compiling /page ... ✓ Compiled successfully此时打开浏览器必须访问http://localhost:3000不能是http://127.0.0.1:3000否则点击“开始录音”按钮时浏览器会静默拒绝麦克风请求控制台无报错但recognition.start()抛异常。3.3 首次麦克风授权一次授权长期有效首次访问http://localhost:3000页面顶部会出现浏览器原生权限弹窗“此网站希望使用您的麦克风”。点击“允许”。提示若误点“阻止”需手动在 Chrome 地址栏左侧点击锁形图标 → “网站设置” → “麦克风” → 将localhost:3000设为“允许”。Safari 用户需在“Safari 设置 → 网站 → 麦克风”中单独授权。授权成功后“开始录音”按钮变为绿色点击即可触发startListening()。此时页面应显示顶部状态栏“正在聆听…实时转录中”中央大文本框灰色暂态文字如“你好今天”随语音实时滚动若停顿 1–2 秒灰色文字转为黑色定稿如“你好今天天气不错”3.4 功能按钮链验证暂停/继续、清空、导出文本页面底部有三组操作按钮需逐一验证暂停/继续按钮点击“暂停”后暂态文字停止更新状态栏变为“已暂停”再次点击“继续”识别自动恢复无需重新授权。清空按钮点击后黑色定稿与灰色暂态文字全部清空状态栏重置为“准备就绪”。导出文本按钮点击后触发浏览器原生download生成transcript_YYYYMMDD_HHMMSS.txt文件内容为当前所有黑色定稿文字不含暂态部分。验证导出文件内容是否与页面显示一致是判断transcriptstate 更新逻辑是否正确的最直接方式。3.5 模拟真实场景在嘈杂环境、不同语速、中英混说下测试鲁棒性不要只在安静书房测试。建议立即进行三轮压力测试环境噪音测试打开空调、播放背景音乐60dB 左右用手机外放《新闻联播》作为干扰源自己用正常音量说话。观察识别是否频繁插入“啊”、“呃”等填充词或把“苹果”识别成“平果”。语速适应测试先用慢速每秒 2 字说“今天学习语音识别技术”再加速到每秒 5 字接近日常对话上限观察 interim 文字是否出现大量乱码或跳字。中英混说测试说“打开 VS Code然后 run npm start”观察英文专有名词VS Code, npm是否被正确保留还是被强行音译为“维艾斯科德”、“恩皮姆”。这些测试不追求 100% 准确率Web Speech API 本身有局限但目标是确认系统不崩溃、不卡死、不丢失整句、暂态与定稿切换逻辑清晰。这是前端语音交互可用性的底线。4. 避坑指南五个真实翻车现场与对应解法——从麦克风静音到 Chrome 125 的 API 变更4.1 现象点击“开始录音”无反应控制台无报错但isListening始终为 false原因Chrome 125 版本对SpeechRecognition的初始化做了更严格的上下文检查。若new SpeechRecognition()被包裹在异步函数如async useEffect或条件判断中实例化会失败且静默忽略。解决严格按 2.3 节代码在useEffect同步块中初始化且确保typeof window ! undefined判断在最外层。删除任何await或if (someCondition)包裹。4.2 现象识别结果中大量出现“嗯”、“啊”、“这个”等填充词且无法通过grammar过滤原因Web Speech API 不支持自定义语法GrammarSpeechGrammarList在 Chrome 中已被废弃。填充词是语音引擎对停顿的默认补全。解决在onresult回调中增加后处理逻辑用正则过滤常见填充词// 在 onresult 处理循环内添加 const cleanTranscript (text: string) text .replace(/(嗯|啊|呃|哦|那个|这个|就是|其实|然后|但是|而且|所以|因为|如果|虽然|不过|然而|因此|于是|总之|另外|还有|比如|例如|看来|显然|当然|确实|真的|非常|特别|有点|稍微|大概|也许|可能|好像|似乎|感觉|觉得|认为|知道|明白|了解|清楚|熟悉|掌握|学会|理解|记住|忘记|想起|回忆|想到|意识到|注意到|发现|看到|听到|感到|体验|经历|尝试|努力|争取|希望|想要|需要|应该|必须|可以|能够|愿意|打算|计划|准备|考虑|决定|同意|拒绝|接受|放弃|支持|反对|赞成|批评|表扬|鼓励|安慰|提醒|警告|建议|推荐|要求|命令|请求|邀请|感谢|道歉|祝贺|慰问|告别|问候)/g, ) .replace(/\s/g, ) .trim(); // 然后用 cleanTranscript(transcript) 替代原始 transcript4.3 现象在 Safari 或 Firefox 中页面空白或提示“浏览器不支持语音识别”原因Safari 完全不支持SpeechRecognitionAPIFirefox 仅支持旧版mozSpeechRecognition且需手动启用media.webspeech.recognition.enable配置已废弃。解决在useVoiceRecognition.ts初始化前增加 UA 检测并在 UI 层降级// 在 useVoiceRecognition.ts 开头添加 const isSupported typeof window ! undefined (SpeechRecognition in window || webkitSpeechRecognition in window); if (!isSupported) { console.warn(Speech Recognition not supported in this browser); return { /* 返回空对象UI 层 render fallback */ }; }并在组件中{!isSupported ? ( div classNamep-4 bg-yellow-50 border-l-4 border-yellow-400 p classNametext-yellow-700⚠️ 当前浏览器不支持语音识别推荐使用 Chrome 或 Edge/p /div ) : ( /* 正常录音控件 */ )}4.4 现象识别过程中页面刷新或切 Tab再切回时识别中断且无法重新 start原因recognition.onend触发后若用户切走recognition.start()在后台被浏览器暂停切回时recognition.state可能为stopped或idle但start()不抛错也不生效。解决在startListening中增加状态校验与重置const startListening () { if (!recognitionRef.current) return; // 强制重置状态 try { recognitionRef.current.abort(); // 清除 pending 状态 } catch (e) { // ignore } try { recognitionRef.current.start(); setIsListening(true); } catch (err) { console.error(Failed to start after abort, err); } };4.5 现象导出的.txt文件编码为 ANSI中文显示为乱码Windows 记事本打开原因Blob默认编码为 UTF-8但 Windows 记事本旧版本默认用 GBK 解码。解决导出时在文本前添加 UTF-8 BOM 头const exportTranscript () { const content transcript.trim(); if (!content) return; // 添加 BOM\uFEFF const blob new Blob([\uFEFF content], { type: text/plain;charsetutf-8 }); const url URL.createObjectURL(blob); const a document.createElement(a); a.href url; a.download transcript_${new Date().toISOString().slice(0, 19).replace(/[-:]/g, )}.txt; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); };5. 进阶技巧如何把“实时转录”升级为“可编辑的语音笔记”并嵌入现有 Next.js 项目5.1 将语音转录结果双向绑定到富文本编辑器从“只读展示”到“说写一体”当前项目输出是纯文本p{transcript}/p但真实场景中用户往往需要边听边改——比如把“平果手机”手动修正为“iPhone 手机”或在句子间插入图片、链接。我们用contenteditableuseRef实现轻量级双向绑定不引入 Quill 或 Tiptap 等重型编辑器// components/EditableTranscript.tsx use client; import { useRef, useEffect } from react; import { useVoiceRecognition } from /hooks/useVoiceRecognition; export default function EditableTranscript() { const { transcript, interimTranscript } useVoiceRecognition(); const contentRef useRefHTMLDivElement(null); // 将 transcript 同步到 contenteditable useEffect(() { if (contentRef.current) { // 防止光标跳到开头保存当前 selection const selection window.getSelection(); const range selection?.getRangeAt(0); contentRef.current.textContent transcript; // 恢复光标位置简化版始终置于末尾 if (range contentRef.current.firstChild) { range.selectNodeContents(contentRef.current.firstChild); range.collapse(false); } } }, [transcript]); // 监听编辑变化反向更新 state可选若需保存编辑后文本 const handleInput () { if (contentRef.current) { // 这里可 dispatch action 更新全局 store或调用 API 保存 console.log(User edited:, contentRef.current.textContent); } }; return ( div classNameborder rounded-lg p-4 min-h-[200px] div ref{contentRef} contentEditable onInput{handleInput} classNameoutline-none w-full min-h-[150px] whitespace-pre-wrap spellCheckfalse {transcript || 语音转录内容将显示在此处...} /div {interimTranscript ( p classNametext-gray-500 text-sm mt-2 暂态{interimTranscript} /p )} /div ); }此方案优势零依赖、体积 2KB、与useVoiceRecognition完全解耦。用户说完后可直接在文本上删改、加粗、换行所有操作实时反映在 DOM 中无需额外“保存”按钮。5.2 参数对照表Web Speech API 关键配置项与实际效果映射配置项可选值默认值实际影响建议值continuoustrue/falsefalsefalse时每句结束需手动start()true时自动续听但需注意onend重入风险true本项目必需interimResultstrue/falsefalsefalse时只返回最终结果无实时反馈true时返回isFinal分层结果true本项目必需langzh-CN,en-US,ja-JP等en-US直接决定识别语言模型中文必须设zh-CN否则识别率暴跌zh-CN中文场景maxAlternatives1–101值越大返回备选结果越多但性能下降、results数组变长1平衡速度与精度serviceURI自定义语音服务地址Chrome 不支持自定义设了也无效仅 Firefox 旧版支持留空注意serviceURI在现代 Chrome 中已失效勿浪费时间配置。5.3 嵌入现有 Next.js 项目三步迁移法不破坏原有路由与样式假设你已有my-existing-app想把语音转录功能作为/notes/voice页面嵌入复制核心文件将 ZIP 中的app/voice/page.tsx、components/VoiceRecorder.tsx、hooks/useVoiceRecognition.ts复制到你的项目对应目录如app/notes/voice/page.tsx。调整路径引用检查page.tsx中的 import 路径将/hooks/...改为相对路径../../hooks/...或在你的tsconfig.json中配置baseUrl和paths。注入全局样式隔离为避免VoiceRecorder的 CSS 影响全局将其样式用 CSS Modules 封装// components/VoiceRecorder.module.css .container { max-width: 800px; margin: 0 auto; padding: 1rem; } .statusBar { background: #f0f9ff; border-left: 4px solid #3b82f6; }然后在组件中import styles from ./VoiceRecorder.module.css; div className{styles.container} div className{styles.statusBar}.../div /div从那以后我每次接到“加个语音输入”需求都不再第一反应去查 Whisper 部署文档而是打开这个 ZIPunzip→npm install→npm run dev→ 用 Chrome 访问localhost:30003 分钟内让客户听到自己的声音变成文字。它不解决所有问题但它把“能不能做”这个疑问压缩到了一次git clone的时间里。希望帮到你。本文还有配套的精品资源点击获取