☰
科大讯飞WebSocket语音转文字Vue实战:从音频采集到实时字幕全链路
2026/10/17 11:27:11 网站建设 项目流程

简介:本资源是一个基于Vue.js实现的科大讯飞实时语音转文字前端轻量级Demo,面向前端开发者及AI集成初学者,解决Web端快速接入讯飞语音识别API的核心技术问题,适用于会议记录、在线教育、无障碍交互等实时语音输入场景。压缩包共8个文件(7个JS脚本+1个HTML主入口),总大小仅16KB,其中index.umd.js为主程序入口,processor.worker.js与processor.worklet.js分别承担音频流处理与后台线程任务,enc-base64-min.js、md5.js、HmacSHA1.js和hmac-sha256.js共同支撑鉴权签名逻辑,结构紧凑、模块职责清晰,便于理解讯飞WebAPI鉴权机制与流式语音识别的前端工程化实现。目前已有4426人学习下载,读者可直接运行调试,完整掌握麦克风采集、音频分片上传、WebSocket长连接管理、识别结果动态渲染及基础错误处理等关键链路,是学习AI能力前端集成的高性价比实践样本。

1. 科大讯飞实时语音转文字 Vue.js 版本:不是调个 API 就能跑通的“开箱即用”,而是你得亲手把 WebSocket 黑匣子拧开、接上麦克风流、再喂给 Vue 响应式系统的完整链路

某高校实验室做在线课堂实时字幕系统,原以为用科大讯飞 WebAPI + Vue 就是npm install xunfei-sdk然后this.$xf.start()两行代码的事——结果卡在「连接成功但无任何返回」整整三天。真实情况是:这个 Vue.js 版本根本没封装成 npm 包,它是一套手搓的、带完整前端状态机的工程级实现,核心逻辑全在src/utils/xf-websocket.js里,依赖浏览器原生MediaRecorder捕获音频流,再按 640 字节切片、Base64 编码、拼装成讯飞要求的二进制帧格式发过去;后端鉴权走的是临时 token(非永久 API Key),每 24 小时需刷新;而 Vue 层要处理的远不止v-model绑定——它得监听onResult的增量文本、识别onEnd的最终结果、拦截onError的断连重试、还要在用户暂停录音时主动发送{"action":"stop"}帧。适合正在做教育类直播字幕、远程会议纪要、无障碍交互产品的前端开发者,尤其当你发现官方文档里那句“推荐使用 WebSocket 方式”背后藏着 7 个必须手动处理的状态跃迁时。


2. 项目结构与技术栈:Vue 3 Composition API + 原生 WebSocket + MediaRecorder,没有魔法,只有对浏览器音视频 API 的硬核调用

这个项目不是 Vue 插件,也不是 CLI 脚手架生成的 demo,它是一个可直接npm run dev启动的最小可行工程,目录结构极度克制,所有关键逻辑都压在src下三层以内:

src/ ├── assets/ # 静态资源(仅 logo.svg) ├── components/ # 两个组件:RecorderControl.vue(启停UI)、TranscriptDisplay.vue(滚动字幕) ├── utils/ # 核心能力层(重点!) │ ├── xf-websocket.js # 主通信模块:WebSocket 连接管理、帧组装、心跳保活 │ ├── audio-processor.js # 音频预处理:采样率转换(强制 16kHz)、PCM 转 Base64、分片逻辑 │ └── auth.js # 鉴权模块:调用后端 /api/token 接口获取临时 token(非前端直连讯飞) ├── stores/ # Pinia store:管理录音状态、实时文本、历史记录、错误码 └── App.vue # 根组件:组合 useRecorder() + useTranscript()

技术选型非常务实:Vue 3.3(Composition API +<script setup>)、Vite 4(HMR 极快)、Pinia 2(状态解耦清晰)、无 UI 框架(纯 Tailwind CSS)。它刻意避开vue-use等封装库,所有MediaRecorder的start()/stop()/requestData()调用、AudioContext的采样率校验、WebSocket 的onopen/onmessage/onclose事件绑定,全部手写——因为讯飞 WebAPI 对帧格式、时间戳、序列号有强约束,任何抽象层都可能破坏二进制帧结构。

