全部產品
Search
文件中心

Alibaba Cloud Model Studio:語音合成CosyVoice WebSocket API

更新時間:Feb 11, 2026

本文介紹如何通過WebSocket串連訪問CosyVoice語音合成服務。

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

使用者指南:關於模型介紹和選型建議請參見即時語音合成-CosyVoice/Sambert

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

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

  • Go:gorilla/websocket

  • PHP:Ratchet

  • Node.js:ws

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

重要

CosyVoice 系列模型僅支援通過 WebSocket 串連調用,不支援 HTTP REST API。如果使用 HTTP 要求(如 POST 方式)調用,將返回 InvalidParameter 或 url error 錯誤。

前提條件

擷取API Key

模型與價格

參見即時語音合成-CosyVoice/Sambert

語音合成文本限制與格式規範

文本長度限制

單次通過continue-task指令發送的待合成文本長度不得超過 20000 字元,多次調用continue-task指令累計發送的文本總長度不得超過 20 萬字元。

字元計算規則

  • 漢字(包括簡/繁體漢字、日文漢字和韓文漢字)按2個字元計算,其他所有字元(如標點符號、字母、數字、日韓文假名/諺文等)均按 1個字元計算

  • 計算文本長度時,不包含SSML 標籤內容

  • 樣本:

    • "你好" → 2(你)+2(好)=4字元

    • "中A文123" → 2(中)+1(A)+2(文)+1(1)+1(2)+1(3)=8字元

    • "中文。" → 2(中)+2(文)+1(。)=5字元

    • "中 文。" → 2(中)+1(空格)+2(文)+1(。)=6字元

    • "<speak>你好</speak>" → 2(你)+2(好)=4字元

編碼格式

需採用UTF-8編碼。

數學運算式支援說明

當前數學運算式解析功能僅適用於cosyvoice-v2cosyvoice-v3-flashcosyvoice-v3-plus模型,支援識別中小學常見的數學運算式,包括但不限於基礎運算、代數、幾何等內容。

詳情請參見LaTeX 方程式轉語音

SSML標記語言支援說明

使用 SSML 功能需要同時滿足以下條件:

  1. 模型支援:僅cosyvoice-v3-flash、cosyvoice-v3-plus和cosyvoice-v2模型支援SSML功能

  2. 音色支援: 必須使用支援 SSML 的音色。支援 SSML 的音色包括

    • 所有複刻音色(通過聲音複刻 API 建立的自訂音色)

    • 音色列表中標記為支援SSML的系統音色

    說明

    如果使用不支援 SSML 的系統音色(如部分基礎音色),即使開啟 enable_ssml 參數,也會報錯“SSML text is not supported at the moment!”。

  3. 參數配置: 在run-task指令中將 enable_ssml 參數設為 true

滿足上述條件後,通過continue-task指令發送包含SSML的文本即可使用 SSML 功能。完整樣本請參見快速開始

互動流程

image

用戶端發送給服務端的訊息稱作指令;服務端返回給用戶端的訊息有兩種:JSON格式的事件和二進位音頻流。

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

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

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

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

  4. 發送待合成文本:

    用戶端按順序向服務端發送一個或多個包含待合成文本的continue-task指令,服務端接收到完整語句後返回result-generated事件和音頻流(文本長度有約束, 詳情參見continue-task指令text欄位描述)。

    說明

    您可以多次發送continue-task指令,按順序提交文本片段。服務端接收文本片段後自動進行分句:

    • 完整語句立即合成,此時用戶端能夠接收到服務端返回的音頻

    • 不完整語句緩衝至完整後合成,語句不完整時服務端不返迴音頻

    當發送finish-task指令時,服務端會強制合成所有緩衝內容。

  5. 接收音頻:通過 binary 通道接收音頻流

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

    待文本發送完畢後,用戶端發送finish-task指令通知服務端結束任務,並繼續接收服務端返回的音頻流(注意不要遺漏該步驟,否則可能收不到語音或收不到結尾部分的語音)。

  7. 任務結束:

    用戶端收到服務端返回的task-finished事件,標誌著任務結束。

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

為提高資源使用率,建議複用 WebSocket 串連處理多個任務,而非為每個任務建立新串連。參見關於建連開銷和串連複用

重要

task_id 必須全程一致:同一次合成任務中,run-task、所有 continue-task、finish-task 必須使用相同的 task_id

錯誤後果:如使用不同的 task_id,會導致:

  • 服務端無法關聯請求,音頻流返回順序混亂

  • 常值內容被錯誤分配到不同任務,語音內容錯位

  • 任務狀態異常,可能收不到 task-finished 事件

  • 無法正確計費,usage 統計不準確

正確做法

  • 在 run-task 時產生唯一的 task_id(如使用UUID)

  • 將該 task_id 儲存在變數中

  • 後續所有 continue-task 和 finish-task 都使用該 task_id

  • 任務結束後(收到 task-finished),如需發起新任務,產生新的 task_id

用戶端實現注意事項

在實現 WebSocket 用戶端時,特別是使用 Flutter、Web 或移動端平台時,需要明確服務端與用戶端的職責劃分,以確保語音合成任務的完整性和穩定性。

服務端與用戶端職責

服務端職責

服務端保證按順序返回完整的音頻流。您無需擔心音頻資料的順序性或完整性,服務端會按照文本順序依次產生並推送所有音頻分區。

用戶端職責

用戶端需要負責以下關鍵任務:

  1. 讀取並拼接所有音頻分區

    服務端返回的音頻是以多個二進位分區(Binary Frame)的形式推送的。用戶端必須完整接收所有分區,並按接收順序拼接成最終的音頻檔案。範例程式碼如下:

    # Python 樣本:拼接音頻分區
    with open("output.mp3", "ab") as f:  # 追加模式寫入
        f.write(audio_chunk)  # audio_chunk 為每次接收到的二進位音頻資料
    // JavaScript 樣本:拼接音頻分區
    const audioChunks = [];
    ws.onmessage = (event) => {
      if (event.data instanceof Blob) {
        audioChunks.push(event.data);  // 收集所有音頻分區
      }
    };
    // 任務完成後合并音頻
    const audioBlob = new Blob(audioChunks, { type: 'audio/mp3' });
  2. 保證 WebSocket 生命週期完整

    在整個語音合成任務過程中(從發送 run-task指令 到接收 task-finished事件),不要提前斷開 WebSocket 串連。常見錯誤包括:

    • 在所有音頻分區返回前就關閉串連,導致音頻不完整;

    • 忘記發送 finish-task指令,導致服務端緩衝的文本未能合成;

    • 頁面跳轉、應用程式切換到後台等情境下,未妥善處理 WebSocket 的保活機制。

    重要

    移動端應用(如 Flutter、iOS、Android)需要特別注意應用進入後台時的網路連接管理。建議在背景工作或服務中維護 WebSocket 串連,或在恢複前台時檢查任務狀態並重建立立串連。

  3. ASR→LLM→TTS 聯動情境的文本完整性

    在語音辨識(ASR)→大語言模型(LLM)→語音合成(TTS)的聯動流程中,確保傳遞給 TTS 的文本是完整的,不被中途截斷。例如:

    • 等待 LLM 產生完整句子或段落後,再發送 continue-task指令,而非逐字推送;

    • 如果需要流式合成(邊產生邊播放),可以按自然語句邊界(如句號、問號)分批發送文本;

    • 在 LLM 輸出完成後,務必發送 finish-task指令,避免遺漏尾部內容。

平台特定提示

  • Flutter:使用 web_socket_channel 包時,注意在 dispose 方法中正確關閉串連,避免記憶體流失。同時,處理應用生命週期事件(如 AppLifecycleState.paused)以應對後台切換情境。

  • Web(瀏覽器):部分瀏覽器對 WebSocket 串連數有限制,建議複用同一串連處理多個任務。另外,使用 beforeunload 事件在頁面關閉前主動中斷連線,避免殘留串連。

  • 移動端(iOS/Android 原生):在應用進入後台時,作業系統可能會暫停或終止網路連接。建議使用背景工作(Background Task)或前台服務(Foreground Service)保持 WebSocket 活躍,或在恢複前台時重新初始化任務。

URL

WebSocket URL固定如下:

國際

國際部署模式下,存取點與資料存放區均位於新加坡地區,模型推理計算資源在全球範圍內動態調度(不含中國內地)。

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

中國內地

中國內地部署模式下,存取點與資料存放區均位於北京地區,模型推理計算資源僅限於中國內地。

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

重要

