微信小程序语音识别全链路实战:从录音到文字,PCM转换与讯飞接口对接
发布时间:2026/10/2 3:30:54来源:尧图网络
简介这份资源面向微信小程序开发者提供一套对接科大讯飞语音识别能力的完整集成方案重点解决音频上传、语音提取、PCM格式转换与实时语音转文字等环节的落地问题适合具备一定小程序开发基础、希望快速为应用添加语音交互功能的工程师参考。压缩包共34个文件约55KB以js业务逻辑与接口调用代码、json配置、wxss样式、wxml页面结构为主另含md说明文档、yml持续集成配置及少量png示意图整体结构清晰便于按模块查阅。目前已有89人学习下载。资源围绕语音文件上传、音频提取、PCM格式转换和实时识别四个关键步骤展开配套接口调用示例与说明文档可帮助开发者理解科大讯飞语音识别接口在小程序中的接入方式减少自行摸索成本为构建自然流畅的语音交互体验提供可复用的参考实现。1. 小程序里接语音识别从录音到文字这套源码把链路跑通了微信小程序做语音交互最卡脖子的不是前端录音而是录音之后那一段——文件格式不对、接口签名算错、长语音超时、识别结果乱码随便一个都能让功能停在演示阶段。这份资源是一套完整的小程序语音识别对接方案核心链路是小程序端录音 → 上传音频文件 → 服务端提取音频并转成 PCM 格式 → 调用科大讯飞语音听写接口 → 返回文字结果。它解决的是「录音容易、转文字难」这个断层适合正在做小程序语音输入、语音搜索、语音笔记类功能的开发者也适合想搞清楚 PCM 转换和讯飞接口签名怎么算的人。下面按实际落地顺序拆一遍。2. 先搞懂音频链路为什么非要转 PCM讯飞接口吃的是什么格式2.1 小程序录音拿到的到底是什么微信小程序的wx.getRecorderManager()默认录出来的是 AAC 编码的.mp3或.aac文件采样率常见 16000Hz 或 44100Hz声道数默认单声道。这个格式人耳听着没问题但科大讯飞的语音听写 WebAPI 对输入有明确要求它接受的是裸 PCM 数据或者特定封装的音频。你直接把 mp3 的二进制流丢过去接口要么报参数错误要么返回空结果。所以链路上必须有一个转换环节。常见做法是服务端用 ffmpeg 把上传的音频转成 16kHz、16bit、单声道的 PCM 文件。这三个参数不是随便定的16kHz 是语音识别模型的标准采样率高了浪费带宽、低了丢高频信息16bit 是位深决定每个采样点的精度单声道是因为识别引擎只处理一路信号双声道反而要额外混音。2.2 PCM 转换的具体命令与参数服务端拿到小程序上传的音频后第一步是落盘第二步是转码。用 ffmpeg 一条命令搞定# 将上传的音频转为讯飞要求的 PCM 格式 # -i 输入文件 -ar 采样率 -ac 声道数 -f 输出格式 -acodec 编码器 ffmpeg -i upload.mp3 -ar 16000 -ac 1 -f s16le -acodec pcm_s16le output.pcm这条命令里几个参数值得盯一下。-ar 16000把采样率统一到 16kHz不管原始录音是 44.1k 还是 48k。-ac 1强制单声道双声道输入会被下混。-f s16le指定输出为有符号 16 位小端格式这是讯飞接口认的裸流格式。-acodec pcm_s16le明确编码器避免 ffmpeg 根据扩展名猜错。转完之后可以用ffprobe output.pcm确认采样率和声道虽然裸 PCM 没有头部信息但文件大小能反推16000 采样 × 2 字节 × 秒数10 秒音频应该是 320000 字节左右对不上就是参数错了。2.3 讯飞接口的鉴权与调用方式科大讯飞语音听写 WebAPI 走的是 WebSocket 协议不是简单的 HTTP POST。鉴权用 HMAC-SHA256 对请求地址签名拼出带authorization参数的 wss 地址。这一步是新手最容易翻车的地方——签名串拼错一个字符服务端直接 401而且报错信息很模糊。import hmac, hashlib, base64, time from urllib.parse import urlencode def build_iflytek_url(host, path, api_key, api_secret): # 生成 RFC1123 格式的时间戳 date time.strftime(%a, %d %b %Y %H:%M:%S GMT, time.gmtime()) # 拼接签名原文注意换行符不能少 signature_origin fhost: {host}\ndate: {date}\nGET {path} HTTP/1.1 # HMAC-SHA256 签名后再 base64 signature base64.b64encode( hmac.new(api_secret.encode(), signature_origin.encode(), hashlib.sha256).digest() ).decode() # 拼出 authorization 参数 authorization_origin ( fapi_key{api_key}, algorithmhmac-sha256, fheadershost date request-line, signature{signature} ) authorization base64.b64encode(authorization_origin.encode()).decode() params {authorization: authorization, date: date, host: host} return fwss://{host}{path}?{urlencode(params)}这段代码的逻辑是先用 host、date、请求行拼出签名原文注意\n换行符必须严格保留少一个就签不过。然后用 api_secret 做 HMAC-SHA256结果 base64 编码。最后把 api_key、算法、签名拼成 authorization 字符串再 base64 一次作为 URL 参数传过去。date参数用的是 GMT 时间本地时间直接拿来用会差 8 小时导致签名过期。api_key 和 api_secret 从讯飞控制台申请别硬编码在代码里放环境变量。3. 小程序端录音与上传参数怎么设、文件怎么传3.1 录音参数的取舍小程序端调用wx.getRecorderManager()时format选mp3还是aac影响不大反正服务端都要转 PCM。但sampleRate和numberOfChannels值得注意如果设成 16000 和 1服务端转码压力小如果设成 44100 和 2音质好但转码多一步。我一般建议直接设 16000 单声道因为语音识别不需要高保真省下的带宽和转码时间更值。const recorderManager wx.getRecorderManager(); const options { duration: 60000, // 最长录音 60 秒超时要分段 sampleRate: 16000, // 与讯飞要求一致减少转码 numberOfChannels: 1, // 单声道 encodeBitRate: 48000, // 编码码率 format: mp3, // 输出格式 frameSize: 50 // 帧大小影响 onFrameRecorded 回调频率 }; recorderManager.start(options); recorderManager.onStop((res) { // res.tempFilePath 是临时文件路径需要上传到服务端 console.log(录音结束, res.tempFilePath, res.duration); });duration设 60000 是因为讯飞语音听写单次请求有时长限制超过 60 秒的音频要切分。frameSize设 50 表示每 50KB 触发一次onFrameRecorded如果做实时识别可以在这里边录边传但本方案是录完再传这个参数不影响主流程。onStop回调里的tempFilePath是本地临时路径小程序重启后会失效必须及时上传。3.2 上传接口与超时处理上传用wx.uploadFile注意它和wx.request不同不支持timeout配置项超时由系统默认值控制。大文件上传失败率不低尤其是 iOS 机型在网络切换时。常见做法是加一个重试逻辑失败后隔 2 秒重传一次最多三次。function uploadAudio(filePath, retry 3) { return new Promise((resolve, reject) { wx.uploadFile({ url: https://your-server.com/api/recognize, filePath: filePath, name: audio, // 服务端用这个字段名接收文件 header: { Content-Type: multipart/form-data }, success: (res) { // 服务端返回 JSON 字符串需要手动 parse const data JSON.parse(res.data); if (data.code 0) resolve(data.text); else reject(new Error(data.msg)); }, fail: (err) { if (retry 0) { setTimeout(() uploadAudio(filePath, retry - 1).then(resolve, reject), 2000); } else { reject(err); } } }); }); }name: audio是服务端 multer 或类似中间件接收文件时的字段名两边要对上。success回调里res.data是字符串必须JSON.parse直接当对象用会报错。重试逻辑放在fail里注意递归调用时要把resolve和reject透传下去否则 Promise 链会断。iOS 上如果遇到uploadFile:fail timeout先检查服务端有没有及时返回超过 10 秒没响应系统会主动断开。3.3 服务端接收与转码的衔接服务端收到文件后先存到临时目录然后调 ffmpeg 转码再把 PCM 数据通过 WebSocket 发给讯飞。这里有个细节讯飞接口要求音频数据分帧发送每帧 1280 字节或 40ms 音频发太快会丢包发太慢会超时。常见做法是按 40ms 一帧16000Hz × 0.04s × 2 字节 1280 字节正好对上。import websocket, json, base64 def send_pcm_to_iflytek(pcm_path, ws_url): ws websocket.create_connection(ws_url) # 发送业务参数status0 表示第一帧 params { common: {app_id: your_app_id}, business: {language: zh_cn, domain: iat, accent: mandarin}, data: {status: 0, format: audio/L16;rate16000, encoding: raw, audio: } } ws.send(json.dumps(params)) with open(pcm_path, rb) as f: while True: chunk f.read(1280) # 每帧 1280 字节 if not chunk: # 最后一帧 status2audio 为空 ws.send(json.dumps({data: {status: 2, audio: }})) break audio_b64 base64.b64encode(chunk).decode() ws.send(json.dumps({data: {status: 1, audio: audio_b64}})) # 接收识别结果 result while True: msg json.loads(ws.recv()) if msg[data][status] 2: break result msg[data][result][ws][0][cw][0][w] ws.close() return resultstatus字段是关键0 表示首帧带参数1 表示中间帧2 表示结束帧。format写audio/L16;rate16000告诉讯飞这是 16kHz 的 16 位 PCM。encoding: raw表示裸数据。每帧 base64 编码后放在audio字段里。接收结果时result.ws是词数组cw是候选词取第一个就是识别文本。注意status 2时结果可能还没收完要循环读到结束帧。4. 避坑与排查签名、格式、超时这三类问题占了八成4.1 签名 401date 格式和换行符是重灾区现象WebSocket 连接直接被拒返回 401 或握手失败。原因通常是签名原文里的 date 格式不对或者换行符被转义。time.strftime生成的 GMT 时间必须是Thu, 01 Jan 2024 00:00:00 GMT这种格式用本地时间或 ISO 格式都签不过。另一个坑是签名原文里的\n在某些编辑器里被自动转成空格肉眼看不出来。解决把签名原文打印出来逐字符比对确认三个换行符都在date 用time.gmtime()而不是time.localtime()。4.2 识别结果为空PCM 参数和帧大小对不上现象接口返回 200但result里没有文字或者只有标点。原因多半是 PCM 采样率不是 16000或者帧大小不是 1280 字节。ffmpeg 转码时如果漏了-ar 16000输出可能是 44100Hz讯飞按 16k 解析就全是噪声。帧大小如果按 1024 字节发讯飞会等不齐一帧导致丢帧。解决转码后用ffprobe确认参数发送前打印每帧字节数确保是 1280 的整数倍。4.3 长语音超时60 秒是硬限制现象录音超过 60 秒后识别失败或者只返回前几秒的文字。原因讯飞语音听写单次请求有时长上限具体值随套餐不同但 60 秒是常见边界。解决小程序端在onStop里判断res.duration超过 55 秒就切分成多段分别上传服务端按顺序拼接结果。切分点选在静音段效果最好但实现复杂简单做法是按固定 50 秒切。4.4 iOS 上传失败率高网络切换和临时路径现象Android 正常iOS 上uploadFile频繁 fail错误码 6001 或 timeout。原因iOS 在小程序切后台或网络从 WiFi 切 4G 时会中断上传且tempFilePath在切后台后可能失效。解决上传前先wx.getFileSystemManager().saveFile把临时文件存到本地缓存目录拿到持久路径再传。上传时加header: { Connection: keep-alive }减少握手开销。4.5 返回文字乱码编码没统一现象识别结果里中文变成问号或方块。原因服务端拼接结果时用了错误的编码或者 WebSocket 接收时没按 UTF-8 解码。Python 的websocket.recv()默认返回 str但如果底层字节流被错误解码就会乱。解决确保json.loads之前数据是 UTF-8 字符串服务端返回给小程序时Content-Type带charsetutf-8。5. 进阶技巧把识别延迟压到 1 秒内的分段策略上面讲的是「录完再传」的离线模式实际体验中用户说完要等两三秒才出文字交互感差。想做到接近实时核心思路是边录边传、分段识别。小程序端在onFrameRecorded回调里拿到音频帧每 40ms 触发一次攒够 1 秒就发一包给服务端服务端转 PCM 后立刻推给讯飞讯飞返回的中间结果先渲染到界面最后用结束帧的结果做校正。这个模式的关键参数是分段长度。太短比如 200ms会导致请求过于频繁服务端和讯飞都扛不住太长比如 3 秒延迟又上去了。我实测下来 800ms 到 1.2 秒是比较舒服的区间对应 16000Hz × 1s × 2 字节 32000 字节的 PCM 数据base64 后约 42KB一次 WebSocket 帧能发完。分段识别的结果拼接有个坑讯飞对每段独立识别段与段之间的词可能重复或断裂。比如「今天天气」切成「今天天」和「气不错」拼起来就错了。常见做法是每段多送 200ms 的重叠音频服务端做去重。具体是在发送第 N 段时把第 N-1 段的最后 200ms 数据一起带上讯飞返回结果后用字符串匹配找到重叠部分只保留新增的文字。def merge_segments(prev_text, new_text, overlap_chars3): # 在 new_text 开头找 prev_text 结尾的重叠 for i in range(min(overlap_chars, len(new_text)), 0, -1): if prev_text.endswith(new_text[:i]): return prev_text new_text[i:] return prev_text new_text这个函数从长到短尝试匹配重叠字符匹配上就拼接匹配不上就直接接。overlap_chars设 3 是经验值中文词一般 2 到 4 个字设太大容易误匹配。实际用的时候还要处理标点讯飞返回的结果里标点是单独字段拼接时要注意别把标点吞了。另一个进阶点是热词优化。讯飞接口支持传hotword参数把业务相关的词比如商品名、人名提前传进去识别准确率能明显提升。热词用竖线分隔最多 200 个每个词不超过 10 个字。这个参数在business字段里加hotword: 词1|词2|词3就行但注意热词太多会拖慢识别速度按需加。从那以后我每次接语音识别都先把「转 PCM → 验参数 → 测签名 → 压延迟」这四步走一遍再动手写业务逻辑。这套源码把前两步的坑都填好了拿过来改改 app_id 和密钥就能跑。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网