
简介这是一份面向Java开发者的讯飞语音转文字ASR实战示例包适合具备Java基础、希望快速接入讯飞RESTful API完成语音识别功能的初中级工程师。资源围绕APP ID与密钥申请、OAuth 2.0获取Access Token、音频文件读取与格式转换、Multipart上传、JSON结果解析、异步回调及错误重试等关键环节展开覆盖从鉴权到文字输出的完整链路。压缩包共8个文件含6个java源码与2个jar依赖整体约143KB源码按控制层、服务接口与实现、上传及工具类分层组织jar包提供讯飞SDK与音频处理支持结构清晰便于按模块阅读。目前已有3962人学习下载读者可借助示例代码理解HTTP请求封装、Token管理与识别结果提取的写法并参考其中的异常处理与日志记录思路将其迁移到智能设备、客服质检或AI交互等实际场景中减少自行摸索接口联调的时间成本。1. Java 讯飞语音转文字从接口签名到生产可用的完整落地路径很多团队第一次接讯飞语音转文字卡住的地方不是识别效果而是 Java 侧那一串鉴权参数——hmacsha256 签名、Base64 编码、RFC1123 时间格式任何一个环节差一个字符服务端就回 401。我见过有开发者在这上面耗掉整整两天最后发现是拼接字符串时多了一个换行符。这篇笔记就围绕「Java 讯飞语音转文字」这条链路把 WebSocket 实时转写和 HTTP 短音频转写两条路都拆开讲清楚签名怎么算、音频怎么送、结果怎么收、并发怎么控、坑在哪里。适合正在做会议记录、客服质检、语音工单转写的 Java 后端也适合需要把语音能力嵌进现有 Spring Boot 服务的团队。2. 先搞懂鉴权Java 侧签名到底在签什么讯飞开放平台的 WebSocket 接口不是简单塞一个 API Key 就能用它要求客户端在建立连接前用 HMAC-SHA256 对一串特定格式的字符串做签名再把签名结果和其余参数拼成带鉴权信息的 URL。这一步是纯 Java 标准库就能完成的事不需要额外依赖但顺序和格式必须严格对齐。2.1 鉴权参数的四个组成部分签名前需要准备四个值host、date、request-line以及你的apiSecret。其中host固定为ws-api.xfyun.cndate必须是 RFC1123 格式的 GMT 时间字符串request-line由GET、目标路径和 HTTP 版本拼成。这四个值按固定顺序用换行符连接形成待签名字符串。// 构造待签名字符串注意每行末尾的 \n 不能少也不能多 String host ws-api.xfyun.cn; String date getRFC1123Time(); // 形如 Mon, 12 May 2025 08:30:00 GMT String requestLine GET /v2/iat HTTP/1.1; String signatureOrigin host: host \n date: date \n requestLine;这里最容易翻车的是date的格式。Java 的SimpleDateFormat默认用本地时区必须显式设置TimeZone.getTimeZone(GMT)和Locale.US否则星期几和月份缩写会变成中文或本地语言签名直接失效。2.2 HMAC-SHA256 签名与 Base64 编码拿到待签名字符串后用apiSecret作为密钥做 HMAC-SHA256结果再 Base64 编码。JDK 自带javax.crypto.Mac就能完成不需要引入第三方加密库。import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.util.Base64; public static String hmacSha256(String secret, String message) throws Exception { Mac mac Mac.getInstance(HmacSHA256); SecretKeySpec keySpec new SecretKeySpec(secret.getBytes(UTF-8), HmacSHA256); mac.init(keySpec); byte[] rawHmac mac.doFinal(message.getBytes(UTF-8)); return Base64.getEncoder().encodeToString(rawHmac); }参数说明secret就是控制台里的 APISecretmessage是上一步拼好的signatureOrigin。注意getBytes必须指定 UTF-8虽然这里内容都是 ASCII但养成习惯能避免后续拼接中文参数时出问题。2.3 把签名拼回 WebSocket URL签名完成后还需要把authorization字段做一次 Base64然后和date、host一起作为查询参数拼到 wss 地址上。String signature hmacSha256(apiSecret, signatureOrigin); String authorizationOrigin api_key\ apiKey \, algorithm\hmac-sha256\, headers\host date request-line\, signature\ signature \; String authorization Base64.getEncoder().encodeToString(authorizationOrigin.getBytes(UTF-8)); String url wss://ws-api.xfyun.cn/v2/iat ?authorization urlEncode(authorization) date urlEncode(date) host host;authorizationOrigin里的引号和逗号位置是固定的多一个空格都会导致鉴权失败。urlEncode建议用URLEncoder.encode(value, UTF-8)但要注意它会把空格转成而讯飞服务端期望的是%20所以编码后需要做一次替换。提示签名用的date和服务端时间偏差不能超过 5 分钟服务器时间不同步会直接返回 401部署前先确认 NTP 正常。3. WebSocket 实时转写从建连到收结果的完整链路实时转写适合麦克风流、电话录音流这类边录边转的场景。Java 侧可以用java.net.http.WebSocketJDK 11或者第三方客户端下面以 JDK 自带 API 为主线把建连、发帧、收结果三个阶段串起来。3.1 建立连接与握手参数建连时除了上面拼好的 URL还需要在Listener.onOpen里确认连接就绪然后才能发送第一帧音频。讯飞要求音频以 Base64 编码后放在 JSON 里发送每帧建议 40ms 音频采样率 16k、位深 16bit、单声道。HttpClient client HttpClient.newHttpClient(); WebSocket webSocket client.newWebSocketBuilder() .buildAsync(URI.create(signedUrl), new WebSocket.Listener() { Override public void onOpen(WebSocket ws) { System.out.println(连接已建立可以开始发送音频); ws.request(1); } Override public CompletionStage? onText(WebSocket ws, CharSequence data, boolean last) { System.out.println(收到识别结果: data); ws.request(1); return null; } }).join();参数说明request(1)是背压控制告诉 WebSocket 每次只推一条消息处理完再请求下一条避免消息堆积。如果做高并发这个值可以适当调大但要配合业务侧的队列长度一起评估。3.2 音频分帧与发送节奏音频不能一次性全发要按 40ms 一帧切分每帧之间保持和实时播放一致的节奏。发太快服务端会丢帧发太慢识别结果会断断续续。// 假设 audioData 是 16k/16bit/单声道 PCM 字节数组 int frameSize 640 * 2; // 40ms * 16000Hz * 2字节 for (int offset 0; offset audioData.length; offset frameSize) { int end Math.min(offset frameSize, audioData.length); byte[] frame Arrays.copyOfRange(audioData, offset, end); String base64Frame Base64.getEncoder().encodeToString(frame); JSONObject payload new JSONObject(); payload.put(data, base64Frame); payload.put(status, offset 0 ? 0 : (end audioData.length ? 2 : 1)); payload.put(format, audio/L16;rate16000); webSocket.sendText(payload.toJSONString(), true); Thread.sleep(40); // 模拟实时节奏 }status字段是关键第一帧传 0中间帧传 1最后一帧传 2。服务端靠这个判断音频流的起止。如果最后一帧没发或者 status 不对识别结果不会返回最终版本。3.3 解析返回结果与拼接文本服务端返回的 JSON 里识别文本在data.result.ws[].cw[].w这个嵌套结构里需要逐层取出来拼接。动态修正模式下后到的结果可能覆盖前面的内容所以不能简单追加。JSONObject root JSON.parseObject(responseText); JSONObject data root.getJSONObject(data); if (data ! null data.getJSONObject(result) ! null) { JSONArray ws data.getJSONObject(result).getJSONArray(ws); StringBuilder segment new StringBuilder(); for (int i 0; i ws.size(); i) { JSONArray cw ws.getJSONObject(i).getJSONArray(cw); segment.append(cw.getJSONObject(0).getString(w)); } // 根据 pgs 字段判断是追加还是替换 boolean isFinal data.getJSONObject(result).getBooleanValue(pgs); if (isFinal) { fullText.append(segment); } else { // 动态修正替换最后一段 lastSegment segment.toString(); } }pgs为 true 表示这一句已经确定可以追加到全文为 false 表示还在动态修正需要暂存并替换。这个逻辑不处理好最终文本会出现重复或丢字。4. HTTP 短音频转写更简单的同步方案与参数调优如果音频是已经录好的文件不需要实时返回HTTP 接口更省事。它没有 WebSocket 的帧节奏要求一次请求传完整个音频同步拿结果。适合工单语音、批量录音文件转写这类场景。4.1 接口调用与请求体构造HTTP 接口同样需要签名但签名算法和 WebSocket 一致只是request-line和host不同。请求体是 JSON音频 Base64 后放在data字段里。String body {\data\:\ base64Audio \, \format\:\audio/L16;rate16000\, \encoding\:\raw\}; HttpRequest request HttpRequest.newBuilder() .uri(URI.create(https://iat-api.xfyun.cn/v2/iat)) .header(Content-Type, application/json) .header(Authorization, authorization) .header(Date, date) .POST(HttpRequest.BodyPublishers.ofString(body)) .build();注意 HTTP 接口的鉴权信息放在 Header 里不是 URL 参数。Authorization的构造方式和 WebSocket 一样但request-line要改成POST /v2/iat HTTP/1.1。4.2 音频格式与采样率怎么选讯飞支持多种音频格式但不同格式对识别率有影响。实测下来16k 采样率、16bit 位深、单声道 PCM 的识别效果最稳。如果原始音频是 8k 电话录音建议先重采样到 16k虽然多一步处理但识别准确率提升明显。参数推荐值说明采样率16000Hz8k 也能识别但准确率下降位深16bit不支持 8bit声道单声道双声道需要先合并格式raw PCM 或 wavmp3 需要解码后送单次时长60秒以内超长音频建议切片4.3 批量转写的并发控制批量转写时不要无限制并发讯飞对 QPS 有配额限制超了会返回 503。建议用信号量控制并发数同时做好失败重试。Semaphore semaphore new Semaphore(5); // 根据配额调整 ExecutorService executor Executors.newFixedThreadPool(10); for (File audio : audioFiles) { executor.submit(() - { semaphore.acquire(); try { String text transcribe(audio); saveResult(audio.getName(), text); } catch (Exception e) { // 记录失败后续重试 failedList.add(audio); } finally { semaphore.release(); } }); }并发数建议从 3 到 5 起步观察服务端返回码再调整。遇到 503 不要立刻重试退避几秒再试否则会触发更严格的限流。5. 避坑与排查那些让转写结果不达预期的细节这一章记录几个我在实际项目里踩过的坑每个都按现象、原因、解决三步说清楚。5.1 返回 401 但签名代码看起来没问题现象本地测试通过部署到服务器后全部 401。原因服务器时区不是 GMTSimpleDateFormat生成的 date 字符串带了本地时区偏移。解决显式设置TimeZone.getTimeZone(GMT)并在部署脚本里加一行date -u确认服务器 UTC 时间正常。5.2 识别结果只返回前半句现象音频明明有 30 秒但只识别出前 10 秒的内容。原因最后一帧的status没有设为 2服务端认为音频流还没结束不返回最终结果。解决发送完所有帧后确保最后一帧status2并且不要立刻关闭连接等收到最终结果再关。5.3 中文识别成同音错字现象专业术语频繁识别错比如「阈值」识别成「玉质」。原因通用模型对垂直领域词汇覆盖不足。解决在请求参数里加pd字段指定领域或者用hotword参数传入自定义热词热词用 Base64 编码后传。5.4 高并发下连接被重置现象单路测试正常10 路并发时部分连接被服务端断开。原因超过了账号的并发配额或者单 IP 连接数过多。解决先确认控制台的并发上限用信号量控制实际并发数如果单机不够拆到多台机器上分散连接。5.5 音频 Base64 后体积膨胀导致超时现象长音频转写时请求超时。原因Base64 编码会让体积增大约 33%60 秒 16k 音频编码后接近 2MB网络差时容易超时。解决控制单次音频时长在 60 秒以内超长音频先切片再并发转写最后按顺序拼接文本。6. 进阶技巧热词优化与结果后处理热词是提升垂直场景识别率最直接的手段。讯飞支持在请求里传hotword参数格式是词1|词2|词3Base64 编码后放入。热词不是越多越好建议控制在 50 个以内太多会稀释权重。我一般会把业务里高频出现的产品名、人名、专业术语整理成列表按周更新。结果后处理方面识别返回的文本没有标点需要自己加。简单做法是用规则匹配在「吗、呢、吧」后面加问号在「了、的」后面加逗号。更稳的做法是接一个轻量标点模型但会增加延迟。如果对实时性要求不高可以在整段转写完成后统一处理。验证转写质量时不要只看一两句。我习惯抽 20 条音频人工核对字准率低于 90% 就回头查音频质量和热词配置。另外pgs字段的动态修正逻辑一定要写单元测试用模拟的返回序列覆盖追加、替换、混合三种情况这块出 bug 最隐蔽线上表现就是文本偶尔重复或丢字。注意热词和领域参数只在部分接口版本生效接入前先用控制台的调试工具确认当前账号支持的参数集。我自己踩过最深的一个坑是早期做实时转写时没处理pgs的动态修正结果会议记录里同一句话出现了两遍排查了一下午才发现是追加逻辑写错了。从那以后凡是涉及增量结果的接口我都会先把状态机画清楚再写代码。希望帮到你。本文还有配套的精品资源点击获取