常見 URL 配置錯誤:

  • 錯誤:使用 http:// 或 https:// 開頭的 URL → 正確:必須使用 wss:// 協議

  • 錯誤:將 Authorization 參數放在 URL 查詢字串中(如 ?Authorization=bearer <your_api_key>)→ 正確:Authorization 必須在 HTTP 握手的 Headers 中設定(參見Headers

  • 錯誤:URL 末尾添加模型名稱或其他路徑參數 → 正確:URL 固定不變,模型通過run-task指令payload.model 參數指定

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。如非必要,請勿啟用該參數。

重要

鑒權驗證時機與常見錯誤

Authorization 鑒權在 WebSocket 握手階段進行驗證,而非後續發送run-task指令時。如果 Authorization 頭缺失或 API Key 無效,服務端將拒絕握手並返回 HTTP 401/403 錯誤,用戶端庫通常解析為 WebSocketBadStatus 異常。

鑒權失敗排查步驟

若 WebSocket 串連失敗,請按以下步驟排查:

  1. 檢查 API Key 格式:確認 Authorization 頭格式為bearer <your_api_key>,注意 bearer 和 API Key 之間有一個空格。

  2. 驗證 API Key 有效性:在百鍊控制台確認 API Key 未被刪除或禁用,且具有調用 CosyVoice 模型的許可權。

  3. 檢查 Headers 設定:確認 Authorization 頭在 WebSocket 握手時正確設定。不同程式設計語言的 WebSocket 庫設定方式不同:

    • Python(websockets 庫):extra_headers={"Authorization": f"bearer {api_key}"}

    • JavaScript:WebSocket 標準 API 不支援自訂 Headers,需使用服務端中轉或其他庫(如 ws)

    • Go(gorilla/websocket):header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))

  4. 網路連通性測試:使用 curl 或 Postman 測試 API Key 是否有效(通過其他支援 HTTP 的 DashScope API)。

瀏覽器環境 WebSocket 使用說明

在瀏覽器環境(如 Vue3、React 等前端架構)中使用 WebSocket 時,存在以下限制:瀏覽器 WebSocket API 不支援自訂 Headers。瀏覽器原生的 new WebSocket(url) API 不支援在握手時設定自訂要求標頭(如 Authorization),這是瀏覽器安全性原則的限制。因此,無法直接在前端代碼中使用 API Key 進行鑒權。

解決方案使用後端代理

  1. 在後端服務(Node.js、Java、Python 等)中建立 WebSocket 串連到 CosyVoice 服務,後端可以正確設定 Authorization 頭。

  2. 前端通過 WebSocket 串連到自己的後端服務,後端作為代理轉寄訊息到 CosyVoice。

  3. 優點:API Key 不暴露在前端,更安全;可以在後端添加額外的商務邏輯(如鑒權、日誌、限流等)。

重要

不要將 API Key 寫入程式碼在前端代碼中或通過瀏覽器直接發送。API Key 泄露會導致帳號被盜用、產生高額費用或資料泄露風險。

範例程式碼

如需其他程式設計語言實現,您可以參考樣本中的邏輯,使用對應語言實現。或者使用AI工具將樣本轉換為目標語言。

指令(用戶端→服務端)

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

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

  1. 發送 run-task指令

  2. 發送continue-task指令

    • 用於發送待合成文本。

    • 必須在接收到服務端返回的task-started事件後,才能發送此指令。

  3. 發送finish-task指令

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

該指令用於開啟語音合成任務。可在該指令中對音色、採樣率等請求參數進行設定。

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

  • 不要發送待合成文本:在 run-task 指令中發送文本不利於問題排查,應避免在此發送文本。待合成文本應通過continue-task指令發送。

  • input 欄位必須存在:payload 中必須包含 input 欄位(格式為 {}),不可省略,否則會報錯“task can not be null”。

樣本:

{
    "header": {
        "action": "run-task",
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx", // 隨機uuid
        "streaming": "duplex"
    },
    "payload": {
        "task_group": "audio",
        "task": "tts",
        "function": "SpeechSynthesizer",
        "model": "cosyvoice-v3-flash",
        "parameters": {
            "text_type": "PlainText",
            "voice": "longanyang",            // 音色
            "format": "mp3",		        // 音頻格式
            "sample_rate": 22050,	        // 採樣率
            "volume": 50,			// 音量
            "rate": 1,				// 語速
            "pitch": 1				// 音調
        },
        "input": {// 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(self):
    # 產生隨機UUID
    return uuid.uuid4().hex

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

header.streaming

string

固定字串:"duplex"

payload參數說明:

參數

類型

是否必選

說明

payload.task_group

string

固定字串:"audio"。

payload.task

string

固定字串:"tts"。

payload.function

string

固定字串:"SpeechSynthesizer"。

payload.model

string

語音合成模型

不同模型版本需要使用對應版本的音色:

  • cosyvoice-v3-flash/cosyvoice-v3-plus:使用longanyang等音色。

  • cosyvoice-v2:使用longxiaochun_v2等音色。

  • 完整音色列表請參見音色列表

payload.input

object

run-task 指令中必須包含 input 欄位(不可省略),但不應在此發送待合成文本(因此應使用Null 物件 {})。待合成文本應通過後續的continue-task指令發送,以便於問題排查和流式合成。

input格式為:

"input": {}
重要

常見錯誤:省略 input 欄位或在 input 中包含非預期欄位(如 mode、content 等)會導致服務端拒絕請求並返回“InvalidParameter: task can not be null”或串連關閉(WebSocket code 1007)錯誤。

payload.parameters

text_type

string

固定字串:“PlainText”。

voice

string

語音合成所使用的音色。

支援系統音色和複刻音色:

  • 系統音色:參見音色列表

  • 複刻音色:通過聲音複刻(CosyVoice)功能定製。使用複刻音色時,請確保聲音複刻與語音合成使用同一帳號。詳細操作步驟請參見CosyVoice聲音複刻API

    使用聲音複刻產生的複刻音色時,本請求的model參數值,必須與建立該音色時所用的模型版本(即target_model參數)完全一致。

format

string

音頻編碼格式。

支援pcm、wav、mp3(預設)和opus。

音頻格式為opus時,支援通過bit_rate參數調整碼率。

sample_rate

integer

音頻採樣率(單位:Hz)。

預設值:22050。

取值範圍:8000, 16000, 22050, 24000, 44100, 48000。

說明

預設採樣率代表當前音色的最佳採樣率,預設條件下預設按照該採樣率輸出,同時支援降採樣或升採樣。

volume

integer

音量。

預設值:50。

取值範圍:[0, 100]。50代表標準音量。音量大小與該值呈線性關係,0為靜音,100為最大音量。

rate

float

語速。

預設值:1.0。

取值範圍:[0.5, 2.0]。1.0為標準語速,小於1.0則減慢,大於1.0則加快。

pitch

float

音高。該值作為音高調節的乘數,但其與聽感上的音高變化並非嚴格的線性或對數關係,建議通過測試選擇合適的值。

預設值:1.0。

取值範圍:[0.5, 2.0]。1.0為音色自然音高。大於1.0則音高變高,小於1.0則音高變低。

enable_ssml

boolean

是否開啟SSML功能。

該參數設為 true 後,僅允許發送一次文本(只允許發送一次continue-task指令)。

bit_rate

int

音頻碼率(單位kbps)。音頻格式為opus時,支援通過bit_rate參數調整碼率。

預設值:32。

取值範圍:[6, 510]。

word_timestamp_enabled

boolean

是否開啟字層級時間戳記。

預設值:false。

  • true:開啟。

  • false:關閉。

該功能僅適用於cosyvoice-v3-flash、cosyvoice-v3-plus和cosyvoice-v2模型的複刻音色,以及音色列表中標記為支援的系統音色。

更多說明請參見時間戳記資料提取最佳實務

seed

int

產生時使用的隨機數種子,使合成的效果產生變化。在模型版本、文本、音色及其他參數均相同的前提下,使用相同的seed可複現相同的合成結果。

預設值0。

取值範圍:[0, 65535]。

language_hints

array[string]

指定語音合成的目標語言,提升合成效果。

當數字、縮寫、符號等朗讀方式或者小語種合成效果不符合預期時使用,例如:

  • 數字朗讀方式不符合預期,“hello, this is 110”讀成“hello, this is one one zero”而非“hello, this is 么么零”

  • 符號朗讀不準確,“@”讀成“艾特”而非“at”

  • 小語種合成效果差,合成不自然

取值範圍:

  • zh:中文

  • en:英文

  • fr:法語

  • de:德語

  • ja:日語

  • ko:韓語

  • ru:俄語

注意:此參數為數組,但目前的版本僅處理第一個元素,因此建議只傳入一個值。

重要

此參數用於指定語音合成的目標語言,該設定與聲音複刻時的樣本音訊語種無關。如果您需要設定複刻任務的源語言,請參見CosyVoice聲音複刻API

instruction

string

設定指令,用於控制方言、情感或角色等合成效果。該功能僅適用於cosyvoice-v3-flash模型的複刻音色,以及音色列表中標記為支援Instruct的系統音色。

長度限制:100字元

漢字(包括簡/繁體漢字、日文漢字和韓文漢字)按2個字元計算,其他所有字元(如標點符號、字母、數字、日韓文假名/諺文等)均按 1個字元計算

使用要求

  • 指令必須使用固定格式和內容(見下方說明)

  • 不設定時不生效(無預設值)

支援的功能

說明

對於cosyvoice-v3-flash複刻音色,可使用任意自然語言控制語音合成效果。為獲得穩定、可預期的效果,建議僅使用下述已支援的功能點,並盡量按照樣本中的格式編寫 instruction。

對於其他音色,需嚴格按照樣本中的格式編寫 instruction。

  • 指定方言

    • 適用音色:僅複刻音色

    • 格式:“請用<方言>表達。”(注意,結尾一定不要遺漏句號,使用時將“<方言>”替換為具體的方言,例如替換為廣東話)。

    • 樣本:“請用廣東話表達。

    • 支援的方言:廣東話、東北話、甘肅話、貴州話、河南話、湖北話、江西話、閩南話、寧夏話、山西話、陝西話、山東話、上海話、四川話、天津話、雲南話。

  • 指定情感

    • 適用音色

      • 複刻音色

      • 音色列表中標記為支援Instruct的系統音色

    • 格式:

      • 複刻音色:

        點擊查看複刻音色指令格式

        • 請儘可能非常大聲地說一句話。

        • 請用儘可能慢地語速說一句話。

        • 請用儘可能快地語速說一句話。

        • 請非常輕聲地說一句話。

        • 你可以慢一點說嗎

        • 你可以非常快一點說嗎

        • 你可以非常慢一點說嗎

        • 你可以快一點說嗎

        • 請非常生氣地說一句話。

        • 請非常開心地說一句話。

        • 請非常恐懼地說一句話。

        • 請非常傷心地說一句話。

        • 請非常驚訝地說一句話。

        • 請儘可能表現出堅定的感覺。

        • 請儘可能表現出憤怒的感覺。

        • 請嘗試一下親和的語調。

        • 請用冷酷的語調講話。

        • 請用威嚴的語調講話。

        • 我想體驗一下自然的語氣。

        • 我想看看你如何表達威脅。

        • 我想看看你怎麼表現智慧。

        • 我想看看你怎麼表現誘惑。

        • 我想聽聽用活潑的方式說話。

        • 我想聽聽你用激昂的感覺說話。

        • 我想聽聽用沉穩的方式說話的樣子。

        • 我想聽聽你用自信的感覺說話。

        • 你能用興奮的感覺和我交流嗎?

        • 你能否展示狂傲的情緒表達?

        • 你能展現一下優雅的情緒嗎?

        • 你可以用幸福的方式回答問題嗎?

        • 你可以做一個溫柔的情感示範嗎?

        • 能用冷靜的語調和我談談嗎?

        • 能用深沉的方法回答我嗎?

        • 能用粗獷的情緒態度和我對話嗎?

        • 用陰森的聲音告訴我這個答案。

        • 用堅韌的聲音告訴我這個答案。

        • 用自然親切的閑聊風格敘述。

        • 用廣播劇部落客的語氣講話。

      • 系統音色:系統音色和複刻音色的情感指令格式不同,詳情請參見音色列表

  • 指定情境、角色或身份等

enable_aigc_tag

boolean

是否在產生的音頻中添加AIGC隱性標識。設定為true時,會將隱性標識嵌入到支援格式(wav/mp3/opus)的音頻中。

預設值:false。

僅cosyvoice-v3-flash、cosyvoice-v3-plus、cosyvoice-v2支援該功能。

aigc_propagator

string

設定AIGC隱性標識中的 ContentPropagator 欄位,用於標識內容的傳播者。僅在 enable_aigc_tag 為 true 時生效。

預設值:阿里雲UID。

僅cosyvoice-v3-flash、cosyvoice-v3-plus、cosyvoice-v2支援該功能。

aigc_propagate_id

string

設定AIGC隱性標識中的 PropagateID 欄位,用於唯一標識一次具體的傳播行為。僅在 enable_aigc_tag 為 true 時生效。

預設值:本次語音合成請求Request ID。

僅cosyvoice-v3-flash、cosyvoice-v3-plus、cosyvoice-v2支援該功能。

2. continue-task指令

該指令專門用來發送待合成文本。

可以在一個continue-task指令中一次性發送待合成文本,也可以將文本分段並按順序在多個continue-task指令中發送。

重要

發送時機:在收到task-started事件後發送。

說明

發送文本片段的間隔不得超過23秒,否則觸發“request timeout after 23 seconds”異常。

若無待發送文本,需及時發送finish-task指令結束任務。

服務端強制設定23秒逾時機制,用戶端無法修改該配置。

樣本:

{
    "header": {
        "action": "continue-task",
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx", // 隨機uuid
        "streaming": "duplex"
    },
    "payload": {
        "input": {
            "text": "床前明月光,疑是地上霜"
        }
    }
}

header參數說明:

參數

類型

是否必選

說明

header.action

string

指令類型。

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

header.task_id

string

當次任務ID。

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

header.streaming

string

固定字串:"duplex"

payload參數說明:

參數

類型

是否必選

說明

input.text

string

待合成文本。

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

該指令用於結束語音合成任務。

請務必確保發送該指令,否則會出現以下問題:

  • 音頻不完整:服務端緩衝的不完整語句不會被強制合成,導致音頻缺失尾部內容。

  • 連線逾時:如果在最後一次continue-task指令後超過 23 秒未發送 finish-task,串連會因逾時而斷開。

  • 計費異常:未正常結束的任務可能無法返回準確的 usage 資訊。

重要

發送時機:finish-task 應在所有continue-task指令發送完畢後立即發送,不要等待音頻返回完畢或延遲發送,否則可能觸發逾時。

樣本:

{
    "header": {
        "action": "finish-task",
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "streaming": "duplex"
    },
    "payload": {
        "input": {}//input不能省去,否則會報錯
    }
}

header參數說明:

參數

類型

是否必選

說明

header.action

string

指令類型。

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

header.task_id

string

當次任務ID。

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

header.streaming

string

固定字串:"duplex"

payload參數說明:

參數

類型

是否必選

說明

payload.input

object

固定格式:{}。

事件(服務端→用戶端)

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

說明

服務端返回給用戶端的二進位音頻不包含在任何事件中,需單獨接收。

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

當監聽到服務端返回的task-started事件時,標誌著任務已成功開啟。只有在接收到該事件後,才能向服務端發送continue-task指令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事件

用戶端發送continue-task指令finish-task指令的同時,服務端持續返回result-generated事件。

為了讓使用者能夠將音頻資料與對應的常值內容關聯,服務端在返迴音頻資料的同時,通過result-generated事件返回句子的元資訊。服務端會對使用者輸入的文本進行自動分句,每個句子的合成過程包含以下3個子事件:

  • sentence-begin:標識句子開始,返回待合成的句子常值內容

  • sentence-synthesis:標識音頻資料區塊,每個此事件後立即通過WebSocket binary通道傳輸一個音頻資料幀

    • 一個句子的合成過程中會產生多個sentence-synthesis事件,每個對應一個音頻資料區塊

    • 用戶端需要按順序接收這些音頻資料區塊並以追加模式寫入同一檔案

    • sentence-synthesis事件與其後的音頻資料幀是一一對應的關係,不會出現錯位

  • sentence-end:標識句子結束,返回句子常值內容和累計的計費字元數

通過payload.output.type欄位區分子事件類型。

樣本:

sentence-begin

{
    "header": {
        "task_id": "3f2d5c86-0550-45c0-801f-xxxxxxxxxx",
        "event": "result-generated",
        "attributes": {}
    },
    "payload": {
        "output": {
            "sentence": {
                "index": 0,
                "words": []
            },
            "type": "sentence-begin",
            "original_text": "床前明月光,"
        }
    }
}

sentence-synthesis

{
    "header": {
        "task_id": "3f2d5c86-0550-45c0-801f-xxxxxxxxxx",
        "event": "result-generated",
        "attributes": {}
    },
    "payload": {
        "output": {
            "sentence": {
                "index": 0,
                "words": []
            },
            "type": "sentence-synthesis"
        }
    }
}

sentence-end

{
    "header": {
        "task_id": "3f2d5c86-0550-45c0-801f-xxxxxxxxxx",
        "event": "result-generated",
        "attributes": {}
    },
    "payload": {
        "output": {
            "sentence": {
                "index": 0,
                "words": []
            },
            "type": "sentence-end",
            "original_text": "床前明月光,"
        },
        "usage": {
            "characters": 11
        }
    }
}

header參數說明:

參數

類型

說明

header.event

string

事件類型。

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

header.task_id

string

用戶端產生的task_id。

header.attributes

object

附加屬性,通常為空白對象。

payload參數說明:

參數

類型

說明

payload.output.type

string

子事件類型。

取值範圍:

  • sentence-begin:標識句子開始,返回待合成的句子常值內容

  • sentence-synthesis:標識音頻資料區塊,每個此事件後立即通過WebSocket binary通道傳輸一個音頻資料幀

    • 一個句子的合成過程中會產生多個sentence-synthesis事件,每個對應一個音頻資料區塊

    • 用戶端需要按順序接收這些音頻資料區塊並以追加模式寫入同一檔案

    • sentence-synthesis事件與其後的音頻資料幀是一一對應的關係,不會出現錯位

  • sentence-end:標識句子結束,返回句子常值內容和累計的計費字元數

完整的事件流程

對於每個待合成的句子,服務端按以下順序返回事件:

  1. sentence-begin:標識句子開始,包含句子常值內容(original_text

  2. sentence-synthesis (多次) :每個事件後立即跟隨一個二進位音頻資料幀

  3. sentence-end:標識句子結束,包含句子常值內容和累計計費字元數

payload.output.sentence.index

integer

句子的編號,從0開始。

payload.output.sentence.words

array

字層級資訊數組,通常為空白數組。

payload.output.original_text

string

對使用者輸入文本進行分句後的句內容。最後一個句子可能沒有此欄位。

payload.usage.characters

integer

截止當前,本次請求中計費的有效字元數。 在一次任務中,usage可能會出現在result-generated事件task-finished事件中。下發的usage欄位為累加後的結果,請按最後一次為準。

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

當監聽到服務端返回的task-finished事件時,說明任務已結束。

結束任務後可以關閉WebSocket串連結束程式,也可以複用WebSocket串連,重新發送run-task指令開啟下一個任務(參見關於建連開銷和串連複用)。

樣本:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "task-finished",
        "attributes": {
            "request_uuid": "0a9dba9e-d3a6-45a4-be6d-xxxxxxxxxxxx"
        }
    },
    "payload": {
        "output": {
            "sentence": {
                "words": []
            }
        },
        "usage": {
            "characters": 13
        }
    }
}

header參數說明:

參數

類型

說明

header.event

string

事件類型。

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

header.task_id

string

用戶端產生的task_id。

header.attributes.request_uuid

string

Request ID,可提供給CosyVoice開發人員定位問題。

payload參數說明:

參數

類型

說明

payload.usage.characters

integer

截止當前,本次請求中計費的有效字元數。 在一次任務中,usage可能會出現在result-generated事件task-finished事件中。下發的usage欄位為累加後的結果,請按最後一次為準。

payload.output.sentence.index

integer

句子的編號,從0開始。

本欄位和以下欄位需要通過word_timestamp_enabled開啟字層級時間戳記

payload.output.sentence.words[k]

text

string

字的文本。

begin_index

integer

字在句子中的開始位置索引,從 0 開始。

end_index

integer

字在句子中的結束位置索引,從 1 開始。

begin_time

integer

字對應音訊開始時間戳,單位為毫秒。

end_time

integer

字對應音訊結束時間戳記,單位為毫秒。

時間戳記資料提取最佳實務

開啟 word_timestamp_enabled 後,時間戳記資訊會在 task-finished 事件中返回。樣本如下:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "task-finished",
        "attributes": {"request_uuid": "0a9dba9e-d3a6-45a4-be6d-xxxxxxxxxxxx"}
    },
    "payload": {
        "output": {
            "sentence": {
                "index": 0,
                "words": [
                    {
                        "text": "今",
                        "begin_index": 0,
                        "end_index": 1,
                        "begin_time": 80,
                        "end_time": 200
                    },
                    {
                        "text": "天",
                        "begin_index": 1,
                        "end_index": 2,
                        "begin_time": 240,
                        "end_time": 360
                    },
                    {
                        "text": "天",
                        "begin_index": 2,
                        "end_index": 3,
                        "begin_time": 360,
                        "end_time": 480
                    },
                    {
                        "text": "氣",
                        "begin_index": 3,
                        "end_index": 4,
                        "begin_time": 480,
                        "end_time": 680
                    },
                    {
                        "text": "怎",
                        "begin_index": 4,
                        "end_index": 5,
                        "begin_time": 680,
                        "end_time": 800
                    },
                    {
                        "text": "麼",
                        "begin_index": 5,
                        "end_index": 6,
                        "begin_time": 800,
                        "end_time": 920
                    },
                    {
                        "text": "樣",
                        "begin_index": 6,
                        "end_index": 7,
                        "begin_time": 920,
                        "end_time": 1000
                    },
                    {
                        "text": "?",
                        "begin_index": 7,
                        "end_index": 8,
                        "begin_time": 1000,
                        "end_time": 1320
                    }
                ]
            }
        },
        "usage": {"characters": 15}
    }
}