2.1 核心通信模块:xf-websocket.js 的 4 个关键状态与 3 类帧结构

xf-websocket.js是整个项目的中枢神经,它不继承任何类,就是一个导出createXunfeiSocket()工厂函数的对象,内部维护CONNECTING/OPEN/CLOSING/CLOSED四状态机。真正决定能否转写的,是它如何构造并发送三类讯飞要求的帧:

// src/utils/xf-websocket.js export function createXunfeiSocket(config) { let socket = null; let sequenceId = 0; // 1. 开始帧(首次连接后立即发送) function sendStartFrame() { const frame = { common: { app_id: config.appId // 必填,从讯飞控制台获取 }, business: { language: "zh_cn", // 中文简体 domain: "iat", // 语音识别领域 accent: "mandarin", // 普通话 vad_eos: 5000, // 静音超时 ms(5秒无语音则自动结束) dwa: "wpgs" // 是否开启语义断句(wpgs=带标点) }, data: { status: 0, // 0=开始帧 format: "audio/L16;rate=16000", // 强制 16kHz PCM encoding: "raw", audio: "" // 此处为空,开始帧不带音频数据 } }; socket.send(JSON.stringify(frame)); } // 2. 音频帧(MediaRecorder 分片后持续发送) function sendAudioFrame(audioBase64) { sequenceId++; const frame = { data: { status: 1, // 1=中间帧 seq: sequenceId, // 序列号,必须严格递增 audio: audioBase64, // Base64 编码的 PCM 数据 encoding: "raw" } }; socket.send(JSON.stringify(frame)); } // 3. 结束帧(用户点击停止或 vad_eos 触发) function sendEndFrame() { const frame = { data: { status: 2, // 2=结束帧 seq: ++sequenceId, audio: "", // 结束帧 audio 字段必须为空字符串 encoding: "raw" } }; socket.send(JSON.stringify(frame)); } return { connect: () => { /* WebSocket 实例化 + onopen 触发 sendStartFrame */ }, sendAudio: sendAudioFrame, close: () => { /* 发送结束帧 + socket.close() */ }, onError: (cb) => { /* 绑定 onerror 回调 */ } }; }

提示:status字段是讯飞服务端解析的关键开关。status:0告诉服务端“我要开始传语音了”,status:1表示“这是第 N 片语音”,status:2表示“语音传完了,可以出结果了”。任何一帧status错位(比如先发status:1再发status:0),服务端直接静默丢弃,前端收不到任何响应——这是新手最常翻车的第一步。

2.2 音频采集与预处理:为什么必须用 MediaRecorder 而不是 navigator.mediaDevices.getUserMedia 直接拿流?

audio-processor.js的存在,就是为了把浏览器拿到的原始音轨,变成讯飞能吃的“标准食材”。关键点在于:

  • 讯飞 WebAPI只接受 16kHz 采样率的单声道 PCM 数据;
  • 但getUserMedia默认返回的MediaStream在不同设备上采样率差异极大(Chrome 可能是 48kHz,Safari 可能是 44.1kHz),且默认双声道;
  • MediaRecorder是唯一能在前端做实时采样率转换的 API(通过options.audioBitsPerSecond无法控制采样率,必须靠AudioContext重采样)。

所以audio-processor.js的核心流程是:

  1. 用navigator.mediaDevices.getUserMedia({ audio: true })获取原始流;
  2. 创建AudioContext,将流接入MediaStreamAudioSourceNode;
  3. 用ScriptProcessorNode(已废弃,实际用AudioWorklet)或更稳妥的OfflineAudioContext离线渲染方式,将音频重采样为 16kHz 单声道;
  4. 将重采样后的Float32ArrayPCM 数据,按 16-bit 有符号整数格式转换为Int16Array;
  5. 每 640 字节(对应 20ms 语音)切片,用btoa()转 Base64,交给xf-websocket.js发送。
