全部產品
Search
文件中心

Alibaba Cloud Model Studio:Paraformer即時語音辨識WebSocket API

更新時間:Mar 21, 2026
重要

本文檔僅適用於“中國內地(北京)”地區。如需使用模型,需使用“中國內地(北京)”地區的API Key

本文介紹如何通過WebSocket串連訪問即時語音辨識服務。

DashScope SDK目前僅支援Java和Python。若想使用其他程式設計語言開發Paraformer即時語音辨識應用程式,可以通過WebSocket串連與服務進行通訊。

使用者指南:關於模型介紹和選型建議請參見即時語音辨識-Fun-ASR/Gummy/Paraformer

WebSocket是一種支援全雙工系統通訊的網路通訊協定。用戶端和伺服器通過一次握手建立持久串連,雙方可以互相主動推送資料,因此在即時性和效率方面具有顯著優勢。

對於常用程式設計語言,有許多現成的WebSocket庫和樣本可供參考,例如:

  • Go:gorilla/websocket

  • PHP:Ratchet

  • Node.js:ws

建議您先瞭解WebSocket的基本原理和技術細節,再參照本文進行開發。

前提條件

已開通服務並擷取API Key。請配置API Key到環境變數(準備下線,併入配置 API Key),而非寫入程式碼在代碼中,防範因代碼泄露導致的安全風險。

說明

當您需要為第三方應用或使用者提供臨時存取權限,或者希望嚴格控制敏感性資料訪問、刪除等高風險操作時,建議使用臨時鑒權Token

與長期有效 API Key 相比,臨時鑒權 Token 具備時效性短(60秒)、安全性高的特點,適用於臨時調用情境,能有效降低API Key泄露的風險。

使用方式:在代碼中,將原本用於鑒權的 API Key 替換為擷取到的臨時鑒權 Token 即可。

模型列表

paraformer-realtime-v2

paraformer-realtime-8k-v2

適用情境

直播、會議等情境

電話客服、語音信箱等 8kHz 音訊識別情境

採樣率

任意

8kHz

語種

中文(包含中文普通話和各種方言)、英文、日語、韓語、德語、法語、俄語

支援的中文方言:上海話、吳語、閩南語、東北話、甘肅話、貴州話、河南話、湖北話、湖南話、江西話、寧夏話、山西話、陝西話、山東話、四川話、天津話、雲南話、粵語

中文

標點符號預測

✅ 預設支援,無需配置

✅ 預設支援,無需配置

逆文本正則化(ITN)

✅ 預設支援,無需配置

✅ 預設支援,無需配置

指定待識別語種

✅ 通過language_hints參數指定

情感識別

✅ (點擊查看使用方式)

情感識別遵循如下約束:

  • 僅限paraformer-realtime-8k-v2模型。

  • 必須關閉語義斷句(可通過run-task指令semantic_punctuation_enabled參數控制)。語義斷句預設為關閉狀態。

  • 只有在payload.output.sentence.sentence_end的值為true時才顯示情感識別結果。

情感識別結果擷取方式:通過解析result-generated事件擷取,payload.output.sentence.emo_tagpayload.output.sentence.emo_confidence欄位分別代表當前句子的情感和情感信賴度。

互動流程

image

用戶端發送給服務端的訊息有兩種:JSON格式的指令二進位音頻(須為單聲道音頻);服務端返回給用戶端的訊息稱作事件

按時間順序,用戶端與服務端的互動流程如下:

  1. 建立串連:用戶端與服務端建立WebSocket串連。

  2. 開啟任務:

    • 用戶端發送run-task指令以開啟任務。

    • 用戶端收到服務端返回的task-started事件,標誌著任務已成功開啟,可以進行後續步驟。

  3. 發送音頻流:

  4. 通知服務端結束任務:

  5. 任務結束:

  6. 關閉串連:用戶端關閉WebSocket串連。

URL

WebSocket URL固定如下:

wss://dashscope.aliyuncs.com/api-ws/v1/inference

Headers

參數

類型

是否必選

說明

Authorization

string

鑒權令牌,格式為Bearer <your_api_key>,使用時,將“<your_api_key>”替換為實際的API Key。

user-agent

string

用戶端標識,便於服務端追蹤來源。

X-DashScope-WorkSpace

string

阿里雲百鍊業務空間ID

X-DashScope-DataInspection

string

是否啟用資料合規檢測功能。預設不傳或設為enable。如非必要,請勿啟用該參數。

指令(用戶端→服務端)

指令是用戶端發送給服務端的訊息,為JSON格式,以Text Frame方式發送,用於控制任務的起止和標識任務邊界。

說明

用戶端發送給服務端的二進位音頻(須為單聲道音頻)不包含在任何指令中,需單獨發送。

發送指令需嚴格遵循以下時序,否則可能導致任務失敗:

  1. 發送run-task指令

    • 用於啟動語音辨識任務。

    • 返回的 task_id 需在後續發送finish-task指令時使用,必須保持一致。

  2. 發送二進位音頻(單聲道)

    • 用於發送待識別音頻。

    • 必須在接收到服務端返回的task-started事件後發送音頻。

  3. 發送finish-task指令

    • 用於結束語音辨識任務。

    • 在音頻發送完畢後發送此指令。

1. run-task指令:開啟任務

該指令用於開啟語音辨識任務。task_id在後續發送finish-task指令時也需要使用,必須保持一致。

重要

發送時機:WebSocket串連建立後。

樣本:

{
    "header": {
        "action": "run-task",
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx", // 隨機uuid
        "streaming": "duplex"
    },
    "payload": {
        "task_group": "audio",
        "task": "asr",
        "function": "recognition",
        "model": "paraformer-realtime-v2",
        "parameters": {
            "format": "pcm", // 音頻格式
            "sample_rate": 16000, // 採樣率
            "disfluency_removal_enabled": false, // 過濾語氣詞
            "language_hints": [
                "en"
            ] // 指定語言,僅支援paraformer-realtime-v2模型
        }
        "input": {}
    }
}

header參數說明:

參數

類型

是否必選

說明

header.action

string

指令類型。

當前指令中,固定為"run-task"。

header.task_id

string

當次任務ID。

為32位通用唯一識別碼(UUID),由32個隨機產生的字母和數字組成。可以帶橫線(如 "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx")或不帶橫線(如 "2bf83b9abaeb4fda8d9axxxxxxxxxxxx")。大多數程式設計語言都內建了產生UUID的API,例如Python:

import uuid

def generateTaskId():
    # 產生隨機UUID
    return uuid.uuid4().hex

在後續發送finish-task指令時,用到的task_id需要和發送run-task指令時使用的task_id保持一致。

header.streaming

string

固定字串:"duplex"

payload參數說明:

參數

類型

是否必選

說明

payload.task_group

string

固定字串:"audio"。

payload.task

string

固定字串:"asr"。

payload.function

string

固定字串:"recognition"。

payload.model

string

模型名稱,支援的模型請參見模型列表

payload.input

object

固定格式:{}。

payload.parameters

format

string

設定待識別音頻格式。

支援的音頻格式:pcm、wav、mp3、opus、speex、aac、amr。

重要

opus/speex:必須使用Ogg封裝;

wav:必須為PCM編碼;

amr:僅支援AMR-NB類型。

sample_rate

integer

設定待識別音頻採樣率(單位Hz)。

因模型而異:

  • paraformer-realtime-v2支援任意採樣率。

  • paraformer-realtime-8k-v2僅支援8000Hz採樣率。

disfluency_removal_enabled

boolean

設定是否過濾語氣詞:

  • true:過濾語氣詞

  • false(預設):不過濾語氣詞

language_hints

array[string]

設定待識別語言代碼。如果無法提前確定語種,可不設定,模型會自動識別語種。

目前支援的語言代碼:

  • zh: 中文

  • en: 英文

  • ja: 日語

  • yue: 粵語

  • ko: 韓語

  • de:德語

  • fr:法語

  • ru:俄語

該參數僅對支援多語言的模型生效(參見模型列表)。

semantic_punctuation_enabled

boolean

設定是否開啟語義斷句,預設關閉。

  • true:開啟語義斷句,關閉VAD(Voice Activity Detection,語音活動檢測)斷句。

  • false(預設):開啟VAD(Voice Activity Detection,語音活動檢測)斷句,關閉語義斷句。

語義斷句準確性更高,適合會議轉寫情境;VAD(Voice Activity Detection,語音活動檢測)斷句延遲較低,適合互動情境。

通過調整semantic_punctuation_enabled參數,可以靈活切換語音辨識的斷句方式以適應不同情境需求。

該參數僅在模型為v2及更高版本時生效。

max_sentence_silence

integer

設定VAD(Voice Activity Detection,語音活動檢測)斷句的靜音時間長度閾值(單位為ms)。

當一段語音後的靜音時間長度超過該閾值時,系統會判定該句子已結束。

參數範圍為200ms至6000ms,預設值為800ms。

該參數僅在semantic_punctuation_enabled參數為false(VAD斷句)且模型為v2及更高版本時生效。

multi_threshold_mode_enabled

boolean

該開關開啟時(true)可以防止VAD斷句切割過長。預設關閉。

該參數僅在semantic_punctuation_enabled參數為false(VAD斷句)且模型為v2及更高版本時生效。

punctuation_prediction_enabled

boolean

設定是否在識別結果中自動添加標點:

  • true(預設):是

  • false:否

該參數僅在模型為v2及更高版本時生效。

heartbeat

boolean

當需要與服務端保持長串連時,可通過該開關進行控制:

  • true:在持續發送靜音音訊情況下,可保持與服務端的串連不中斷。

  • false(預設):即使持續發送靜音音頻,串連也將在60秒後因逾時而斷開。

    靜音音頻指的是在音頻檔案或資料流中沒有聲音訊號的內容。靜音音頻可以通過多種方法產生,例如使用音頻編輯軟體如Audacity或Adobe Audition,或者通過命令列工具如FFmpeg。

該參數僅在模型為v2及更高版本時生效。

inverse_text_normalization_enabled

boolean

設定是否開啟ITN(Inverse Text Normalization,逆文本正則化)。

預設開啟(true)。開啟後,中文數字將轉換為阿拉伯數字。

該參數僅在模型為v2及更高版本時生效。

2. finish-task指令:結束任務

該指令用於結束語音辨識任務。音頻發送完畢後,用戶端可以發送此指令以結束任務。

重要

發送時機:音頻發送完成後。

樣本:

{
    "header": {
        "action": "finish-task",
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "streaming": "duplex"
    },
    "payload": {
        "input": {}
    }
}

header參數說明:

參數

類型

是否必選

說明

header.action

string

指令類型。

當前指令中,固定為"finish-task"。

header.task_id

string

當次任務ID。

需要和發送run-task指令時使用的task_id保持一致。

header.streaming

string

固定字串:"duplex"

payload參數說明:

參數

類型

是否必選

說明

payload.input

object

固定格式:{}。

二進位音頻(用戶端→服務端)

用戶端需在收到task-started事件後,再發送待識別的音頻流。

可以發送即時音頻流(比如從話筒中即時擷取到的)或者錄音檔案音頻流,音頻應是單聲道。

音頻通過WebSocket的二進位通道上傳。建議每次發送100ms的音頻,並間隔100ms。

事件(服務端→用戶端)

事件是服務端返回給用戶端的訊息,為JSON格式,代表不同的處理階段。

1. task-started事件:任務已開啟

當監聽到服務端返回的task-started事件時,標誌著任務已成功開啟。只有在接收到該事件後,才能向伺服器發送待識別音頻或finish-task指令;否則,任務將執行失敗。

task-started事件的payload沒有內容。

樣本:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "task-started",
        "attributes": {}
    },
    "payload": {}
}

header參數說明:

參數

類型

說明

header.event

string

事件類型。

當前事件中,固定為"task-started"。

header.task_id

string

用戶端產生的task_id

2. result-generated事件:語音辨識結果

用戶端發送待識別音頻和finish-task指令的同時,服務端持續返回result-generated事件,該事件包含語音辨識的結果。

可以通過result-generated事件中的payload.sentence.endTime是否為空白來判斷該結果是中間結果還是最終結果。

樣本:

{
  "header": {
    "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
    "event": "result-generated",
    "attributes": {}
  },
  "payload": {
    "output": {
      "sentence": {
        "begin_time": 170,
        "end_time": null,
        "text": "好,我知道了",
        "heartbeat": false,
        "sentence_end": true,
        "emo_tag": "neutral", //只有請求參數model為paraformer-realtime-8k-v2,且請求參數semantic_punctuation_enabled為false,並且result-generated事件的sentence_end為true時才顯示該欄位
        "emo_confidence": 0.914, //只有請求參數model為paraformer-realtime-8k-v2,且請求參數semantic_punctuation_enabled為false,並且result-generated事件的sentence_end為true時才顯示該欄位
        "words": [
          {
            "begin_time": 170,
            "end_time": 295,
            "text": "好",
            "punctuation": ","
          },
          {
            "begin_time": 295,
            "end_time": 503,
            "text": "我",
            "punctuation": ""
          },
          {
            "begin_time": 503,
            "end_time": 711,
            "text": "知道",
            "punctuation": ""
          },
          {
            "begin_time": 711,
            "end_time": 920,
            "text": "了",
            "punctuation": ""
          }
        ]
      }
    },
    "usage": {
      "duration": 3
    }
  }
}

header參數說明:

參數

類型

說明

header.event

string

事件類型。

當前事件中,固定為"result-generated"。

header.task_id

string

用戶端產生的task_id。

payload參數說明:

參數

類型

說明

output

object

output.sentence為識別結果,詳細內容見下文。

usage

object

payload.output.sentence.sentence_endfalse(當前句子未結束,參見payload.output.sentence參數說明)時,usagenull

payload.output.sentence.sentence_endtrue(當前句子已結束,參見payload.output.sentence參數說明)時,usage.duration為當前任務計費時間長度(秒)。

payload.output.usage格式如下:

參數

類型

說明

duration

integer

任務計費時間長度(單位為秒)。

payload.output.sentence格式如下:

參數

類型

說明

begin_time

integer

句子開始時間,單位為ms。

end_time

integer | null

句子結束時間,如果為中間識別結果則為null,單位為ms。

text

string

識別文本。

words

array

字時間戳記資訊。

heartbeat

boolean | null

若該值為true,可跳過識別結果的處理。

sentence_end

boolean

判斷給定句子是否已結束。

emo_tag

string

當前句子的情感:

  • positive:正面情感,如開心、滿意

  • negative:負面情感,如憤怒、沉悶

  • neutral:無明顯情感

情感識別遵循如下約束:

  • 僅限paraformer-realtime-8k-v2模型。

  • 必須關閉語義斷句(可通過run-task指令semantic_punctuation_enabled參數控制)。語義斷句預設為關閉狀態。

  • 只有在payload.output.sentence.sentence_end的值為true時才顯示情感識別結果。

emo_confidence

number

當前句子識別情感的信賴度,取值範圍:[0.0,1.0],值越大表示信賴度越高。

情感識別遵循如下約束:

  • 僅限paraformer-realtime-8k-v2模型。

  • 必須關閉語義斷句(可通過run-task指令semantic_punctuation_enabled參數控制)。語義斷句預設為關閉狀態。

  • 只有在payload.output.sentence.sentence_end的值為true時才顯示情感識別結果。

payload.output.sentence.words為字時間戳記列表,其中每一個word格式如下:

參數

類型

說明

begin_time

integer

字開始時間,單位為ms。

end_time

integer

字結束時間,單位為ms。

text

string

字。

punctuation

string

標點。

3. task-finished事件:任務已結束

當監聽到服務端返回的task-finished事件時,說明任務已結束。此時可以關閉WebSocket串連並結束程式。

樣本:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "task-finished",
        "attributes": {}
    },
    "payload": {
        "output": {},
        "usage": null
    }
}

header參數說明:

參數

類型

說明

header.event

string

事件類型。

當前事件中,固定為"task-finished"。

header.task_id

string

用戶端產生的task_id。

4. task-failed事件:任務失敗

如果接收到task-failed事件,表示任務失敗。此時需要關閉WebSocket串連並處理錯誤。通過分析報錯資訊,如果是由於編程問題導致的任務失敗,您可以調整代碼進行修正。

樣本:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "task-failed",
        "error_code": "CLIENT_ERROR",
        "error_message": "request timeout after 23 seconds.",
        "attributes": {}
    },
    "payload": {}
}

header參數說明:

參數

類型

說明

header.event

string

事件類型。

當前事件中,固定為"task-failed"。

header.task_id

string

用戶端產生的task_id。

header.error_code

string

報錯類型描述。

header.error_message

string

具體報錯原因。

關於建連開銷和串連複用

WebSocket服務支援串連複用以提升資源的利用效率,避免建立串連開銷。

服務端收到用戶端發送的run-task指令後,將啟動一個新的任務,用戶端發送finish-task指令後,服務端在任務完成時返回task-finished事件以結束該任務。結束任務後WebSocket串連可以被複用,用戶端重新發送run-task指令即可開啟下一個任務。

重要
  1. 在複用串連中的不同任務需要使用不同 task_id。

  2. 如果在任務執行過程中發生失敗,服務將依然返回task-failed事件,並關閉該串連。此時這個串連無法繼續複用。

  3. 如果在任務結束後60秒沒有新的任務,串連會逾時自動斷開。

範例程式碼

範例程式碼僅提供最基礎的服務調通實現,實際業務情境的相關代碼需您自行開發。

在編寫WebSocket用戶端代碼時,為了同時發送和接收訊息,通常採用非同步編程。您可以按照以下步驟來編寫程式:

  1. 建立WebSocket串連

    調用WebSocket庫函數(具體實現方式因程式設計語言或庫函數而異),傳入HeadersURL建立WebSocket串連。

  2. 監聽服務端訊息

    通過 WebSocket 庫提供的回呼函數(觀察者模式),您可以監聽服務端返回的訊息。具體實現方式因程式設計語言不同而有所差異。

    服務端返回的訊息分為兩類:二進位音頻流和事件。

    監聽事件

    • task-started:當接收到task-started事件時,表示任務已成功開啟。只有在此事件觸發後,才能向服務端發送二進位音頻或finish-task指令;否則任務會失敗。

    • result-generated:用戶端發送二進位音頻時,服務端可能會持續返回result-generated事件,該事件包含語音辨識結果。

    • task-finished:接收到task-finished事件時,表示任務已完成。此時可以關閉 WebSocket 串連並結束程式。

    • task-failed:如果接收到task-failed事件,表示任務失敗。需要關閉 WebSocket 串連,並根據報錯資訊調整代碼進行修正。

  3. 向服務端發送訊息(請務必注意時序)

    在不同於監聽服務端訊息的線程(如主線程,具體實現因程式設計語言而異)中,向服務端發送指令和二進位音頻。

    發送指令需嚴格遵循以下時序,否則可能導致任務失敗:

    1. 發送run-task指令

      • 用於啟動語音辨識任務。

      • 返回的 task_id 需在後續發送finish-task指令時使用,必須保持一致。

    2. 發送二進位音頻(單聲道)

      • 用於發送待識別音頻。

      • 必須在接收到服務端返回的task-started事件後發送音頻。

    3. 發送finish-task指令

      • 用於結束語音辨識任務。

      • 在音頻發送完畢後發送此指令。

  4. 關閉WebSocket串連

    在程式正常結束、運行中出現異常或接收到task-finished事件task-failed事件時,關閉WebSocket串連。通常通過調用工具庫中的close函數來實現。

