Skip to main content

CosyVoice WebSocket API參考

本文介紹通過WebSocket串連訪問CosyVoice即時語音合成服務的互動流程、介面地址和要求標頭。

介面地址

WebSocket 接口使用 wss:// 协议,地址如下:
  • 新加坡
  • 華北2(北京)
wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference調用時請將{WorkspaceId}替換為真實的Workspace ID。
阿里雲百鍊為華北2(北京)、新加坡地區推出了業務空間專屬網域名稱,能夠為推理請求提供卓越的效能和更高的穩定性,建議遷移至新網域名稱:
  • 華北2(北京)地區:從 dashscope.aliyuncs.com 遷移至 {WorkspaceId}.cn-beijing.maas.aliyuncs.com
  • 新加坡地區:從 dashscope-intl.aliyuncs.com 遷移至 {WorkspaceId}.ap-southeast-1.maas.aliyuncs.com

要求標頭

要求標頭中需添加如下資訊:
參數類型是否必選說明
Authorizationstring是鑒權令牌,格式為 Bearer <your_api_key>,將 <your_api_key> 替換為實際的 API Key。
user-agentstring否用戶端標識,便於服務端追蹤來源。
X-DashScope-WorkSpacestring否阿里雲百鍊業務空間ID。
X-DashScope-DataInspectionstring否是否啟用資料合規檢測功能。預設不傳或設為enable。如非必要,請勿啟用該參數。
Authorization 鑒權在 WebSocket 握手階段驗證。如果 API Key 無效或缺失,握手將失敗並返回 HTTP 401/403 錯誤。

互動流程

用戶端事件和服務端事件的詳細說明,請參見用戶端事件和服務端事件。 按時間順序,用戶端與服務端的互動流程如下:
  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 串連處理多個任務,而非為每個任務建立新串連。
同一次合成任務中,run-task、所有 continue-task、finish-task 必須使用相同的 task_id。每次發起新任務時產生新的 task_id(如使用 UUID)。使用不同 task_id 會導致音頻錯亂或任務失敗。

範例程式碼

以下範例使用通用 WebSocket 程式庫直接傳送協議事件,使用 cosyvoice-v3-flash 和音色 longanyang。先取得 API Key並設定 DASHSCOPE_API_KEY 環境變數,將 {WorkspaceId} 替換為對應地區的業務空間 ID;使用其他地區時同時替換介面位址和 API Key。
  • Java
  • Python
  • Go
  • C#
  • PHP
  • Node.js
建議使用 Java DashScope SDK 進行開發,請參見CosyVoice 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>
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);
            // 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
            TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://{WorkspaceId}.ap-southeast-1.maas.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();
        }
    }
}