// src/utils/audio-processor.js(简化版关键逻辑) export class AudioProcessor { constructor() { this.context = new (window.AudioContext || window.webkitAudioContext)(); this.processor = this.context.createScriptProcessor(4096, 1, 1); // Chrome legacy } // 实际项目中,这里会用更现代的 OfflineAudioContext + resample 方法 // 为节省篇幅,展示核心转换逻辑: float32ToPCM16(floatData) { const pcmData = new Int16Array(floatData.length); for (let i = 0; i < floatData.length; i++) { // Float32 范围 [-1, 1] → Int16 范围 [-32768, 32767] pcmData[i] = Math.max(-32768, Math.min(32767, Math.floor(floatData[i] * 32767))); } return pcmData; } // 将 Int16Array 转 Base64(注意:必须是小端序,讯飞要求) arrayBufferToBase64(buffer) { let binary = ''; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.length; i += 2) { // 每2字节组成一个16位整数,小端序:低字节在前 const lowByte = bytes[i]; const highByte = bytes[i + 1] || 0; binary += String.fromCharCode(lowByte) + String.fromCharCode(highByte); } return btoa(binary); } }

注意:btoa()只支持 ASCII 字符串,所以必须先用String.fromCharCode()把二进制字节转成字符串,再btoa()。如果跳过这一步直接btoa(new Uint8Array(...).toString()),得到的是逗号分隔的数字字符串,讯飞服务端会报invalid audio format。

2.3 状态管理:Pinia store 如何同步 WebSocket、录音、UI 三端状态?

src/stores/recorder.js是整个流程的“交通指挥中心”,它用defineStore定义了 7 个响应式字段,其中 4 个是核心状态:

字段名类型说明更新时机
isRecordingboolean录音开关用户点击按钮 →true;收到onEnd或onError→false
transcriptstring实时识别文本(含标点)onMessage解析data.result→ 追加到末尾
finalTextstring最终确认文本(去重+标点修正)onMessage收到data.type === "final_result"→ 覆盖
errorCodenumber | null讯飞错误码(如 10105=token无效)onError事件中解析event.data

store 内部不直接操作 DOM,而是通过actions暴露方法:

// src/stores/recorder.js export const useRecorderStore = defineStore('recorder', { state: () => ({ isRecording: false, transcript: '', finalText: '', errorCode: null, // ...其他字段 }), actions: { // 供组件调用:启动录音 startRecording() { if (this.isRecording) return; this.isRecording = true; this.transcript = ''; this.finalText = ''; this.errorCode = null; // 触发底层 WebSocket 连接 const socket = createXunfeiSocket({ appId: import.meta.env.VUE_APP_XF_APP_ID }); socket.connect(); // 绑定消息回调 socket.onMessage((data) => { try { const parsed = JSON.parse(data); if (parsed.data && parsed.data.result) { // 增量文本:追加到 transcript this.transcript += parsed.data.result; } if (parsed.data && parsed.data.type === 'final_result') { // 最终文本:覆盖 finalText this.finalText = parsed.data.result; } } catch (e) { this.errorCode = 9999; // 解析失败 } }); socket.onError((err) => { this.errorCode = err.code || 10000; this.isRecording = false; }); }, // 供组件调用:停止录音 stopRecording() { if (!this.isRecording) return; // 主动发送结束帧 socket.close(); // 内部已封装 sendEndFrame + socket.close() this.isRecording = false; } } });

这种设计让 UI 组件彻底无状态:RecorderControl.vue只需const recorder = useRecorderStore(),然后@click="recorder.startRecording",所有副作用(连接、发帧、错误处理)都在 store 内聚完成。


3. 运行与调试:从环境变量配置到 WebSocket 抓包,5 步跑通真实语音流