點擊查看完整樣本

如下樣本中,使用的音頻檔案為asr_example.wav

Go

package main

import (
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"time"

	"github.com/google/uuid"
	"github.com/gorilla/websocket"
)

const (
	wsURL     = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/" // WebSocket伺服器位址
	audioFile = "asr_example.wav"                                   // 替換為您的音頻檔案路徑
)

var dialer = websocket.DefaultDialer

func main() {
	// 若沒有將API Key配置到環境變數,可將下行替換為:apiKey := "your_api_key"。不建議在生產環境中直接將API Key寫入程式碼到代碼中,以減少API Key泄露風險。
	apiKey := os.Getenv("DASHSCOPE_API_KEY")

	// 串連WebSocket服務
	conn, err := connectWebSocket(apiKey)
	if err != nil {
		log.Fatal("串連WebSocket失敗:", err)
	}
	defer closeConnection(conn)

	// 啟動一個goroutine來接收結果
	taskStarted := make(chan bool)
	taskDone := make(chan bool)
	startResultReceiver(conn, taskStarted, taskDone)

	// 發送run-task指令
	taskID, err := sendRunTaskCmd(conn)
	if err != nil {
		log.Fatal("發送run-task指令失敗:", err)
	}

	// 等待task-started事件
	waitForTaskStarted(taskStarted)

	// 發送待識別音頻檔案流
	if err := sendAudioData(conn); err != nil {
		log.Fatal("發送音頻失敗:", err)
	}

	// 發送finish-task指令
	if err := sendFinishTaskCmd(conn, taskID); err != nil {
		log.Fatal("發送finish-task指令失敗:", err)
	}

	// 等待任務完成或失敗
	<-taskDone
}

// 定義結構體來表示JSON資料
type Header struct {
	Action       string                 `json:"action"`
	TaskID       string                 `json:"task_id"`
	Streaming    string                 `json:"streaming"`
	Event        string                 `json:"event"`
	ErrorCode    string                 `json:"error_code,omitempty"`
	ErrorMessage string                 `json:"error_message,omitempty"`
	Attributes   map[string]interface{} `json:"attributes"`
}

type Output struct {
	Sentence struct {
		BeginTime int64  `json:"begin_time"`
		EndTime   *int64 `json:"end_time"`
		Text      string `json:"text"`
		Words     []struct {
			BeginTime   int64  `json:"begin_time"`
			EndTime     *int64 `json:"end_time"`
			Text        string `json:"text"`
			Punctuation string `json:"punctuation"`
		} `json:"words"`
	} `json:"sentence"`
}

type Payload struct {
	TaskGroup  string `json:"task_group"`
	Task       string `json:"task"`
	Function   string `json:"function"`
	Model      string `json:"model"`
	Parameters Params `json:"parameters"`
	Input  Input  `json:"input"`
	Output Output `json:"output,omitempty"`
	Usage  *struct {
		Duration int `json:"duration"`
	} `json:"usage,omitempty"`
}

type Params struct {
	Format                   string   `json:"format"`
	SampleRate               int      `json:"sample_rate"`
	DisfluencyRemovalEnabled bool     `json:"disfluency_removal_enabled"`
	LanguageHints            []string `json:"language_hints"`
}

type Input struct {
}

type Event struct {
	Header  Header  `json:"header"`
	Payload Payload `json:"payload"`
}

// 串連WebSocket服務
func connectWebSocket(apiKey string) (*websocket.Conn, error) {
	header := make(http.Header)
	header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
	conn, _, err := dialer.Dial(wsURL, header)
	return conn, err
}

// 啟動一個goroutine非同步接收WebSocket訊息
func startResultReceiver(conn *websocket.Conn, taskStarted chan<- bool, taskDone chan<- bool) {
	go func() {
		for {
			_, message, err := conn.ReadMessage()
			if err != nil {
				log.Println("解析伺服器訊息失敗:", err)
				return
			}
			var event Event
			err = json.Unmarshal(message, &event)
			if err != nil {
				log.Println("解析事件失敗:", err)
				continue
			}
			if handleEvent(conn, event, taskStarted, taskDone) {
				return
			}
		}
	}()
}

// 發送run-task指令
func sendRunTaskCmd(conn *websocket.Conn) (string, error) {
	runTaskCmd, taskID, err := generateRunTaskCmd()
	if err != nil {
		return "", err
	}
	err = conn.WriteMessage(websocket.TextMessage, []byte(runTaskCmd))
	return taskID, err
}

// 產生run-task指令
func generateRunTaskCmd() (string, string, error) {
	taskID := uuid.New().String()
	runTaskCmd := Event{
		Header: Header{
			Action:    "run-task",
			TaskID:    taskID,
			Streaming: "duplex",
		},
		Payload: Payload{
			TaskGroup: "audio",
			Task:      "asr",
			Function:  "recognition",
			Model:     "paraformer-realtime-v2",
			Parameters: Params{
				Format:     "wav",
				SampleRate: 16000,
			},
			Input: Input{},
		},
	}
	runTaskCmdJSON, err := json.Marshal(runTaskCmd)
	return string(runTaskCmdJSON), taskID, err
}

// 等待task-started事件
func waitForTaskStarted(taskStarted chan bool) {
	select {
	case <-taskStarted:
		fmt.Println("任務開啟成功")
	case <-time.After(10 * time.Second):
		log.Fatal("等待task-started逾時,任務開啟失敗")
	}
}

// 發送音頻資料
func sendAudioData(conn *websocket.Conn) error {
	file, err := os.Open(audioFile)
	if err != nil {
		return err
	}
	defer file.Close()

	buf := make([]byte, 1024) // 假設100ms的音頻資料大約為1024位元組
	for {
		n, err := file.Read(buf)
		if n == 0 {
			break
		}
		if err != nil && err != io.EOF {
			return err
		}
		err = conn.WriteMessage(websocket.BinaryMessage, buf[:n])
		if err != nil {
			return err
		}
		time.Sleep(100 * time.Millisecond)
	}
	return nil
}

