全部產品
Search
文件中心

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

更新時間:Dec 18, 2025
重要

如需使用“中國大陸(北京)”地區的模型,請前往“中國大陸(北京)”地區的API-KEY頁面

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

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

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

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 即可。

模型與價格

模型名稱

單價

cosyvoice-v3-plus

$0.286706/萬字元

cosyvoice-v3-flash

$0.14335/萬字元

cosyvoice-v2

$0.286706/萬字元

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

文本長度限制

單次通過continue-task指令發送的待合成文本長度不得超過 2000 字元,多次調用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(Speech Synthesis Markup Language,語音合成標記語言)功能僅適用於cosyvoice-v3-flash、cosyvoice-v3-plus和cosyvoice-v2模型的複刻音色,以及音色列表中標記為支援的系統音色,使用時需滿足以下條件:

使用方式如下:

  1. 在發送run-task指令時,將參數enable_ssml設定為true,以開啟SSML支援;

  2. 隨後通過continue-task指令發送包含SSML的文本。

重要

開啟 SSML 支援(即將 enable_ssml 參數設為 true)後,僅允許通過一次continue-task指令提交完整的待合成文本,不支援多次發送。

互動流程

image

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

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

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

  2. 開啟任務:

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

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

  3. 發送待合成文本:

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

    說明

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

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

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

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

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

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

  5. 任務結束:

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

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

URL

WebSocket URL固定如下:

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

Headers

要求標頭中需添加如下資訊:

{
    "Authorization": "bearer <your_dashscope_api_key>", // 必選 將<your_dashscope_api_key>替換成您自己的API Key
    "user-agent": "your_platform_info", // 可選
    "X-DashScope-WorkSpace": workspace, // 可選,阿里雲百鍊業務空間ID
    "X-DashScope-DataInspection": "enable"
}

指令(用戶端→服務端)

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

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

  1. 發送 run-task指令

  2. 發送continue-task指令

    • 用於發送待合成文本。

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

  3. 發送finish-task指令

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

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

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

  • 不要發送待合成文本:此處發送合成文本不利於問題排查,因此應避免在此發送文本。

樣本:

{
    "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

  • 如果不在此時發送待合成文本,input格式為:

    "input": {}
  • 如果在此時發送待合成文本,input格式為:

    "input": {
      "text": "今天天氣怎麼樣?" // 待合成文本
    }

payload.parameters

text_type

string

固定字串:“PlainText”。

voice

string

語音合成所使用的音色。

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

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

  • 複刻音色:通過聲音複刻功能定製。使用複刻音色時,請確保聲音複刻與語音合成使用同一帳號。詳細操作步驟請參見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]

提供語言提示,僅cosyvoice-v3-flash、cosyvoice-v3-plus支援該功能。

無預設值,不設定不生效。

在語音合成中有如下作用:

  1. 指定 TN(Text Normalization,文本正常化)處理所用的語言,影響數字、縮寫、符號等的朗讀方式(僅中文、英文生效)。

    取值範圍:

    • zh:中文

    • en:英文

  2. 指定語音合成的目標語言(僅限複刻音色),協助提升合成效果準確性,對英文、法語、德語、日語、韓語、俄語生效(無需填寫中文)。須和聲音複刻時使用的languageHints/language_hints一致。

    取值範圍:

    • en:英文

    • fr:法語

    • de:德語

    • ja:日語

    • ko:韓語

    • ru:俄語

若設定的語言提示與常值內容明顯不符(如為中文文本設定en),將忽略此提示,並依據常值內容自動檢測語言。

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

instruction

string

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

無預設值,不設定不生效。

在語音合成中有如下作用:

  1. 指定小語種(僅限複刻音色)

    • 格式:“你會用<小語種>說出來。”(注意,結尾一定不要遺漏句號,使用時將“<小語種>”替換為具體的小語種,例如替換為德語)。

    • 樣本:“你會用德語說出來。

    • 支援的小語種:法語、德語、日語、韓語、俄語。

  2. 指定方言(僅限複刻音色)

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

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

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

  3. 指定情感、情境、角色或身份等:僅部分系統音色支援該功能,且因音色而異,詳情請參見音色列表

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指令:結束任務

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

