简介本资源面向C#后端开发者与语音应用集成人员聚焦在WebAPI环境下调用科大讯飞语音听写服务将上传的音频文件转换为文字可应用于智能客服、在线教育、语音助手等场景。压缩包共47个文件约5.08MB包含5个cs源码文件、10个dll依赖库、10个xml配置、3个config与2个wav测试音频另有sln解决方案、csproj工程文件及packages.config等结构完整可直接运行调试。资源重点解决gb2312编码报错问题需引入System.Text.Encoding.CodePages包并通过HttpClient构造MultipartFormDataContent发送请求解析JSON响应获取听写文本。目前已有1747人学习下载适合希望快速跑通讯飞语音听写接口、理解编码处理与请求构造细节的开发者参考。1. 从一次“听写接口调不通”说起这套 C# WebAPI 方案到底能解决什么去年帮一个做在线教育题库的朋友排查问题他们的场景很典型老师用小程序录一段口述题目后端要实时转成文字入库。团队一开始想在前端直接调第三方语音听写接口结果 AppID 和密钥暴露在客户端被刷了一笔不小的调用量。后来改成后端中转用 C# WebAPI 封装语音听写前端只传音频、后端统一鉴权转发。这个思路就是本文要拆的这套方案的核心——把科大讯飞的语音听写能力收进自己的 WebAPI 里对外只暴露一个干净的 HTTP 接口。它解决的不是“语音识别算法怎么写”而是工程落地里的三件事密钥不出服务端、音频格式统一收口、识别结果结构化返回。适合谁手上是 .NET 技术栈、需要给 App 或小程序做语音转文字后端、又不想把第三方凭证散落在客户端的开发者。整套东西不复杂但坑集中在鉴权签名、音频编码和 WebSocket 分帧上下面一层层拆。2. 鉴权与签名把 WebAPI 密钥挡在服务端的第一道门2.1 为什么不能前端直连签名到底签了什么科大讯飞的语音听写走的是 WebSocket 协议连接前要先拼一个带签名的 URL。这个签名基于 HMAC-SHA256用你的 APISecret 对一串特定格式的字符串做摘要再把结果 Base64 编码塞进 query 参数。问题就在这APISecret 一旦出现在前端等于把账号交出去了。所以正确姿势是后端生成签名和完整 URL前端只拿到一个“可以连的地址”或者干脆连音频都走后端转发。签名的原文格式是固定的常见做法是拼成host: xxx\n date: xxx\n GET /v1/iat HTTP/1.1这种带换行的结构顺序和换行符一个都不能错。我见过最多的翻车就是换行写成\r\n或者漏了末尾那个换行服务端直接返回 401日志里还看不出原因纯玄学。// 生成讯飞 WebSocket 鉴权 URL 的核心逻辑 public string BuildAuthUrl(string host, string path, string apiKey, string apiSecret) { // 1. 生成 RFC1123 格式的 UTC 时间讯飞要求 date 必须用这个格式 var date DateTime.UtcNow.ToString(r); // 2. 拼签名原文注意 \n 是 LF不是 CRLF末尾也要有一个 \n var signatureOrigin $host: {host}\ndate: {date}\nGET {path} HTTP/1.1; // 3. HMAC-SHA256 摘要后用 Base64 编码 using var hmac new HMACSHA256(Encoding.UTF8.GetBytes(apiSecret)); var signature Convert.ToBase64String( hmac.ComputeHash(Encoding.UTF8.GetBytes(signatureOrigin))); // 4. 再对 authorization 整体做一次 Base64作为最终 authorization 参数 var authorizationOrigin $api_key\{apiKey}\, algorithm\hmac-sha256\, headers\host date request-line\, signature\{signature}\; var authorization Convert.ToBase64String(Encoding.UTF8.GetBytes(authorizationOrigin)); // 5. 拼成 wss 地址三个参数都要 UrlEncode return $wss://{host}{path}?authorization{Uri.EscapeDataString(authorization)} $date{Uri.EscapeDataString(date)}host{Uri.EscapeDataString(host)}; }逻辑说明date用DateTime.UtcNow.ToString(r)得到的是 RFC1123 格式讯飞服务端会校验时间偏差一般允许几分钟的窗口服务器时间不同步会直接失败。signatureOrigin里的换行必须是\n这是最容易踩的坑。authorizationOrigin里的headers字段顺序要和签名原文一致写反了签名对不上。最后拼 URL 时三个参数都要Uri.EscapeDataString否则 Base64 里的/会被 URL 解析搞坏。参数说明host固定是iat-api.xfyun.cnpath是/v1/iat这两个值不要自己改。apiKey和apiSecret从控制台拿建议放配置中心或环境变量别硬编码进代码提交到仓库——这是血泪经验密钥泄露基本等于账单失控。2.2 在 WebAPI 里怎么组织这套鉴权我一般会把鉴权逻辑单独抽一个XunfeiAuthService注册成单例因为它无状态、纯计算。Controller 里只负责接收前端请求、调用服务拿 URL、再决定是“把 URL 返给前端让前端直连”还是“后端自己连”。两种模式各有取舍模式密钥位置前端复杂度适用场景后端返 URL服务端前端需实现 WebSocket前端有实时交互需求音频不上传后端后端全代理服务端前端只发 HTTP音频需落库、需二次处理、前端不想碰 WS如果选后端全代理WebAPI 里要开 WebSocket 或者用ClientWebSocket连讯飞再把识别结果通过 SignalR 或 SSE 推给前端。这一步的复杂度会明显上升但密钥和音频都留在服务端安全性最好。新手建议先用“后端返 URL”跑通链路再考虑全代理。提示签名 URL 是有时效的别生成一次缓存起来反复用每次连接前重新生成避免时间窗口过期。3. 音频格式与 WebSocket 分帧识别率上不去的真正原因3.1 音频参数不对识别结果就是一堆乱码讯飞语音听写对音频有明确要求采样率 16k 或 8k、位深 16bit、单声道、PCM 或特定编码。前端录出来的音频五花八门微信小程序默认可能是 mp3 或 aac直接丢给讯飞大概率识别失败或者结果离谱。常见做法是在后端做一次转码用 FFmpeg 统一转成 16k 16bit 单声道 PCM。# 把任意格式音频转成讯飞要求的 PCM ffmpeg -i input.mp3 -ar 16000 -ac 1 -f s16le -acodec pcm_s16le output.pcm参数说明-ar 16000指定采样率 16k-ac 1单声道-f s16le输出 signed 16-bit little-endian-acodec pcm_s16le明确编码。转出来的.pcm是裸流没有头信息正好适合按帧切分发送。如果前端能直接录 PCM 最好省一次转码不能的话后端转码这步别省识别率差距很明显。3.2 分帧发送每 40ms 一帧最后一帧要标记讯飞要求音频按帧发送建议每帧 1280 字节16k 采样率下约 40ms。发送时用 WebSocket 的文本帧内容是 JSON音频数据 Base64 编码放在data.audio字段里。关键在data.status第一帧传 0中间帧传 1最后一帧传 2。最后一帧的标记错了服务端会一直等直到超时。// 按 1280 字节分帧发送音频 public async Task SendAudioAsync(ClientWebSocket ws, byte[] pcmData) { const int frameSize 1280; var totalFrames (int)Math.Ceiling(pcmData.Length / (double)frameSize); for (int i 0; i totalFrames; i) { var offset i * frameSize; var length Math.Min(frameSize, pcmData.Length - offset); var frame new byte[length]; Array.Copy(pcmData, offset, frame, 0, length); // status: 0 首帧, 1 中间帧, 2 末帧 int status (i 0) ? 0 : (i totalFrames - 1 ? 2 : 1); var payload new { data new { status status, format audio/L16;rate16000, encoding raw, audio Convert.ToBase64String(frame) } }; var json JsonSerializer.Serialize(payload); await ws.SendAsync(Encoding.UTF8.GetBytes(json), WebSocketMessageType.Text, true, CancellationToken.None); // 控制发送节奏太快可能被限流 await Task.Delay(40); } }逻辑说明frameSize取 1280 是官方建议值对应 40ms 音频。status的三态是协议要求首帧 0、末帧 2中间全 1。format字段写audio/L16;rate16000encoding写raw表示裸 PCM。Task.Delay(40)是模拟实时发送节奏如果音频已经录完可以适当加快但别一次性全发服务端有缓冲限制。参数说明如果采样率是 8kframeSize改成 640format里的rate改成 8000。encoding如果传的是 speex 等压缩格式要相应改但 PCM 最稳。发送太快触发限流的表现是连接被断日志里能看到 1006 异常关闭。3.3 接收结果动态修正和最终结果要分开处理讯飞返回的 JSON 里data.result包含识别文本data.status为 2 时表示最后一帧结果。中间结果会带pgs和rg字段用于动态修正——同一个词可能先返回拼音再返回汉字。我一般只取ws数组里cw[0].w拼接中间结果用于前端实时显示最终结果以 status2 的那条为准。// 解析讯飞返回的识别结果 private string ParseResult(string json) { using var doc JsonDocument.Parse(json); var root doc.RootElement; if (!root.TryGetProperty(data, out var data)) return string.Empty; if (!data.TryGetProperty(result, out var result)) return string.Empty; var ws result.GetProperty(ws); var sb new StringBuilder(); foreach (var w in ws.EnumerateArray()) { var cw w.GetProperty(cw)[0]; sb.Append(cw.GetProperty(w).GetString()); } return sb.ToString(); }逻辑说明ws是词数组每个元素里的cw是候选词取第一个w就是识别文本。中间结果和最终结果结构一样区别只在data.status。如果要处理动态修正需要维护一个文本缓冲区根据pgs的replace标记决定是追加还是替换这块逻辑复杂新手可以先只取最终结果。4. 避坑与排查五个让接口“时好时坏”的细节4.1 现象连接直接 401日志无有效信息原因签名原文格式错误最常见的是换行符用了\r\n或者date不是 RFC1123 格式或者headers字段顺序和签名原文不一致。解决把签名原文打印出来逐字符比对确认是\n不是\r\ndate用ToString(r)headers严格按host date request-line顺序。4.2 现象识别结果为空或全是乱码原因音频格式不匹配。前端传了 mp3 但format声明成 PCM或者采样率对不上。解决后端统一转码成 16k 16bit 单声道 PCMformat字段和实际音频严格一致。用ffprobe确认转码后的参数。4.3 现象发送到一半连接断开错误码 1006原因发送速度过快触发限流或者帧大小不对。解决每帧之间加Task.Delay(40)帧大小控制在 1280 字节16k或 640 字节8k。如果音频很长考虑分段发送而不是一次性全推。4.4 现象最后一帧发完服务端不返回最终结果原因status标记错误末帧没传 2服务端一直在等更多数据。解决确认最后一帧的status是 2且只发一次。如果音频为空也要发一个 status2 的空帧表示结束。4.5 现象本地正常部署到服务器就失败原因服务器时间不同步导致签名里的date和服务端偏差过大。解决服务器开启 NTP 时间同步或者用DateTime.UtcNow而不是本地时间。另外检查服务器出站 WebSocket 是否被防火墙拦截。5. 进阶把识别结果落库并做二次校验的完整链路跑通基础链路后真正在生产里用还得解决“识别结果怎么存、怎么纠错”。我一般的做法是WebAPI 收到音频后先落一份原始文件到对象存储转码后调听写识别文本连同音频 URL、时间戳、置信度一起入库。置信度可以从返回结果里取虽然讯飞不直接给整句置信度但可以通过ws里每个词的cw候选数量粗略判断——候选越多说明越不确定。-- 识别结果落库表结构 CREATE TABLE speech_recognition ( id BIGINT PRIMARY KEY AUTO_INCREMENT, audio_url VARCHAR(512) NOT NULL COMMENT 原始音频地址, raw_text TEXT COMMENT 识别原始文本, corrected_text TEXT COMMENT 人工校正后文本, duration_ms INT COMMENT 音频时长毫秒, status TINYINT DEFAULT 0 COMMENT 0待校正 1已校正, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );落库之后可以做一个简单的校验接口把识别文本和业务词库做匹配命中率低的标记出来让人工复核。这一步不是必须的但题库、医疗这类对准确率敏感的场景纯靠识别结果直接入库风险很大。验证方法上我习惯用一段固定音频反复跑对比每次返回的文本是否一致同时记录耗时。如果耗时波动大多半是网络或分帧节奏问题。另外可以故意传一段静音看服务端是否正确返回空结果而不是报错——这能验证末帧标记逻辑是否健壮。从那以后我每次接语音听写都强制先把签名原文和音频参数打印出来核对一遍再跑一段 10 秒的测试音频确认首帧、中间帧、末帧的 status 都对最后才接业务逻辑。这套流程看着笨但能省掉大量“时好时坏”的排查时间。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?