// 發送finish-task指令
func sendFinishTaskCmd(conn *websocket.Conn, taskID string) error {
	finishTaskCmd, err := generateFinishTaskCmd(taskID)
	if err != nil {
		return err
	}
	err = conn.WriteMessage(websocket.TextMessage, []byte(finishTaskCmd))
	return err
}

// 產生finish-task指令
func generateFinishTaskCmd(taskID string) (string, error) {
	finishTaskCmd := Event{
		Header: Header{
			Action:    "finish-task",
			TaskID:    taskID,
			Streaming: "duplex",
		},
		Payload: Payload{
			Input: Input{},
		},
	}
	finishTaskCmdJSON, err := json.Marshal(finishTaskCmd)
	return string(finishTaskCmdJSON), err
}

// 處理事件
func handleEvent(conn *websocket.Conn, event Event, taskStarted chan<- bool, taskDone chan<- bool) bool {
	switch event.Header.Event {
	case "task-started":
		fmt.Println("收到task-started事件")
		taskStarted <- true
	case "result-generated":
		if event.Payload.Output.Sentence.Text != "" {
			fmt.Println("識別結果:", event.Payload.Output.Sentence.Text)
		}
		if event.Payload.Usage != nil {
			fmt.Println("任務計費時間長度(秒):", event.Payload.Usage.Duration)
		}
	case "task-finished":
		fmt.Println("任務完成")
		taskDone <- true
		return true
	case "task-failed":
		handleTaskFailed(event, conn)
		taskDone <- true
		return true
	default:
		log.Printf("預料之外的事件:%v", event)
	}
	return false
}

// 處理任務失敗事件
func handleTaskFailed(event Event, conn *websocket.Conn) {
	if event.Header.ErrorMessage != "" {
		log.Fatalf("任務失敗:%s", event.Header.ErrorMessage)
	} else {
		log.Fatal("未知原因導致任務失敗")
	}
}

// 關閉串連
func closeConnection(conn *websocket.Conn) {
	if conn != nil {
		conn.Close()
	}
}

C#

範例程式碼如下:

using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;

class Program {
    private static ClientWebSocket _webSocket = new ClientWebSocket();
    private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
    private static bool _taskStartedReceived = false;
    private static bool _taskFinishedReceived = false;
    // 若沒有將API Key配置到環境變數,可將下行替換為:private const string ApiKey="your_api_key"。不建議在生產環境中直接將API Key寫入程式碼到代碼中,以減少API Key泄露風險。
    private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");

    // WebSocket伺服器位址
    private const string WebSocketUrl = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/";
    // 替換為您的音頻檔案路徑
    private const string AudioFilePath = "asr_example.wav";

    static async Task Main(string[] args) {
        // 建立WebSocket串連,配置headers進行鑒權
        _webSocket.Options.SetRequestHeader("Authorization", ApiKey);

        await _webSocket.ConnectAsync(new Uri(WebSocketUrl), _cancellationTokenSource.Token);

        // 啟動線程非同步接收WebSocket訊息
        var receiveTask = ReceiveMessagesAsync();

        // 發送run-task指令
        string _taskId = Guid.NewGuid().ToString("N"); // 產生32位隨機ID
        var runTaskJson = GenerateRunTaskJson(_taskId);
        await SendAsync(runTaskJson);

        // 等待task-started事件
        while (!_taskStartedReceived) {
            await Task.Delay(100, _cancellationTokenSource.Token);
        }

        // 讀取本地檔案,向伺服器發送待識別音頻流
        await SendAudioStreamAsync(AudioFilePath);

        // 發送finish-task指令結束任務
        var finishTaskJson = GenerateFinishTaskJson(_taskId);
        await SendAsync(finishTaskJson);

        // 等待task-finished事件
        while (!_taskFinishedReceived && !_cancellationTokenSource.IsCancellationRequested) {
            try {
                await Task.Delay(100, _cancellationTokenSource.Token);
            } catch (OperationCanceledException) {
                // 任務已被取消,退出迴圈
                break;
            }
        }

        // 關閉串連
        if (!_cancellationTokenSource.IsCancellationRequested) {
            await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cancellationTokenSource.Token);
        }

        _cancellationTokenSource.Cancel();
        try {
            await receiveTask;
        } catch (OperationCanceledException) {
            // 忽略操作取消異常
        }
    }

    private static async Task ReceiveMessagesAsync() {
        try {
            while (_webSocket.State == WebSocketState.Open && !_cancellationTokenSource.IsCancellationRequested) {
                var message = await ReceiveMessageAsync(_cancellationTokenSource.Token);
                if (message != null) {
                    var eventValue = message["header"]?["event"]?.GetValue<string>();
                    switch (eventValue) {
                        case "task-started":
                            Console.WriteLine("任務開啟成功");
                            _taskStartedReceived = true;
                            break;
                        case "result-generated":
                            Console.WriteLine($"識別結果:{message["payload"]?["output"]?["sentence"]?["text"]?.GetValue<string>()}");
                            if (message["payload"]?["usage"] != null && message["payload"]?["usage"]?["duration"] != null) {
                                Console.WriteLine($"任務計費時間長度(秒):{message["payload"]?["usage"]?["duration"]?.GetValue<int>()}");
                            }
                            break;
                        case "task-finished":
                            Console.WriteLine("任務完成");
                            _taskFinishedReceived = true;
                            _cancellationTokenSource.Cancel();
                            break;
                        case "task-failed":
                            Console.WriteLine($"任務失敗:{message["header"]?["error_message"]?.GetValue<string>()}");
                            _cancellationTokenSource.Cancel();
                            break;
                    }
                }
            }
        } catch (OperationCanceledException) {
            // 忽略操作取消異常
        }
    }

    private static async Task<JsonNode?> ReceiveMessageAsync(CancellationToken cancellationToken) {
        var buffer = new byte[1024 * 4];
        var segment = new ArraySegment<byte>(buffer);
        var result = await _webSocket.ReceiveAsync(segment, cancellationToken);

        if (result.MessageType == WebSocketMessageType.Close) {
            await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
            return null;
        }

        var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
        return JsonNode.Parse(message);
    }

    private static async Task SendAsync(string message) {
        var buffer = Encoding.UTF8.GetBytes(message);
        var segment = new ArraySegment<byte>(buffer);
        await _webSocket.SendAsync(segment, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
    }

    private static async Task SendAudioStreamAsync(string filePath) {
        using (var audioStream = File.OpenRead(filePath)) {
            var buffer = new byte[1024]; // 每次發送100ms的音頻資料
            int bytesRead;

            while ((bytesRead = await audioStream.ReadAsync(buffer, 0, buffer.Length)) > 0) {
                var segment = new ArraySegment<byte>(buffer, 0, bytesRead);
                await _webSocket.SendAsync(segment, WebSocketMessageType.Binary, true, _cancellationTokenSource.Token);
                await Task.Delay(100); // 間隔100ms
            }
        }
    }

    private static string GenerateRunTaskJson(string taskId) {
        var runTask = new JsonObject {
            ["header"] = new JsonObject {
                ["action"] = "run-task",
                ["task_id"] = taskId,
                ["streaming"] = "duplex"
            },
            ["payload"] = new JsonObject {
                ["task_group"] = "audio",
                ["task"] = "asr",
                ["function"] = "recognition",
                ["model"] = "paraformer-realtime-v2",
                ["parameters"] = new JsonObject {
                    ["format"] = "wav",
                    ["sample_rate"] = 16000,
                    ["disfluency_removal_enabled"] = false
                },
                ["input"] = new JsonObject()
            }
        };
        return JsonSerializer.Serialize(runTask);
    }

    private static string GenerateFinishTaskJson(string taskId) {
        var finishTask = new JsonObject {
            ["header"] = new JsonObject {
                ["action"] = "finish-task",
                ["task_id"] = taskId,
                ["streaming"] = "duplex"
            },
            ["payload"] = new JsonObject {
                ["input"] = new JsonObject()
            }
        };
        return JsonSerializer.Serialize(finishTask);
    }
}

