文字:從接口簽名到生產(chǎn)可用的完整落地路徑)
簡介這是一份面向Java開發(fā)者的訊飛語音轉(zhuǎn)文字ASR實戰(zhàn)示例包適合具備Java基礎(chǔ)、希望快速接入訊飛RESTful API完成語音識別功能的初中級工程師。資源圍繞APP ID與密鑰申請、OAuth 2.0獲取Access Token、音頻文件讀取與格式轉(zhuǎn)換、Multipart上傳、JSON結(jié)果解析、異步回調(diào)及錯誤重試等關(guān)鍵環(huán)節(jié)展開覆蓋從鑒權(quán)到文字輸出的完整鏈路。壓縮包共8個文件含6個java源碼與2個jar依賴整體約143KB源碼按控制層、服務(wù)接口與實現(xiàn)、上傳及工具類分層組織jar包提供訊飛SDK與音頻處理支持結(jié)構(gòu)清晰便于按模塊閱讀。目前已有3962人學習下載讀者可借助示例代碼理解HTTP請求封裝、Token管理與識別結(jié)果提取的寫法并參考其中的異常處理與日志記錄思路將其遷移到智能設(shè)備、客服質(zhì)檢或AI交互等實際場景中減少自行摸索接口聯(lián)調(diào)的時間成本。1. Java 訊飛語音轉(zhuǎn)文字從接口簽名到生產(chǎn)可用的完整落地路徑很多團隊第一次接訊飛語音轉(zhuǎn)文字卡住的地方不是識別效果而是 Java 側(cè)那一串鑒權(quán)參數(shù)——hmacsha256 簽名、Base64 編碼、RFC1123 時間格式任何一個環(huán)節(jié)差一個字符服務(wù)端就回 401。我見過有開發(fā)者在這上面耗掉整整兩天最后發(fā)現(xiàn)是拼接字符串時多了一個換行符。這篇筆記就圍繞「Java 訊飛語音轉(zhuǎn)文字」這條鏈路把 WebSocket 實時轉(zhuǎn)寫和 HTTP 短音頻轉(zhuǎn)寫兩條路都拆開講清楚簽名怎么算、音頻怎么送、結(jié)果怎么收、并發(fā)怎么控、坑在哪里。適合正在做會議記錄、客服質(zhì)檢、語音工單轉(zhuǎn)寫的 Java 后端也適合需要把語音能力嵌進現(xiàn)有 Spring Boot 服務(wù)的團隊。2. 先搞懂鑒權(quán)Java 側(cè)簽名到底在簽什么訊飛開放平臺的 WebSocket 接口不是簡單塞一個 API Key 就能用它要求客戶端在建立連接前用 HMAC-SHA256 對一串特定格式的字符串做簽名再把簽名結(jié)果和其余參數(shù)拼成帶鑒權(quán)信息的 URL。這一步是純 Java 標準庫就能完成的事不需要額外依賴但順序和格式必須嚴格對齊。2.1 鑒權(quán)參數(shù)的四個組成部分簽名前需要準備四個值host、date、request-line以及你的apiSecret。其中host固定為ws-api.xfyun.cndate必須是 RFC1123 格式的 GMT 時間字符串request-line由GET、目標路徑和 HTTP 版本拼成。這四個值按固定順序用換行符連接形成待簽名字符串。// 構(gòu)造待簽名字符串注意每行末尾的 \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默認用本地時區(qū)必須顯式設(shè)置TimeZone.getTimeZone(GMT)和Locale.US否則星期幾和月份縮寫會變成中文或本地語言簽名直接失效。2.2 HMAC-SHA256 簽名與 Base64 編碼拿到待簽名字符串后用apiSecret作為密鑰做 HMAC-SHA256結(jié)果再 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); }參數(shù)說明secret就是控制臺里的 APISecretmessage是上一步拼好的signatureOrigin。注意getBytes必須指定 UTF-8雖然這里內(nèi)容都是 ASCII但養(yǎng)成習慣能避免后續(xù)拼接中文參數(shù)時出問題。2.3 把簽名拼回 WebSocket URL簽名完成后還需要把authorization字段做一次 Base64然后和date、host一起作為查詢參數(shù)拼到 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里的引號和逗號位置是固定的多一個空格都會導致鑒權(quán)失敗。urlEncode建議用URLEncoder.encode(value, UTF-8)但要注意它會把空格轉(zhuǎn)成而訊飛服務(wù)端期望的是%20所以編碼后需要做一次替換。提示簽名用的date和服務(wù)端時間偏差不能超過 5 分鐘服務(wù)器時間不同步會直接返回 401部署前先確認 NTP 正常。3. WebSocket 實時轉(zhuǎn)寫從建連到收結(jié)果的完整鏈路實時轉(zhuǎn)寫適合麥克風流、電話錄音流這類邊錄邊轉(zhuǎn)的場景。Java 側(cè)可以用java.net.http.WebSocketJDK 11或者第三方客戶端下面以 JDK 自帶 API 為主線把建連、發(fā)幀、收結(jié)果三個階段串起來。3.1 建立連接與握手參數(shù)建連時除了上面拼好的 URL還需要在Listener.onOpen里確認連接就緒然后才能發(fā)送第一幀音頻。訊飛要求音頻以 Base64 編碼后放在 JSON 里發(fā)送每幀建議 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(連接已建立可以開始發(fā)送音頻); ws.request(1); } Override public CompletionStage? onText(WebSocket ws, CharSequence data, boolean last) { System.out.println(收到識別結(jié)果: data); ws.request(1); return null; } }).join();參數(shù)說明request(1)是背壓控制告訴 WebSocket 每次只推一條消息處理完再請求下一條避免消息堆積。如果做高并發(fā)這個值可以適當調(diào)大但要配合業(yè)務(wù)側(cè)的隊列長度一起評估。3.2 音頻分幀與發(fā)送節(jié)奏音頻不能一次性全發(fā)要按 40ms 一幀切分每幀之間保持和實時播放一致的節(jié)奏。發(fā)太快服務(wù)端會丟幀發(fā)太慢識別結(jié)果會斷斷續(xù)續(xù)。// 假設(shè) audioData 是 16k/16bit/單聲道 PCM 字節(jié)數(shù)組 int frameSize 640 * 2; // 40ms * 16000Hz * 2字節(jié) 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); // 模擬實時節(jié)奏 }status字段是關(guān)鍵第一幀傳 0中間幀傳 1最后一幀傳 2。服務(wù)端靠這個判斷音頻流的起止。如果最后一幀沒發(fā)或者 status 不對識別結(jié)果不會返回最終版本。3.3 解析返回結(jié)果與拼接文本服務(wù)端返回的 JSON 里識別文本在data.result.ws[].cw[].w這個嵌套結(jié)構(gòu)里需要逐層取出來拼接。動態(tài)修正模式下后到的結(jié)果可能覆蓋前面的內(nèi)容所以不能簡單追加。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)); } // 根據(jù) pgs 字段判斷是追加還是替換 boolean isFinal data.getJSONObject(result).getBooleanValue(pgs); if (isFinal) { fullText.append(segment); } else { // 動態(tài)修正替換最后一段 lastSegment segment.toString(); } }pgs為 true 表示這一句已經(jīng)確定可以追加到全文為 false 表示還在動態(tài)修正需要暫存并替換。這個邏輯不處理好最終文本會出現(xiàn)重復或丟字。4. HTTP 短音頻轉(zhuǎn)寫更簡單的同步方案與參數(shù)調(diào)優(yōu)如果音頻是已經(jīng)錄好的文件不需要實時返回HTTP 接口更省事。它沒有 WebSocket 的幀節(jié)奏要求一次請求傳完整個音頻同步拿結(jié)果。適合工單語音、批量錄音文件轉(zhuǎn)寫這類場景。4.1 接口調(diào)用與請求體構(gòu)造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 接口的鑒權(quán)信息放在 Header 里不是 URL 參數(shù)。Authorization的構(gòu)造方式和 WebSocket 一樣但request-line要改成POST /v2/iat HTTP/1.1。4.2 音頻格式與采樣率怎么選訊飛支持多種音頻格式但不同格式對識別率有影響。實測下來16k 采樣率、16bit 位深、單聲道 PCM 的識別效果最穩(wěn)。如果原始音頻是 8k 電話錄音建議先重采樣到 16k雖然多一步處理但識別準確率提升明顯。參數(shù)推薦值說明采樣率16000Hz8k 也能識別但準確率下降位深16bit不支持 8bit聲道單聲道雙聲道需要先合并格式raw PCM 或 wavmp3 需要解碼后送單次時長60秒以內(nèi)超長音頻建議切片4.3 批量轉(zhuǎn)寫的并發(fā)控制批量轉(zhuǎn)寫時不要無限制并發(fā)訊飛對 QPS 有配額限制超了會返回 503。建議用信號量控制并發(fā)數(shù)同時做好失敗重試。Semaphore semaphore new Semaphore(5); // 根據(jù)配額調(diào)整 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) { // 記錄失敗后續(xù)重試 failedList.add(audio); } finally { semaphore.release(); } }); }并發(fā)數(shù)建議從 3 到 5 起步觀察服務(wù)端返回碼再調(diào)整。遇到 503 不要立刻重試退避幾秒再試否則會觸發(fā)更嚴格的限流。5. 避坑與排查那些讓轉(zhuǎn)寫結(jié)果不達預期的細節(jié)這一章記錄幾個我在實際項目里踩過的坑每個都按現(xiàn)象、原因、解決三步說清楚。5.1 返回 401 但簽名代碼看起來沒問題現(xiàn)象本地測試通過部署到服務(wù)器后全部 401。原因服務(wù)器時區(qū)不是 GMTSimpleDateFormat生成的 date 字符串帶了本地時區(qū)偏移。解決顯式設(shè)置TimeZone.getTimeZone(GMT)并在部署腳本里加一行date -u確認服務(wù)器 UTC 時間正常。5.2 識別結(jié)果只返回前半句現(xiàn)象音頻明明有 30 秒但只識別出前 10 秒的內(nèi)容。原因最后一幀的status沒有設(shè)為 2服務(wù)端認為音頻流還沒結(jié)束不返回最終結(jié)果。解決發(fā)送完所有幀后確保最后一幀status2并且不要立刻關(guān)閉連接等收到最終結(jié)果再關(guān)。5.3 中文識別成同音錯字現(xiàn)象專業(yè)術(shù)語頻繁識別錯比如「閾值」識別成「玉質(zhì)」。原因通用模型對垂直領(lǐng)域詞匯覆蓋不足。解決在請求參數(shù)里加pd字段指定領(lǐng)域或者用hotword參數(shù)傳入自定義熱詞熱詞用 Base64 編碼后傳。5.4 高并發(fā)下連接被重置現(xiàn)象單路測試正常10 路并發(fā)時部分連接被服務(wù)端斷開。原因超過了賬號的并發(fā)配額或者單 IP 連接數(shù)過多。解決先確認控制臺的并發(fā)上限用信號量控制實際并發(fā)數(shù)如果單機不夠拆到多臺機器上分散連接。5.5 音頻 Base64 后體積膨脹導致超時現(xiàn)象長音頻轉(zhuǎn)寫時請求超時。原因Base64 編碼會讓體積增大約 33%60 秒 16k 音頻編碼后接近 2MB網(wǎng)絡(luò)差時容易超時。解決控制單次音頻時長在 60 秒以內(nèi)超長音頻先切片再并發(fā)轉(zhuǎn)寫最后按順序拼接文本。6. 進階技巧熱詞優(yōu)化與結(jié)果后處理熱詞是提升垂直場景識別率最直接的手段。訊飛支持在請求里傳hotword參數(shù)格式是詞1|詞2|詞3Base64 編碼后放入。熱詞不是越多越好建議控制在 50 個以內(nèi)太多會稀釋權(quán)重。我一般會把業(yè)務(wù)里高頻出現(xiàn)的產(chǎn)品名、人名、專業(yè)術(shù)語整理成列表按周更新。結(jié)果后處理方面識別返回的文本沒有標點需要自己加。簡單做法是用規(guī)則匹配在「嗎、呢、吧」后面加問號在「了、的」后面加逗號。更穩(wěn)的做法是接一個輕量標點模型但會增加延遲。如果對實時性要求不高可以在整段轉(zhuǎn)寫完成后統(tǒng)一處理。驗證轉(zhuǎn)寫質(zhì)量時不要只看一兩句。我習慣抽 20 條音頻人工核對字準率低于 90% 就回頭查音頻質(zhì)量和熱詞配置。另外pgs字段的動態(tài)修正邏輯一定要寫單元測試用模擬的返回序列覆蓋追加、替換、混合三種情況這塊出 bug 最隱蔽線上表現(xiàn)就是文本偶爾重復或丟字。注意熱詞和領(lǐng)域參數(shù)只在部分接口版本生效接入前先用控制臺的調(diào)試工具確認當前賬號支持的參數(shù)集。我自己踩過最深的一個坑是早期做實時轉(zhuǎn)寫時沒處理pgs的動態(tài)修正結(jié)果會議記錄里同一句話出現(xiàn)了兩遍排查了一下午才發(fā)現(xiàn)是追加邏輯寫錯了。從那以后凡是涉及增量結(jié)果的接口我都會先把狀態(tài)機畫清楚再寫代碼。希望幫到你。本文還有配套的精品資源點擊獲取