請務必確保發送該指令,否則可能會出現合成語音缺失的問題。

該指令發送後,服務端會將剩餘的文本轉成語音,語音合成完成後,服務端向用戶端返回task-finished事件

重要

發送時機: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事件。

在CosyVoice服務中,result-generated事件為協議預留介面,封裝了Request ID等資訊,現階段可以忽略。

樣本:

{
    "header": {
        "task_id": "2bf83b9a-baeb-4fda-8d9a-xxxxxxxxxxxx",
        "event": "result-generated",
        "attributes": {
            "request_uuid": "0a9dba9e-d3a6-45a4-be6d-xxxxxxxxxxxx"
        }
    },
    "payload": {}
}

header參數說明:

參數

類型

說明

header.event

string

事件類型。

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

header.task_id

string

用戶端產生的task_id。

header.attributes.request_uuid

string

Request ID。

payload參數說明:

參數

類型

說明

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開啟字層級時間戳記後會返回時間戳記資訊,樣本如下:

點擊查看開啟字層級時間戳記後的響應

{
    "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}
    }
}

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

具體報錯原因。

關於建連開銷和串連複用

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

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

    監聽事件

    處理二進位音頻流:服務端通過 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"
	"time"

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

const (
	wsURL      = "wss://dashscope.aliyuncs.com/api-ws/v1/inference/" // WebSocket服務端地址
	outputFile = "output.mp3"                                        // 輸出檔案路徑
)

func main() {
	// 若沒有將API Key配置到環境變數,可將下行替換為:apiKey := "your_api_key"。不建議在生產環境中直接將API Key寫入程式碼到代碼中,以減少API Key泄露風險。
	apiKey := os.Getenv("DASHSCOPE_API_KEY")
	// 檢查並清空輸出檔案
	if err := clearOutputFile(outputFile); err != nil {
		fmt.Println("清空輸出檔案失敗:", err)
		return
	}

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

	// 啟動一個goroutine來接收結果
	done, taskStarted := startResultReceiver(conn)

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

	// 等待task-started事件
	for !*taskStarted {
		time.Sleep(100 * time.Millisecond)
	}

	// 發送待合成文本
	if err := sendContinueTaskCmd(conn, taskID); err != nil {
		fmt.Println("發送待合成文本失敗:", err)
		return
	}

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

	// 等待接收結果的goroutine完成
	<-done
}

var dialer = websocket.DefaultDialer

// 定義結構體來表示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 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"`
}

type Params struct {
	TextType   string `json:"text_type"`
	Voice      string `json:"voice"`
	Format     string `json:"format"`
	SampleRate int    `json:"sample_rate"`
	Volume     int    `json:"volume"`
	Rate       int    `json:"rate"`
	Pitch      int    `json:"pitch"`
	EnableSSML bool   `json:"enable_ssml"`
}

type Input struct {
	Text string `json:"text"`
}

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("X-DashScope-DataInspection", "enable")
	header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
	conn, _, err := dialer.Dial(wsURL, header)
	if err != nil {
		fmt.Println("串連WebSocket失敗:", err)
		return nil, err
	}
	return conn, nil
}

// 發送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:      "tts",
			Function:  "SpeechSynthesizer",
			Model:     "cosyvoice-v3-flash",
			Parameters: Params{
				TextType:   "PlainText",
				Voice:      "longanyang",
				Format:     "mp3",
				SampleRate: 22050,
				Volume:     50,
				Rate:       1,
				Pitch:      1,
				// 如果enable_ssml設為true,只允許發送一次continue-task指令,否則會報錯“Text request limit violated, expected 1.”
				EnableSSML: false,
			},
			Input: Input{},
		},
	}
	runTaskCmdJSON, err := json.Marshal(runTaskCmd)
	return string(runTaskCmdJSON), taskID, err
}