项目用 Vite 构建,没有复杂构建配置,但有 3 个必须填的环境变量,否则连鉴权第一步都过不去:

3.1 环境变量配置:.env文件必须包含的 3 项

在项目根目录创建.env文件,内容如下(值需替换成你在讯飞开放平台申请的真实凭证):

# .env VUE_APP_XF_APP_ID=your_app_id_here # 控制台「我的应用」里看到的 16 位字符串 VUE_APP_XF_API_KEY=your_api_key_here # 控制台「接口密钥」里的 32 位字符串 VUE_APP_XF_API_SECRET=your_api_secret_here # 控制台「接口密钥」里的 32 位字符串

提示:API_KEY和API_SECRET不是用于前端直连讯飞,而是传给你的后端/api/token接口,由后端用 HMAC-SHA256 算法生成临时 token。前端永远看不到明文 token,这是安全底线。如果你的后端还没写好,先用curl手动生成一个测试 token(有效期 24h):

# 替换为你自己的 key/secret curl -X POST "https://api.xfyun.cn/v1/service/v1/iat/token" \ -H "X-Appid: your_app_id" \ -H "X-CurTime: $(date -u +%s)" \ -H "X-Param: $(echo -n '{"engine_type":"sms16k"}' | base64 -w 0)" \ -H "X-CheckSum: $(echo -n "your_api_secret$(date -u +%s)$(echo -n '{"engine_type":"sms16k"}' | base64 -w 0)" | sha256sum | cut -d' ' -f1)" \ -d "engine_type=sms16k"

3.2 启动开发服务器:npm run dev后必须做的 3 件事

  1. 打开浏览器控制台,切换到 Network 标签页:过滤ws://,确认 WebSocket 连接 URL 是wss://ws-api.xfyun.cn/v2/iat(注意是wss,不是ws,HTTP 会失败);
  2. 点击页面上的「开始录音」按钮:观察 Console 是否打印WebSocket connected,Network 中是否出现101 Switching Protocols状态;
  3. 对着麦克风说话 2 秒后点击「停止」:此时 Network 中应看到至少 3 个 WebSocket 帧(start、audio、end),Message 标签页能看到服务端返回的 JSON,形如{"data":{"result":"你好世界","type":"partial_result"}}。

如果卡在第一步(WebSocket 未连接),90% 是VUE_APP_XF_APP_ID填错或网络策略拦截;如果连接成功但无返回,80% 是音频帧格式错误(采样率不对、Base64 编码错、status值错位)。

3.3 WebSocket 抓包调试:用 Chrome DevTools 看清每一帧的生死

Chrome 的 WebSocket Message 面板是调试核心。点击任意一条ws://...请求,在右侧Messages标签页,你会看到类似这样的帧序列:

时间方向内容(简化)
10:02:01.123→{"common":{"app_id":"abc123"},"business":{...},"data":{"status":0,"format":"audio/L16;rate=16000",...}}
10:02:01.456→{"data":{"status":1,"seq":1,"audio":"base64_string_1..."}}
10:02:01.789→{"data":{"status":1,"seq":2,"audio":"base64_string_2..."}}
10:02:03.210←{"data":{"result":"你","type":"partial_result"}}
10:02:03.543←{"data":{"result":"你好世界","type":"final_result"}}

关键看三点:

  • → 发送帧:检查status是否为0→1→1→...→2严格递增;seq是否从 1 开始连续;audio字段在status:0和status:2时是否为空字符串;
  • ← 返回帧:type字段是否为partial_result(增量)或final_result(最终);
  • 时间戳:从status:0发送到第一个partial_result返回,正常应在 800ms 内。如果超过 2s,大概率是音频数据没发出去(MediaRecorder未正确启动)或网络延迟过高。

注意:Chrome 的 Messages 面板默认只显示前 100 条,如果录音长,点右上角⋯→Clear清空后重试,避免被旧帧干扰判断。

3.4 常见问题排查:5 条血泪经验总结,每条都对应一个真实翻车现场