正確的提取方式:

  1. 僅在 task-finished 事件中提取完整時間戳記:完整的句子時間戳記資料僅在任務結束時(task-finished 事件)返回,包含 payload.output.sentence.words 數組。

  2. result-generated 事件不包含時間戳記:result-generated 事件主要用於標識音頻流的產生進度,不包含字層級時間戳記資訊。

  3. 事件過濾樣本(Python):

    def on_event(message):
        event_type = message["header"]["event"]
        # 僅在 task-finished 事件中提取時間戳記
        if event_type == "task-finished":
            words = message["payload"]["output"]["sentence"]["words"]
            for word in words:
                print(f"文字: {word['text']}, 開始: {word['begin_time']}ms, 結束: {word['end_time']}ms")
    
        # result-generated 事件用於音頻流處理
        elif event_type == "result-generated":
            # 處理音頻流,不提取時間戳記
            pass
重要

如果在多個事件中提取時間戳記資料,會導致重複。請確保僅在 task-finished 事件中提取。

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

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

樣本:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "task-failed",
        "error_code": "InvalidParameter",
        "error_message": "[tts:]Engine return error code: 418",
        "attributes": {}
    },
    "payload": {}
}

header參數說明:

參數

類型

說明

header.event

string

事件類型。

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

header.task_id

string

用戶端產生的task_id。

header.error_code

string

報錯類型描述。

header.error_message

string

具體報錯原因。

任務中斷方式

在流式合成過程中,如需提前終止當前任務(如使用者取消播放、即時對話中打斷等),可通過以下方式實現:

中斷方式

服務端行為

適用情境

直接關閉串連

  • 服務端立即停止合成

  • 已產生但未發送的音頻被丟棄

  • 用戶端不會收到 task-finished 事件

  • 串連斷開後無法複用

立即中斷:使用者取消播放、切換內容、應用退出等

發送 finish-task

  • 服務端強制合成所有緩衝文本

  • 返回剩餘音頻片段

  • 返回 task-finished 事件

  • 串連可複用(可發起新任務)

優雅結束:停止發送新文本,但需接收已緩衝內容的音頻

發起新 run-task

  • 服務端自動終止當前任務

  • 當前任務的未完成音頻被丟棄

  • 立即開始新任務的合成

  • 串連保持,無需重建

任務切換:即時對話中使用者打斷,立即切換到新內容

關於建連開銷和串連複用

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

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

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

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

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

效能指標與並發限制

並發限制

具體限制請參見限流

如需提升並發配額(如支援更多並發串連數),請聯絡客服申請。配額調整可能需要審核,一般在 1~3 個工作日內完成。

說明

最佳實務:為提高資源使用率,建議複用 WebSocket 串連處理多個任務,而非為每個任務建立新串連。參見關於建連開銷和串連複用

串連效能與延遲

正常串連耗時

  • 中國內地用戶端:WebSocket 串連建立(從 newWebSocket 到 onOpen)通常耗時 200~1000 毫秒。

  • 跨境串連(如香港、海外地區):可能出現 1~3 秒的串連延遲,偶發情況下可能達到 10~30 秒。

串連耗時過長排查

如果 WebSocket 串連建立耗時超過 30 秒,可能的原因包括:

  1. 網路問題:用戶端與服務端之間的網路延遲較高(如跨境串連、電訊廠商網路品質問題)。

  2. DNS 解析慢:dashscope.aliyuncs.com 的 DNS 解析耗時較長。可嘗試使用公用 DNS(如 8.8.8.8)或配置本地 hosts 檔案。

  3. TLS 握手慢:用戶端 TLS 版本過低或認證校正耗時。建議使用 TLS 1.2 或更高版本。

  4. 代理或防火牆:商業網路可能限制 WebSocket 串連或需要通過代理。

排查工具:

  • 使用 Wireshark 或 tcpdump 抓包分析 TCP 握手、TLS 握手、WebSocket Upgrade 各階段耗時。

  • 使用 curl 測試 HTTP 串連延遲:curl -w "@curl-format.txt" -o /dev/null -s https://dashscope.aliyuncs.com

說明

CosyVoice WebSocket API 部署在中國內地(北京)地區。如果用戶端位於其他地區(如香港、海外),建議使用就近的代理服務器或 CDN 加速串連。

音頻產生效能

合成速度

  • 即時率(RTF): CosyVoice 各模型的合成速度通常為 0.1~0.5 倍即時率(即 1 秒音頻約需 0.1~0.5 秒產生),具體速度取決於模型版本、文本長度和服務端負載。

  • 首包延遲:從發送 continue-task 指令到接收到第一個音頻分區,通常在 200~800 毫秒之間。