// 發送待合成文本
func sendContinueTaskCmd(conn *websocket.Conn, taskID string) error {
	texts := []string{"床前明月光", "疑是地上霜", "舉頭望明月", "低頭思故鄉"}

	for _, text := range texts {
		runTaskCmd, err := generateContinueTaskCmd(text, taskID)
		if err != nil {
			return err
		}

		err = conn.WriteMessage(websocket.TextMessage, []byte(runTaskCmd))
		if err != nil {
			return err
		}
	}

	return nil
}

// 產生continue-task指令
func generateContinueTaskCmd(text string, taskID string) (string, error) {
	runTaskCmd := Event{
		Header: Header{
			Action:    "continue-task",
			TaskID:    taskID,
			Streaming: "duplex",
		},
		Payload: Payload{
			Input: Input{
				Text: text,
			},
		},
	}
	runTaskCmdJSON, err := json.Marshal(runTaskCmd)
	return string(runTaskCmdJSON), err
}

// 啟動一個goroutine來接收結果
func startResultReceiver(conn *websocket.Conn) (chan struct{}, *bool) {
	done := make(chan struct{})
	taskStarted := new(bool)
	*taskStarted = false

	go func() {
		defer close(done)
		for {
			msgType, message, err := conn.ReadMessage()
			if err != nil {
				fmt.Println("解析伺服器訊息失敗:", err)
				return
			}

			if msgType == websocket.BinaryMessage {
				// 處理二進位音頻流
				if err := writeBinaryDataToFile(message, outputFile); err != nil {
					fmt.Println("寫入位元據失敗:", err)
					return
				}
			} else {
				// 處理簡訊
				var event Event
				err = json.Unmarshal(message, &event)
				if err != nil {
					fmt.Println("解析事件失敗:", err)
					continue
				}
				if handleEvent(conn, event, taskStarted) {
					return
				}
			}
		}
	}()

	return done, taskStarted
}

// 處理事件
func handleEvent(conn *websocket.Conn, event Event, taskStarted *bool) bool {
	switch event.Header.Event {
	case "task-started":
		fmt.Println("收到task-started事件")
		*taskStarted = true
	case "result-generated":
		// 忽略result-generated事件
		return false
	case "task-finished":
		fmt.Println("任務完成")
		return true
	case "task-failed":
		handleTaskFailed(event, conn)
		return true
	default:
		fmt.Printf("預料之外的事件:%v\n", event)
	}
	return false
}

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

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

// 寫入位元據到檔案
func writeBinaryDataToFile(data []byte, filePath string) error {
	file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer file.Close()

	_, err = file.Write(data)
	if err != nil {
		return err
	}

	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 clearOutputFile(filePath string) error {
	file, err := os.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	file.Close()
	return nil
}

C#

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

class Program {
    // 若沒有將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 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="your_api_key"。不建議在生產環境中直接將API Key寫入程式碼到代碼中,以減少API Key泄露風險。
$api_key = getenv("DASHSCOPE_API_KEY");
$websocket_url = 'wss://dashscope.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配置到環境變數,可將下行替換為:apiKey = 'your_api_key'。不建議在生產環境中直接將API Key寫入程式碼到代碼中,以減少API Key泄露風險。
const apiKey = process.env.DASHSCOPE_API_KEY;
// WebSocket伺服器位址
const url = 'wss://dashscope.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 {
            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);
            TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://dashscope.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 = os.environ.get("DASHSCOPE_API_KEY")  # 如您未將API Key配置到環境變數,請將API_KEY 設定為您的 API Key
    SERVER_URI = "wss://dashscope.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:我希望我的 API Key 僅用於 CosyVoice 語音合成服務,而不被百鍊其他模型使用(許可權隔離),我該如何做

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

更多問題

請參見GitHub QA