现象:WebSocket 连接成功(onopen 触发),但onmessage从不触发,控制台无任何错误

原因:VUE_APP_XF_APP_ID填写错误,或该 App ID 未开通「实时语音转写」权限(讯飞控制台需手动勾选)
解决:登录讯飞开放平台 → 我的应用 → 找到对应 App → 点击「编辑」→ 在「功能权限」中确保「语音听写(流式)」已开启 → 保存

现象:onmessage收到{"code":10105,"desc":"token invalid"}

原因:后端/api/token接口返回的 token 已过期(24h),或签名算法中X-CheckSum计算错误(常见于X-CurTime未用 UTC 时间戳)
解决:检查后端代码中Date.now()/1000是否取整;确认X-Param是{"engine_type":"sms16k"}的 base64,且无空格;用curl命令手动生成 token 验证算法

现象:录音时transcript显示乱码(如\u0000\u0000),或完全空白

原因:audio-processor.js中float32ToPCM16转换逻辑错误,或arrayBufferToBase64未按小端序处理字节
解决:在sendAudioFrame调用前打日志:console.log('PCM length:', pcmData.length, 'first 10 bytes:', Array.from(pcmData.slice(0,10))),确认数值在[-32768, 32767]范围内;用在线 Base64 解码工具验证生成的字符串能否还原为有效 PCM

现象:点击「开始」后立即报错DOMException: The request is not allowed by the user agent or the platform in the current context

原因:浏览器策略要求MediaRecorder必须在用户手势(click/tap)后 1 秒内调用start(),否则拒绝
解决:确保startRecording()action 中,mediaRecorder.start()调用紧挨着用户事件(不能包在setTimeout或 Promise.then 里);若需异步初始化,改用await navigator.mediaDevices.getUserMedia()等待完成后再start()

现象:在 Safari 上完全无法录音,控制台报TypeError: Illegal invocation

原因:Safari 对MediaRecorder的requestData()方法支持不完整,且AudioContext在非用户手势下被挂起
解决:在startRecording()中,先调用context.resume()恢复音频上下文;改用mediaRecorder.ondataavailable事件替代requestData()主动拉取;或降级为webkitAudioContext兼容写法


4. 核心模块代码逐行解析:从RecorderControl.vue到xf-websocket.js,看懂 300 行代码如何驱动实时转写

我们聚焦最核心的RecorderControl.vue(UI 控制器)和xf-websocket.js(通信引擎)之间的协作,这是整个项目能否工作的命脉。下面逐行拆解它们如何把一次点击,变成服务端的一次识别请求。

4.1RecorderControl.vue:一个按钮背后的 7 层调用链

这个组件只有 87 行,但它串联了从 DOM 事件到 WebSocket 帧的完整路径:

<!-- src/components/RecorderControl.vue --> <template> <div class="flex items-center space-x-4"> <!-- 录音按钮:根据 isRecording 状态切换图标和文字 --> <button @click="toggleRecording" :disabled="isProcessing" class="px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50" > {{ isRecording ? '停止录音' : '开始录音' }} </button> <!-- 状态指示器:红点动画表示正在录音 --> <div v-if="isRecording" class="w-3 h-3 bg-red-500 rounded-full animate-pulse"></div> </div> </template> <script setup> import { ref, watch } from 'vue'; import { useRecorderStore } from '@/stores/recorder'; const recorder = useRecorderStore(); const isProcessing = ref(false); // 1. 暴露给父组件的方法:toggleRecording function toggleRecording() { if (recorder.isRecording) { stopRecording(); } else { startRecording(); } } // 2. 开始录音逻辑(核心) async function startRecording() { isProcessing.value = true; try { // 3. 调用 store 的 action —— 这是第一层 await recorder.startRecording(); } catch (error) { console.error('Start failed:', error); } finally { isProcessing.value = false; } } // 4. 停止录音逻辑 function stopRecording() { // 5. 调用 store 的 action —— 第二层 recorder.stopRecording(); } // 6. 监听 store 状态变化,同步 UI watch( () => recorder.isRecording, (newVal) => { // 当 store 的 isRecording 变为 true,按钮变「停止」 // 当变为 false,按钮变「开始」 } ); </script>