範例程式碼

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

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

  1. 建立WebSocket串連

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

  2. 監聽服務端訊息

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

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

    監聽事件

    處理二進位音頻流:服務端通過 binary 通道分幀下發音頻流。完整的音頻資料被分成多個資料包傳輸。

    • 流式語音合成中,對於mp3/opus等壓縮格式,音頻分段傳輸需使用流式播放器,不可逐幀播放,避免解碼失敗。

      支援流式播放的播放器:ffmpeg、pyaudio (Python)、AudioFormat (Java)、MediaSource (Javascript)等。
    • 將音頻資料合成完整的音頻檔案時,應以追加模式寫入同一檔案。

    • 流式語音合成的wav/mp3 格式音頻僅首幀包含頭資訊,後續幀為純音頻資料。

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

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

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

    1. 發送 run-task指令

    2. 發送continue-task指令

      • 用於發送待合成文本。

      • 必須在接收到服務端返回的task-started事件後,才能發送此指令。

    3. 發送finish-task指令

  4. 關閉WebSocket串連

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

點擊查看完整樣本

Go

package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"strings"
	"time"

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

const (
	// 以下為新加坡地區url,若使用北京地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/inference/
	wsURL      = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/"
	outputFile = "output.mp3"
)

func main() {
	// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
	// 若沒有配置環境變數,請用百鍊API Key將下行替換為:apiKey := "sk-xxx"
	apiKey := os.Getenv("DASHSCOPE_API_KEY")

	// 清空輸出檔案
	os.Remove(outputFile)
	os.Create(outputFile)

	// 串連WebSocket
	header := make(http.Header)
	header.Add("X-DashScope-DataInspection", "enable")
	header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))

	conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header)
	if err != nil {
		if resp != nil {
			fmt.Printf("串連失敗 HTTP狀態代碼: %d\n", resp.StatusCode)
		}
		fmt.Println("串連失敗:", err)
		return
	}
	defer conn.Close()

	// 產生任務ID
	taskID := uuid.New().String()
	fmt.Printf("產生任務ID: %s\n", taskID)

	// 發送run-task指令
	runTaskCmd := map[string]interface{}{
		"header": map[string]interface{}{
			"action":    "run-task",
			"task_id":   taskID,
			"streaming": "duplex",
		},
		"payload": map[string]interface{}{
			"task_group": "audio",
			"task":       "tts",
			"function":   "SpeechSynthesizer",
			"model":      "cosyvoice-v3-flash",
			"parameters": map[string]interface{}{
				"text_type":   "PlainText",
				"voice":       "longanyang",
				"format":      "mp3",
				"sample_rate": 22050,
				"volume":      50,
				"rate":        1,
				"pitch":       1,
				// 如果enable_ssml設為true,只允許發送一次continue-task指令,否則會報錯“Text request limit violated, expected 1.”
				"enable_ssml": false,
			},
			"input": map[string]interface{}{},
		},
	}

	runTaskJSON, _ := json.Marshal(runTaskCmd)
	fmt.Printf("發送run-task指令: %s\n", string(runTaskJSON))

	err = conn.WriteMessage(websocket.TextMessage, runTaskJSON)
	if err != nil {
		fmt.Println("發送run-task失敗:", err)
		return
	}

	textSent := false

	// 處理訊息
	for {
		messageType, message, err := conn.ReadMessage()
		if err != nil {
			fmt.Println("讀取訊息失敗:", err)
			break
		}

		// 處理二進位訊息
		if messageType == websocket.BinaryMessage {
			fmt.Printf("收到二進位訊息,長度: %d\n", len(message))
			file, _ := os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
			file.Write(message)
			file.Close()
			continue
		}

		// 處理簡訊
		messageStr := string(message)
		fmt.Printf("收到簡訊: %s\n", strings.ReplaceAll(messageStr, "\n", ""))

		// 簡單解析JSON擷取event類型
		var msgMap map[string]interface{}
		if json.Unmarshal(message, &msgMap) == nil {
			if header, ok := msgMap["header"].(map[string]interface{}); ok {
				if event, ok := header["event"].(string); ok {
					fmt.Printf("事件類型: %s\n", event)

					switch event {
					case "task-started":
						fmt.Println("=== 收到task-started事件 ===")

						if !textSent {
							// 發送continue-task指令

							texts := []string{"床前明月光,疑是地上霜。", "舉頭望明月,低頭思故鄉。"}

							for _, text := range texts {
								continueTaskCmd := map[string]interface{}{
									"header": map[string]interface{}{
										"action":    "continue-task",
										"task_id":   taskID,
										"streaming": "duplex",
									},
									"payload": map[string]interface{}{
										"input": map[string]interface{}{
											"text": text,
										},
									},
								}

								continueTaskJSON, _ := json.Marshal(continueTaskCmd)
								fmt.Printf("發送continue-task指令: %s\n", string(continueTaskJSON))

								err = conn.WriteMessage(websocket.TextMessage, continueTaskJSON)
								if err != nil {
									fmt.Println("發送continue-task失敗:", err)
									return
								}
							}

							textSent = true

							// 延遲發送finish-task
							time.Sleep(500 * time.Millisecond)

							// 發送finish-task指令
							finishTaskCmd := map[string]interface{}{
								"header": map[string]interface{}{
									"action":    "finish-task",
									"task_id":   taskID,
									"streaming": "duplex",
								},
								"payload": map[string]interface{}{
									"input": map[string]interface{}{},
								},
							}

							finishTaskJSON, _ := json.Marshal(finishTaskCmd)
							fmt.Printf("發送finish-task指令: %s\n", string(finishTaskJSON))

							err = conn.WriteMessage(websocket.TextMessage, finishTaskJSON)
							if err != nil {
								fmt.Println("發送finish-task失敗:", err)
								return
							}
						}

					case "task-finished":
						fmt.Println("=== 任務完成 ===")
						return

					case "task-failed":
						fmt.Println("=== 任務失敗 ===")
						if header["error_message"] != nil {
							fmt.Printf("錯誤資訊: %s\n", header["error_message"])
						}
						return

					case "result-generated":
						fmt.Println("收到result-generated事件")
					}
				}
			}
		}
	}
}

C#

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

class Program {
    // 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    // 若沒有配置環境變數,請用百鍊API Key將下行替換為:private static readonly string ApiKey = "sk-xxx"
    private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");

    // 以下為新加坡地區url,若使用北京地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/inference/
    private const string WebSocketUrl = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/";
    // 輸出檔案路徑
    private const string OutputFilePath = "output.mp3";

    // WebSocket用戶端
    private static ClientWebSocket _webSocket = new ClientWebSocket();
    // 取消令牌源
    private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
    // 任務ID
    private static string? _taskId;
    // 任務是否已啟動
    private static TaskCompletionSource<bool> _taskStartedTcs = new TaskCompletionSource<bool>();

    static async Task Main(string[] args) {
        try {
            // 清空輸出檔案
            ClearOutputFile(OutputFilePath);

            // 串連WebSocket服務
            await ConnectToWebSocketAsync(WebSocketUrl);

            // 啟動接收訊息的任務
            Task receiveTask = ReceiveMessagesAsync();

            // 發送run-task指令
            _taskId = GenerateTaskId();
            await SendRunTaskCommandAsync(_taskId);

            // 等待task-started事件
            await _taskStartedTcs.Task;

            // 持續發送continue-task指令
            string[] texts = {
                "床前明月光",
                "疑是地上霜",
                "舉頭望明月",
                "低頭思故鄉"
            };
            foreach (string text in texts) {
                await SendContinueTaskCommandAsync(text);
            }

            // 發送finish-task指令
            await SendFinishTaskCommandAsync(_taskId);

            // 等待接收任務完成
            await receiveTask;

            Console.WriteLine("任務完成,串連已關閉。");
        } catch (OperationCanceledException) {
            Console.WriteLine("任務被取消。");
        } catch (Exception ex) {
            Console.WriteLine($"發生錯誤:{ex.Message}");
        } finally {
            _cancellationTokenSource.Cancel();
            _webSocket.Dispose();
        }
    }

    private static void ClearOutputFile(string filePath) {
        if (File.Exists(filePath)) {
            File.WriteAllText(filePath, string.Empty);
            Console.WriteLine("輸出檔案已清空。");
        } else {
            Console.WriteLine("輸出檔案不存在,無需清空。");
        }
    }

    private static async Task ConnectToWebSocketAsync(string url) {
        var uri = new Uri(url);
        if (_webSocket.State == WebSocketState.Connecting || _webSocket.State == WebSocketState.Open) {
            return;
        }

        // 設定WebSocket串連的頭部資訊
        _webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
        _webSocket.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");

        try {
            await _webSocket.ConnectAsync(uri, _cancellationTokenSource.Token);
            Console.WriteLine("已成功串連到WebSocket服務。");
        } catch (OperationCanceledException) {
            Console.WriteLine("WebSocket串連被取消。");
        } catch (Exception ex) {
            Console.WriteLine($"WebSocket串連失敗: {ex.Message}");
            throw;
        }
    }

    private static async Task SendRunTaskCommandAsync(string taskId) {
        var command = CreateCommand("run-task", taskId, "duplex", new {
            task_group = "audio",
            task = "tts",
            function = "SpeechSynthesizer",
            model = "cosyvoice-v3-flash",
            parameters = new
            {
                text_type = "PlainText",
                voice = "longanyang",
                format = "mp3",
                sample_rate = 22050,
                volume = 50,
                rate = 1,
                pitch = 1,
                // 如果enable_ssml設為true,只允許發送一次continue-task指令,否則會報錯“Text request limit violated, expected 1.”
                enable_ssml = false
            },
            input = new { }
        });

        await SendJsonMessageAsync(command);
        Console.WriteLine("已發送run-task指令。");
    }