PHP

範例程式碼目錄結構為:

my-php-project/

├── composer.json

├── vendor/

└── index.php

composer.json內容如下,相關依賴的版本號碼請根據實際情況自行決定:

{
    "require": {
        "react/event-loop": "^1.3",
        "react/socket": "^1.11",
        "react/stream": "^1.2",
        "react/http": "^1.1",
        "ratchet/pawl": "^0.4"
    },
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

index.php內容如下:

<?php

require __DIR__ . '/vendor/autoload.php';

use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;
use Ratchet\rfc6455\Messaging\Frame;

# 若沒有將API Key配置到環境變數,可將下行替換為:$api_key="your_api_key"。不建議在生產環境中直接將API Key寫入程式碼到代碼中,以減少API Key泄露風險。
$api_key = getenv("DASHSCOPE_API_KEY");
$websocket_url = 'wss://dashscope.aliyuncs.com/api-ws/v1/inference/'; // WebSocket伺服器位址
$audio_file_path = 'asr_example.wav'; // 替換為您的音頻檔案路徑

$loop = Loop::get();

// 建立自訂的連接器
$socketConnector = new SocketConnector($loop, [
    'tcp' => [
        'bindto' => '0.0.0.0:0',
    ],
    'tls' => [
        'verify_peer' => false,
        'verify_peer_name' => false,
    ],
]);

$connector = new Connector($loop, $socketConnector);

$headers = [
    'Authorization' => 'bearer ' . $api_key
];

$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $audio_file_path) {
    echo "串連到WebSocket伺服器\n";

    // 啟動非同步接收WebSocket訊息的線程
    $conn->on('message', function($msg) use ($conn, $loop, $audio_file_path) {
        $response = json_decode($msg, true);

        if (isset($response['header']['event'])) {
            handleEvent($conn, $response, $loop, $audio_file_path);
        } else {
            echo "未知的訊息格式\n";
        }
    });

    // 監聽串連關閉
    $conn->on('close', function($code = null, $reason = null) {
        echo "串連已關閉\n";
        if ($code !== null) {
            echo "關閉代碼: " . $code . "\n";
        }
        if ($reason !== null) {
            echo "關閉原因:" . $reason . "\n";
        }
    });

    // 產生任務ID
    $taskId = generateTaskId();

    // 發送 run-task 指令
    sendRunTaskMessage($conn, $taskId);

}, function ($e) {
    echo "無法串連:{$e->getMessage()}\n";
});

$loop->run();

/**
 * 產生任務ID
 * @return string
 */
function generateTaskId(): string {
    return bin2hex(random_bytes(16));
}

/**
 * 發送 run-task 指令
 * @param $conn
 * @param $taskId
 */
function sendRunTaskMessage($conn, $taskId) {
    $runTaskMessage = json_encode([
        "header" => [
            "action" => "run-task",
            "task_id" => $taskId,
            "streaming" => "duplex"
        ],
        "payload" => [
            "task_group" => "audio",
            "task" => "asr",
            "function" => "recognition",
            "model" => "paraformer-realtime-v2",
            "parameters" => [
                "format" => "wav",
                "sample_rate" => 16000
            ],
            "input" => []
        ]
    ]);
    echo "準備發送run-task指令:" . $runTaskMessage . "\n";
    $conn->send($runTaskMessage);
    echo "run-task指令已發送\n";
}

/**
 * 讀取音頻檔案
 * @param string $filePath
 * @return bool|string
 */
function readAudioFile(string $filePath) {
    $voiceData = file_get_contents($filePath);
    if ($voiceData === false) {
        echo "無法讀取音頻檔案\n";
    }
    return $voiceData;
}

/**
 * 分割音頻資料
 * @param string $data
 * @param int $chunkSize
 * @return array
 */
function splitAudioData(string $data, int $chunkSize): array {
    return str_split($data, $chunkSize);
}

/**
 * 發送 finish-task 指令
 * @param $conn
 * @param $taskId
 */
function sendFinishTaskMessage($conn, $taskId) {
    $finishTaskMessage = json_encode([
        "header" => [
            "action" => "finish-task",
            "task_id" => $taskId,
            "streaming" => "duplex"
        ],
        "payload" => [
            "input" => []
        ]
    ]);
    echo "準備發送finish-task指令: " . $finishTaskMessage . "\n";
    $conn->send($finishTaskMessage);
    echo "finish-task指令已發送\n";
}

/**
 * 處理事件
 * @param $conn
 * @param $response
 * @param $loop
 * @param $audio_file_path
 */
