第38期 | 语音AI前端

发布时间:2026/6/27 4:01:10

第38期 | 语音AI前端 第38期 | 语音AI前端 今天你将学会理解浏览器语音能力的两大 APIWeb Speech API Audio API实现语音识别界面录音 → 转文字 → 交给 AI 处理实现语音合成界面AI 文字 → 转语音 → 播放处理语音交互的特殊 UX 设计录音状态、中断、实时反馈 核心知识语音 AI 的前端能力矩阵语音交互涉及两种核心能力听识别和说合成。能力API状态支持度语音识别Web Speech API (SpeechRecognition)实验性Chrome/Edge/三星浏览器语音合成Web Speech API (SpeechSynthesis)稳定所有主流浏览器音频录制MediaRecorder API稳定所有主流浏览器音频分析AudioContext / AnalyserNode稳定所有主流浏览器关键限制SpeechRecognition 在 Firefox/Safari 中不支持。解决方案方案优点缺点Web Speech API免费、浏览器原生Chrome 限定Whisper APIOpenAI准确度高、多语言每分钟 $0.006、需要后端云厂商语音服务准确度高、稳定按量计费推荐方案优先 Web Speech API免费降级到 Whisper API付费。语音识别界面实现核心组件拆分VoiceChat语音聊天界面 ├── VoiceRecorder录音控制 │ ├── RecordButton录音按钮点击开始/停止 │ ├── AudioVisualizer实时音频波形可视化 │ └── TranscriptionDisplay实时转文字显示 ├── VoiceResponse语音回复 │ ├── TextDisplayAI 回复文字 │ ├── PlayButton播放语音按钮 │ └── PlaybackProgress播放进度条 └── VoiceChatStoreZustand 状态管理VoiceRecorder 组件// features/voice/components/VoiceRecorder.tsx import { useState, useRef, useEffect } from react; import { Mic, MicOff, Loader2 } from lucide-react; interface VoiceRecorderProps { onTranscription: (text: string) void; onError?: (error: string) void; } export function VoiceRecorder({ onTranscription, onError }: VoiceRecorderProps) { const [isRecording, setIsRecording] useState(false); const [interimText, setInterimText] useState(); // 实时中间结果 const recognitionRef useRefSpeechRecognition | null(null); // 初始化 SpeechRecognition useEffect(() { const SpeechRecognition window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { onError?.(您的浏览器不支持语音识别请使用 Chrome); return; } const recognition new SpeechRecognition(); recognition.continuous true; // 持续录音不是单次 recognition.interimResults true; // 返回中间结果实时显示 recognition.lang zh-CN; // 中文识别 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; } } // 实时显示中间结果 setInterimText(interim); // 最终结果传给回调 if (final) { onTranscription(final); setInterimText(); } }; recognition.onerror (event) { setIsRecording(false); switch (event.error) { case not-allowed: onError?.(请允许浏览器使用麦克风); break; case no-speech: onError?.(没有检测到语音请再试一次); break; default: onError?.(语音识别错误: ${event.error}); } }; recognition.onend () { setIsRecording(false); }; recognitionRef.current recognition; }, [onTranscription, onError]); const startRecording () { if (recognitionRef.current) { recognitionRef.current.start(); setIsRecording(true); } }; const stopRecording () { if (recognitionRef.current) { recognitionRef.current.stop(); setIsRecording(false); } }; return ( div classNamespace-y-3 {/* 录音按钮 */} div classNameflex items-center justify-center gap-4 button onClick{isRecording ? stopRecording : startRecording} className{rounded-full p-4 transition-all ${ isRecording ? bg-red-500 text-white animate-pulse shadow-lg shadow-red-200 : bg-gray-100 text-gray-600 hover:bg-gray-200 }} {isRecording ? MicOff size{24} / : Mic size{24} /} /button {isRecording ( span classNametext-sm text-red-500 animate-pulse正在录音.../span )} /div {/* 实时转文字显示 */} {interimText ( div classNametext-center text-sm text-gray-400 italic {interimText} /div )} /div ); }实时音频波形可视化录音时的波形可视化让用户知道「系统正在听」——这是语音交互的关键反馈// features/voice/components/AudioVisualizer.tsx import { useRef, useEffect } from react; interface AudioVisualizerProps { isRecording: boolean; } export function AudioVisualizer({ isRecording }: AudioVisualizerProps) { const canvasRef useRefHTMLCanvasElement(null); const analyserRef useRefAnalyserNode | null(null); const streamRef useRefMediaStream | null(null); useEffect(() { if (!isRecording) { // 停止录音时清空画布 if (streamRef.current) { streamRef.current.getTracks().forEach(t t.stop()); } return; } // 开始录音时获取麦克风音频流 navigator.mediaDevices.getUserMedia({ audio: true }).then((stream) { streamRef.current stream; const audioContext new AudioContext(); const source audioContext.createMediaStreamSource(stream); const analyser audioContext.createAnalyser(); analyser.fftSize 256; source.connect(analyser); analyserRef.current analyser; // 绘制波形 const canvas canvasRef.current!; const ctx canvas.getContext(2d)!; const bufferLength analyser.frequencyBinCount; const dataArray new Uint8Array(bufferLength); function draw() { if (!isRecording) return; requestAnimationFrame(draw); analyser.getByteFrequencyData(dataArray); ctx.clearRect(0, 0, canvas.width, canvas.height); // 绘制柱状频谱图 const barWidth (canvas.width / bufferLength) * 2.5; let x 0; for (let i 0; i bufferLength; i) { const barHeight (dataArray[i] / 255) * canvas.height * 0.8; // 渐变色从蓝色到红色 const hue (dataArray[i] / 255) * 120 200; // 200(蓝) → 320(紫) ctx.fillStyle hsla(${hue}, 80%, 60%, 0.6); ctx.fillRect( x, canvas.height - barHeight, barWidth, barHeight ); x barWidth 1; } } draw(); }); }, [isRecording]); return ( canvas ref{canvasRef} width{300} height{80} classNamew-full h-20 rounded-lg bg-gray-50 dark:bg-gray-800 / ); }语音合成界面AI 回复的文字 → 转语音 → 播放// features/voice/components/VoicePlayer.tsx import { useState, useRef } from react; import { Volume2, VolumeX, Pause, Play } from lucide-react; interface VoicePlayerProps { text: string; lang?: string; } export function VoicePlayer({ text, lang zh-CN }: VoicePlayerProps) { const [isPlaying, setIsPlaying] useState(false); const [isPaused, setIsPaused] useState(false); const utteranceRef useRefSpeechSynthesisUtterance | null(null); const speak () { // 先停止当前播放 window.speechSynthesis.cancel(); const utterance new SpeechSynthesisUtterance(text); utterance.lang lang; utterance.rate 1.0; // 语速0.1-10默认1 utterance.pitch 1.0; // 音调0-2默认1 // 选择中文语音 const voices window.speechSynthesis.getVoices(); const zhVoice voices.find(v v.lang.startsWith(zh)); if (zhVoice) { utterance.voice zhVoice; } utterance.onstart () setIsPlaying(true); utterance.onend () { setIsPlaying(false); setIsPaused(false); }; utterance.onerror () { setIsPlaying(false); setIsPaused(false); }; utteranceRef.current utterance; window.speechSynthesis.speak(utterance); }; const pause () { window.speechSynthesis.pause(); setIsPaused(true); }; const resume () { window.speechSynthesis.resume(); setIsPaused(false); }; const stop () { window.speechSynthesis.cancel(); setIsPlaying(false); setIsPaused(false); }; return ( div classNameflex items-center gap-2 {/* 播放/暂停按钮 */} {!isPlaying ? ( button onClick{speak} classNamerounded-full p-2 bg-gray-100 text-gray-600 hover:bg-gray-200 title播放语音 Volume2 size{16} / /button ) : isPaused ? ( button onClick{resume} classNamerounded-full p-2 bg-blue-500 text-white title继续播放 Play size{16} / /button ) : ( button onClick{pause} classNamerounded-full p-2 bg-blue-500 text-white animate-pulse title暂停播放 Pause size{16} / /button )} {/* 停止按钮 */} {isPlaying ( button onClick{stop} classNamerounded-full p-2 bg-gray-100 text-gray-600 hover:bg-gray-200 title停止播放 VolumeX size{16} / /button )} {/* 播放状态文字 */} {isPlaying !isPaused ( span classNametext-xs text-blue-500正在播放.../span )} {isPaused ( span classNametext-xs text-gray-500已暂停/span )} /div ); }Whisper API 降级方案当浏览器不支持 Web Speech API 时用 OpenAI Whisper API 做语音识别// lib/voice-recognition.tsexportasyncfunctionrecognizeWithWhisper(audioBlob:Blob):Promisestring{constformDatanewFormData();formData.append(file,audioBlob,recording.webm);formData.append(model,whisper-1);formData.append(language,zh);// 中文constresponseawaitfetch(/api/ai/voice/transcribe,{method:POST,body:formData,});constdataawaitresponse.json();returndata.text;}// app/api/ai/voice/transcribe/route.tsimportOpenAIfromopenai;import{NextRequest,NextResponse}fromnext/server;constopenainewOpenAI({apiKey:process.env.OPENAI_API_KEY});exportasyncfunctionPOST(req:NextRequest){constformDataawaitreq.formData();constfileformData.get(file)asFile;consttranscriptionawaitopenai.audio.transcriptions.create({model:whisper-1,file:file,language:zh,});returnNextResponse.json({text:transcription.text});}降级策略// lib/voice-strategy.tsexportfunctiongetVoiceStrategy():web-speech|whisper|unsupported{constSpeechRecognitionwindow.SpeechRecognition||window.webkitSpeechRecognition;if(SpeechRecognition){returnweb-speech;// 免费优先}if(navigator.mediaDevices?.getUserMedia){returnwhisper;// 有麦克风但需要后端}returnunsupported;// 没有麦克风或不支持}语音交互的 UX 设计原则原则实现原因实时反馈录音按钮动画 波形可视化 实时转文字用户需要确认「系统在听」可中断录音随时停、播放随时停/暂停语音不能像文字那样快速扫描文字兜底语音输入后同时显示文字、AI回复可切换文字/语音有些人不方便听语音隐私提示录音前提示「麦克风权限仅用于语音识别」录音涉及隐私必须透明降级友好不支持语音时优雅降级到文字输入不要让用户无法使用常见误区误区1Web Speech API 到处能用只有 Chrome/Edge 支持 SpeechRecognition。必须检测支持度 准备降级方案。误区2不做实时转文字反馈用户说了话但看不到任何反馈 → 以为系统没在听 → 会重复说或者放弃。实时转文字是必需的。误区3只做语音不做文字语音交互必须同时提供文字选项——有些场景不适合听语音开会时、公共场合。 AI协作实战实战场景实现完整的语音聊天界面我给 AI 的 prompt实现一个语音聊天界面包含 1. VoiceRecorder录音按钮 实时波形 实时转文字 2. VoicePlayerAI回复的语音播放播放/暂停/停止 3. 文字/语音模式切换用户可以选择输入方式文字/语音 4. AI 回复可以切换文字/语音展示 5. 降级处理不支持语音识别时自动切换到文字输入 Whisper API 用 React TypeScript Tailwind CSS。 需要同时兼容 Web Speech API 和 Whisper API。AI 输出的关键设计决策输入模式切换用 ToggleButton文字/语音图标切换录音按钮在语音模式下显示文字模式下隐藏AI 回复默认文字展示每条消息旁有「播放语音」按钮不支持语音识别时录音按钮替换为 Whisper 上传按钮我的调整✅ 输入模式切换清晰❌ 波形可视化颜色太亮 → 改为更柔和的蓝紫色渐变✅ 降级方案完整——Web Speech → Whisper → 纯文字学到了什么语音交互的关键不是技术实现而是 UX 设计——实时反馈、可中断、文字兜底、降级友好这四个原则比代码更重要。 动手练习练习1简单实现 VoiceRecorder用 Web Speech API 实现录音 → 实时转文字 → 最终结果回调。先只做录音和转文字不连 AI。练习2中等实现 VoicePlayer 文字/语音切换实现 AI 回复的语音播放功能。添加文字/语音输入模式切换按钮。在 Chrome 中测试完整的语音输入 → AI 回复 → 语音播放流程。练习3挑战实现降级方案 完整语音聊天检测浏览器支持度 → 不支持 Web Speech 时用 Whisper API → 实现完整的语音聊天界面录音/播放/波形/实时转文字/降级。 本期要点Web Speech API 免费但 Chrome 限定优先用它降级到 Whisper API付费实时反馈是必需的录音动画 波形可视化 实时转文字让用户确认「系统在听」语音合成用 SpeechSynthesis选择中文语音、控制语速和音调、支持暂停/继续/停止文字兜底不可省略语音输入同时显示文字、AI回复可切换文字/语音——有些人不方便听语音降级策略web-speech → whisper → unsupported每级都有对应的界面 下期预告下一期进入多模态 AI 应用——图片文字语音的综合交互界面。你将学会如何让 AI 同时理解和生成多种模态的内容。如果你没有苹果电脑需要上传ios到APPStore可以访问以下网站iPA上传工具 - IPA解析与AppStore提交

相关新闻