    private static async Task SendContinueTaskCommandAsync(string text) {
        if (_taskId == null) {
            throw new InvalidOperationException("任務ID未初始化。");
        }

        var command = CreateCommand("continue-task", _taskId, "duplex", new {
            input = new {
                text
            }
        });

        await SendJsonMessageAsync(command);
        Console.WriteLine("已發送continue-task指令。");
    }

    private static async Task SendFinishTaskCommandAsync(string taskId) {
        var command = CreateCommand("finish-task", taskId, "duplex", new {
            input = new { }
        });

        await SendJsonMessageAsync(command);
        Console.WriteLine("已發送finish-task指令。");
    }

    private static async Task SendJsonMessageAsync(string message) {
        var buffer = Encoding.UTF8.GetBytes(message);
        try {
            await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
        } catch (OperationCanceledException) {
            Console.WriteLine("訊息發送被取消。");
        }
    }

    private static async Task ReceiveMessagesAsync() {
        while (_webSocket.State == WebSocketState.Open) {
            var response = await ReceiveMessageAsync();
            if (response != null) {
                var eventStr = response.RootElement.GetProperty("header").GetProperty("event").GetString();
                switch (eventStr) {
                    case "task-started":
                        Console.WriteLine("任務已啟動。");
                        _taskStartedTcs.TrySetResult(true);
                        break;
                    case "task-finished":
                        Console.WriteLine("任務已完成。");
                        _cancellationTokenSource.Cancel();
                        break;
                    case "task-failed":
                        Console.WriteLine("任務失敗:" + response.RootElement.GetProperty("header").GetProperty("error_message").GetString());
                        _cancellationTokenSource.Cancel();
                        break;
                    default:
                        // result-generated可在此處理
                        break;
                }
            }
        }
    }

    private static async Task<JsonDocument?> ReceiveMessageAsync() {
        var buffer = new byte[1024 * 4];
        var segment = new ArraySegment<byte>(buffer);

        try {
            WebSocketReceiveResult result = await _webSocket.ReceiveAsync(segment, _cancellationTokenSource.Token);

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

            if (result.MessageType == WebSocketMessageType.Binary) {
                // 處理位元據
                Console.WriteLine("接收到位元據...");

                // 將位元據儲存到檔案
                using (var fileStream = new FileStream(OutputFilePath, FileMode.Append)) {
                    fileStream.Write(buffer, 0, result.Count);
                }

                return null;
            }

            string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
            return JsonDocument.Parse(message);
        } catch (OperationCanceledException) {
            Console.WriteLine("訊息接收被取消。");
            return null;
        }
    }

    private static string GenerateTaskId() {
        return Guid.NewGuid().ToString("N").Substring(0, 32);
    }

    private static string CreateCommand(string action, string taskId, string streaming, object payload) {
        var command = new {
            header = new {
                action,
                task_id = taskId,
                streaming
            },
            payload
        };

        return JsonSerializer.Serialize(command);
    }
}

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;

// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用百鍊API Key將下行替換為:$api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// 以下為新加坡地區url,若使用北京地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/inference/
$websocket_url = 'wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/'; // WebSocket伺服器位址
$output_file = 'output.mp3'; // 輸出檔案路徑

$loop = Loop::get();

if (file_exists($output_file)) {
    // 清空檔案內容
    file_put_contents($output_file, '');
}

// 建立自訂的連接器
$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,
    'X-DashScope-DataInspection' => 'enable'
];

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

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

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

    // 定義發送 continue-task 指令的函數
    $sendContinueTask = function() use ($conn, $loop, $taskId) {
        // 待發送的文本
        $texts = ["床前明月光", "疑是地上霜", "舉頭望明月", "低頭思故鄉"];
        $continueTaskCount = 0;
        foreach ($texts as $text) {
            $continueTaskMessage = json_encode([
                "header" => [
                    "action" => "continue-task",
                    "task_id" => $taskId,
                    "streaming" => "duplex"
                ],
                "payload" => [
                    "input" => [
                        "text" => $text
                    ]
                ]
            ]);
            echo "準備發送continue-task指令: " . $continueTaskMessage . "\n";
            $conn->send($continueTaskMessage);
            $continueTaskCount++;
        }
        echo "發送的continue-task指令個數為:" . $continueTaskCount . "\n";

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

    // 標記是否收到 task-started 事件
    $taskStarted = false;

    // 監聽訊息
    $conn->on('message', function($msg) use ($conn, $sendContinueTask, $loop, &$taskStarted, $taskId, $output_file) {
        if ($msg->isBinary()) {
            // 寫入位元據到本地檔案
            file_put_contents($output_file, $msg->getPayload(), FILE_APPEND);
        } else {
            // 處理非二進位訊息
            $response = json_decode($msg, true);

            if (isset($response['header']['event'])) {
                handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, $taskStarted);
            } 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";
        }
    });
}, 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" => "tts",
            "function" => "SpeechSynthesizer",
            "model" => "cosyvoice-v3-flash",
            "parameters" => [
                "text_type" => "PlainText",
                "voice" => "longanyang",
                "format" => "mp3",
                "sample_rate" => 22050,
                "volume" => 50,
                "rate" => 1,
                "pitch" => 1,
                // 如果enable_ssml設為true,只允許發送一次continue-task指令,否則會報錯“Text request limit violated, expected 1.”
                "enable_ssml" => false
            ],
            "input" => (object) []
        ]
    ]);
    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" => (object) []
        ]
    ]);
    echo "準備發送finish-task指令: " . $finishTaskMessage . "\n";
    $conn->send($finishTaskMessage);
    echo "finish-task指令已發送\n";
}

/**
 * 處理事件
 * @param $conn
 * @param $response
 * @param $sendContinueTask
 * @param $loop
 * @param $taskId
 * @param $taskStarted
 */
function handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, &$taskStarted) {
    switch ($response['header']['event']) {
        case 'task-started':
            echo "任務開始,發送continue-task指令...\n";
            $taskStarted = true;
            // 發送 continue-task 指令
            $sendContinueTask();
            break;
        case 'result-generated':
            // 收到result-generated事件
            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 ($response['header']['event'] == 'task-finished') {
        // 等待1秒以確保所有資料都已傳輸完畢
        $loop->addTimer(1, function() use ($conn) {
            $conn->close();
            echo "用戶端關閉串連\n";
        });
    }

    // 如果沒有收到 task-started 事件,關閉串連
    if (!$taskStarted && in_array($response['header']['event'], ['task-failed', 'error'])) {
        $conn->close();
    }
}

Node.js

需安裝相關依賴:

npm install ws
npm install uuid

範例程式碼如下:

const WebSocket = require('ws');
const fs = require('fs');
const uuid = require('uuid').v4;

// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用百鍊API Key將下行替換為:const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// 以下為新加坡地區url,若使用北京地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/inference/
const url = 'wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/';
// 輸出檔案路徑
const outputFilePath = 'output.mp3';

// 清空輸出檔案
fs.writeFileSync(outputFilePath, '');

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

let taskStarted = false;
let taskId = uuid();

ws.on('open', () => {
  console.log('已串連到WebSocket伺服器');

  // 發送run-task指令
  const runTaskMessage = JSON.stringify({
    header: {
      action: 'run-task',
      task_id: taskId,
      streaming: 'duplex'
    },
    payload: {
      task_group: 'audio',
      task: 'tts',
      function: 'SpeechSynthesizer',
      model: 'cosyvoice-v3-flash',
      parameters: {
        text_type: 'PlainText',
        voice: 'longanyang', // 音色
        format: 'mp3', // 音頻格式
        sample_rate: 22050, // 採樣率
        volume: 50, // 音量
        rate: 1, // 語速
        pitch: 1, // 音調
        enable_ssml: false // 是否開啟SSML功能。如果enable_ssml設為true,只允許發送一次continue-task指令,否則會報錯“Text request limit violated, expected 1.”
      },
      input: {}
    }
  });
  ws.send(runTaskMessage);
  console.log('已發送run-task訊息');
});

const fileStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
ws.on('message', (data, isBinary) => {
  if (isBinary) {
    // 寫入位元據到檔案
    fileStream.write(data);
  } else {
    const message = JSON.parse(data);

    switch (message.header.event) {
      case 'task-started':
        taskStarted = true;
        console.log('任務已開始');
        // 發送continue-task指令
        sendContinueTasks(ws);
        break;
      case 'task-finished':
        console.log('任務已完成');
        ws.close();
        fileStream.end(() => {
          console.log('檔案流已關閉');
        });
        break;
      case 'task-failed':
        console.error('任務失敗:', message.header.error_message);
        ws.close();
        fileStream.end(() => {
          console.log('檔案流已關閉');
        });
        break;
      default:
        // 可以在這裡處理result-generated
        break;
    }
  }
});

function sendContinueTasks(ws) {
  const texts = [
    '床前明月光,',
    '疑是地上霜。',
    '舉頭望明月,',
    '低頭思故鄉。'
  ];

  texts.forEach((text, index) => {
    setTimeout(() => {
      if (taskStarted) {
        const continueTaskMessage = JSON.stringify({
          header: {
            action: 'continue-task',
            task_id: taskId,
            streaming: 'duplex'
          },
          payload: {
            input: {
              text: text
            }
          }
        });
        ws.send(continueTaskMessage);
        console.log(`已發送continue-task,文本:${text}`);
      }
    }, index * 1000); // 每隔1秒發送一次
  });

  // 發送finish-task指令
  setTimeout(() => {
    if (taskStarted) {
      const finishTaskMessage = JSON.stringify({
        header: {
          action: 'finish-task',
          task_id: taskId,
          streaming: 'duplex'
        },
        payload: {
          input: {}
        }
      });
      ws.send(finishTaskMessage);
      console.log('已發送finish-task');
    }
  }, texts.length * 1000 + 1000); // 在所有continue-task指令發送完畢後1秒發送
}

ws.on('close', () => {
  console.log('已斷開與WebSocket伺服器的串連');
});

Java

如您使用Java程式設計語言,建議採用Java DashScope SDK進行開發,詳情請參見Java SDK

以下是Java WebSocket的調用樣本。在運行樣本前,請確保已匯入以下依賴:

  • Java-WebSocket

  • jackson-databind

推薦您使用Maven或Gradle管理依賴包,其配置如下:

pom.xml

<dependencies>
    <!-- WebSocket Client -->
    <dependency>
        <groupId>org.java-websocket</groupId>
        <artifactId>Java-WebSocket</artifactId>
        <version>1.5.3</version>
    </dependency>

    <!-- JSON Processing -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
        <version>2.13.0</version>
    </dependency>
</dependencies>

build.gradle

// 省略其它代碼
dependencies {
  // WebSocket Client
  implementation 'org.java-websocket:Java-WebSocket:1.5.3'
  // JSON Processing
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}
// 省略其它代碼

Java代碼如下:

import com.fasterxml.jackson.databind.ObjectMapper;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;

import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.*;

public class TTSWebSocketClient extends WebSocketClient {
    private final String taskId = UUID.randomUUID().toString();
    private final String outputFile = "output_" + System.currentTimeMillis() + ".mp3";
    private boolean taskFinished = false;

    public TTSWebSocketClient(URI serverUri, Map<String, String> headers) {
        super(serverUri, headers);
    }

    @Override
    public void onOpen(ServerHandshake serverHandshake) {
        System.out.println("串連成功");

        // 發送run-task指令
        // 如果enable_ssml設為true,只允許發送一次continue-task指令,否則會報錯“Text request limit violated, expected 1.”
        String runTaskCommand = "{ \"header\": { \"action\": \"run-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"task_group\": \"audio\", \"task\": \"tts\", \"function\": \"SpeechSynthesizer\", \"model\": \"cosyvoice-v3-flash\", \"parameters\": { \"text_type\": \"PlainText\", \"voice\": \"longanyang\", \"format\": \"mp3\", \"sample_rate\": 22050, \"volume\": 50, \"rate\": 1, \"pitch\": 1, \"enable_ssml\": false }, \"input\": {} }}";
        send(runTaskCommand);
    }

    @Override
    public void onMessage(String message) {
        System.out.println("收到服務端返回的訊息:" + message);
        try {
            // Parse JSON message
            Map<String, Object> messageMap = new ObjectMapper().readValue(message, Map.class);

            if (messageMap.containsKey("header")) {
                Map<String, Object> header = (Map<String, Object>) messageMap.get("header");

                if (header.containsKey("event")) {
                    String event = (String) header.get("event");

                    if ("task-started".equals(event)) {
                        System.out.println("收到服務端返回的task-started事件");

                        List<String> texts = Arrays.asList(
                                "床前明月光,疑是地上霜",
                                "舉頭望明月,低頭思故鄉"
                        );

                        for (String text : texts) {
                            // 發送continue-task指令
                            sendContinueTask(text);
                        }

                        // 發送finish-task指令
                        sendFinishTask();
                    } else if ("task-finished".equals(event)) {
                        System.out.println("收到服務端返回的task-finished事件");
                        taskFinished = true;
                        closeConnection();
                    } else if ("task-failed".equals(event)) {
                        System.out.println("任務失敗:" + message);
                        closeConnection();
                    }
                }
            }
        } catch (Exception e) {
            System.err.println("出現異常:" + e.getMessage());
        }
    }

    @Override
    public void onMessage(ByteBuffer message) {
        System.out.println("收到的二進位音頻資料大小為:" + message.remaining());

        try (FileOutputStream fos = new FileOutputStream(outputFile, true)) {
            byte[] buffer = new byte[message.remaining()];
            message.get(buffer);
            fos.write(buffer);
            System.out.println("音頻資料已寫入本地檔案" + outputFile + "中");
        } catch (IOException e) {
            System.err.println("音頻資料寫入本地檔案失敗:" + e.getMessage());
        }
    }

    @Override
    public void onClose(int code, String reason, boolean remote) {
        System.out.println("串連關閉:" + reason + " (" + code + ")");
    }

    @Override
    public void onError(Exception ex) {
        System.err.println("報錯:" + ex.getMessage());
        ex.printStackTrace();
    }

    private void sendContinueTask(String text) {
        String command = "{ \"header\": { \"action\": \"continue-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": { \"text\": \"" + text + "\" } }}";
        send(command);
    }

    private void sendFinishTask() {
        String command = "{ \"header\": { \"action\": \"finish-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": {} }}";
        send(command);
    }

    private void closeConnection() {
        if (!isClosed()) {
            close();
        }
    }

    public static void main(String[] args) {
        try {
            // 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
            // 若沒有配置環境變數,請用百鍊API Key將下行替換為:String apiKey = "sk-xxx"
            String apiKey = System.getenv("DASHSCOPE_API_KEY");
            if (apiKey == null || apiKey.isEmpty()) {
                System.err.println("請設定 DASHSCOPE_API_KEY 環境變數");
                return;
            }

            Map<String, String> headers = new HashMap<>();
            headers.put("Authorization", "bearer " + apiKey);
            // 以下為新加坡地區url,若使用北京地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/inference/
            TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/"), headers);

            client.connect();

            while (!client.isClosed() && !client.taskFinished) {
                Thread.sleep(1000);
            }
        } catch (Exception e) {
            System.err.println("串連WebSocket服務失敗:" + e.getMessage());
            e.printStackTrace();
        }
    }
}

Python

如您使用Python程式設計語言,建議採用Python DashScope SDK進行開發,詳情請參見Python SDK

以下是Python WebSocket的調用樣本。在運行樣本前,請確保通過如下方式匯入依賴:

pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client
重要

請不要將運行範例程式碼的Python檔案命名為“websocket.py”,否則會報錯(AttributeError: module 'websocket' has no attribute 'WebSocketApp'. Did you mean: 'WebSocket'?)。

import websocket
import json
import uuid
import os
import time