function handleEvent($conn, $response, $loop, $audio_file_path) {
    static $taskId;
    static $chunks;
    static $allChunksSent = false;

    if (is_null($taskId)) {
        $taskId = generateTaskId();
    }

    switch ($response['header']['event']) {
        case 'task-started':
            echo "任務開始,發送音頻資料...\n";
            // 讀取音頻檔案
            $voiceData = readAudioFile($audio_file_path);
            if ($voiceData === false) {
                echo "無法讀取音頻檔案\n";
                $conn->close();
                return;
            }

            // 分割音頻資料
            $chunks = splitAudioData($voiceData, 1024);

            // 定義發送函數
            $sendChunk = function() use ($conn, &$chunks, $loop, &$sendChunk, &$allChunksSent, $taskId) {
                if (!empty($chunks)) {
                    $chunk = array_shift($chunks);
                    $binaryMsg = new Frame($chunk, true, Frame::OP_BINARY);
                    $conn->send($binaryMsg);
                    // 100ms後發送下一個片段
                    $loop->addTimer(0.1, $sendChunk);
                } else {
                    echo "所有資料區塊已發送\n";
                    $allChunksSent = true;

                    // 發送 finish-task 指令
                    sendFinishTaskMessage($conn, $taskId);
                }
            };

            // 開始發送音頻資料
            $sendChunk();
            break;
        case 'result-generated':
            $result = $response['payload']['output']['sentence'];
            echo "識別結果:" . $result['text'] . "\n";
            if (isset($response['payload']['usage']['duration'])) {
                echo "任務計費時間長度(秒):" . $response['payload']['usage']['duration'] . "\n";
            }
            break;
        case 'task-finished':
            echo "任務完成\n";
            $conn->close();
            break;
        case 'task-failed':
            echo "任務失敗\n";
            echo "錯誤碼:" . $response['header']['error_code'] . "\n";
            echo "錯誤資訊:" . $response['header']['error_message'] . "\n";
            $conn->close();
            break;
        case 'error':
            echo "錯誤:" . $response['payload']['message'] . "\n";
            break;
        default:
            echo "未知事件:" . $response['header']['event'] . "\n";
            break;
    }

    // 如果所有資料已發送且任務已完成,關閉串連
    if ($allChunksSent && $response['header']['event'] == 'task-finished') {
        // 等待1秒以確保所有資料都已傳輸完畢
        $loop->addTimer(1, function() use ($conn) {
            $conn->close();
            echo "用戶端關閉串連\n";
        });
    }
}

Node.js

需安裝相關依賴:

npm install ws
npm install uuid

範例程式碼如下:

import fs from 'fs';
import WebSocket from 'ws';
import { v4 as uuidv4 } from 'uuid'; // 用於產生UUID

// 若沒有將API Key配置到環境變數,可將下行替換為:apiKey = 'your_api_key'。不建議在生產環境中直接將API Key寫入程式碼到代碼中,以減少API Key泄露風險。
const apiKey = process.env.DASHSCOPE_API_KEY;
const url = 'wss://dashscope.aliyuncs.com/api-ws/v1/inference/'; // WebSocket伺服器位址
const audioFile = 'asr_example.wav'; // 替換為您的音頻檔案路徑

// 產生32位隨機ID
const TASK_ID = uuidv4().replace(/-/g, '').slice(0, 32);

// 建立WebSocket用戶端
const ws = new WebSocket(url, {
  headers: {
    Authorization: `bearer ${apiKey}`
  }
});

let taskStarted = false; // 標記任務是否已啟動

// 串連開啟時發送run-task指令
ws.on('open', () => {
  console.log('串連到伺服器');
  sendRunTask();
});

// 接收訊息處理
ws.on('message', (data) => {
  const message = JSON.parse(data);
  switch (message.header.event) {
    case 'task-started':
      console.log('任務開始');
      taskStarted = true;
      sendAudioStream();
      break;
    case 'result-generated':
      console.log('識別結果:', message.payload.output.sentence.text);
      if (message.payload.usage) {
        console.log('任務計費時間長度(秒):', message.payload.usage.duration);
      }
      break;
    case 'task-finished':
      console.log('任務完成');
      ws.close();
      break;
    case 'task-failed':
      console.error('任務失敗:', message.header.error_message);
      ws.close();
      break;
    default:
      console.log('未知事件:', message.header.event);
  }
});

// 如果沒有收到task-started事件,關閉串連
ws.on('close', () => {
  if (!taskStarted) {
    console.error('任務未啟動,關閉串連');
  }
});

// 發送run-task指令
function sendRunTask() {
  const runTaskMessage = {
    header: {
      action: 'run-task',
      task_id: TASK_ID,
      streaming: 'duplex'
    },
    payload: {
      task_group: 'audio',
      task: 'asr',
      function: 'recognition',
      model: 'paraformer-realtime-v2',
      parameters: {
        sample_rate: 16000,
        format: 'wav'
      },
      input: {}
    }
  };
  ws.send(JSON.stringify(runTaskMessage));
}

// 發送音頻流
function sendAudioStream() {
  const audioStream = fs.createReadStream(audioFile);
  let chunkCount = 0;

  function sendNextChunk() {
    const chunk = audioStream.read();
    if (chunk) {
      ws.send(chunk);
      chunkCount++;
      setTimeout(sendNextChunk, 100); // 每100ms發送一次
    }
  }

  audioStream.on('readable', () => {
    sendNextChunk();
  });

  audioStream.on('end', () => {
    console.log('音頻流結束');
    sendFinishTask();
  });

  audioStream.on('error', (err) => {
    console.error('讀取音頻檔案錯誤:', err);
    ws.close();
  });
}

// 發送finish-task指令
function sendFinishTask() {
  const finishTaskMessage = {
    header: {
      action: 'finish-task',
      task_id: TASK_ID,
      streaming: 'duplex'
    },
    payload: {
      input: {}
    }
  };
  ws.send(JSON.stringify(finishTaskMessage));
}

// 錯誤處理
ws.on('error', (error) => {
  console.error('WebSocket錯誤:', error);
});

錯誤碼

如遇報錯問題,請參見錯誤資訊進行排查。

若問題仍未解決,請加入開發人員群反饋遇到的問題,並提供Request ID,以便進一步排查問題。

常見問題

功能特性

Q:在長時間靜默的情況下,如何保持與服務端長串連?

將請求參數heartbeat設定為true,並持續向服務端發送靜音音頻。

靜音音頻指的是在音頻檔案或資料流中沒有聲音訊號的內容。靜音音頻可以通過多種方法產生,例如使用音頻編輯軟體如Audacity或Adobe Audition,或者通過命令列工具如FFmpeg。