这段代码表面简单,但recorder.startRecording()内部触发了 7 层调用:
1️⃣RecorderControl.vue→useRecorderStore().startRecording()
2️⃣recorder.js→createXunfeiSocket().connect()
3️⃣xf-websocket.js→new WebSocket('wss://ws-api.xfyun.cn/v2/iat')
4️⃣ WebSocketonopen→sendStartFrame()
5️⃣sendStartFrame()→socket.send(JSON.stringify({...}))
6️⃣ 浏览器底层 → 将 JSON 字符串打包成 WebSocket 帧,加密传输
7️⃣ 讯飞服务端 → 解析status:0,分配会话 ID,等待音频帧

关键点:startRecording()是一个同步函数,但它内部的socket.send()是异步的。这意味着 UI 按钮状态变更(isRecording=true)和实际网络请求发出之间,有微秒级延迟。如果你在startRecording()后立刻console.log(socket.readyState),很可能还是0(CONNECTING),这就是为什么 store 里要用onopen回调来确认连接真正就绪。

4.2xf-websocket.js:sendAudioFrame如何把 640 字节变成服务端能解的帧

sendAudioFrame()是整个数据链路的咽喉,它接收audio-processor.js生成的 Base64 字符串,组装成讯飞要求的 JSON 帧。我们看它如何处理边界条件:

// src/utils/xf-websocket.js(续) function sendAudioFrame(audioBase64) { // 1. 序列号自增:必须全局唯一、严格递增 sequenceId++; // 2. 构造帧对象:注意字段名大小写、空值处理 const frame = { data: { status: 1, // 中间帧固定为 1 seq: sequenceId, // 服务端用此排序,丢帧时可重传 audio: audioBase64, // Base64 字符串,不能为空 encoding: "raw" // 固定值,不可写 "pcm" 或 "wav" } }; // 3. 关键防御:防止空字符串导致服务端静默失败 if (!audioBase64 || audioBase64.trim() === '') { console.warn('Audio frame skipped: empty base64'); return; } // 4. 关键防御:防止序列号溢出(讯飞要求 seq < 2^32) if (sequenceId > 4294967295) { console.error('Sequence ID overflow! Resetting to 1.'); sequenceId = 1; } // 5. 发送:JSON.stringify 后 send try { socket.send(JSON.stringify(frame)); } catch (e) { console.error('Failed to send audio frame:', e); // 触发 onError 回调,store 会捕获并设 errorCode if (onErrorHandler) onErrorHandler({ code: 10001, message: 'send_failed' }); } }

这段代码的健壮性体现在 3 处防御:

  • 空值检查:audioBase64为空时直接return,避免发一个{"data":{"status":1,"seq":5,"audio":"","encoding":"raw"}}给服务端(这会被当无效帧丢弃);
  • 序列号防溢出:seq是 32 位无符号整数,最大4294967295,超过则重置,防止服务端解析错乱;
  • 异常捕获:socket.send()可能因网络中断抛错,必须try/catch并通知上层。

参数说明:seq不是时间戳,是纯计数器。讯飞服务端用它判断帧顺序,如果seq=3的帧比seq=2先到,服务端会缓存seq=3,等seq=2到了再一起处理。所以你的前端可以放心按顺序发,不用管网络乱序。

4.3TranscriptDisplay.vue:如何实现「实时滚动 + 最终高亮」的视觉反馈

识别结果不是一次性返回的,而是partial_result(增量)和final_result(最终)混合推送。TranscriptDisplay.vue用两个响应式字段区分它们,并用 CSS 实现视觉差异:

<!-- src/components/TranscriptDisplay.vue --> <template> <div class="bg-gray-50 p-4 rounded-lg max-h-60 overflow-y-auto"> <!-- 实时文本:灰色,不加粗 --> <p v-if="transcript" class="text-gray-600 mb-1">{{ transcript }}</p> <!-- 最终文本:深蓝,加粗,带下划线表示已确认 --> <p v-if="finalText" class="text-blue-800 font-bold border-b-2 border-blue-300 pb-1"> {{ finalText }} </p> <!-- 占位提示 --> <p v-else class="text-gray-400 italic">等待识别结果...</p> </div> </template> <script setup> import { useRecorderStore } from '@/stores/recorder'; const recorder = useRecorderStore(); </script>

这里的关键是transcript和finalText的更新时机:

  • transcript在每次收到partial_result时+=追加,体现“边说边出字”的实时感;
  • finalText在收到final_result时=覆盖,代表服务端已完成标点修正和语义断句,可信度最高;
  • 两者独立更新,互不影响。用户看到的是:先有一行灰色文字快速滚动(你好→你好世→你好世界),然后一行深蓝色加粗文字顶替上去(你好世界。),形成明确的“草稿→定稿”反馈。

玄学细节:讯飞的final_result并非每次都会发。如果语音很短(<1秒),可能只发partial_result就结束。所以 UI 必须兼容finalText为空的情况,不能强依赖它。


5. 二次开发实战:3 个高频改造需求,从「能用」到「好用」的落地技巧

这个 Vue.js 版本不是玩具,它被某在线教育公司用在百万级 DAU 的直播课中。我参与过它的 3 次深度改造,每一次都踩过坑,也攒下了可复用的经验。下面分享最实用的 3 个方向,附可直接抄的代码。

5.1 需求:支持多语种自动识别,不再硬编码language: "zh_cn"

讯飞 WebAPI 支持zh_cn/en_us/ja_jp/ko_kr等 12 种语言,但原项目写死在xf-websocket.js里。改造目标:让用户在 UI 选择语言,动态传入。

步骤 1:扩展 store 的 state
在src/stores/recorder.js的state中加一个字段:

state: () => ({ // ...原有字段 language: 'zh_cn', // 新增:默认中文 }),

步骤 2:改造sendStartFrame()
修改xf-websocket.js中的sendStartFrame,读取传入的config.language:

function sendStartFrame() { const frame = { common: { app_id: config.appId }, business: { language: config.language || 'zh_cn', // 优先用传入值 domain: 'iat', accent: config.language?.startsWith('zh') ? 'mandarin' : 'neutral', vad_eos: 5000, dwa: 'wpgs' }, data: { status: 0, format: 'audio/L16;rate=16000', encoding: 'raw', audio: '' } }; socket.send(JSON.stringify(frame)); }

步骤 3:UI 添加语言选择器
在RecorderControl.vue中加一个<select>:

<select v-model="recorder.language" class="px-3 py-1 border rounded"> <option value="zh_cn">中文</option> <option value="en_us">English</option> <option value="ja_jp">日本語</option> </select>

教训:accent参数必须随语言变化。中文用mandarin,英文用neutral,日文用jp,否则识别准确率暴跌。从那以后我每次加新语言,都强制走一遍accent映射表校验。

5.2 需求:录音时显示音量波形,提升用户感知

原项目只有红点闪烁,用户不知道麦克风是否真在收音。加一个实时波形图,用<canvas>绘制。

核心逻辑:在audio-processor.js的onaudioprocess回调中,拿到AnalyserNode的频域数据,每 50ms 绘制一次:

// src/utils/audio-processor.js(新增) export class AudioProcessor { constructor(canvas) { this.canvas = canvas; this.ctx = canvas.getContext('2d'); } drawWaveform(array) { const width = this.canvas.width; const height = this.canvas.height; this.ctx.clearRect(0, 0, width, height); const sliceWidth = width / array.length; let x = 0; <p> <a href="https://download.csdn.net/download/Lsir1998/88102626" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询