尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java接入讯飞语音转文字:WebSocket实时流式接口全解析

Java接入讯飞语音转文字:WebSocket实时流式接口全解析 简介这是一份面向Java开发者与后端工程师的讯飞语音转文字集成示例核心解决了在Java后端中快速接入讯飞ASR音频识别能力的问题适合有基础API调用经验、正为智能设备或AI交互功能寻找语音方案的程序员。资源包为zip格式共八个文件包括六个Java源文件与两个Jar依赖包总计一百四十三KB体量很小但完整呈现了典型调用链。源码按控制器、服务层、工具类分层覆盖HTTP请求、OAuth2.0授权、音频文件处理、Multipart上传、JSON结果解析、异步回调与异常重试等关键环节并带有日志记录辅助问题定位。据平台显示已有3937人学习下载参考热度较高。通过阅读和复用这些代码可大幅缩短讯飞语音接口的调试时间也为后续扩展音频格式或并发处理提供了可借鉴的模板。1. 讯飞语音转文字Java接入前先想清楚这几件事先交代一下背景最近在做一个需要把录音文件转成文字的项目调研了一圈之后选了讯飞的语音转文字接口。选择它的原因也简单识别准确率在中文场景下属于第一梯队而且对国内开发者的文档支持比较完善星火大模型相关的配套资料也很多。最关键的是它提供WebSocket实时流式接口可以在说话的同时出结果对用户体验的提升非常明显。这篇文章主要聊聊Java接入讯飞语音转文字的完整流程。如果你手里有现成的录音文件mp3、wav、pcm等格式都行想把它们批量转成文字内容或者你是想做实时语音对话、会议记录、客服质检这类功能那这篇文章可以帮你少走弯路。我会从环境准备、鉴权逻辑、核心代码、踩坑实录到性能优化一条线讲透。需要说明的是我这里讲的是基于讯飞开放平台官方文档的常见接入实践具体参数以你在控制台看到的最新文档为准思路是通用的照着跑通完全没问题。1.1 语音转文字的常见实现路径语音转文字这块业内其实有好几种玩法先说清楚你才知道自己该选哪条路。第一种是实时流式转写通过WebSocket长连接边录音边传音频数据服务端边识别边返回结果。适合语音对话、实时字幕、会议纪要这类对实时性有要求的场景。第二种是录音文件转写把完整音频文件提交上去服务端异步处理轮询或回调拿结果。适合对实时性没要求、但音频文件大的场景比如历史录音、客服电话录音的批量转写。讯飞这个接口还支持导出带时间戳的SRT字幕做视频字幕很方便。第三种是用HTTP短连接直接传一段不超过60秒的音频同步返回结果。适合录音片段比较短、想快速试用的场景。我在实际项目里主要用的是WebSocket实时流式方案它最灵活既能处理实时语音也能把本地音频文件按照分帧逻辑推给服务端。这篇文章就以它为主线来讲。1.2 选择WebSocket方案的三个理由为什么不直接选HTTP同步接口这里说下我的考量。第一实时性。语音交互产品的体验核心就是边说边出字WebSocket长连接天然支持流式返回服务端每识别出一句话就能立刻推给你。HTTP同步方案需要等整段录音传完用户体验是两个层级。第二协议上的自由度。WebSocket可以自己控制分帧节奏音频从哪开始、什么时候结束、要不要半路再加一段音频都由你控制。HTTP方案在传大文件时经常遇到超时问题处理起来很麻烦。第三WebSocket鉴权链路中涉及URL签名、HMAC-SHA256加密这套逻辑在很多后端语言里都是通用的。把鉴权逻辑吃透了不只是讯飞很多云服务商的流式接口你都能很快上手。2. 环境准备与鉴权JAVA端最容易栽跟头的一步2.1 开发环境与依赖清单在开始写代码之前先把环境准备好。我这边用的是这样一个组合组件版本/说明JDK8 或 11 均可推荐 11长期支持更稳构建工具Maven 3.6Gradle 也行WebSocket客户端Java-WebSocketorg.java-websocket:Java-WebSocket:1.5.3JSON处理Fastjson 或 Gson用于解析返回结果音频工具有转码需求时引入 JLayerMP3解码、javax.sound.sampledWAV读取Java-WebSocket这个库是我实测下来在Java端最省心的WebSocket实现依赖少API友好。Maven坐标如下dependency groupIdorg.java-websocket/groupId artifactIdJava-WebSocket/artifactId version1.5.3/version /dependency2.2 鉴权URL生成原理RFC3986编码与HMAC-SHA256签名讯飞语音转文字的WebSocket接口鉴权方式和大多数云服务商一样走的是签名URL模式。思路很简单把请求参数加上时间戳、签名算法信息用密钥签名后拼成一个URL。服务端拿到URL后用同样的方式验签验过了才允许建立连接。这一步写代码不算难但有两个细节处理不好就很容易翻车。第一个坑是RFC3986编码。按照讯飞文档要求要签名的字符串得先做RFC3986规范下的URL编码。这里特别容易出问题的是Java原生的URLEncoder.encode()方法会把空格编码成号而RFC3986要求空格编码成%20。所以签名之前必须先做一次替换String encoded URLEncoder.encode(originStr, UTF-8) .replace(, %20) .replace(*, %2A) .replace(%7E, ~);第二个坑是base64编码默认带换行符。Java早期的Base64编码会把长字符串自动加换行这在签名过程中是致命的。签名生成后一比对怎么都对不上多半就是这个原因。解决方式很简单——用java.util.Base64不要用sun.misc也不要自己拼String signature Base64.getEncoder().encodeToString(hmacBytes);2.3 鉴权URL完整实现代码下面给出一份可以直接跑通的鉴权URL生成代码。这段逻辑是整个接入过程中最核心的前置环节建议仔细看import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.SimpleDateFormat; import java.util.Base64; import java.util.Date; import java.util.Locale; import java.util.TimeZone; public class XfAuthUrlGenerator { private static final String HOST iat-api.xfyun.cn; private static final String PATH /v2/iat; public static String generateAuthUrl(String apiKey, String apiSecret) throws Exception { // 1. 生成RFC1123格式的时间戳必须使用GMT时区 SimpleDateFormat format new SimpleDateFormat(EEE, dd MMM yyyy HH:mm:ss zzz, Locale.US); format.setTimeZone(TimeZone.getTimeZone(GMT)); String date format.format(new Date()); // 2. 拼接签名原文字符串 String signatureOrigin host: HOST \n date: date \n GET PATH HTTP/1.1; // 3. HMAC-SHA256加密 Mac mac Mac.getInstance(HmacSHA256); SecretKeySpec spec new SecretKeySpec(apiSecret.getBytes(StandardCharsets.UTF_8), HmacSHA256); mac.init(spec); byte[] rawSignature mac.doFinal(signatureOrigin.getBytes(StandardCharsets.UTF_8)); String signature Base64.getEncoder().encodeToString(rawSignature); // 4. 拼接鉴权参数并做RFC3986编码 String authorizationOrigin api_key\ apiKey \, algorithm\hmac-sha256\, headers\host date request-line\, signature\ signature \; String authorization URLEncoder.encode(authorizationOrigin, UTF-8) .replace(, %20) .replace(*, %2A) .replace(%7E, ~); // 5. 拼装最终URL return ws:// HOST PATH ?authorization authorization date URLEncoder.encode(date, UTF-8) host HOST; } }这里解释一下签名原文为什么要这样拼。讯飞要求把HOST、DATE、请求行三部分用换行符连起来这个格式不是随便定的它遵循HTTP签名规范RFC 7235里的Signature方案。服务端拿到请求后会从请求头里取出同样的三部分内容做校验。所以拼接格式一个字都不能错换行符必须是\n冒号后面有个空格这些都是硬性要求。2.4 控制台参数去哪找打开讯飞开放平台控制台创建应用后在语音听写流式版服务详情页能看到三个关键参数AppID应用的唯一标识一般在请求体里的common参数中携带。APIKey用于生成鉴权URL相当于你的身份凭证。APISecret用于签名和APIKey配套使用。这三个参数建议放到环境变量或配置中心里千万不要硬编码提交到Git仓库。我见过不止一个人在GitHub上把自己的密钥公开了然后被别人刷爆了配额这个教训挺肉疼的。3. 核心代码实现连接、发数据、收结果一条龙3.1 建立WebSocket连接鉴权URL生成之后连接建立就很简单了。用Java-WebSocket客户端核心逻辑是重写三个回调方法连接成功回调、收到消息回调、连接断开回调。import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; import java.net.URI; import java.nio.ByteBuffer; public class XfIatClient extends WebSocketClient { public XfIatClient(URI serverUri) { super(serverUri); } Override public void onOpen(ServerHandshake handshakedata) { System.out.println(WebSocket连接已建立); } Override public void onMessage(String message) { // 这里是服务端推送的识别结果JSON System.out.println(收到结果: message); } Override public void onMessage(ByteBuffer bytes) { // 二进制消息处理 } Override public void onClose(int code, String reason, boolean remote) { System.out.println(连接已关闭: reason); } Override public void onError(Exception ex) { ex.printStackTrace(); } }连接建立之后第一件事就是发一个带音频参数的开帧消息。这个动作相当于告诉服务端我要开始传音频了音频格式是这些识别语言是中文。 请求体是JSON格式的String firstFrame { \common\: { \app_id\: \ appId \ }, \business\: { \language\: \zh_cn\, \domain\: \iat\, \accent\: \mandarin\, \vad_eos\: 5000, \dwa\: \wpgs\ }, \data\: { \status\: 0, \format\: \audio/L16;rate16000\, \encoding\: \raw\, \audio\: \\ } }; client.send(firstFrame);参数说明一下language语言中文传zh_cn。domain应用领域语音听写固定传iat。accent方言/口音普通话传mandarin粤语传cantonese四川话传lmz等可以根据场景调整。vad_eos静默断句时间单位毫秒。5000表示检测到5秒静音就自动结束识别适合录音较长的情况如果做实时对话可以调小一点。dwa结果动态修正模式传wpgs时返回带词级时间戳的中间结果适合做字幕。3.2 音频分帧发送不是一次扔完而是边读边发音频数据发送是整个过程中最容易踩坑的部分需要理解讯飞的分帧要求。服务端要求音频数据按分帧的方式发送每一帧的音频时长建议在40ms到200ms之间我一般按80ms切分。如果一次把所有音频全塞进去不仅容易触发服务端的帧长限制还会增加网络负载导致首包数据延迟变长。如果是实时录音数据这个逻辑很自然——直接把录音缓冲区的数据按帧发送。如果是本地音频文件就得自己写一个读取循环。逻辑如下public void sendAudioFile(String filePath) throws Exception { File file new File(filePath); byte[] audioData readAllBytes(file); int frameSize 1280; // 80ms 16kHz 16bit单声道等于16000*2*0.08 int offset 0; int totalLength audioData.length; while (offset totalLength) { int end Math.min(offset frameSize, totalLength); byte[] frame Arrays.copyOfRange(audioData, offset, end); String base64Audio Base64.getEncoder().encodeToString(frame); String dataFrame { \data\: { \status\: 1, \format\: \audio/L16;rate16000\, \encoding\: \raw\, \audio\: \ base64Audio \ } }; client.send(dataFrame); offset end; Thread.sleep(50); // 适当限速避免发送速度超过服务端处理速度 } // 音频发完发送结束帧 String lastFrame { \data\: { \status\: 2, \format\: \audio/L16;rate16000\, \encoding\: \raw\, \audio\: \\ } }; client.send(lastFrame); }这里有几个关键点分帧大小是怎么算出来的16kHz采样率、16bit位深、单声道的PCM裸流每秒数据量是16000 × 2 × 1 32000字节。80ms的数据量就是32000 × 0.08 2560字节不对等等——仔细算一下32000字节每秒除以1000毫秒每毫秒32字节80毫秒就是2560字节1280字节是40ms的数据量。我上面代码里写1280就对应40ms一帧这个没有绝对的对错在接口允许范围内就行。但要注意音频引擎对帧长有限制一般只要在40ms~200ms之间都是允许的选太小的帧会增加请求数量网络往返多选太大的帧会增大延迟实测下来40~80ms体验最稳。为什么要sleep限速本地文件读取速度远快于网络发送速度如果疯狂发数据服务端会有保护策略直接断开连接。加一个短暂的sleep模拟实时发送的节奏实测下来连接会更稳定。帧的status字段含义0表示第一帧携带参数1表示中间音频帧2表示最后一帧通知服务端结束识别。必须按这个顺序走先发status0中间全是status1最后发status2顺序反了服务端会报错。3.3 接收结果并解析服务端返回的结果JSON结构大概是这样的{ code: 0, message: 成功, sid: xxx, data: { result: { text: Base64编码的中间识别结果, ws: [...], pgs: apd }, status: 2 } }data.result.text字段的值是一段Base64编码的JSON字符串解码后是长这样的{text:你好世界,ws:[{bg:0,cw:[{w:你好}]},{bg:800,cw:[{w:世界}]}]}解析的时候注意text字段不是直接可读的字符串必须先Base64解码再解析JSON。这个细节很多人第一次都会漏掉然后发现返回的东西看不懂。最终识别完毕的标志是data.status 2收到这个字段后就可以关闭连接了。不过要注意data.status和发送帧里的status是两回事前者表示服务端识别状态后者表示发送阶段容易搞混。4. 实操过程与踩坑记录这些坑我替你踩过了4.1 音频格式转换是第一步讯飞语音听写支持的音频编码格式包括rawPCM裸流、speex、opus等。实际项目中用户上传的文件五花八门最常见的是mp3和wav而PCM裸流是最通用的。如果你手里的是mp3文件不能直接塞给讯飞接口得先解码成PCM。wav文件如果采样率不是16kHz也需要重采样。最省事的方式是先用工具转一下Windows下可以用FFmpegLinux/macOS下一条命令搞定ffmpeg -i input.mp3 -ar 16000 -ac 1 -f s16le output.pcm这条命令的意思是把音频转成16kHz采样率、单声道、16位整型的PCM裸流正好匹配audio/L16;rate16000的参数。实测下来用FFmpeg转出来的音频识别准确率比直接用原格式高不少因为接口对格式的支持有细微差异。4.2 常见错误码排查速查表把我见过的错误码和对应处理方式整理成一张表给大家错误码含义处理方法10105鉴权失败签名错误检查APIKey/APISecret是否正确重点排查RFC3986编码和Base64编码问题10110参数错误检查请求体JSON格式特别是business参数枚举值是否拼写正确10160音频数据格式错误确认编码格式、采样率参数与实际音频数据一致407并发超过限制检查是否有多个连接复用同一鉴权URL或超出套餐并发数1000后台识别出错一般是音频质量太差或过短换一段清晰音频重试鉴权失败10105是最常见的90%的情况下都是编码问题。如果你确认密钥没写错那就去看两处一是authorization参数有没有做%20替换二是时间戳格式对不对——必须用GMT时区不能带当前时区的偏移量。4.3 几个容易被坑的细节第一个是WebSocket连接空闲超时。如果发了第一帧之后超过一定时间没有传音频数据服务端会主动断开连接而且不会发任何提示。排查这类什么都没报错就是连接断了的问题时先检查是不是音频发送逻辑卡住了。第二个是连接复用和并发限制。讯飞的WebSocket鉴权URL是一次性的连接建立之后该URL不能再次使用。另外免费套餐的并发数一般较小通常是2路如果同时发起多个识别任务超出并发会直接返回错误。生产环境上线前务必评估好并发量和套餐匹配度。第三个是Java-WebSocket在JDK 8和JDK 11下的兼容性。Java-WebSocket 1.5.x在JDK 11下没问题但如果你用的是更高版本需要看是否适配。我在JDK 17下跑通过一次但中间调试过程比较折腾建议生产环境别追新版JDKJDK 11最稳。4.4 实时录音接入的补充建议如果你的项目是实时语音识别比如做语音助手或者实时字幕核心逻辑基本一样只是音频来源从文件变成了麦克风。Java端采集麦克风可以用javax.sound.sampled的TargetDataLine设置好采样率16000、位深16位、单声道后从DataLine读出来的字节数组就是PCM数据可以直接复用上面的分帧发送逻辑。这里有个小细节麦克风采集线程和WebSocket发送线程之间的缓冲队列设计很重要。采集线程负责往队列里写数据发送线程从队列里取数据发送如果队列满了要丢帧还是阻塞需要根据业务场景决定。我做实时对话时用的是有界阻塞队列队列满了就丢最老的数据优先保证实时性宁可丢掉一点语音也不能延迟太多。5. 性能优化与整体架构建议5.1 并发设计在实际生产场景里单个语音转文字的需求很少是一次只转一个文件。更多的情况是用户批量上传了一批录音后台需要并发处理。我的建议是引入线程池来控制并发度ThreadPoolExecutor executor new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue(100), new ThreadPoolExecutor.CallerRunsPolicy() );线程池大小要根据讯飞套餐的并发上限来设置。不是线程越多越好超过接口并发限制只会拿到407错误。另外每个识别任务最好都走独立的WebSocket连接不要把不同任务的音频混在同一个连接里发会乱套。5.2 结果后处理和格式化工具识别完成后通常需要对结果做进一步处理。比如把多句话合并成段落或者生成带时间戳的字幕文件。识别结果里每个词的bg字段是词开始时间单位毫秒利用它可以生成SRT格式的字幕private String formatTimestamp(long millis) { long totalSeconds millis / 1000; long hours totalSeconds / 3600; long minutes (totalSeconds % 3600) / 60; long seconds totalSeconds % 60; long millisPart millis % 1000; return String.format(%02d:%02d:%02d,%03d, hours, minutes, seconds, millisPart); }这个思路做视频自动字幕特别实用。录音文件传上去识别结果直接生成SRT配合视频剪辑工具就能出带字幕的视频。5.3 实用扩展对接大模型做会议纪要现在很多团队在做的场景是音频转文字之后再让大模型整理会议纪要。讯飞的识别结果拿到手之后直接喂给大模型API让模型提取要点、生成总结。我试过把一段45分钟的会议录音转成文字再喂给星火API做摘要效果出奇地好完全可以省掉人工听录音整理的时间。这条链路的本质是讯飞解决听到的问题大模型解决听懂的问题两者结合就构成了一个完整的音频内容处理流水线。6. 写在最后的经验总结根据我实际做这个项目的体会讯飞语音转文字Java接入手把手过一遍其实不复杂核心难点就两个鉴权URL生成时的编码细节以及分帧发送节奏的控制。把这两个点吃透了整个流程基本就通了。再分享一个我踩过比较多坑后的习惯接这类带签名鉴权的接口时先单独写一个鉴权URL生成工具用官方提供的调试工具验证签名通过了再去写业务逻辑。这样可以把问题隔离在最小范围不会出现业务代码写完了排查了一下午发现是签名不对的尴尬局面。这个方案后续还可以扩展的方向很多比如结合实时语音识别做直播弹幕过滤、接客服系统做自动工单录入、做音频内容审核等等。技术底座是同一套业务层怎么包装就看你的想象力了。本文还有配套的精品资源点击获取
返回列表