class TTSClient:
    def __init__(self, api_key, uri):
        """
    初始化 TTSClient 執行個體

    參數:
        api_key (str): 鑒權用的 API Key
        uri (str): WebSocket 服務地址
    """
        self.api_key = api_key  # 替換為你的 API Key
        self.uri = uri  # 替換為你的 WebSocket 地址
        self.task_id = str(uuid.uuid4())  # 產生唯一任務 ID
        self.output_file = f"output_{int(time.time())}.mp3"  # 輸出音頻檔案路徑
        self.ws = None  # WebSocketApp 執行個體
        self.task_started = False  # 是否收到 task-started
        self.task_finished = False  # 是否收到 task-finished / task-failed

    def on_open(self, ws):
        """
    WebSocket 串連建立時回呼函數
    發送 run-task 指令開啟語音合成任務
    """
        print("WebSocket 已串連")

        # 構造 run-task 指令
        run_task_cmd = {
            "header": {
                "action": "run-task",
                "task_id": self.task_id,
                "streaming": "duplex"
            },
            "payload": {
                "task_group": "audio",
                "task": "tts",
                "function": "SpeechSynthesizer",
                "model": "cosyvoice-v3-flash",
                "parameters": {
                    "text_type": "PlainText",
                    "voice": "longanyang",
                    "format": "mp3",
                    "sample_rate": 22050,
                    "volume": 50,
                    "rate": 1,
                    "pitch": 1,
                    # 如果enable_ssml設為True,只允許發送一次continue-task指令,否則會報錯“Text request limit violated, expected 1.”
                    "enable_ssml": False
                },
                "input": {}
            }
        }

        # 發送 run-task 指令
        ws.send(json.dumps(run_task_cmd))
        print("已發送 run-task 指令")

    def on_message(self, ws, message):
        """
    接收到訊息時的回呼函數
    區分文本和二進位訊息處理
    """
        if isinstance(message, str):
            # 處理 JSON 簡訊
            try:
                msg_json = json.loads(message)
                print(f"收到 JSON 訊息: {msg_json}")

                if "header" in msg_json:
                    header = msg_json["header"]

                    if "event" in header:
                        event = header["event"]

                        if event == "task-started":
                            print("任務已啟動")
                            self.task_started = True

                            # 發送 continue-task 指令
                            texts = [
                                "床前明月光,疑是地上霜",
                                "舉頭望明月,低頭思故鄉"
                            ]

                            for text in texts:
                                self.send_continue_task(text)

                            # 所有 continue-task 發送完成後發送 finish-task
                            self.send_finish_task()

                        elif event == "task-finished":
                            print("任務已完成")
                            self.task_finished = True
                            self.close(ws)

                        elif event == "task-failed":
                            error_msg = msg_json.get("error_message", "未知錯誤")
                            print(f"任務失敗: {error_msg}")
                            self.task_finished = True
                            self.close(ws)

            except json.JSONDecodeError as e:
                print(f"JSON 解析失敗: {e}")
        else:
            # 處理二進位訊息(音頻資料)
            print(f"收到二進位訊息,大小: {len(message)} 位元組")
            with open(self.output_file, "ab") as f:
                f.write(message)
            print(f"已將音頻資料寫入本地檔案{self.output_file}中")

    def on_error(self, ws, error):
        """發生錯誤時的回調"""
        print(f"WebSocket 出錯: {error}")

    def on_close(self, ws, close_status_code, close_msg):
        """串連關閉時的回調"""
        print(f"WebSocket 已關閉: {close_msg} ({close_status_code})")

    def send_continue_task(self, text):
        """發送 continue-task 指令,附帶要合成的常值內容"""
        cmd = {
            "header": {
                "action": "continue-task",
                "task_id": self.task_id,
                "streaming": "duplex"
            },
            "payload": {
                "input": {
                    "text": text
                }
            }
        }

        self.ws.send(json.dumps(cmd))
        print(f"已發送 continue-task 指令,常值內容: {text}")

    def send_finish_task(self):
        """發送 finish-task 指令,結束語音合成任務"""
        cmd = {
            "header": {
                "action": "finish-task",
                "task_id": self.task_id,
                "streaming": "duplex"
            },
            "payload": {
                "input": {}
            }
        }

        self.ws.send(json.dumps(cmd))
        print("已發送 finish-task 指令")

    def close(self, ws):
        """主動關閉串連"""
        if ws and ws.sock and ws.sock.connected:
            ws.close()
            print("已主動關閉串連")

    def run(self):
        """啟動 WebSocket 用戶端"""
        # 佈建要求頭部(鑒權)
        header = {
            "Authorization": f"bearer {self.api_key}",
            "X-DashScope-DataInspection": "enable"
        }

        # 建立 WebSocketApp 執行個體
        self.ws = websocket.WebSocketApp(
            self.uri,
            header=header,
            on_open=self.on_open,
            on_message=self.on_message,
            on_error=self.on_error,
            on_close=self.on_close
        )

        print("正在監聽 WebSocket 訊息...")
        self.ws.run_forever()  # 啟動長串連監聽


# 樣本使用方式
if __name__ == "__main__":
    # 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # 若沒有配置環境變數,請用百鍊API Key將下行替換為:API_KEY = "sk-xxx"
    API_KEY = os.environ.get("DASHSCOPE_API_KEY")
    # 以下為新加坡地區url,若使用北京地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/inference/
    SERVER_URI = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/"  # 替換為你的 WebSocket 地址

    client = TTSClient(API_KEY, SERVER_URI)
    client.run()

錯誤碼

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

常見問題

功能特性/計量計費/限流

Q:當遇到發音不準的情況時,有什麼解決方案可以嘗試?

通過SSML可以對語音合成效果進行個人化定製。

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

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

Q:語音合成是按文本字元數計費的,要如何查看或擷取每次合成的文本長度?

通過服務端返回的result-generated事件payload.usage.characters參數擷取字元數,請以收到的最後一個result-generated事件為準。

故障排查

重要

代碼報錯時,建議您檢查發送至服務端的指令是否正確:可以通過列印指令內容,檢查是否存在格式有誤或必填參數遺漏的情況。如指令正確,請根據錯誤碼中的資訊進行排查。

Q:如何擷取Request ID

通過以下兩種方式可以擷取:

Q:使用SSML功能失敗是什麼原因?

請按以下步驟排查:

  1. 確保限制與約束正確

  2. 確保用正確的方式進行調用,詳情請參見SSML標記語言支援說明

  3. 確保待合成文本為純文字格式且符合格式要求,詳情請參見SSML標記語言介紹

Q:為什麼音頻無法播放?

請根據以下情境逐一排查:

  1. 音頻儲存為完整檔案(如xx.mp3)的情況

    1. 音頻格式一致性:確保請求參數中設定的音頻格式與檔案尾碼一致。例如,如果請求參數設定的音頻格式為wav,但檔案尾碼為mp3,可能會導致播放失敗。

    2. 播放器相容性:確認使用的播放器是否支援該音頻檔案的格式和採樣率。例如,某些播放器可能不支援高採樣率或特定編碼的音頻檔案。

  2. 流式播放音訊情況

    1. 將音頻流儲存為完整檔案,嘗試使用播放器播放。如果檔案無法播放,請參考情境 1 的排查方法。

    2. 如果檔案可以正常播放,則問題可能出在流式播放的實現上。請確認使用的播放器是否支援流式播放。

      常見的支援流式播放的工具和庫包括:ffmpeg、pyaudio (Python)、AudioFormat (Java)、MediaSource (Javascript)等。

Q:為什麼音頻播放卡頓?

請根據以下情境逐一排查:

  1. 檢查文本發送速度: 確保發送文本的間隔合理,避免前一句音頻播放完畢後,下一句文本未能及時發送。

  2. 檢查回呼函數效能:

    • 檢查回呼函數中是否存在過多商務邏輯,導致阻塞。

    • 回呼函數運行在 WebSocket 線程中,若被阻塞,可能會影響 WebSocket 接收網路資料包,進而導致音頻接收卡頓。

    • 建議將音頻資料寫入一個獨立的音頻緩衝區(audio buffer),然後在其他線程中讀取並處理,避免阻塞 WebSocket 線程。

  3. 檢查網路穩定性: 確保網路連接穩定,避免因網路波動導致音頻傳輸中斷或延遲。

Q:語音合成慢(合成時間長)是什麼原因?

請按以下步驟排查:

  1. 檢查輸入間隔

    如果是流式語音合成,請確認文字發送間隔是否過長(如上段發出後延遲數秒才發送下段),過久間隔會導致合成總時間長度增加。

  2. 分析效能指標

    • 首包延遲:正常500ms左右。

    • RTF(RTF = 合成總耗時/音頻時間長度):正常小於1.0。

Q:合成的語音發音錯誤如何處理?

請使用SSML的<phoneme>標籤指定正確的發音。

Q:為什麼沒有返回語音?為什麼結尾部分的文本沒能成功轉換成語音?(合成語音缺失)

請確認是否忘記發送finish-task指令。在語音合成過程中,服務端會在緩衝足夠文本後才開始合成。如果忘記發送finish-task指令,可能會導致緩衝中的結尾部分文本未能被合成為語音。

Q:為什麼返回的音頻流順序錯亂?導致播放內容混亂

請從以下兩個方面排查:

Q:WebSocket 串連錯誤如何處理?

  • WebSocket 串連關閉(code 1007)如何處理?

    WebSocket 串連在發送 run-task 指令後立即關閉,且關閉碼為 1007。

    • 錯誤原因:服務端檢測到協議或資料格式錯誤,主動中斷連線。常見原因包括:

      • run-task 指令的 payload 中包含非法欄位(如在 payload 中誤加了 "input": {} 以外的其他欄位)。

      • JSON 格式錯誤(如缺少逗號、括弧不匹配等)。

      • 必要欄位缺失(如 task_id、action 等)。

    • 解決方案

      1. 檢查 JSON 格式:驗證請求體格式是否正確。

      2. 檢查必要欄位:確認 header.action、header.task_id、header.streaming、payload.task_group、payload.task、payload.function、payload.model、payload.input 均已正確設定。

      3. 移除無效欄位:run-task 的 payload.input 中僅允許Null 物件 {} 或包含 text 欄位,不要添加其他欄位。

  • WebSocket 串連報錯 WebSocketBadStatus 或 401/403 如何處理?

    在建立 WebSocket 串連時報錯 WebSocketBadStatus、401 Unauthorized 或 403 Forbidden。

    • 錯誤原因: 鑒權失敗。服務端在 WebSocket 握手階段驗證 Authorization 頭,如果 API Key 無效或缺失,將拒絕串連。

    • 解決方案:參見鑒權失敗排查步驟

許可權與認證

Q:我希望我的 API Key 僅用於 CosyVoice 語音合成服務,而不被百鍊其他模型使用(許可權隔離),我該如何做

可以通過建立業務空間並只授權特定模型來限制API Key的使用範圍。詳情請參見業務空間管理

更多問題

請參見GitHub QA