简介:这份资源面向微信小程序开发者与语音交互功能集成人员,提供一套对接科大讯飞语音识别能力的完整示例工程,重点解决音频上传、语音提取、PCM格式转换与实时语音转文字等环节的落地问题。压缩包共34个文件,约55KB,以js业务逻辑与json配置为主,辅以wxss样式、wxml页面结构、yml持续集成配置及md说明文档,并附docx补充资料,便于快速理解项目组织方式。已有89人学习下载,适合希望在小程序中引入语音识别、又不想从零搭建接口链路的初中级开发者参考。读者可从中获取音频上传与格式转换的实现思路、语音转文字接口的调用示例,以及前后端目录划分与配置文件的组织方式,从而降低集成难度,提升小程序的语音交互体验。
1. 小程序语音识别对接科大讯飞:从录音到文字,一条能跑通的链路
微信小程序里做语音输入,最直接的做法是调wx.getRecorderManager()录音,拿到一个临时文件路径,然后把这个文件传给后端或云函数,由后端去调科大讯飞的语音听写接口。听起来简单,但真正动手时会发现几个绕不开的问题:小程序录音默认输出的是aac或mp3格式,而科大讯飞的实时语音转写接口对音频格式有明确要求,最常见的就是 PCM;录音采样率、位深、声道数如果和接口要求对不上,返回的就是空结果或者乱码。这套方案要解决的核心,就是「小程序端采集 → 音频格式转换 → 科大讯飞接口调用 → 文字回传展示」这条完整链路。适合正在做小程序语音输入、语音搜索、语音笔记这类功能的开发者,尤其是那些已经试过直接传录音文件却发现识别率极低的人。下面按实际落地顺序拆开讲,每一步都给到能直接抄的参数和代码。
2. 小程序端录音采集:参数怎么设才不白干
2.1 录音格式选型:为什么我最终锁定了 PCM
小程序wx.getRecorderManager()支持的format有aac、mp3、wav、PCM几种。很多人第一反应是选mp3,因为体积小、通用。但科大讯飞的语音听写(实时转写)接口,对音频的底层要求是 PCM 编码的裸数据,采样率 16kHz、位深 16bit、单声道。如果你传 mp3 或 aac,要么接口直接报格式错误,要么需要后端先做一次解码,多一步就多一个故障点。
我一般直接在小程序端就录成 PCM。虽然文件体积大一些(16kHz 16bit 单声道,一秒约 32KB),但省掉了后端解码环节,链路更短,排查问题也更容易。实测一分钟的 PCM 文件大约 1.9MB,对于短语音指令场景完全够用。
录音参数这样设:
// 小程序端录音初始化 const recorderManager = wx.getRecorderManager(); const recordOptions = { duration: 60000, // 最长录音时长,单位 ms,按需调整 sampleRate: 16000, // 必须 16000,科大讯飞接口要求 numberOfChannels: 1, // 单声道,多声道会导致识别异常 encodeBitRate: 256000, // 编码码率,PCM 格式下此参数影响不大 format: 'PCM', // 关键:直接录成 PCM frameSize: 1280 // 每帧大小,影响 onFrameRecorded 回调频率 }; recorderManager.start(recordOptions);sampleRate设成 16000 是硬性要求,设成 44100 虽然录音更清晰,但讯飞接口会按 16k 解析,结果就是语速变慢、音调变低,识别出来全是错的。numberOfChannels必须是 1,双声道数据交错排列,接口只取前一半,后半段直接丢失。frameSize这个参数在需要实时流式上传时才有意义,如果只是录完再传,可以不管。
2.2 录音事件监听与临时文件处理
录音结束后,onStop回调会返回一个tempFilePath,这个路径在小程序运行期间有效,但重启后就没了。所以拿到路径后要尽快处理,要么上传到后端,要么转成 ArrayBuffer 直接发给云函数。
recorderManager.onStop((res) => { const { tempFilePath, duration, fileSize } = res; console.log('录音结束,临时路径:', tempFilePath); console.log('时长:', duration, '文件大小:', fileSize); // 方案一:上传到后端服务器处理 wx.uploadFile({ url: 'https://your-server.com/api/speech/recognize', filePath: tempFilePath, name: 'audio', formData: { format: 'pcm', sampleRate: 16000 }, success: (uploadRes) => { const text = JSON.parse(uploadRes.data).text; this.setData({ recognizedText: text }); }, fail: (err) => { console.error('上传失败', err); } }); });这里有个细节:wx.uploadFile的filePath必须是本地临时路径,不能是网络地址。formData里带上格式和采样率信息,后端拿到后可以直接校验,避免格式不匹配导致识别失败。如果后端用的是 Node.js,接收到的文件默认存在临时目录,需要用fs.readFileSync读成 Buffer 再转发给讯飞接口。
提示:录音权限需要提前申请,
wx.authorize({ scope: 'scope.record' }),用户拒绝后要引导去设置页手动开启,否则recorderManager.start会直接失败且没有明显报错。
3. 音频格式转换与后端对接:PCM 数据怎么喂给讯飞
3.1 后端接收 PCM 并调用讯飞 WebAPI
科大讯飞的语音听写 WebAPI 接口地址是https://iat-api.xfyun.cn/v2/iat,鉴权方式是基于 HMAC-SHA256 的签名。很多人卡在鉴权这一步,因为官方示例是 Python 的,换成 Node.js 或 Java 时容易漏掉参数。
下面是一个 Node.js 版本的调用示例,假设已经拿到了 PCM 文件的 Buffer:
const crypto = require('crypto'); const axios = require('axios'); function buildXfyunAuth(apiKey, apiSecret) { const date = new Date().toUTCString(); const signatureOrigin = `host: iat-api.xfyun.cn\ndate: ${date}\nGET /v2/iat HTTP/1.1`; const signature = crypto .createHmac('sha256', apiSecret) .update(signatureOrigin) .digest('base64'); const authorizationOrigin = `api_key="${apiKey}", algorithm="hmac-sha256", headers="host date request-line", signature="${signature}"`; const authorization = Buffer.from(authorizationOrigin).toString('base64'); return { date, authorization }; } async function recognizePcm(pcmBuffer) { const { date, authorization } = buildXfyunAuth('你的API_KEY', '你的API_SECRET'); const audioBase64 = pcmBuffer.toString('base64'); const response = await axios.post( 'https://iat-api.xfyun.cn/v2/iat', { common: { app_id: '你的APP_ID' }, business: { language: 'zh_cn', domain: 'iat', accent: 'mandarin', vad_eos: 3000, // 静音检测,超过 3 秒无声音则结束 dwa: 'wpgs' // 开启动态修正,实时场景建议开启 }, data: { status: 2, // 2 表示最后一帧,一次性上传用 2 format: 'audio/L16;rate=16000', encoding: 'raw', audio: audioBase64 } }, { headers: { 'Content-Type': 'application/json', 'Date': date, 'Authorization': authorization, 'Host': 'iat-api.xfyun.cn' } } ); // 解析返回结果 const result = response.data; if (result.code !== 0) { throw new Error(`讯飞接口错误:${result.code} - ${result.message}`); } // 拼接识别文字 let text = ''; result.data.result.ws.forEach(item => { item.cw.forEach(cw => { text += cw.w; }); }); return text; }business里的vad_eos是静音检测阈值,单位毫秒。设太小会导致说话稍一停顿就截断,设太大则响应变慢。3000 是一个比较平衡的值。dwa: 'wpgs'是动态修正功能,开启后返回结果会分段推送,适合实时转写场景;如果是一次性上传整段音频,可以不开。
data.status这个字段容易搞错:0表示第一帧,1表示中间帧,2表示最后一帧。如果一次性上传完整音频,直接设2即可。format字段的写法是audio/L16;rate=16000,L16表示 16 位线性 PCM,rate必须和实际采样率一致。
3.2 实时流式上传:把 PCM 分帧推送
如果要做「边说边识别」的效果,就不能等录完再传,而是要在录音过程中通过onFrameRecorded回调拿到每一帧 PCM 数据,实时推给后端,后端再通过 WebSocket 转发给讯飞。
小程序端这样拿帧数据:
recorderManager.onFrameRecorded((res) => { const { frameBuffer } = res; // frameBuffer 是 ArrayBuffer,转成 Base64 后通过 WebSocket 发送 const base64 = wx.arrayBufferToBase64(frameBuffer); wx.sendSocketMessage({ data: JSON.stringify({ type: 'audio', data: base64 }) }); });后端收到后,需要维护一个 WebSocket 连接池,每个用户对应一个到讯飞的连接。讯飞的 WebSocket 地址是wss://iat-api.xfyun.cn/v2/iat,鉴权参数拼在 URL 上。每一帧数据作为一条消息发送,status依次为0、1、2。
注意:实时流式上传对网络稳定性要求很高,小程序端如果网络抖动导致帧丢失,识别结果会出现断字。我一般会在后端加一个缓冲队列,收到帧后先存起来,按固定间隔发送,避免网络抖动直接影响讯飞侧。
4. 避坑与排查:那些让我加班到凌晨的细节
4.1 识别结果为空或乱码
现象:接口返回code: 0但result为空,或者识别出一堆无意义字符。
原因:九成是音频格式不对。PCM 数据如果带了文件头(比如 wav 的 44 字节头),讯飞会把它当成音频数据解析,导致开头一段全是噪音。另外,采样率不匹配也会导致这个问题,比如实际是 44100Hz 但format里写了 16000。
解决:确认传给讯飞的audio字段是纯 PCM 裸数据,不含任何文件头。如果是 wav 文件,需要跳过前 44 字节再 Base64 编码。采样率用ffprobe或小程序端日志确认,不要凭感觉写。
4.2 录音权限被拒绝后无提示
现象:用户点击录音按钮没反应,控制台也没有明显报错。
原因:wx.authorize在用户拒绝后不会再次弹窗,recorderManager.start会静默失败。
解决:在调用录音前先wx.getSetting检查scope.record状态,如果是false,弹窗引导用户去wx.openSetting手动开启。代码逻辑要覆盖「首次授权」「拒绝后再次点击」「从设置页返回」三种情况。
4.3 长音频识别超时
现象:录音超过 30 秒后,接口返回超时或部分结果。
原因:讯飞语音听写接口对单次请求的音频时长有限制,虽然官方文档写的是 60 秒,但实际测试中超过 30 秒的音频,如果网络稍慢就容易超时。
解决:超过 30 秒的音频,在后端做切分,按 20 秒一段切成多段分别识别,最后拼接结果。切分点选在静音段,避免把一句话切成两半。可以用vad_eos配合静音检测来做自动切分。
4.4 Base64 编码后体积膨胀导致请求过大
现象:上传一分钟以上的 PCM 音频时,请求体超过 1MB,接口返回 413 或直接断开。
原因:PCM 裸数据 Base64 编码后体积增大约 33%,一分钟 16kHz 16bit 单声道 PCM 约 1.9MB,Base64 后约 2.5MB。
解决:两个方向。一是后端接收时用express.json({ limit: '10mb' })放宽限制;二是如果音频较长,改用 WebSocket 分帧上传,避免单次请求体过大。我一般建议超过 30 秒的音频都走流式。
4.5 小程序端 PCM 录音在 iOS 上异常
现象:Android 正常,iOS 上录出来的 PCM 文件识别率极低或完全无结果。
原因:iOS 的音频采集底层实现和 Android 不同,部分机型上sampleRate: 16000会被系统重采样,实际输出可能是 48000Hz。
解决:在onStart回调里打印实际参数,确认系统是否按预期采样。如果发现 iOS 上采样率被改,可以在后端加一个重采样步骤,用ffmpeg统一转成 16kHz。命令是ffmpeg -i input.pcm -ar 16000 -ac 1 -f s16le output.pcm。
5. 进阶技巧:用动态修正和热词提升识别准确率
5.1 开启动态修正(dwa)让实时转写更顺滑
讯飞的dwa: 'wpgs'参数开启后,返回结果不再是等整句说完才给,而是分段推送,并且会对前面的结果做修正。比如你说「今天天气」,先返回「今天」,再说「天气」时返回「今天天气」,如果发现前面识别错了,还会回退修正。
这个功能在实时场景下体验提升很明显,但处理逻辑要改:不能简单拼接,而是要根据返回的pgs字段判断是追加还是替换。pgs: 'apd'表示追加,pgs: 'rpl'表示替换。下面是一个处理示例:
let finalText = ''; let currentSegment = ''; function handleXfyunMessage(data) { const { pgs, ws } = data; let segmentText = ''; ws.forEach(item => { item.cw.forEach(cw => { segmentText += cw.w; }); }); if (pgs === 'rpl') { // 替换当前段 currentSegment = segmentText; } else { // 追加 currentSegment += segmentText; } // 最终文本 = 已确认文本 + 当前段 return finalText + currentSegment; }实际使用时,还需要在status: 2的最后一帧到达时,把currentSegment合并进finalText,否则最后一段会丢。
5.2 用热词表提升专有名词识别率
科大讯飞支持上传热词表,对特定词汇做加权。比如你的小程序是做医疗咨询的,「阿司匹林」「二甲双胍」这类词默认识别率很低,加到热词表里就能明显改善。
热词表通过讯飞控制台配置,每个词可以设置权重,范围 1-10。权重越高,识别时越倾向输出该词。但权重不宜设太高,否则会把发音相近的普通词也强行纠正成热词。我一般把核心业务词设 7-8,边缘词设 3-5。
配置好后,在business参数里加上hotword: '你的热词表ID'即可。注意热词表有数量限制,免费版一般支持 3 个表,每个表 500 个词左右,具体以控制台为准。
5.3 验证识别效果的三个指标
做完对接后,怎么判断效果好不好?我一般看三个数:
| 指标 | 含义 | 合格线 |
|---|---|---|
| 字准确率 | 正确字数 / 总字数 | 安静环境 ≥ 95% |
| 首字延迟 | 从说话到出现第一个字的时间 | 实时场景 ≤ 800ms |
| 断句准确率 | 标点符号位置正确的句子占比 | ≥ 85% |
测试时用同一段音频反复跑 10 次,看结果是否稳定。如果每次差异很大,说明音频格式或网络环节有问题,优先排查 PCM 数据是否纯净。
提示:讯飞接口的免费额度是按日计算的,调试阶段建议用短音频(5 秒以内)反复测试,避免一天就把额度跑完。
这套方案我从第一个版本到现在改了七八次,最大的教训是:不要在小程序端做任何音频处理,PCM 录成什么样就传什么样,所有转换和校验都放后端。小程序端的计算能力和调试手段都有限,一旦出问题很难定位。后端至少能打日志、能抓包、能复现。希望帮到你。
本文还有配套的精品资源,点击获取