Q:如何將音頻格式轉換為滿足要求的格式?

可使用FFmpeg工具,更多用法請參見FFmpeg官網。

# 基礎轉換命令(萬能模板)
# -i,作用:輸入檔案路徑,常用值樣本:audio.wav
# -c:a,作用:音頻編碼器,常用值樣本:aac, libmp3lame, pcm_s16le
# -b:a,作用:位元速率(音質控制),常用值樣本:192k, 320k
# -ar,作用:採樣率,常用值樣本:44100 (CD), 48000, 16000
# -ac,作用:聲道數,常用值樣本:1(單聲道), 2(立體聲)
# -y,作用:覆蓋已存在檔案(無需值)
ffmpeg -i input_audio.ext -c:a 編碼器名 -b:a 位元速率 -ar 採樣率 -ac 聲道數 output.ext

# 例如:WAV → MP3(保持原始品質)
ffmpeg -i input.wav -c:a libmp3lame -q:a 0 output.mp3
# 例如:MP3 → WAV(16bit PCM標準格式)
ffmpeg -i input.mp3 -c:a pcm_s16le -ar 44100 -ac 2 output.wav
# 例如:M4A → AAC(提取/轉換蘋果音頻)
ffmpeg -i input.m4a -c:a copy output.aac  # 直接提取不重編碼
ffmpeg -i input.m4a -c:a aac -b:a 256k output.aac  # 重編碼提高品質
# 例如:FLAC無損 → Opus(高壓縮)
ffmpeg -i input.flac -c:a libopus -b:a 128k -vbr on output.opus
Q:是否支援查看每句話對應的時間範圍?

支援。語音辨識結果中會包含每句話的開始時間戳和結束時間戳記,可通過它們確定每句話的時間範圍。

Q:為什麼使用WebSocket協議而非HTTP/HTTPS協議?為什麼不提供RESTful API?

Voice Messaging Service選擇 WebSocket 而非 HTTP/HTTPS/RESTful,根本在於其依賴全雙工系統通訊能力——WebSocket 允許服務端與用戶端主動雙向傳輸資料(如即時推送語音合成/識別進度),而基於 HTTP 的 RESTful 僅支援用戶端發起的單向要求-回應模式,無法滿足即時互動需求。

Q:如何識別本地檔案(錄音檔案)?

將本地檔案轉成二進位音頻流,通過WebSocket的二進位通道上傳二進位音頻流進行識別(通常為WebSocket庫的send方法)。程式碼片段如下所示,完整樣本請參見範例程式碼

點擊查看程式碼片段

// 發送音頻資料
func sendAudioData(conn *websocket.Conn) error {
	file, err := os.Open(audioFile)
	if err != nil {
		return err
	}
	defer file.Close()

	buf := make([]byte, 1024) // 假設100ms的音頻資料大約為1024位元組
	for {
		n, err := file.Read(buf)
		if n == 0 {
			break
		}
		if err != nil && err != io.EOF {
			return err
		}
		err = conn.WriteMessage(websocket.BinaryMessage, buf[:n])
		if err != nil {
			return err
		}
		time.Sleep(100 * time.Millisecond)
	}
	return nil
}
private static async Task SendAudioStreamAsync(string filePath) {
    using (var audioStream = File.OpenRead(filePath)) {
        var buffer = new byte[1024]; // 每次發送100ms的音頻資料
        int bytesRead;

        while ((bytesRead = await audioStream.ReadAsync(buffer, 0, buffer.Length)) > 0) {
            var segment = new ArraySegment<byte>(buffer, 0, bytesRead);
            await _webSocket.SendAsync(segment, WebSocketMessageType.Binary, true, _cancellationTokenSource.Token);
            await Task.Delay(100); // 間隔100ms
        }
    }
}
// 讀取音頻檔案
$voiceData = readAudioFile($audio_file_path);
if ($voiceData === false) {
    echo "無法讀取音頻檔案\n";
    $conn->close();
    return;
}

// 分割音頻資料
$chunks = splitAudioData($voiceData, 1024);

// 定義發送函數
$sendChunk = function() use ($conn, &$chunks, $loop, &$sendChunk, &$allChunksSent, $taskId) {
    if (!empty($chunks)) {
        $chunk = array_shift($chunks);
        $binaryMsg = new Frame($chunk, true, Frame::OP_BINARY);
        $conn->send($binaryMsg);
        // 100ms後發送下一個片段
        $loop->addTimer(0.1, $sendChunk);
    } else {
        echo "所有資料區塊已發送\n";
        $allChunksSent = true;

        // 發送 finish-task 指令
        sendFinishTaskMessage($conn, $taskId);
    }
};

// 開始發送音頻資料
$sendChunk();
// 發送音頻流
function sendAudioStream() {
  const audioStream = fs.createReadStream(audioFile);
  let chunkCount = 0;

  function sendNextChunk() {
    const chunk = audioStream.read();
    if (chunk) {
      ws.send(chunk);
      chunkCount++;
      setTimeout(sendNextChunk, 100); // 每100ms發送一次
    }
  }

  audioStream.on('readable', () => {
    sendNextChunk();
  });

  audioStream.on('end', () => {
    console.log('音頻流結束');
    sendFinishTask();
  });

  audioStream.on('error', (err) => {
    console.error('讀取音頻檔案錯誤:', err);
    ws.close();
  });
}

故障排查

如遇代碼報錯問題,請根據錯誤碼中的資訊進行排查。

Q:無法識別語音(無識別結果)是什麼原因?

  1. 請檢查請求參數中的音頻格式(format)和採樣率(sampleRate/sample_rate)設定是否正確且符合參數約束。以下為常見錯誤樣本:

    • 音頻副檔名為 .wav,但實際為 MP3 格式,而請求參數 format 設定為 mp3(參數設定錯誤)。

    • 音頻採樣率為 3600Hz,但請求參數 sampleRate/sample_rate 設定為 48000(參數設定錯誤)。

    可以使用ffprobe工具擷取音訊容器、編碼、採樣率、聲道等資訊:

    ffprobe -v error -show_entries format=format_name -show_entries stream=codec_name,sample_rate,channels -of default=noprint_wrappers=1 input.xxx
  2. 使用paraformer-realtime-v2模型時,請檢查language_hints設定的語言是否與音頻實際語言一致。

    例如:音頻實際為中文,但language_hints設定為en(英文)。