全部產品
Search
文件中心

Alibaba Cloud Model Studio:即時語音合成

更新時間:Sep 09, 2026

即時語音合成將文本即時轉換為自然語音,支援流式輸入與輸出,具備聲音複刻、聲音設計及精細化音頻控制能力,適用於語音助手、有聲讀物、智能客服等情境。

概述

實現低延遲文字轉換語音。

  • 支援流式輸入與輸出,首包延遲低
  • 可調節語速、語調、音量與碼率,實現精細的語音效果控制
  • 相容主流音頻格式(PCM、WAV、MP3、Opus),最高支援 48kHz 採樣率輸出
  • 支援指令控制,可通過自然語言指令控制語音表現力
  • 支援聲音複刻與聲音設計音色定製
  • 支援情感與富語言標籤,可在文本中嵌入標籤控制情感表達或插入擬聲效果

批量情境(有聲讀物、課件配音等)可使用非即時語音合成。各模型選型建議請參見語音合成。

前提條件

快速開始

以下是各模型的語音合成樣本。更多樣本和參數說明請參見各模型的API參考。

Qwen-Audio-TTS

以下樣本示範如何使用系統音色進行語音合成。

如需使用指令控制功能,請通過 instruction 參數設定指令。

# coding=utf-8

import os
import dashscope
from dashscope.audio.tts_v2 import *

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

# 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'

# 模型
# qwen-audio-3.0-tts-flash/qwen-audio-3.0-tts-plus:使用longanhuan_v3.6等音色。
# 每個音色支援的語言不同,合成日語、韓語等非中文語言時,需選擇支援對應語言的音色。詳見音色列表。
model = "qwen-audio-3.0-tts-flash"
# 音色
voice = "longanhuan_v3.6"

# 執行個體化SpeechSynthesizer,並在構造方法中傳入模型(model)、音色(voice)等請求參數
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# 發送待合成文本,擷取二進位音頻
audio = synthesizer.call("今天天氣怎麼樣?")
# 首次發送文本時需建立 WebSocket 串連,因此首包延遲會包含串連建立的耗時
print('[Metric] requestId為:{},首包延遲為:{}毫秒'.format(
    synthesizer.get_last_request_id(),
    synthesizer.get_first_package_delay()))

# 將音頻儲存至本地
with open('output.mp3', 'wb') as f:
    f.write(audio)
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.utils.Constants;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;

public class Main {
    // 模型
    // qwen-audio-3.0-tts-flash/qwen-audio-3.0-tts-plus:使用longanhuan_v3.6等音色。
    // 每個音色支援的語言不同,合成日語、韓語等非中文語言時,需選擇支援對應語言的音色。詳見音色列表。
    private static String model = "qwen-audio-3.0-tts-flash";
    // 音色
    private static String voice = "longanhuan_v3.6";

    public static void streamAudioDataToSpeaker() {
        // 請求參數
        SpeechSynthesisParam param =
                SpeechSynthesisParam.builder()
                        // 新加坡地區和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                        // 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:.apiKey("sk-xxx")
                        .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                        .model(model) // 模型
                        .voice(voice) // 音色
                        .build();

        // 同步模式:禁用回調(第二個參數為null)
        SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
        ByteBuffer audio = null;
        try {
            // 阻塞直至音頻返回
            audio = synthesizer.call("今天天氣怎麼樣?");
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // 任務結束關閉websocket串連
            synthesizer.getDuplexApi().close(1000, "bye");
        }
        if (audio != null) {
            // 將音頻資料儲存到本地檔案"output.mp3"中
            File file = new File("output.mp3");
            // 首次發送文本時需建立 WebSocket 串連,因此首包延遲會包含串連建立的耗時
            // 注意:getFirstPackageDelay() 需要 dashscope-sdk-java 2.18.0 及以上版本
            System.out.println(
                    "[Metric] requestId為:"
                            + synthesizer.getLastRequestId()
                            + "首包延遲(毫秒)為:"
                            + synthesizer.getFirstPackageDelay());
            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write(audio.array());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) {
        // 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
        Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
        streamAudioDataToSpeaker();
        System.exit(0);
    }
}

CosyVoice

該模型除 WebSocket 通訊協定外,還支援通過 AOQ 協議接入;如果是用戶端對接,且更看重穩定的延遲、弱網下的互動能力、即時雙工的降噪與回聲消除,可優先考慮 AOQ,協議對比與選型請參見模型/應用支援力度。

重要cosyvoice-v3.5-plus 和 cosyvoice-v3.5-flash 僅在北京地區可用,且僅支援聲音設計和聲音複刻情境(無系統音色)。使用前需先通過聲音複刻或聲音設計建立音色,然後在代碼中將 voice 設為音色 ID、model 設為對應模型名稱。

以下樣本示範如何使用系統音色(參見CosyVoice音色列表)進行語音合成。

如需使用指令控制功能,請通過 instruction 參數設定指令。

# coding=utf-8

import os
import dashscope
from dashscope.audio.tts_v2 import *

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

# 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'

# 模型
# 不同模型版本需要使用對應版本的音色:
# cosyvoice-v3-flash/cosyvoice-v3-plus:使用longanyang等音色。
# cosyvoice-v2:使用longxiaochun_v2等音色。
# 每個音色支援的語言不同,合成日語、韓語等非中文語言時,需選擇支援對應語言的音色。詳見CosyVoice音色列表。
model = "cosyvoice-v3-flash"
# 音色
voice = "longanyang"

# 執行個體化SpeechSynthesizer,並在構造方法中傳入模型(model)、音色(voice)等請求參數
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# 發送待合成文本,擷取二進位音頻
audio = synthesizer.call("今天天氣怎麼樣?")
# 首次發送文本時需建立 WebSocket 串連,因此首包延遲會包含串連建立的耗時
print('[Metric] requestId為:{},首包延遲為:{}毫秒'.format(
    synthesizer.get_last_request_id(),
    synthesizer.get_first_package_delay()))

# 將音頻儲存至本地
with open('output.mp3', 'wb') as f:
    f.write(audio)
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
    import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
    import com.alibaba.dashscope.utils.Constants;

    import java.io.File;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import java.nio.ByteBuffer;

    public class Main {
        // 模型
        // 不同模型版本需要使用對應版本的音色:
        // cosyvoice-v3-flash/cosyvoice-v3-plus:使用longanyang等音色。
        // cosyvoice-v2:使用longxiaochun_v2等音色。
        // 每個音色支援的語言不同,合成日語、韓語等非中文語言時,需選擇支援對應語言的音色。詳見CosyVoice音色列表。
        private static String model = "cosyvoice-v3-flash";
        // 音色
        private static String voice = "longanyang";

        public static void streamAudioDataToSpeaker() {
            // 請求參數
            SpeechSynthesisParam param =
                    SpeechSynthesisParam.builder()
                            // 新加坡地區和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                            // 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:.apiKey("sk-xxx")
                            .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                            .model(model) // 模型
                            .voice(voice) // 音色
                            .build();

            // 同步模式:禁用回調(第二個參數為null)
            SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
            ByteBuffer audio = null;
            try {
                // 阻塞直至音頻返回
                audio = synthesizer.call("今天天氣怎麼樣?");
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                // 任務結束關閉websocket串連
                synthesizer.getDuplexApi().close(1000, "bye");
            }
            if (audio != null) {
                // 將音頻資料儲存到本地檔案"output.mp3"中
                File file = new File("output.mp3");
                // 首次發送文本時需建立 WebSocket 串連,因此首包延遲會包含串連建立的耗時
                // 注意:getFirstPackageDelay() 需要 dashscope-sdk-java 2.18.0 及以上版本
                System.out.println(
                        "[Metric] requestId為:"
                                + synthesizer.getLastRequestId()
                                + "首包延遲(毫秒)為:"
                                + synthesizer.getFirstPackageDelay());
                try (FileOutputStream fos = new FileOutputStream(file)) {
                    fos.write(audio.array());
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        public static void main(String[] args) {
            // 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
            Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
            streamAudioDataToSpeaker();
            System.exit(0);
        }
    }

Qwen-TTS

以下樣本示範如何使用系統音色(參見支援的音色)進行語音合成。

如需使用指令控制功能,請將 model 替換為 qwen3-tts-instruct-flash-realtime,並通過 instructions 參數設定指令。

Python

import os
import base64
import threading
import time
import dashscope
from dashscope.audio.qwen_tts_realtime import *

qwen_tts_realtime: QwenTtsRealtime = None
text_to_synthesize = [
    '對吧~我就特別喜歡這種超市,',
    '尤其是過年的時候',
    '去逛超市',
    '就會覺得',
    '超級超級開心!',
    '想買好多好多的東西呢!'
]

DO_VIDEO_TEST = False

def init_dashscope_api_key():
    """
        Set your DashScope API-key. More information:
        https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
    """

    # 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    if 'DASHSCOPE_API_KEY' in os.environ:
        dashscope.api_key = os.environ[
            'DASHSCOPE_API_KEY']  # load API-key from environment variable DASHSCOPE_API_KEY
    else:
        dashscope.api_key = 'your-dashscope-api-key'  # set API-key manually

class MyCallback(QwenTtsRealtimeCallback):
    def __init__(self):
        self.complete_event = threading.Event()
        self.file = open('result_24k.pcm', 'wb')

    def on_open(self) -> None:
        print('connection opened, init player')

    def on_close(self, close_status_code, close_msg) -> None:
        self.file.close()
        print('connection closed with code: {}, msg: {}, destroy player'.format(close_status_code, close_msg))

    def on_event(self, response: str) -> None:
        try:
            global qwen_tts_realtime
            type = response['type']
            if 'session.created' == type:
                print('start session: {}'.format(response['session']['id']))
            if 'response.audio.delta' == type:
                recv_audio_b64 = response['delta']
                self.file.write(base64.b64decode(recv_audio_b64))
            if 'response.done' == type:
                print(f'response {qwen_tts_realtime.get_last_response_id()} done')
            if 'session.finished' == type:
                print('session finished')
                self.complete_event.set()
        except Exception as e:
            print('[Error] {}'.format(e))
            return

    def wait_for_finished(self):
        self.complete_event.wait()

if __name__  == '__main__':
    init_dashscope_api_key()

    print('Initializing ...')

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        # 如需使用指令控制功能,請將model替換為qwen3-tts-instruct-flash-realtime
        model='qwen3-tts-flash-realtime',
        callback=callback,
        # 以下為新加坡地區的配置。
        url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime'
        )

    qwen_tts_realtime.connect()
    qwen_tts_realtime.update_session(
        voice = 'Cherry',
        response_format = AudioFormat.PCM_24000HZ_MONO_16BIT,
        # 如需使用指令控制功能,請取消下方注釋,並將model替換為qwen3-tts-instruct-flash-realtime
        # instructions='語速較快,帶有明顯的上揚語調,適合介紹時尚產品。',
        # optimize_instructions=True,
        mode = 'server_commit'
    )
    for text_chunk in text_to_synthesize:
        print(f'send text: {text_chunk}')
        qwen_tts_realtime.append_text(text_chunk)
        time.sleep(0.1)
    qwen_tts_realtime.finish()
    callback.wait_for_finished()
    print('[Metric] session: {}, first audio delay: {}'.format(
                    qwen_tts_realtime.get_session_id(),
                    qwen_tts_realtime.get_first_audio_delay(),
                    ))
import base64
import os
import threading
import dashscope
from dashscope.audio.qwen_tts_realtime import *

qwen_tts_realtime: QwenTtsRealtime = None
text_to_synthesize = [
    '這是第一句話。',
    '這是第二句話。',
    '這是第三句話。',
]

DO_VIDEO_TEST = False

def init_dashscope_api_key():
    """
        Set your DashScope API-key. More information:
        https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
    """

    # 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    if 'DASHSCOPE_API_KEY' in os.environ:
        dashscope.api_key = os.environ[
            'DASHSCOPE_API_KEY']  # load API-key from environment variable DASHSCOPE_API_KEY
    else:
        dashscope.api_key = 'your-dashscope-api-key'  # set API-key manually

class MyCallback(QwenTtsRealtimeCallback):
    def __init__(self):
        super().__init__()
        self.response_counter = 0
        self.complete_event = threading.Event()
        self.file = open(f'result_{self.response_counter}_24k.pcm', 'wb')

    def reset_event(self):
        self.response_counter += 1
        self.file = open(f'result_{self.response_counter}_24k.pcm', 'wb')
        self.complete_event = threading.Event()

    def on_open(self) -> None:
        print('connection opened, init player')

    def on_close(self, close_status_code, close_msg) -> None:
        print('connection closed with code: {}, msg: {}, destroy player'.format(close_status_code, close_msg))

    def on_event(self, response: str) -> None:
        try:
            global qwen_tts_realtime
            type = response['type']
            if 'session.created' == type:
                print('start session: {}'.format(response['session']['id']))
            if 'response.audio.delta' == type:
                recv_audio_b64 = response['delta']
                self.file.write(base64.b64decode(recv_audio_b64))
            if 'response.done' == type:
                print(f'response {qwen_tts_realtime.get_last_response_id()} done')
                self.complete_event.set()
                self.file.close()
            if 'session.finished' == type:
                print('session finished')
                self.complete_event.set()
        except Exception as e:
            print('[Error] {}'.format(e))
            return

    def wait_for_response_done(self):
        self.complete_event.wait()

if __name__  == '__main__':
    init_dashscope_api_key()

    print('Initializing ...')

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        # 如需使用指令控制功能,請將model替換為qwen3-tts-instruct-flash-realtime
        model='qwen3-tts-flash-realtime',
        callback=callback,
        # 以下為新加坡地區的配置。
        url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime'
        )

    qwen_tts_realtime.connect()
    qwen_tts_realtime.update_session(
        voice = 'Cherry',
        response_format = AudioFormat.PCM_24000HZ_MONO_16BIT,
        # 如需使用指令控制功能,請取消下方注釋,並將model替換為qwen3-tts-instruct-flash-realtime
        # instructions='語速較快,帶有明顯的上揚語調,適合介紹時尚產品。',
        # optimize_instructions=True,
        mode = 'commit'
    )
    print(f'send text: {text_to_synthesize[0]}')
    qwen_tts_realtime.append_text(text_to_synthesize[0])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    callback.reset_event()

    print(f'send text: {text_to_synthesize[1]}')
    qwen_tts_realtime.append_text(text_to_synthesize[1])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    callback.reset_event()

    print(f'send text: {text_to_synthesize[2]}')
    qwen_tts_realtime.append_text(text_to_synthesize[2])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()

    qwen_tts_realtime.finish()
    print('[Metric] session: {}, first audio delay: {}'.format(
                    qwen_tts_realtime.get_session_id(),
                    qwen_tts_realtime.get_first_audio_delay(),
                    ))

Java

import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.AudioSystem;
import java.io.*;
import java.util.Base64;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;

public class Main {
    static String[] textToSynthesize = {
            "對吧~我就特別喜歡這種超市",
            "尤其是過年的時候",
            "去逛超市",
            "就會覺得",
            "超級超級開心!",
            "想買好多好多的東西呢!"
    };
    public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;

    // 即時PCM音頻播放器類
    public static class RealtimePcmPlayer {
        private int sampleRate;
        private SourceDataLine line;
        private AudioFormat audioFormat;
        private Thread decoderThread;
        private Thread playerThread;
        private AtomicBoolean stopped = new AtomicBoolean(false);
        private Queue<String> b64AudioBuffer = new ConcurrentLinkedQueue<>();
        private Queue<byte[]> RawAudioBuffer = new ConcurrentLinkedQueue<>();
        private ByteArrayOutputStream totalAudioStream = new ByteArrayOutputStream();

        // 建構函式初始化音頻格式和音頻線路
        public RealtimePcmPlayer(int sampleRate) throws LineUnavailableException {
            this.sampleRate = sampleRate;
            this.audioFormat = new AudioFormat(this.sampleRate, 16, 1, true, false);
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(audioFormat);
            line.start();
            decoderThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!stopped.get()) {
                        String b64Audio = b64AudioBuffer.poll();
                        if (b64Audio != null) {
                            byte[] rawAudio = Base64.getDecoder().decode(b64Audio);
                            RawAudioBuffer.add(rawAudio);
                            // 將音頻資料寫入 totalAudioStream
                            try {
                                totalAudioStream.write(rawAudio);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            });
            playerThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!stopped.get()) {
                        byte[] rawAudio = RawAudioBuffer.poll();
                        if (rawAudio != null) {
                            try {
                                playChunk(rawAudio);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            });
            decoderThread.start();
            playerThread.start();
        }

        // 播放一個音頻塊並阻塞直到播放完成
        private void playChunk(byte[] chunk) throws IOException, InterruptedException {
            if (chunk == null || chunk.length == 0) return;

            int bytesWritten = 0;
            while (bytesWritten < chunk.length) {
                bytesWritten += line.write(chunk, bytesWritten, chunk.length - bytesWritten);
            }
            int audioLength = chunk.length / (this.sampleRate*2/1000);
            // 等待緩衝區中的音頻播放完成
            Thread.sleep(audioLength - 10);
        }

        public void write(String b64Audio) {
            b64AudioBuffer.add(b64Audio);
        }

        public void cancel() {
            b64AudioBuffer.clear();
            RawAudioBuffer.clear();
        }

        public void waitForComplete() throws InterruptedException {
            while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
                Thread.sleep(100);
            }
            line.drain();
        }

        public void shutdown() throws InterruptedException, IOException {
            stopped.set(true);
            decoderThread.join();
            playerThread.join();

            // 儲存完整音頻檔案
            File file = new File("TotalAudio_"+ttsFormat.getSampleRate()+"."+ttsFormat.getFormat());
            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write(totalAudioStream.toByteArray());
            }

            if (line != null && line.isRunning()) {
                line.drain();
                line.close();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException, LineUnavailableException, IOException {
        QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
                // 如需使用指令控制功能,請將model替換為qwen3-tts-instruct-flash-realtime
                .model("qwen3-tts-flash-realtime")
                // 以下為新加坡地區的配置。
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                // 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                .apikey(System.getenv("DASHSCOPE_API_KEY"))
                .build();
        AtomicReference<CountDownLatch> completeLatch = new AtomicReference<>(new CountDownLatch(1));
        final AtomicReference<QwenTtsRealtime> qwenTtsRef = new AtomicReference<>(null);

        // 建立即時音頻播放器執行個體
        RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);

        QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
            @Override
            public void onOpen() {
                // 串連建立時的處理
            }
            @Override
            public void onEvent(JsonObject message) {
                String type = message.get("type").getAsString();
                switch(type) {
                    case "session.created":
                        // 會話建立時的處理
                        if (message.has("session")) {
                            String eventId = message.get("event_id").getAsString();
                            String sessionId = message.get("session").getAsJsonObject().get("id").getAsString();
                            System.out.println("[onEvent] session.created, session_id: "
                                    + sessionId + ", event_id: " + eventId);
                        }
                        break;
                    case "response.audio.delta":
                        String recvAudioB64 = message.get("delta").getAsString();
                        // 即時播放音頻
                        audioPlayer.write(recvAudioB64);
                        break;
                    case "response.done":
                        // 響應完成時的處理
                        break;
                    case "session.finished":
                        // 會話結束時的處理
                        completeLatch.get().countDown();
                    default:
                        break;
                }
            }
            @Override
            public void onClose(int code, String reason) {
                // 串連關閉時的處理
            }
        });
        qwenTtsRef.set(qwenTtsRealtime);
        try {
            qwenTtsRealtime.connect();
        } catch (NoApiKeyException e) {
            throw new RuntimeException(e);
        }
        QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
                .voice("Cherry")
                .responseFormat(ttsFormat)
                .mode("server_commit")
                // 如需使用指令控制功能,請取消下方注釋,並將model替換為qwen3-tts-instruct-flash-realtime
                // .instructions("")
                // .optimizeInstructions(true)
                .build();
        qwenTtsRealtime.updateSession(config);
        for (String text:textToSynthesize) {
            qwenTtsRealtime.appendText(text);
            Thread.sleep(100);
        }
        qwenTtsRealtime.finish();
        completeLatch.get().await();
        qwenTtsRealtime.close();

        // 等待音頻播放完成並關閉播放器
        audioPlayer.waitForComplete();
        audioPlayer.shutdown();
        System.exit(0);
    }
}
import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.AudioSystem;
import java.io.*;
import java.util.Base64;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;

public class Main {
    public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;
    // 即時PCM音頻播放器類
    public static class RealtimePcmPlayer {
        private int sampleRate;
        private SourceDataLine line;
        private AudioFormat audioFormat;
        private Thread decoderThread;
        private Thread playerThread;
        private AtomicBoolean stopped = new AtomicBoolean(false);
        private Queue<String> b64AudioBuffer = new ConcurrentLinkedQueue<>();
        private Queue<byte[]> RawAudioBuffer = new ConcurrentLinkedQueue<>();
        private ByteArrayOutputStream totalAudioStream = new ByteArrayOutputStream();

        // 建構函式初始化音頻格式和音頻線路
        public RealtimePcmPlayer(int sampleRate) throws LineUnavailableException {
            this.sampleRate = sampleRate;
            this.audioFormat = new AudioFormat(this.sampleRate, 16, 1, true, false);
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(audioFormat);
            line.start();
            decoderThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!stopped.get()) {
                        String b64Audio = b64AudioBuffer.poll();
                        if (b64Audio != null) {
                            byte[] rawAudio = Base64.getDecoder().decode(b64Audio);
                            RawAudioBuffer.add(rawAudio);
                            // 將音頻資料寫入 totalAudioStream
                            try {
                                totalAudioStream.write(rawAudio);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            });
            playerThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!stopped.get()) {
                        byte[] rawAudio = RawAudioBuffer.poll();
                        if (rawAudio != null) {
                            try {
                                playChunk(rawAudio);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            });
            decoderThread.start();
            playerThread.start();
        }

        // 播放一個音頻塊並阻塞直到播放完成
        private void playChunk(byte[] chunk) throws IOException, InterruptedException {
            if (chunk == null || chunk.length == 0) return;

            int bytesWritten = 0;
            while (bytesWritten < chunk.length) {
                bytesWritten += line.write(chunk, bytesWritten, chunk.length - bytesWritten);
            }
            int audioLength = chunk.length / (this.sampleRate*2/1000);
            // 等待緩衝區中的音頻播放完成
            Thread.sleep(audioLength - 10);
        }

        public void write(String b64Audio) {
            b64AudioBuffer.add(b64Audio);
        }

        public void cancel() {
            b64AudioBuffer.clear();
            RawAudioBuffer.clear();
        }

        public void waitForComplete() throws InterruptedException {
            // 等待所有緩衝區中的音頻資料播放完成
            while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
                Thread.sleep(100);
            }
            // 等待音頻線路播放完成
            line.drain();
        }

        public void shutdown() throws InterruptedException {
            stopped.set(true);
            decoderThread.join();
            playerThread.join();
            // 儲存完整音頻檔案
            File file = new File("TotalAudio_"+ttsFormat.getSampleRate()+"."+ttsFormat.getFormat());
            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write(totalAudioStream.toByteArray());
            } catch (FileNotFoundException e) {
                throw new RuntimeException(e);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            if (line != null && line.isRunning()) {
                line.drain();
                line.close();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException, LineUnavailableException, FileNotFoundException {
        Scanner scanner = new Scanner(System.in);

        QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
                // 如需使用指令控制功能,請將model替換為qwen3-tts-instruct-flash-realtime
                .model("qwen3-tts-flash-realtime")
                // 以下為新加坡地區的配置。
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                // 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                .apikey(System.getenv("DASHSCOPE_API_KEY"))
                .build();

        AtomicReference<CountDownLatch> completeLatch = new AtomicReference<>(new CountDownLatch(1));

        // 建立即時播放器執行個體
        RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);

        final AtomicReference<QwenTtsRealtime> qwenTtsRef = new AtomicReference<>(null);
        QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
            //            File file = new File("result_24k.pcm");
//            FileOutputStream fos = new FileOutputStream(file);
            @Override
            public void onOpen() {
                System.out.println("connection opened");
                System.out.println("輸入文本並按Enter發送,輸入'quit'退出程式");
            }
            @Override
            public void onEvent(JsonObject message) {
                String type = message.get("type").getAsString();
                switch(type) {
                    case "session.created":
                        System.out.println("start session: " + message.get("session").getAsJsonObject().get("id").getAsString());
                        break;
                    case "response.audio.delta":
                        String recvAudioB64 = message.get("delta").getAsString();
                        byte[] rawAudio = Base64.getDecoder().decode(recvAudioB64);
                        //                            fos.write(rawAudio);
                        // 即時播放音頻
                        audioPlayer.write(recvAudioB64);
                        break;
                    case "response.done":
                        System.out.println("response done");
                        // 等待音頻播放完成
                        try {
                            audioPlayer.waitForComplete();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        // 為下一次輸入做準備
                        completeLatch.get().countDown();
                        break;
                    case "session.finished":
                        System.out.println("session finished");
                        if (qwenTtsRef.get() != null) {
                            System.out.println("[Metric] response: " + qwenTtsRef.get().getResponseId() +
                                    ", first audio delay: " + qwenTtsRef.get().getFirstAudioDelay() + " ms");
                        }
                        completeLatch.get().countDown();
                    default:
                        break;
                }
            }
            @Override
            public void onClose(int code, String reason) {
                System.out.println("connection closed code: " + code + ", reason: " + reason);
                try {
//                    fos.close();
                    // 等待播放完成並關閉播放器
                    audioPlayer.waitForComplete();
                    audioPlayer.shutdown();
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        qwenTtsRef.set(qwenTtsRealtime);
        try {
            qwenTtsRealtime.connect();
        } catch (NoApiKeyException e) {
            throw new RuntimeException(e);
        }
        QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
                .voice("Cherry")
                .responseFormat(ttsFormat)
                .mode("commit")
                // 如需使用指令控制功能,請取消下方注釋,並將model替換為qwen3-tts-instruct-flash-realtime
                // .instructions("")
                // .optimizeInstructions(true)
                .build();
        qwenTtsRealtime.updateSession(config);

        // 迴圈讀取使用者輸入
        while (true) {
            System.out.print("請輸入要合成的文本: ");
            String text = scanner.nextLine();

            // 如果使用者輸入quit,則退出程式
            if ("quit".equalsIgnoreCase(text.trim())) {
                System.out.println("正在關閉串連...");
                qwenTtsRealtime.finish();
                completeLatch.get().await();
                break;
            }

            // 如果使用者輸入為空白,跳過
            if (text.trim().isEmpty()) {
                continue;
            }

            // 重新初始化倒計時鎖存器
            completeLatch.set(new CountDownLatch(1));

            // 發送文本
            qwenTtsRealtime.appendText(text);
            qwenTtsRealtime.commit();

            // 等待本次合成完成
            completeLatch.get().await();
        }

        // 清理資源
        audioPlayer.waitForComplete();
        audioPlayer.shutdown();
        scanner.close();
        System.exit(0);
    }
}

會話配置

Qwen-TTS 互動模式

Qwen-TTS Realtime API 提供兩種互動模式:

  • server_commit 模式:由服務端智能處理文本分段與合成時機,適合大段文本的連續合成情境。用戶端只需持續追加文本,無需關注分段和提交。
  • commit 模式:由用戶端主動提交文本緩衝區以觸發合成,適合需要精確控制合成時機的情境(如對話式 AI 逐輪合成)。

切換互動模式:

  • WebSocket:通過 session.update 事件中的 mode 欄位設定。
{
    "type": "session.update",
    "session": {
        "mode": "server_commit"
    }
}
  • Python SDK:在 update_session 方法中通過 mode 參數設定。
qwen_tts_realtime.update_session(
    voice='Cherry',
    response_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
    mode='server_commit'
)
  • Java SDK:通過 QwenTtsRealtimeConfig.builder() 設定 mode 參數。
QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
        .voice("Cherry")
        .responseFormat(ttsFormat)
        .mode("server_commit")
        .build();
qwenTtsRealtime.updateSession(config);

完整的 SDK 程式碼範例請參見Python SDK和Java SDK。WebSocket 事件生命週期和串連複用說明請參見WebSocket API參考。

進階功能

指令控制

指令控制通過自然語言描述控制語音的音調、語速、情感和音色特點,無需調整複雜的音頻參數。

各模型指令規格:

Qwen-Audio-TTS

支援的模型:qwen-audio-3.0-tts-plus、qwen-audio-3.0-tts-flash

系統音色和聲音複刻音色:均可輸入任意指令。

CosyVoice

支援的模型:cosyvoice-v3.5-plus、cosyvoice-v3.5-flash、cosyvoice-v3-plus、cosyvoice-v3-flash

不同模型對指令的格式要求不同:

  • cosyvoice-v3.5-plus、cosyvoice-v3.5-flash:

    • 聲音複刻/設計音色:可輸入任意指令。
    • 系統音色:v3.5不支援系統音色。
  • cosyvoice-v3-plus:

    • 聲音複刻/設計音色:不支援指令控制。
    • 系統音色:指令必須使用固定格式和內容,參見CosyVoice音色列表。
  • cosyvoice-v3-flash:

    • 聲音複刻/設計音色:可輸入任意指令。
    • 系統音色:指令必須使用固定格式和內容,參見CosyVoice音色列表。

使用方式:通過 instruction 參數指定指令內容。

指令文本支援的語言:

  • cosyvoice-v3.5-plus、cosyvoice-v3.5-flash:

    • 聲音複刻/設計音色:中文、英文、法語、德語、日語、韓語、俄語、葡萄牙語、泰語、印尼語、越南語。
    • 系統音色:v3.5不支援系統音色。
  • cosyvoice-v3-plus:

    • 聲音複刻/設計音色:中文、英文、法語、德語、日語、韓語、俄語。
    • 系統音色:指令必須使用固定格式和內容,參見CosyVoice音色列表。
  • cosyvoice-v3-flash:

    • 聲音複刻/設計音色:中文、英文、法語、德語、日語、韓語、俄語。
    • 系統音色:中文。

指令文本長度限制:不超過 100 字元。漢字(包括簡體/繁體漢字、日文漢字和韓文漢字)按 2 個字元計算,其他字元(如標點符號、字母、數字、日韓文假名/諺文等)按 1 個字元計算。

Qwen-TTS

支援的模型:僅支援Qwen3-TTS-Instruct-Flash-Realtime系列模型。

使用方式:通過 instructions 參數指定指令內容。

指令文本支援的語言:僅支援中文和英文。

指令文本長度限制:不超過 1600 Token。

適用情境:

  • 有聲書和廣播劇配音
  • 廣告和宣傳片配音
  • 遊戲角色和動畫配音
  • 情感化的智能語音助手
  • 紀錄片和新聞播報

如何編寫高品質的聲音描述:

  • 核心原則:

    1. 具體而非模糊:使用描繪聲音特質的詞語,如“低沉”、“清脆”、“語速偏快”,避免“好聽”、“普通”等主觀或模糊的表述。
    2. 多維而非單一:好的描述通常涵蓋多個維度(如性別、年齡、情感等)。僅寫“女聲”過於寬泛,難以產生有特色的音色。
    3. 客觀而非主觀:聚焦聲音的物理和感知特徵。例如,用”音調偏高,帶有活力“代替”我最喜歡的聲音”。
    4. 原創而非模仿:描述聲音的特質,而非要求模仿特定人物(如名人、演員)。模型不支援模仿,且可能涉及著作權風險。
    5. 簡潔而非冗餘:確保每個詞都有明確作用,避免重複的同義字或無意義的修飾。
  • 描述維度參考:

    建議組合以下維度描述聲音,維度越豐富,產生效果越精準。

    維度

    描述樣本

    性別

    男性、女性、中性

    年齡

    兒童(5-12 歲)、青少年(13-18 歲)、青年(19-35 歲)、中年(36-55 歲)、老年(55 歲以上)

    音調

    高音、中音、低音、偏高、偏低

    語速

    快速、中速、緩慢、偏快、偏慢

    情感

    開朗、沉穩、溫柔、嚴肅、活潑、冷靜、治癒

    特點

    有磁性、清脆、沙啞、圓潤、甜美、渾厚、有力

    用途

    新聞播報、廣告配音、有聲書、動畫角色、語音助手、紀錄片解說

  • 樣本:

    • 標準播音風格:吐字清晰精準,字正腔圓
    • 年輕活潑的女性聲音,語速較快,帶有明顯的上揚語調,適合介紹時尚產品
    • 沉穩的中年男性,語速緩慢,音色低沉有磁性,適合朗讀新聞或紀錄片解說
    • 溫柔知性的女性,30 歲左右,語調平和,適合有聲書朗讀
    • 可愛的兒童聲音,大約 8 歲女孩,說話略帶稚氣,適合動畫角色配音

方言

本節介紹如何讓模型用中文方言(如河南話、四川話、粵語等)輸出語音。不同模型和音色類型的設定方式不同。

各模型方言設定方式:

Qwen-Audio-TTS

  • 系統音色:在Qwen-Audio-TTS音色列表中選擇以下任一種音色:

    • 支援方言的系統音色,無需額外設定即可輸出對應方言。
    • 支援指令控制且可指定方言的音色,通過指令文本指定方言。
  • 聲音複刻音色:通過指令控制功能設定,例如指令文本寫 請用河南話表達。

具體支援哪些方言:參見Qwen-Audio-TTS中各模型“支援的語言”。

CosyVoice

  • 系統音色:在CosyVoice音色列表中選擇以下任一種音色:

    • 支援方言的系統音色(例如 longshange_v3),無需額外設定即可輸出對應方言。
    • 支援指令控制且可指定方言的音色(例如 longanhuan_v3),通過指令文本指定方言。
  • 聲音複刻音色:通過指令控制功能設定,例如指令文本寫 請用河南話表達。

  • 聲音設計音色:暫不支援方言。

具體支援哪些方言:參見CosyVoice中各模型“支援的語言”。

樣本:以 cosyvoice-v3-flash + longanhuan_v3 音色,通過指令文本 "請用河南話表達。" 輸出河南話語音。

# coding=utf-8

import os
import dashscope
from dashscope.audio.tts_v2 import *

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

# 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'

# 模型
# 不同模型版本需要使用對應版本的音色:
# cosyvoice-v3-flash/cosyvoice-v3-plus:使用longanyang等音色。
# cosyvoice-v2:使用longxiaochun_v2等音色。
# 不同語言選擇對應音色
model = "cosyvoice-v3-flash"
# 音色
voice = "longanhuan_v3"

# 執行個體化SpeechSynthesizer,並在構造方法中傳入模型(model)、音色(voice)等請求參數
synthesizer = SpeechSynthesizer(model=model, voice=voice, instruction="請用河南話表達。")
# 發送待合成文本,擷取二進位音頻
audio = synthesizer.call("叫你去買鹽,你買回來一袋面,這不是弄啥嘞嗎!")
# 首次發送文本時需建立 WebSocket 串連,因此首包延遲會包含串連建立的耗時
print('[Metric] requestId為:{},首包延遲為:{}毫秒'.format(
    synthesizer.get_last_request_id(),
    synthesizer.get_first_package_delay()))

# 將音頻儲存至本地
with open('output.mp3', 'wb') as f:
    f.write(audio)

Qwen-TTS

  • 系統音色:使用支援方言的系統音色,參見支援的音色中的 Qwen-TTS音色列表。
  • 聲音複刻音色:不支援方言。
  • 聲音設計音色:不支援方言。

具體支援哪些方言:參見Qwen3-TTS中各模型“支援的語言”。

情感與富語言標籤

Qwen-Audio-TTS 系列模型支援在待合成文本(text 參數)中直接嵌入情感與富語言標籤,用於控制語音的情感表達或在指定位置插入擬聲效果(如笑聲、歎息等),無需調整複雜的音頻參數即可產生更具表現力的語音。

重要支援的模型:僅 qwen-audio-3.0-tts-plus 和 qwen-audio-3.0-tts-flash。

限制:僅支援單向流式模式。

控制類標籤

控制類標籤用於設定語音的情感或風格。將標籤寫在文本中,標籤會作用於其後的所有文本,直到遇到下一個控制類標籤,或因句子較長被自動切分為止。

標籤

說明

[sad]

悲傷

[amazed]

驚歎

[deep and loud shouting]

深沉大聲呐喊

[trembling]

顫抖

[angry]

憤怒

[excited]

興奮

[sarcastic]

諷刺

[curious]

好奇

[like dracula]

德古拉風格(低沉、陰森)

[bored]

無聊

[tired]

疲憊

[scornful]

輕蔑

[shouting]

大喊

[asmr]

ASMR 輕柔耳語

[panicked]

恐慌

[mischievously]

調皮

[empathetic]

共情

[whispers]

耳語

[reluctantly]

不情願

[crying]

哭泣

[serious]

嚴肅

[very slowly]

非常緩慢地說話

[very fast]

非常快速地說話

富語言類標籤

富語言類標籤用於在文本的當前位置插入一段擬聲效果,不影響前後文本的情感風格。

標籤

說明

[gasp]

倒吸一口氣

[sighing]

歎息

[clears throat]

清嗓

[giggles]

咯咯笑

[laughing]

大笑

[cough]

咳嗽

[snorts]

哼聲、嗤笑

使用樣本

以下樣本展示如何在 text 參數中組合使用控制類標籤和富語言類標籤:

[excited]今天的天氣真不錯![laughing]我們一起出去玩吧!

上述文本中,[excited] 是控制類標籤,作用於其後的所有文本,使語音帶有興奮的情感;[laughing] 是富語言類標籤,在該位置插入一段笑聲效果後繼續合成後續文本。

您也可以在同一段文本中切換不同情感:

[serious]請注意安全事項。[excited]好了,現在讓我們開始吧!

其中 [serious] 控制第一句為嚴肅語氣,[excited] 從第二句起切換為興奮語氣。

取消任務

在即時語音合成過程中,如果需要中斷當前輪次合成,可以發送取消指令。取消後服務端會立即結束當前任務並返回結束事件,您可在當前 WebSocket 串連上繼續發起新的合成任務,無需重建立立串連。

使用方式:

  • Python SDK:1.26.4 及以上版本,調用 SpeechSynthesizer.streaming_cancel()。
  • Java SDK:2.22.26 及以上版本,調用 SpeechSynthesizer.streamingCancel()。
  • WebSocket 原始協議:發送 finish-task 事件,並在 input 中設定 directive=cancel。

重要模型限制:

  • 華北2(北京)地區:Qwen-Audio-TTS 系列模型的所有模型都支援該功能;CosyVoice 系列模型僅 v2 及以上版本支援該功能。
  • 新加坡地區:Qwen-Audio-TTS 系列模型的所有模型都支援該功能;CosyVoice 系列模型不支援該功能。

WebSocket 原始協議調用

以下樣本展示如何通過 WebSocket 原始協議直連服務端,適用於不使用 DashScope SDK 的情境。此為最小可運行實現,WebSocket 通訊協定請參見各模型的 API 參考。

點擊查看 WebSocket 原始協議調用樣本

Qwen-Audio-TTS/CosyVoice

Qwen-Audio-TTS 和 CosyVoice 使用相同的 WebSocket 通訊協定,只需替換 model 和 voice 參數。以下樣本以 qwen-audio-3.0-tts-flash 為例,使用 CosyVoice 時將 model 替換為 cosyvoice-v3-flash 等,voice 替換為對應音色即可。

Go

package main

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

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

const (
	// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
	wsURL      = "wss://{WorkspaceId}.ap-southeast-1.maas.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":      "qwen-audio-3.0-tts-flash",
			"parameters": map[string]interface{}{
				"text_type":   "PlainText",
				"voice":       "longanhuan_v3.6",
				"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.");

    // 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
    private const string WebSocketUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.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 = "qwen-audio-3.0-tts-flash",
            parameters = new
            {
                text_type = "PlainText",
                voice = "longanhuan_v3.6",
                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");
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.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/verify_peer_name 設為 true
    '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" => "qwen-audio-3.0-tts-flash",
            "parameters" => [
                "text_type" => "PlainText",
                "voice" => "longanhuan_v3.6",
                "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;
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
const url = 'wss://{WorkspaceId}.ap-southeast-1.maas.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: 'qwen-audio-3.0-tts-flash',
      parameters: {
        text_type: 'PlainText',
        voice: 'longanhuan_v3.6', // 音色
        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 DashScope SDK 進行開發,請參見Java SDK。

以下是 Java WebSocket 直連樣本,運行前請匯入以下依賴:

  • Java-WebSocket
  • jackson-databind

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

<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>
// 省略其它代碼
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\": \"qwen-audio-3.0-tts-flash\", \"parameters\": { \"text_type\": \"PlainText\", \"voice\": \"longanhuan_v3.6\", \"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();
        }
    }
}

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": "qwen-audio-3.0-tts-flash",
                "parameters": {
                    "text_type": "PlainText",
                    "voice": "longanhuan_v3.6",
                    "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")
    # 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
    SERVER_URI = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"  # 替換為你的 WebSocket 地址

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

Qwen-TTS

  1. 建立用戶端

    Python

    在本地建立 Python 檔案,命名為tts_realtime_client.py並複製以下代碼到檔案中:

    # -- coding: utf-8 --
    
    import asyncio
    import websockets
    import json
    import base64
    import time
    from typing import Optional, Callable, Dict, Any
    from enum import Enum
    
    class SessionMode(Enum):
        SERVER_COMMIT = "server_commit"
        COMMIT = "commit"
    
    class TTSRealtimeClient:
        """
        與 TTS Realtime API 互動的用戶端。
    
        該類提供了串連 TTS Realtime API、發送文本資料、擷取音訊輸出以及管理 WebSocket 串連的相關方法。
    
        屬性說明:
            base_url (str):
                Realtime API 的基礎地址。
            api_key (str):
                用於身分識別驗證的 API Key。
            voice (str):
                服務端合成語音所使用的聲音。
            mode (SessionMode):
                會話模式,可選 server_commit 或 commit。
            audio_callback (Callable[[bytes], None]):
                接收音頻資料的回呼函數。
            language_type(str)
                合成的語音的語種,可選值Chinese、English、German、Italian、Portuguese、Spanish、Japanese、Korean、French、Russian、Auto
        """
    
        def __init__(
                self,
                base_url: str,
                api_key: str,
                voice: str = "Cherry",
                mode: SessionMode = SessionMode.SERVER_COMMIT,
                audio_callback: Optional[Callable[[bytes], None]] = None,
            language_type: str = "Auto"):
            self.base_url = base_url
            self.api_key = api_key
            self.voice = voice
            self.mode = mode
            self.ws = None
            self.audio_callback = audio_callback
            self.language_type = language_type
    
            # 當前回複狀態
            self._current_response_id = None
            self._current_item_id = None
            self._is_responding = False
            self._response_done_future = None
    
        async def connect(self) -> None:
            """與 TTS Realtime API 建立 WebSocket 串連。"""
            headers = {
                "Authorization": f"Bearer {self.api_key}"
            }
    
            self.ws = await websockets.connect(self.base_url, additional_headers=headers)
    
            # 設定預設會話配置
            await self.update_session({
                "mode": self.mode.value,
                "voice": self.voice,
                # 如需使用指令控制功能,請取消下方注釋,並在server_commit.py或commit.py中將model替換為qwen3-tts-instruct-flash-realtime
                # "instructions": "語速較快,帶有明顯的上揚語調,適合介紹時尚產品。",
                # "optimize_instructions": true
                "language_type": self.language_type,
                "response_format": "pcm",
                "sample_rate": 24000
            })
    
        async def send_event(self, event) -> None:
            """發送事件到伺服器。"""
            event['event_id'] = "event_" + str(int(time.time() * 1000))
            print(f"發送事件: type={event['type']}, event_id={event['event_id']}")
            await self.ws.send(json.dumps(event))
    
        async def update_session(self, config: Dict[str, Any]) -> None:
            """更新會話配置。"""
            event = {
                "type": "session.update",
                "session": config
            }
            print("更新會話配置: ", event)
            await self.send_event(event)
    
        async def append_text(self, text: str) -> None:
            """向 API 發送文本資料。"""
            event = {
                "type": "input_text_buffer.append",
                "text": text
            }
            await self.send_event(event)
    
        async def commit_text_buffer(self) -> None:
            """提交文本緩衝區以觸發處理。"""
            event = {
                "type": "input_text_buffer.commit"
            }
            await self.send_event(event)
    
        async def clear_text_buffer(self) -> None:
            """清除文本緩衝區。"""
            event = {
                "type": "input_text_buffer.clear"
            }
            await self.send_event(event)
    
        async def finish_session(self) -> None:
            """結束會話。"""
            event = {
                "type": "session.finish"
            }
            await self.send_event(event)
    
        async def wait_for_response_done(self):
            """等待 response.done 事件"""
            if self._response_done_future:
                await self._response_done_future
    
        async def handle_messages(self) -> None:
            """處理來自伺服器的訊息。"""
            try:
                async for message in self.ws:
                    event = json.loads(message)
                    event_type = event.get("type")
    
                    if event_type != "response.audio.delta":
                        print(f"收到事件: {event_type}")
    
                    if event_type == "error":
                        print("錯誤: ", event.get('error', {}))
                        continue
                    elif event_type == "session.created":
                        print("會話建立,ID: ", event.get('session', {}).get('id'))
                    elif event_type == "session.updated":
                        print("會話更新,ID: ", event.get('session', {}).get('id'))
                    elif event_type == "input_text_buffer.committed":
                        print("文本緩衝區已提交,專案ID: ", event.get('item_id'))
                    elif event_type == "input_text_buffer.cleared":
                        print("文本緩衝區已清除")
                    elif event_type == "response.created":
                        self._current_response_id = event.get("response", {}).get("id")
                        self._is_responding = True
                        # 建立新的 future 來等待 response.done
                        self._response_done_future = asyncio.Future()
                        print("響應已建立,ID: ", self._current_response_id)
                    elif event_type == "response.output_item.added":
                        self._current_item_id = event.get("item", {}).get("id")
                        print("輸出項已添加,ID: ", self._current_item_id)
                    # 處理音頻增量
                    elif event_type == "response.audio.delta" and self.audio_callback:
                        audio_bytes = base64.b64decode(event.get("delta", ""))
                        self.audio_callback(audio_bytes)
                    elif event_type == "response.audio.done":
                        print("音頻產生完成")
                    elif event_type == "response.done":
                        self._is_responding = False
                        self._current_response_id = None
                        self._current_item_id = None
                        # 標記 future 完成
                        if self._response_done_future and not self._response_done_future.done():
                            self._response_done_future.set_result(True)
                        print("響應完成")
                    elif event_type == "session.finished":
                        print("會話已結束")
    
            except websockets.exceptions.ConnectionClosed:
                print("串連已關閉")
            except Exception as e:
                print("訊息處理出錯: ", str(e))
    
        async def close(self) -> None:
            """關閉 WebSocket 串連。"""
            if self.ws:
                await self.ws.close()
    

    Java

    在本地建立 Java 檔案,命名為TTSRealtimeClient.java並複製以下代碼到檔案中:

    import com.google.gson.Gson;
    import com.google.gson.JsonObject;
    import org.java_websocket.client.WebSocketClient;
    import org.java_websocket.handshake.ServerHandshake;
    
    import java.net.URI;
    import java.util.Base64;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.CountDownLatch;
    import java.util.function.Consumer;
    
    /**
     * 與 TTS Realtime API 互動的用戶端。
     *
     * 該類提供了串連 TTS Realtime API、發送文本資料、擷取音訊輸出以及管理 WebSocket 串連的相關方法。
     */
    public class TTSRealtimeClient {
    
        public enum SessionMode {
            SERVER_COMMIT("server_commit"),
            COMMIT("commit");
            private final String value;
            SessionMode(String value) { this.value = value; }
            public String getValue() { return value; }
        }
    
        /**
         * 音頻回調介面
         */
        public interface AudioCallback {
            void onAudio(byte[] audioData);
        }
    
        private final String baseUrl;
        private final String apiKey;
        private final String voice;
        private final SessionMode mode;
        private final String languageType;
        private final AudioCallback audioCallback;
        private final Gson gson = new Gson();
    
        private WebSocketClient ws;
        private CountDownLatch responseDoneLatch;
        private CountDownLatch sessionFinishedLatch;
    
        public TTSRealtimeClient(String baseUrl, String apiKey, String voice,
                                 SessionMode mode, AudioCallback audioCallback,
                                 String languageType) {
            this.baseUrl = baseUrl;
            this.apiKey = apiKey;
            this.voice = voice;
            this.mode = mode;
            this.audioCallback = audioCallback;
            this.languageType = languageType;
        }
    
        public TTSRealtimeClient(String baseUrl, String apiKey, String voice,
                                 SessionMode mode, AudioCallback audioCallback) {
            this(baseUrl, apiKey, voice, mode, audioCallback, "Auto");
        }
    
        /**
         * 與 TTS Realtime API 建立 WebSocket 串連。
         */
        public void connect() throws Exception {
            Map<String, String> headers = new HashMap<>();
            headers.put("Authorization", "Bearer " + apiKey);
    
            responseDoneLatch = new CountDownLatch(0);
            sessionFinishedLatch = new CountDownLatch(1);
    
            ws = new WebSocketClient(new URI(baseUrl), headers) {
                @Override
                public void onOpen(ServerHandshake handshake) {
                    System.out.println("WebSocket 串連已建立");
                    // 發送預設會話配置
                    JsonObject session = new JsonObject();
                    session.addProperty("mode", mode.getValue());
                    session.addProperty("voice", TTSRealtimeClient.this.voice);
                    // 如需使用指令控制功能,請取消下方注釋,並將model替換為qwen3-tts-instruct-flash-realtime
                    // session.addProperty("instructions", "語速較快,帶有明顯的上揚語調,適合介紹時尚產品。");
                    // session.addProperty("optimize_instructions", true);
                    session.addProperty("language_type", languageType);
                    session.addProperty("response_format", "pcm");
                    session.addProperty("sample_rate", 24000);
                    updateSession(session);
                }
    
                @Override
                public void onMessage(String message) {
                    JsonObject event = gson.fromJson(message, JsonObject.class);
                    String eventType = event.has("type") ? event.get("type").getAsString() : "";
    
                    if (!"response.audio.delta".equals(eventType)) {
                        System.out.println("收到事件: " + eventType);
                    }
    
                    switch (eventType) {
                        case "error":
                            System.err.println("錯誤: " + event.get("error"));
                            break;
                        case "session.created":
                            System.out.println("會話建立,ID: " +
                                event.getAsJsonObject("session").get("id").getAsString());
                            break;
                        case "session.updated":
                            System.out.println("會話更新,ID: " +
                                event.getAsJsonObject("session").get("id").getAsString());
                            break;
                        case "input_text_buffer.committed":
                            System.out.println("文本緩衝區已提交,專案ID: " + event.get("item_id"));
                            break;
                        case "input_text_buffer.cleared":
                            System.out.println("文本緩衝區已清除");
                            break;
                        case "response.created":
                            System.out.println("響應已建立,ID: " +
                                event.getAsJsonObject("response").get("id").getAsString());
                            responseDoneLatch = new CountDownLatch(1);
                            break;
                        case "response.output_item.added":
                            System.out.println("輸出項已添加,ID: " +
                                event.getAsJsonObject("item").get("id").getAsString());
                            break;
                        case "response.audio.delta":
                            if (audioCallback != null) {
                                byte[] audioBytes = Base64.getDecoder().decode(
                                    event.get("delta").getAsString());
                                audioCallback.onAudio(audioBytes);
                            }
                            break;
                        case "response.audio.done":
                            System.out.println("音頻產生完成");
                            break;
                        case "response.done":
                            System.out.println("響應完成");
                            responseDoneLatch.countDown();
                            break;
                        case "session.finished":
                            System.out.println("會話已結束");
                            sessionFinishedLatch.countDown();
                            break;
                    }
                }
    
                @Override
                public void onClose(int code, String reason, boolean remote) {
                    System.out.println("串連已關閉: " + reason);
                }
    
                @Override
                public void onError(Exception ex) {
                    System.err.println("WebSocket 錯誤: " + ex.getMessage());
                }
            };
            ws.connectBlocking();
        }
    
        /**
         * 發送事件到伺服器。
         */
        public void sendEvent(JsonObject event) {
            String eventId = "event_" + System.currentTimeMillis();
            event.addProperty("event_id", eventId);
            System.out.println("發送事件: type=" + event.get("type").getAsString()
                + ", event_id=" + eventId);
            ws.send(gson.toJson(event));
        }
    
        /**
         * 更新會話配置。
         */
        public void updateSession(JsonObject config) {
            JsonObject event = new JsonObject();
            event.addProperty("type", "session.update");
            event.add("session", config);
            System.out.println("更新會話配置: " + event);
            sendEvent(event);
        }
    
        /**
         * 向 API 發送文本資料。
         */
        public void appendText(String text) {
            JsonObject event = new JsonObject();
            event.addProperty("type", "input_text_buffer.append");
            event.addProperty("text", text);
            sendEvent(event);
        }
    
        /**
         * 提交文本緩衝區以觸發處理。
         */
        public void commitTextBuffer() {
            JsonObject event = new JsonObject();
            event.addProperty("type", "input_text_buffer.commit");
            sendEvent(event);
        }
    
        /**
         * 清除文本緩衝區。
         */
        public void clearTextBuffer() {
            JsonObject event = new JsonObject();
            event.addProperty("type", "input_text_buffer.clear");
            sendEvent(event);
        }
    
        /**
         * 結束會話。
         */
        public void finishSession() {
            JsonObject event = new JsonObject();
            event.addProperty("type", "session.finish");
            sendEvent(event);
        }
    
        /**
         * 等待 response.done 事件。
         */
        public void waitForResponseDone() throws InterruptedException {
            responseDoneLatch.await();
        }
    
        /**
         * 等待 session.finished 事件。
         */
        public void waitForSessionFinished() throws InterruptedException {
            sessionFinishedLatch.await();
        }
    
        /**
         * 關閉 WebSocket 串連。
         */
        public void close() {
            if (ws != null) {
                ws.close();
            }
        }
    }
    
  2. 選擇語音合成模式

    Realtime API支援以下兩種模式:

    • server_commit 模式

      服務端智能判斷文本分段與合成時機,用戶端只需發送文本。適合低延遲情境(如 GPS 導航)。

    • commit 模式

      用戶端將文本添加至緩衝區後主動觸發合成。適合精細控制斷句的情境(如新聞播報)。

    server_commit 模式

    Python

    在tts_realtime_client.py的同級目錄下建立另一個 Python 檔案,命名為server_commit.py,並將以下代碼複製進檔案中:

    import os
    import asyncio
    import logging
    import wave
    from tts_realtime_client import TTSRealtimeClient, SessionMode
    import pyaudio
    
    # QwenTTS 服務配置
    # 如需使用指令控制功能,請將model替換為qwen3-tts-instruct-flash-realtime,並在tts_realtime_client.py中取消instructions的注釋
    # 以下為新加坡地區的配置。
    URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"
    # 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:API_KEY="sk-xxx"
    API_KEY = os.getenv("DASHSCOPE_API_KEY")
    
    if not API_KEY:
        raise ValueError("Please set DASHSCOPE_API_KEY environment variable")
    
    # 收集音頻資料
    _audio_chunks = []
    # 即時播放相關
    _AUDIO_SAMPLE_RATE = 24000
    _audio_pyaudio = pyaudio.PyAudio()
    _audio_stream = None  # 將在運行時開啟
    
    def _audio_callback(audio_bytes: bytes):
        """TTSRealtimeClient 音頻回調: 即時播放並緩衝"""
        global _audio_stream
        if _audio_stream is not None:
            try:
                _audio_stream.write(audio_bytes)
            except Exception as exc:
                logging.error(f"PyAudio playback error: {exc}")
        _audio_chunks.append(audio_bytes)
        logging.info(f"Received audio chunk: {len(audio_bytes)} bytes")
    
    def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool:
        """將收集到的音頻資料儲存為 WAV 檔案"""
        if not _audio_chunks:
            logging.warning("No audio data to save")
            return False
    
        try:
            audio_data = b"".join(_audio_chunks)
            with wave.open(filename, 'wb') as wav_file:
                wav_file.setnchannels(1)  # 單聲道
                wav_file.setsampwidth(2)  # 16-bit
                wav_file.setframerate(sample_rate)
                wav_file.writeframes(audio_data)
            logging.info(f"Audio saved to: {filename}")
            return True
        except Exception as exc:
            logging.error(f"Failed to save audio: {exc}")
            return False
    
    async def _produce_text(client: TTSRealtimeClient):
        """向伺服器發送文本片段"""
        text_fragments = [
            "阿里雲的大模型服務平台阿里雲百鍊是一站式的大模型開發及應用構建平台。",
            "不論是開發人員還是業務人員,都能深入參與大模型應用的設計和構建。",
            "您可以通過簡單的介面操作,在5分鐘內開發出一款大模型應用,",
            "或在幾小時內訓練出一個專屬模型,從而將更多精力專註於應用創新。",
        ]
    
        logging.info("Sending text fragments…")
        for text in text_fragments:
            logging.info(f"Sending fragment: {text}")
            await client.append_text(text)
            await asyncio.sleep(0.1)  # 片段間稍作延時
    
        # 等待伺服器完成內部處理後結束會話
        await asyncio.sleep(1.0)
        await client.finish_session()
    
    async def _run_demo():
        """運行完整 Demo"""
        global _audio_stream
        # 開啟 PyAudio 輸出資料流
        _audio_stream = _audio_pyaudio.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=_AUDIO_SAMPLE_RATE,
            output=True,
            frames_per_buffer=1024
        )
    
        client = TTSRealtimeClient(
            base_url=URL,
            api_key=API_KEY,
            voice="Cherry",
            mode=SessionMode.SERVER_COMMIT,
            audio_callback=_audio_callback
        )
    
        # 建立串連
        await client.connect()
    
        # 並存執行訊息處理與文本發送
        consumer_task = asyncio.create_task(client.handle_messages())
        producer_task = asyncio.create_task(_produce_text(client))
    
        await producer_task  # 等待文本發送完成
    
        # 等待 response.done
        await client.wait_for_response_done()
    
        # 關閉串連並取消消費者任務
        await client.close()
        consumer_task.cancel()
    
        # 關閉音頻流
        if _audio_stream is not None:
            _audio_stream.stop_stream()
            _audio_stream.close()
        _audio_pyaudio.terminate()
    
        # 儲存音頻資料
        os.makedirs("outputs", exist_ok=True)
        _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav"))
    
    def main():
        """同步入口"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s [%(levelname)s] %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S'
        )
        logging.info("Starting QwenTTS Realtime Client demo…")
        asyncio.run(_run_demo())
    
    if __name__ == "__main__":
        main()
    

    運行server_commit.py,即可聽到 Realtime API即時產生的音頻。

    Java

    在TTSRealtimeClient.java的同級目錄下建立另一個 Java 檔案,命名為ServerCommit.java,並將以下代碼複製進檔案中:

    import javax.sound.sampled.*;
    import java.io.*;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.concurrent.ConcurrentLinkedQueue;
    import java.util.concurrent.atomic.AtomicBoolean;
    
    public class ServerCommit {
        // 以下為新加坡地區的配置。
        private static final String URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime";
        // 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
        // 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:private static final String API_KEY = "sk-xxx";
        private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY");
        private static final int SAMPLE_RATE = 24000;
    
        // 音頻資料緩衝
        private static final List<byte[]> audioChunks = new ArrayList<>();
        // 即時播放隊列
        private static final ConcurrentLinkedQueue<byte[]> playbackQueue = new ConcurrentLinkedQueue<>();
        private static final AtomicBoolean playing = new AtomicBoolean(true);
    
        public static void main(String[] args) throws Exception {
            if (API_KEY == null || API_KEY.isEmpty()) {
                throw new IllegalStateException("請設定 DASHSCOPE_API_KEY 環境變數");
            }
    
            // 初始化音頻播放
            AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false);
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
            SourceDataLine audioLine = (SourceDataLine) AudioSystem.getLine(info);
            audioLine.open(format);
            audioLine.start();
    
            // 啟動播放線程
            Thread playerThread = new Thread(() -> {
                while (playing.get() || !playbackQueue.isEmpty()) {
                    byte[] chunk = playbackQueue.poll();
                    if (chunk != null) {
                        audioLine.write(chunk, 0, chunk.length);
                    } else {
                        try { Thread.sleep(10); } catch (InterruptedException ignored) {}
                    }
                }
            });
            playerThread.start();
    
            // 建立 TTS 用戶端
            // 如需使用指令控制功能,請將model替換為qwen3-tts-instruct-flash-realtime,並在TTSRealtimeClient.java中取消instructions的注釋
            TTSRealtimeClient client = new TTSRealtimeClient(
                URL, API_KEY, "Cherry",
                TTSRealtimeClient.SessionMode.SERVER_COMMIT,
                audioData -> {
                    playbackQueue.add(audioData);
                    audioChunks.add(audioData);
                    System.out.println("收到音頻資料: " + audioData.length + " bytes");
                }
            );
    
            client.connect();
    
            // 發送文本片段
            String[] textFragments = {
                "阿里雲的大模型服務平台阿里雲百鍊是一站式的大模型開發及應用構建平台。",
                "不論是開發人員還是業務人員,都能深入參與大模型應用的設計和構建。",
                "您可以通過簡單的介面操作,在5分鐘內開發出一款大模型應用,",
                "或在幾小時內訓練出一個專屬模型,從而將更多精力專註於應用創新。"
            };
    
            System.out.println("開始發送文本...");
            for (String text : textFragments) {
                System.out.println("發送片段: " + text);
                client.appendText(text);
                Thread.sleep(100);
            }
    
            Thread.sleep(1000);
            client.finishSession();
    
            // 等待響應完成
            client.waitForResponseDone();
            client.waitForSessionFinished();
            client.close();
    
            // 等待播放完成
            playing.set(false);
            playerThread.join();
            audioLine.drain();
            audioLine.close();
    
            // 儲存音頻檔案
            saveWav("output.wav");
            System.out.println("完成");
        }
    
        private static void saveWav(String filename) throws IOException {
            if (audioChunks.isEmpty()) {
                System.out.println("沒有音頻資料可儲存");
                return;
            }
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            for (byte[] chunk : audioChunks) {
                bos.write(chunk);
            }
            byte[] allAudio = bos.toByteArray();
            AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false);
            AudioInputStream ais = new AudioInputStream(
                new ByteArrayInputStream(allAudio), format, allAudio.length / 2);
            new File("outputs").mkdirs();
            AudioSystem.write(ais, AudioFileFormat.Type.WAVE,
                new File("outputs/" + filename));
            System.out.println("音頻已儲存到: outputs/" + filename);
        }
    }
    

    編譯並運行ServerCommit.java,即可聽到 Realtime API即時產生的音頻。

    commit 模式

    Python

    在tts_realtime_client.py的同級目錄下建立另一個 Python 檔案,命名為commit.py,並將以下代碼複製進檔案中:

    import os
    import asyncio
    import logging
    import wave
    from tts_realtime_client import TTSRealtimeClient, SessionMode
    import pyaudio
    
    # QwenTTS 服務配置
    # 如需使用指令控制功能,請將model替換為qwen3-tts-instruct-flash-realtime,並在tts_realtime_client.py中取消instructions的注釋
    # 以下為新加坡地區的配置。
    URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"
    # 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:API_KEY="sk-xxx"
    API_KEY = os.getenv("DASHSCOPE_API_KEY")
    
    if not API_KEY:
        raise ValueError("Please set DASHSCOPE_API_KEY environment variable")
    
    # 收集音頻資料
    _audio_chunks = []
    _AUDIO_SAMPLE_RATE = 24000
    _audio_pyaudio = pyaudio.PyAudio()
    _audio_stream = None
    
    def _audio_callback(audio_bytes: bytes):
        """TTSRealtimeClient 音頻回調: 即時播放並緩衝"""
        global _audio_stream
        if _audio_stream is not None:
            try:
                _audio_stream.write(audio_bytes)
            except Exception as exc:
                logging.error(f"PyAudio playback error: {exc}")
        _audio_chunks.append(audio_bytes)
        logging.info(f"Received audio chunk: {len(audio_bytes)} bytes")
    
    def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool:
        """將收集到的音頻資料儲存為 WAV 檔案"""
        if not _audio_chunks:
            logging.warning("No audio data to save")
            return False
    
        try:
            audio_data = b"".join(_audio_chunks)
            with wave.open(filename, 'wb') as wav_file:
                wav_file.setnchannels(1)  # 單聲道
                wav_file.setsampwidth(2)  # 16-bit
                wav_file.setframerate(sample_rate)
                wav_file.writeframes(audio_data)
            logging.info(f"Audio saved to: {filename}")
            return True
        except Exception as exc:
            logging.error(f"Failed to save audio: {exc}")
            return False
    
    async def _user_input_loop(client: TTSRealtimeClient):
        """持續擷取使用者輸入並發送文本,當使用者輸入空文本時發送commit事件並結束本次會話"""
        print("請輸入文本(直接按Enter發送commit事件並結束本次會話,按Ctrl+C或Ctrl+D結束整個程式):")
    
        while True:
            try:
                user_text = input("> ")
                if not user_text:  # 使用者輸入為空白
                    # 空輸入視為一次對話的結束: 提交緩衝區 -> 結束會話 -> 跳出迴圈
                    logging.info("空輸入,發送 commit 事件並結束本次會話")
                    await client.commit_text_buffer()
                    # 適當等待伺服器處理 commit,防止過早結束會話導致丟失音頻
                    await asyncio.sleep(0.3)
                    await client.finish_session()
                    break  # 直接退出使用者輸入迴圈,無需再次斷行符號
                else:
                    logging.info(f"發送文本: {user_text}")
                    await client.append_text(user_text)
    
            except EOFError:  # 使用者按下Ctrl+D
                break
            except KeyboardInterrupt:  # 使用者按下Ctrl+C
                break
    
        # 結束會話
        logging.info("結束會話...")
    async def _run_demo():
        """運行完整 Demo"""
        global _audio_stream
        # 開啟 PyAudio 輸出資料流
        _audio_stream = _audio_pyaudio.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=_AUDIO_SAMPLE_RATE,
            output=True,
            frames_per_buffer=1024
        )
    
        client = TTSRealtimeClient(
            base_url=URL,
            api_key=API_KEY,
            voice="Cherry",
            mode=SessionMode.COMMIT,  # 修改為COMMIT模式
            audio_callback=_audio_callback
        )
    
        # 建立串連
        await client.connect()
    
        # 並存執行訊息處理與使用者輸入
        consumer_task = asyncio.create_task(client.handle_messages())
        producer_task = asyncio.create_task(_user_input_loop(client))
    
        await producer_task  # 等待使用者輸入完成
    
        # 等待 response.done
        await client.wait_for_response_done()
    
        # 關閉串連並取消消費者任務
        await client.close()
        consumer_task.cancel()
    
        # 關閉音頻流
        if _audio_stream is not None:
            _audio_stream.stop_stream()
            _audio_stream.close()
        _audio_pyaudio.terminate()
    
        # 儲存音頻資料
        os.makedirs("outputs", exist_ok=True)
        _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav"))
    
    def main():
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s [%(levelname)s] %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S'
        )
        logging.info("Starting QwenTTS Realtime Client demo…")
        asyncio.run(_run_demo())
    
    if __name__ == "__main__":
        main()
    

    運行commit.py,可多次輸入要合成的文本。在未輸入文本的情況下單擊 Enter 鍵,將從擴音器聽到 Realtime API返回的音頻。

    Java

    在TTSRealtimeClient.java的同級目錄下建立另一個 Java 檔案,命名為Commit.java,並將以下代碼複製進檔案中:

    import javax.sound.sampled.*;
    import java.io.*;
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Scanner;
    import java.util.concurrent.ConcurrentLinkedQueue;
    import java.util.concurrent.atomic.AtomicBoolean;
    
    public class Commit {
        // 以下為新加坡地區的配置。
        private static final String URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime";
        // 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
        // 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:private static final String API_KEY = "sk-xxx";
        private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY");
        private static final int SAMPLE_RATE = 24000;
    
        private static final List<byte[]> audioChunks = new ArrayList<>();
        private static final ConcurrentLinkedQueue<byte[]> playbackQueue = new ConcurrentLinkedQueue<>();
        private static final AtomicBoolean playing = new AtomicBoolean(true);
    
        public static void main(String[] args) throws Exception {
            if (API_KEY == null || API_KEY.isEmpty()) {
                throw new IllegalStateException("請設定 DASHSCOPE_API_KEY 環境變數");
            }
    
            // 初始化音頻播放
            AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false);
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
            SourceDataLine audioLine = (SourceDataLine) AudioSystem.getLine(info);
            audioLine.open(format);
            audioLine.start();
    
            // 啟動播放線程
            Thread playerThread = new Thread(() -> {
                while (playing.get() || !playbackQueue.isEmpty()) {
                    byte[] chunk = playbackQueue.poll();
                    if (chunk != null) {
                        audioLine.write(chunk, 0, chunk.length);
                    } else {
                        try { Thread.sleep(10); } catch (InterruptedException ignored) {}
                    }
                }
            });
            playerThread.start();
    
            // 建立 TTS 用戶端(commit 模式)
            // 如需使用指令控制功能,請將model替換為qwen3-tts-instruct-flash-realtime,並在TTSRealtimeClient.java中取消instructions的注釋
            TTSRealtimeClient client = new TTSRealtimeClient(
                URL, API_KEY, "Cherry",
                TTSRealtimeClient.SessionMode.COMMIT,
                audioData -> {
                    playbackQueue.add(audioData);
                    audioChunks.add(audioData);
                    System.out.println("收到音頻資料: " + audioData.length + " bytes");
                }
            );
    
            client.connect();
    
            // 互動式輸入
            System.out.println("請輸入文本(直接按Enter發送commit事件並結束本次會話,按Ctrl+D結束程式):");
            Scanner scanner = new Scanner(System.in);
            while (true) {
                System.out.print("> ");
                if (!scanner.hasNextLine()) {
                    client.finishSession();
                    break;
                }
                String userText = scanner.nextLine();
                if (userText.isEmpty()) {
                    // 空輸入:提交緩衝區並結束會話
                    System.out.println("空輸入,發送 commit 事件並結束本次會話");
                    client.commitTextBuffer();
                    Thread.sleep(300);
                    client.finishSession();
                    break;
                } else {
                    System.out.println("發送文本: " + userText);
                    client.appendText(userText);
                }
            }
            scanner.close();
    
            // 等待響應完成
            client.waitForResponseDone();
            client.waitForSessionFinished();
            client.close();
    
            // 等待播放完成
            playing.set(false);
            playerThread.join();
            audioLine.drain();
            audioLine.close();
    
            // 儲存音頻檔案
            saveWav("output.wav");
            System.out.println("完成");
        }
    
        private static void saveWav(String filename) throws IOException {
            if (audioChunks.isEmpty()) {
                System.out.println("沒有音頻資料可儲存");
                return;
            }
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            for (byte[] chunk : audioChunks) {
                bos.write(chunk);
            }
            byte[] allAudio = bos.toByteArray();
            AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false);
            AudioInputStream ais = new AudioInputStream(
                new ByteArrayInputStream(allAudio), format, allAudio.length / 2);
            new File("outputs").mkdirs();
            AudioSystem.write(ais, AudioFileFormat.Type.WAVE,
                new File("outputs/" + filename));
            System.out.println("音頻已儲存到: outputs/" + filename);
        }
    }
    

    編譯並運行Commit.java,可多次輸入要合成的文本。在未輸入文本的情況下單擊 Enter 鍵,將從擴音器聽到 Realtime API返回的音頻。

應用於生產環境

串連複用(WebSocket)

WebSocket 串連支援複用:一個合成任務結束後,無需重建立立串連即可開啟下一個任務。

複用流程:

  • Qwen-Audio-TTS / CosyVoice:用戶端發送 finish-task,服務端返回 task-finished 後,可重新發送 run-task 開啟新任務。
  • Qwen-TTS:用戶端發送 session.finish,服務端返回 session.finished 後,可建立新會話開啟下一個任務。

取消任務後複用:對於 Qwen-Audio-TTS / CosyVoice,如果使用 cancel 指令取消當前任務,服務端返回 task-finished 後,同樣可以在當前串連上重新發送 run-task 開啟新任務。詳情請參見取消任務。

重要

  1. 必須等服務端返回結束事件(task-finished 或 session.finished)後才可發起新任務。
  2. Qwen-Audio-TTS、CosyVoice 在複用串連中的不同任務需要使用不同的 task_id。
  3. 任務失敗時服務端返回錯誤事件並關閉串連,該串連不可複用。
  4. 任務結束後 60 秒無新任務,串連自動斷開。

各模型事件說明請參見對應的API參考。

模型限流

模型調用受限流規則約束,超出限制時服務端返回 Requests rate limit exceeded, please try again later. 報錯,需降低調用頻率或並發數後重試。

各模型的限流條件請參見限流。

高並發最佳實務

DashScope SDK 內建池化機制,可複用 WebSocket 串連和合成對象,避免頻繁建立銷毀帶來的開銷。

點擊查看高並發最佳實務

Qwen-Audio-TTS/CosyVoice

Qwen-Audio-TTS 和 CosyVoice 使用相同的 SDK 介面,以下樣本同樣適用於 Qwen-Audio-TTS 系列模型,只需替換 model 和 voice 參數。

前提條件

Python SDK

Python SDK 通過 SpeechSynthesizerObjectPool 管理和複用 SpeechSynthesizer 對象。

對象池在初始化時即建立指定數量的 SpeechSynthesizer 執行個體並建立 WebSocket 串連,擷取對象時可直接發起請求,降低首包延遲。歸還後串連保持活躍,等待下次複用。

實現步驟

  1. 安裝依賴:安裝DashScope依賴(pip install -U dashscope)

  2. 建立並設定物件池

    對象池大小推薦設為峰值並發數的 1.5~2 倍,且不應超過賬戶的 QPS 限制。

    建立全域單例對象池(初始化時建立串連,有一定耗時):

from dashscope.audio.tts_v2 import SpeechSynthesizerObjectPool

connectionPool = SpeechSynthesizerObjectPool(max_size=20)
import dashscope
# 以下為華北2(北京)地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"

重要

  • 在對象池情境中,SpeechSynthesizerObjectPool在初始化時即按當前全域dashscope.api_key與服務端建立 WebSocket 串連。apiKey 僅在 WebSocket 建連握手時寫入Authorization要求標頭用於鑒權,後續任務訊息(如run-task)本身不攜帶 apiKey。池建立後修改dashscope.api_key不會影響池內已建串連——borrow_synthesizer取出的對象(包括歸還後再次複用的對象)仍使用握手時的 apiKey,新值會被靜默忽略,可能導致身份、配額或計費歸屬與預期不一致。注意:borrow_synthesizer也不支援通過參數指定 apiKey。
  • 如確需使用多個不同的 API Key,請為每個 API Key 維護獨立的SpeechSynthesizerObjectPool執行個體。
  1. 從對象池中擷取SpeechSynthesizer對象

    如果當前未歸還的對象數已超過池容量,系統會額外建立新對象。

    此類對象需重建立立串連,不具備複用效果。

speech_synthesizer = connectionPool.borrow_synthesizer(
    model='cosyvoice-v3-flash',
    voice='longanyang',
    seed=12382,
    callback=synthesizer_callback
)
  1. 進行語音合成

    調用SpeechSynthesizer對象的call或streaming_call方法進行語音合成。

  2. 歸還SpeechSynthesizer對象

    任務結束後歸還對象以供複用。

    不要歸還未完成任務或任務失敗的對象。

connectionPool.return_synthesizer(speech_synthesizer)
完整代碼

重要複製使用前請注意:SpeechSynthesizerObjectPool在初始化時即按當前全域dashscope.api_key與服務端建立 WebSocket 串連並完成鑒權;池建立後再修改dashscope.api_key不會影響池內已建串連,新值會被靜默忽略。多 API Key 情境請為每個 API Key 維護獨立的池執行個體。詳見上文重要說明。

# !/usr/bin/env python3
# Copyright (C) Alibaba Group. All Rights Reserved.
# MIT License (https://opensource.org/licenses/MIT)

import os
import time
import threading

import dashscope
from dashscope.audio.tts_v2 import *

USE_CONNECTION_POOL = True
text_to_synthesize = [
    '第一句、歡迎使用阿里巴巴語音合成服務。',
    '第二句、歡迎使用阿里巴巴語音合成服務。',
    '第三句、歡迎使用阿里巴巴語音合成服務。',
]
connectionPool = None

def init_dashscope_api_key():
    '''
    Set your DashScope API-key. More information:
    https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
    '''
    # 新加坡地區和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    if 'DASHSCOPE_API_KEY' in os.environ:
        dashscope.api_key = os.environ[
            'DASHSCOPE_API_KEY']  # load API-key from environment variable DASHSCOPE_API_KEY
    else:
        dashscope.api_key = '<your-dashscope-api-key>'  # set API-key manually

def synthesis_text_to_speech_and_play_by_streaming_mode(text, task_id):
    global USE_CONNECTION_POOL, connectionPool
    '''
    Synthesize speech with given text by streaming mode, async call and play the synthesized audio in real-time.
    for more information, please refer to https://www.alibabacloud.com/help/document_detail/2712523.html
    '''

    complete_event = threading.Event()

    # Define a callback to handle the result

    class Callback(ResultCallback):
        def on_open(self):
            # when using object pool, on_open will be called after task start
            self.file = open(f'result_{task_id}.mp3', 'wb')
            print(f'[task_{task_id}] start')

        def on_complete(self):
            print(f'[task_{task_id}] speech synthesis task complete successfully.')
            complete_event.set()

        def on_error(self, message: str):
            print(f'[task_{task_id}] speech synthesis task failed, {message}')

        def on_close(self):
            # when using object pool, on_close will be called after task finished
            print(f'[task_{task_id}] finished')

        def on_event(self, message):
            # print(f'recv speech synthsis message {message}')
            pass

        def on_data(self, data: bytes) -> None:
            # send to player
            # save audio to file
            self.file.write(data)

    # Call the speech synthesizer callback
    synthesizer_callback = Callback()

    # Initialize the speech synthesizer
    # you can customize the synthesis parameters, like voice, format, sample_rate or other parameters
    if USE_CONNECTION_POOL:
        speech_synthesizer = connectionPool.borrow_synthesizer(
            model='cosyvoice-v3-flash',
            voice='longanyang',
            seed=12382,
            callback=synthesizer_callback
        )
    else:
        speech_synthesizer = SpeechSynthesizer(model='cosyvoice-v3-flash',
                                               voice='longanyang',
                                               seed=12382,
                                               callback=synthesizer_callback)
    try:
        speech_synthesizer.call(text)
    except Exception as e:
        print(f'[task_{task_id}] speech synthesis task failed, {e}')
        if USE_CONNECTION_POOL:
            # close the synthesizer connection manually if task failed when using connection pool.
            speech_synthesizer.close()
        return

    print('[task_{}] Synthesized text: {}'.format(task_id, text))
    complete_event.wait()
    print('[task_{}][Metric] requestId: {}, first package delay ms: {}'.format(
        task_id,
        speech_synthesizer.get_last_request_id(),
        speech_synthesizer.get_first_package_delay()))
    if USE_CONNECTION_POOL:
        connectionPool.return_synthesizer(speech_synthesizer)

# main function
if __name__ == '__main__':
    # 必須先設定 dashscope.api_key 和 base_websocket_api_url,再建立 SpeechSynthesizerObjectPool。
    # 池在初始化時即按當前全域 dashscope.api_key 建立 WebSocket 串連,
    # 池建立後再修改 dashscope.api_key 不會影響池內已建串連。
    # 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
    dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
    init_dashscope_api_key()

    if USE_CONNECTION_POOL:
        print('creating connection pool')
        start_time = time.time() * 1000
        connectionPool = SpeechSynthesizerObjectPool(max_size=3)
        end_time = time.time() * 1000
        print('connection pool created, cost: {} ms'.format(end_time - start_time))

    task_thread_list = []
    for task_id in range(3):
        thread = threading.Thread(
            target=synthesis_text_to_speech_and_play_by_streaming_mode,
            args=(text_to_synthesize[task_id], task_id))
        task_thread_list.append(thread)

    for task_thread in task_thread_list:
        task_thread.start()

    for task_thread in task_thread_list:
        task_thread.join()

    if USE_CONNECTION_POOL:
        connectionPool.shutdown()

資源管理與異常處理

  • 任務成功:當語音合成任務正常完成時,必須調用 connectionPool.return_synthesizer(speech_synthesizer) 將 SpeechSynthesizer 對象歸還到池中,以便複用。

    重要不要歸還未完成任務或任務失敗的SpeechSynthesizer對象。

  • 任務失敗:當 SDK 內部或商務邏輯拋出異常導致任務中斷時,主動關閉底層的 WebSocket 串連:speech_synthesizer.close()

  • 在所有語音合成任務完成後,要通過如下方式關閉對象池:connectionPool.shutdown()。

  • 在服務出現TaskFailed報錯時,不需要額外處理。

Java SDK

Java SDK通過內建的串連池和自訂的對象池協同工作,實現最佳效能。

  • 串連池:SDK 內部整合的 OkHttp3 串連池,負責管理和複用底層的 WebSocket 串連,減少網路握手開銷。此功能預設開啟。
  • 對象池:基於 commons-pool2 實現,用於維護一組已預先建立好串連的 SpeechSynthesizer 對象。從池中擷取對象可消除串連建立的延遲,顯著降低首包延遲。

實現步驟

  1. 添加依賴

    根據專案構建工具,在依賴設定檔中添加 dashscope-sdk-java 和 commons-pool2。

    以Maven和Gradle為例,配置如下:

    Maven

    1. 開啟Maven專案的pom.xml檔案。
    2. 在<dependencies>標籤內添加以下依賴資訊。
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dashscope-sdk-java</artifactId>
        <!-- 請將 'the-latest-version' 替換為2.16.9及以上版本,可在如下連結查詢相關版本號碼:https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
        <version>the-latest-version</version>
    </dependency>
    
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <!-- 請將 'the-latest-version' 替換為最新版本,可在如下連結查詢相關版本號碼:https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
        <version>the-latest-version</version>
    </dependency>
    
    1. 儲存pom.xml檔案。
    2. 使用Maven命令(如mvn clean install或mvn compile)來更新專案依賴

    Gradle

    1. 開啟Gradle專案的build.gradle檔案。
    2. 在dependencies塊內添加以下依賴資訊。
    dependencies {
        // 請將 'the-latest-version' 替換為2.16.6及以上版本,可在如下連結查詢相關版本號碼:https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java
        implementation group: 'com.alibaba', name: 'dashscope-sdk-java', version: 'the-latest-version'
    
        // 請將 'the-latest-version' 替換為最新版本,可在如下連結查詢相關版本號碼:https://mvnrepository.com/artifact/org.apache.commons/commons-pool2
        implementation group: 'org.apache.commons', name: 'commons-pool2', version: 'the-latest-version'
    }
    
    1. 儲存build.gradle檔案。
    2. 在命令列中,切換到專案根目錄,執行以下Gradle命令來更新專案依賴。
    ./gradlew build --refresh-dependencies
    

    或者,如果使用Windows系統,命令應為:

    gradlew build --refresh-dependencies
    
  2. 配置串連池

    通過環境變數配置串連池關鍵參數:

    環境變數

    描述

    DASHSCOPE_CONNECTION_POOL_SIZE

    串連池大小。

    推薦值:峰值並發數的 2 倍以上。

    預設值:32。

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS

    最大非同步請求數。

    推薦值:與 DASHSCOPE_CONNECTION_POOL_SIZE 保持一致。

    預設值:32。

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST

    單主機最大非同步請求數。

    推薦值:與 DASHSCOPE_CONNECTION_POOL_SIZE 保持一致。

    預設值:32。

  3. 設定物件池

    通過環境變數設定物件池大小:

    環境變數

    描述

    COSYVOICE_OBJECTPOOL_SIZE

    對象池大小。

    推薦值:峰值並發數的 1.5 至 2 倍。

    預設值:500。

    重要

    • 對象池的大小(COSYVOICE_OBJECTPOOL_SIZE)必須小於或等於串連池的大小(DASHSCOPE_CONNECTION_POOL_SIZE)。否則,當對象池請求對象時,若串連池已滿,會導致調用線程阻塞,等待可用串連。
    • 對象池大小不應超過賬戶的 QPS(每秒查詢率)限制。

    通過如下代碼建立對象池:

class CosyvoiceObjectPool {
    // 。。。這裡省略其它代碼,完整樣本請參見完整代碼
    public static GenericObjectPool<SpeechSynthesizer> getInstance() {
        lock.lock();
        if (synthesizerPool == null) {
            // 您可以在這裡設定對象池的大小。或在環境變數COSYVOICE_OBJECTPOOL_SIZE中設定。
            // 建議設定為伺服器最大並發串連數的1.5到2倍。
            int objectPoolSize = getObjectivePoolSize();
            SpeechSynthesizerObjectFactory speechSynthesizerObjectFactory =
                    new SpeechSynthesizerObjectFactory();
            GenericObjectPoolConfig<SpeechSynthesizer> config =
                    new GenericObjectPoolConfig<>();
            config.setMaxTotal(objectPoolSize);
            config.setMaxIdle(objectPoolSize);
            config.setMinIdle(objectPoolSize);
            synthesizerPool =
                    new GenericObjectPool<>(speechSynthesizerObjectFactory, config);
        }
        lock.unlock();
        return synthesizerPool;
    }
}
  1. 從對象池中擷取SpeechSynthesizer對象

    如果當前未歸還的對象數量已超過對象池的最大容量,系統會額外建立一個新的SpeechSynthesizer對象。

    此類新建立的對象需要重新進行初始化並建立 WebSocket 串連,無法利用對象池的既有串連資源,因此不具備複用效果。

synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
  1. 進行語音合成

    從對象池借出SpeechSynthesizer對象後,需先調用updateParamAndCallback(param, callback)關聯本次任務的參數與回調,再調用streamingCall或call方法進行語音合成。

    重要

    • 在對象池情境中,updateParamAndCallback會被多次調用(每次借出對象時都需調用一次,用於切換該次任務的回調和任務級參數,如voice、format等)。多次調用時傳入的apiKey必須始終相同。updateParamAndCallback只更新當前SpeechSynthesizer執行個體的本地欄位,不會重建底層 WebSocket 串連;而 SDK 僅在 WebSocket 建連握手時將apiKey寫入Authorization要求標頭用於鑒權,後續任務訊息(如run-task)本身不攜帶apiKey。因此只要複用的串連未斷開,傳入新的apiKey不會被發送到服務端,請求實際仍會使用串連首次握手時的apiKey,可能導致身份、配額或計費歸屬與預期不一致。
    • 如確需使用多個不同的 API Key,請為每個 API Key 維護獨立的對象池執行個體。
  2. 歸還SpeechSynthesizer對象

    語音合成任務結束後,歸還SpeechSynthesizer對象,以便後續任務可以複用該對象。

    不要歸還未完成任務或任務失敗的對象。

CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
完整代碼

重要複製使用前請注意:對象池情境下多次調用updateParamAndCallback時傳入的 apiKey必須始終相同——SDK 不會更新已建立串連的 apiKey,傳入不同的 apiKey 不會生效。多 API Key 情境請為每個 API Key 維護獨立的對象池執行個體。詳見上文重要說明。

import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisAudioFormat;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.exception.NoApiKeyException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

/**
 * 您需要在專案中引入org.apache.commons.pool2和DashScope相關的包。
 *
 * DashScope SDK 2.16.6及後續版本針對高並發情境進行了最佳化,
 * DashScope SDK 2.16.6之前的版本不推薦在高並發情境下使用。
 *
 *
 * 在對TTS服務進行高並發調用之前,
 * 請通過以下環境變數配置串連池的相關參數。
 *
 * DASHSCOPE_MAXIMUM_ASYNC_REQUESTS
 * DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST
 * DASHSCOPE_CONNECTION_POOL_SIZE
 *
 */

class SpeechSynthesizerObjectFactory
        extends BasePooledObjectFactory<SpeechSynthesizer> {
    public SpeechSynthesizerObjectFactory() {
        super();
    }
    @Override
    public SpeechSynthesizer create() throws Exception {
        return new SpeechSynthesizer();
    }

    @Override
    public PooledObject<SpeechSynthesizer> wrap(SpeechSynthesizer obj) {
        return new DefaultPooledObject<>(obj);
    }
}

class CosyvoiceObjectPool {
    public static GenericObjectPool<SpeechSynthesizer> synthesizerPool;
    public static String COSYVOICE_OBJECTPOOL_SIZE_ENV = "COSYVOICE_OBJECTPOOL_SIZE";
    public static int DEFAULT_OBJECT_POOL_SIZE = 500;
    private static Lock lock = new java.util.concurrent.locks.ReentrantLock();
    public static int getObjectivePoolSize() {
        try {
            Integer n = Integer.parseInt(System.getenv(COSYVOICE_OBJECTPOOL_SIZE_ENV));
            System.out.println("Using Object Pool Size In Env: "+ n);
            return n;
        } catch (NumberFormatException e) {
            System.out.println("Using Default Object Pool Size: "+ DEFAULT_OBJECT_POOL_SIZE);
            return DEFAULT_OBJECT_POOL_SIZE;
        }
    }
    public static GenericObjectPool<SpeechSynthesizer> getInstance() {
        lock.lock();
        if (synthesizerPool == null) {
            // 您可以在這裡設定對象池的大小。或在環境變數COSYVOICE_OBJECTPOOL_SIZE中設定。
            // 建議設定為伺服器最大並發串連數的1.5到2倍。
            int objectPoolSize = getObjectivePoolSize();
            SpeechSynthesizerObjectFactory speechSynthesizerObjectFactory =
                    new SpeechSynthesizerObjectFactory();
            GenericObjectPoolConfig<SpeechSynthesizer> config =
                    new GenericObjectPoolConfig<>();
            config.setMaxTotal(objectPoolSize);
            config.setMaxIdle(objectPoolSize);
            config.setMinIdle(objectPoolSize);
            synthesizerPool =
                    new GenericObjectPool<>(speechSynthesizerObjectFactory, config);
        }
        lock.unlock();
        return synthesizerPool;
    }
}

class SynthesizeTaskWithCallback implements Runnable {
    String[] textArray;
    String requestId;
    long timeCost;
    public SynthesizeTaskWithCallback(String[] textArray) {
        this.textArray = textArray;
    }
    @Override
    public void run() {
        SpeechSynthesizer synthesizer = null;
        long startTime = System.currentTimeMillis();
        // if recv onError
        final boolean[] hasError = {false};
        try {
            class ReactCallback extends ResultCallback<SpeechSynthesisResult> {
                ReactCallback() {}

                @Override
                public void onEvent(SpeechSynthesisResult message) {
                    if (message.getAudioFrame() != null) {
                        try {
                            byte[] bytesArray = message.getAudioFrame().array();
                            System.out.println("收到音頻,音頻檔案流length為:" + bytesArray.length);
                        } catch (Exception e) {
                            throw new RuntimeException(e);
                        }
                    }
                }

                @Override
                public void onComplete() {}

                @Override
                public void onError(Exception e) {
                    System.out.println(e.getMessage());
                    e.printStackTrace();
                    hasError[0] = true;
                }
            }

            SpeechSynthesisParam param =
                    SpeechSynthesisParam.builder()
                            .model("cosyvoice-v3-flash")
                            .voice("longanyang")
                            // 新加坡地區和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                            // 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:.apiKey("sk-xxx")
                            .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                            .format(SpeechSynthesisAudioFormat
                                    .MP3_22050HZ_MONO_256KBPS) // 流式合成使用PCM或者MP3
                            .build();

            try {
                synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
                // 注意:對象池情境下,多次調用 updateParamAndCallback 時傳入的 apiKey 必須始終相同;SDK 不會更新已建立串連的 apiKey,傳入不同的 apiKey 不會生效。詳見上文“進行語音合成”步驟的重要說明。
                synthesizer.updateParamAndCallback(param, new ReactCallback());
                for (String text : textArray) {
                    synthesizer.streamingCall(text);
                }
                Thread.sleep(20);
                synthesizer.streamingComplete(60000);
                requestId = synthesizer.getLastRequestId();
            } catch (Exception e) {
                System.out.println("Exception e: " + e.toString());
                hasError[0] = true;
            }
        } catch (Exception e) {
            hasError[0] = true;
            throw new RuntimeException(e);
        }
        if (synthesizer != null) {
            try {
                if (hasError[0] == true) {
                    // 如果出現異常,則關閉串連並在對象池中禁用該對象。
                    synthesizer.getDuplexApi().close(1000, "bye");
                    CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
                } else {
                    // 如果任務正常結束,則歸還對象。
                    CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            long endTime = System.currentTimeMillis();
            timeCost = endTime - startTime;
            System.out.println("[線程 " + Thread.currentThread() + "] 語音合成任務結束。耗時 " + timeCost + " ms, RequestId " + requestId);
        }
    }
}

@Slf4j
public class SynthesizeTextToSpeechWithCallbackConcurrently {
    public static void checkoutEnv(String envName, int defaultSize) {
        if (System.getenv(envName) != null) {
            System.out.println("[ENV CHECK]: " + envName + " "
                    + System.getenv(envName));
        } else {
            System.out.println("[ENV CHECK]: " + envName
                    + " Using Default which is " + defaultSize);
        }
    }

    public static void main(String[] args)
            throws InterruptedException, NoApiKeyException {
        // 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
        Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
        // Check for connection pool env
        checkoutEnv("DASHSCOPE_CONNECTION_POOL_SIZE", 32);
        checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS", 32);
        checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST", 32);
        checkoutEnv(CosyvoiceObjectPool.COSYVOICE_OBJECTPOOL_SIZE_ENV, CosyvoiceObjectPool.DEFAULT_OBJECT_POOL_SIZE);

        int runTimes = 3;
        // Create the pool of SpeechSynthesis objects
        ExecutorService executorService = Executors.newFixedThreadPool(runTimes);

        for (int i = 0; i < runTimes; i++) {
            // Record the task submission time
            LocalDateTime submissionTime = LocalDateTime.now();
            executorService.submit(new SynthesizeTaskWithCallback(new String[] {
                    "床前明月光,", "疑似地上霜。", "舉頭望明月,", "低頭思故鄉。"}));
        }

        // Shut down the ExecutorService and wait for all tasks to complete
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);
        System.exit(0);
    }
}

推薦配置

以下配置基於在指定規格的阿里雲伺服器上僅運行 CosyVoice 語音合成服務的測試結果。過高的並發數可能導致任務處理延遲。

其中單機並發數指的是同一時刻正在啟動並執行CosyVoice語音合成任務數,也可以理解為背景工作執行緒數。

機器配置(阿里雲)

單機最大並發數

對象池大小

串連池大小

4核8GiB

100

500

2000

8核16GiB

150

500

2000

16核32GiB

200

500

2000

資源管理與異常處理

  • 任務成功:當語音合成任務正常完成時,必須調用GenericObjectPool的returnObject方法將SpeechSynthesizer對象歸還到池中,以便複用。

    在當前代碼中,對應CosyvoiceObjectPool.getInstance().returnObject(synthesizer)

    重要不要歸還未完成任務或任務失敗的SpeechSynthesizer對象。

  • 任務失敗:當 SDK 內部或商務邏輯拋出異常導致任務中斷時,必須執行以下兩個操作:

    1. 主動關閉底層的 WebSocket 串連
    2. 從對象池中廢棄該對象,防止被再次使用
// 在當前代碼中對應如下內容
// 關閉串連
synthesizer.getDuplexApi().close(1000, "bye");
// 在對象池中廢棄出現異常的synthesizer
CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
  • 在服務出現TaskFailed報錯時,不需要額外處理。

調用預熱與耗時統計說明

在對 DashScope Java SDK 進行並發調用延遲等效能評估時,建議先執行充分的預熱操作,確保測量結果反映穩定點下的真實效能,避免初始串連耗時導致資料偏差。

串連複用機制

DashScope Java SDK 通過全域單例的串連池高效管理和複用 WebSocket 串連,旨在減少頻繁建連和斷連的開銷,提升高並發情境下的處理能力。

該機制的工作特點如下:

  • 按需建立:SDK 不會在服務啟動時預建立 WebSocket 串連,而是在首次調用時按需建立。

  • 限時複用:請求完成後,串連將在池中保留最多 60 秒以備複用。

    • 若 60 秒內有新請求,將複用現有串連,避免重複握手開銷。
    • 若串連空閑超過 60 秒,將被自動關閉以釋放資源。
預熱的重要性

在以下情境中,串連池中可能沒有可複用的活躍串連,導致請求需要建立串連:

  • 應用剛啟動,尚未發起任何調用。
  • 服務空閑時間超過 60 秒,池中串連已因逾時而關閉。

在這些情境下,首次請求需完成 WebSocket 建連(TCP 握手、TLS 協商、協議升級),延遲顯著高於後續複用串連的請求。若未預熱,效能測試結果會因包含建連耗時而產生偏差。

SDK側延遲與實際首包延遲的區別

SDK側列印的首包延遲(如通過 get_first_package_delay() 擷取的值)包含了 WebSocket 建聯和網路傳輸等耗時,並不等同於模型服務的實際首包延遲。

實際首包延遲是指從服務端收到 run-task 指令到返回第一個 result-generated 事件的時間間隔,該值可通過服務端日誌查看。

在高並發情境下,由於大量串連的建立和資源調度,SDK側列印的延遲數值可能顯著高於服務端的實際首包延遲。如果觀察到 SDK 報告的首包延遲較高,建議:

  • 對比服務端日誌中的首包延遲(從 run-task 到首個 result-generated),確認模型推理效能是否正常。
  • 使用上述對象池或串連池機制進行預熱,消除 WebSocket 建連開銷,使 SDK 側列印的延遲更接近實際首包延遲。
推薦做法

為擷取可靠的效能資料,在正式進行效能壓測或延遲統計前,請遵循以下預熱步驟:

  1. 類比正式測試的並發層級,提前發起一定數量的調用(例如,持續 1-2 分鐘),以充分填充串連池。
  2. 確認串連池已建立並維持足夠的活躍串連後,再開始正式的效能資料採集。

通過合理的預熱,可使 SDK 串連池進入穩定複用狀態,從而測量出更具代表性的延遲指標,真實反映服務線上上平穩運行時的效能。

Java SDK常見異常

異常 1、 業務流量平穩,但是伺服器 TCP 串連數持續上升

出錯原因:類型一:

每一個 SDK 對象建立時都會申請一個串連。如果沒有使用對象池,每一次任務結束後對象都被析構。此時這一個串連將進入無引用狀態,需要等待 61s 秒後服務端報錯連線逾時才會真正斷開,這會導致這個串連在 61 秒內不可複用。

在高並發情境下,新的任務在發現沒有可複用串連時會建立新串連,會造成如下後果:

  1. 串連數持續上升。
  2. 由於串連數過多,伺服器資源不足,伺服器卡頓。
  3. 串連池被打滿、新任務由於啟動時需要等待可用串連而阻塞。
類型二:

對象池配置的MaxIdle小於MaxTotal,導致在對象閑置時,超過MaxIdle的對象被銷毀,從而造成串連泄漏。泄漏的串連需要等待61秒逾時後斷連,同類型一造成串連數持續上升。

解決方案:

對於類型一,使用對象池解決。

對於類型二,檢查對象池配置參數,設定MaxIdle和MaxTotal相等,關閉對象池自動銷毀策略解決。

異常 2、任務耗時比正常調用多 60 秒

同“異常 1”,串連池已經達到最大串連限制,新的任務需要等待無引用狀態的串連 61 秒觸發逾時後才可以獲得串連。

異常 3、服務啟動時任務慢,之後慢慢恢複正常

出錯原因:

在高並發調用時,同一個對象會複用同一個WebSocket串連,因此WebSocket串連只會在服務啟動時建立。需要注意的是,任務啟動階段如果立刻開始較高並發調用,同時建立過多的WebSocket串連會導致阻塞。

解決方案:

啟動服務後逐步提升並發量,或增加預熱任務。

異常 4、服務端報錯 Invalid action('run-task')! Please follow the protocol!

出錯原因:

這是由於出現了用戶端報錯後,服務端不知道用戶端出錯,串連處於任務中狀態。此時串連和對象被複用並開啟下一個任務,導致流程錯誤,下一個任務失敗。

解決方案:

在拋出異常後主動關閉 WebSocket 串連後歸還對象池。

異常 5、業務流量平穩,調用量出現異常尖刺

出錯原因:

同時建立過多 WebSocket 串連導致阻塞,但業務流量持續打進來,導致任務短時間積壓,並且在阻塞後所有積壓任務立刻調用。這會造成調用量尖刺,並且有可能造成瞬時超過帳號的並發數限制導致部分任務失敗、伺服器卡頓等。

這種瞬間建立過多 WebSocket 的情況多發生於:

  • 服務啟動階段
  • 網路出現異常,大量 WebSocket 串連同時中斷重連
  • 某一時刻出現大量服務端報錯,導致大量 WebSocket 重連。常見報錯如並發數超過帳號限制(“Requests rate limit exceeded, please try again later.”)。

解決方案:

  1. 檢查網路情況。
  2. 排查尖刺前是否出現大量其他服務端報錯。
  3. 提高帳號並發限制。
  4. 調小對象池和串連池大小,通過對象池上限限制最大並發數。
  5. 提升伺服器配置或擴充機器數。

異常 6、隨著並發數提升,所有任務都變慢

解決方案:

  1. 檢查是否已經達到網路頻寬上限。
  2. 檢查實際並發數是否已經過高。

支援的模型與地區

新加坡

調用以下模型時,請選擇新加坡地區的API Key:

  • Qwen-Audio-TTS:qwen-audio-3.0-tts-plus、qwen-audio-3.0-tts-flash

  • CosyVoice:cosyvoice-v3-plus、cosyvoice-v3-flash

  • Qwen-TTS:

    • Qwen3-TTS-Instruct-Flash-Realtime:qwen3-tts-instruct-flash-realtime(穩定版,當前等同qwen3-tts-instruct-flash-realtime-2026-01-22)、qwen3-tts-instruct-flash-realtime-2026-01-22(最新快照版)
    • Qwen3-TTS-VD-Realtime:qwen3-tts-vd-realtime-2026-01-15(最新快照版)、qwen3-tts-vd-realtime-2025-12-16(快照版)
    • Qwen3-TTS-VC-Realtime:qwen3-tts-vc-realtime-2026-01-15(最新快照版)、qwen3-tts-vc-realtime-2025-11-27(快照版)
    • Qwen3-TTS-Flash-Realtime:qwen3-tts-flash-realtime(穩定版,當前等同qwen3-tts-flash-realtime-2025-11-27)、qwen3-tts-flash-realtime-2025-11-27(最新快照版)、qwen3-tts-flash-realtime-2025-09-18(快照版)

華北2(北京)

調用以下模型時,請選擇北京地區的API Key:

  • Qwen-Audio-TTS:qwen-audio-3.0-tts-plus、qwen-audio-3.0-tts-flash

  • CosyVoice:cosyvoice-v3.5-plus、cosyvoice-v3.5-flash、cosyvoice-v3-plus、cosyvoice-v3-flash、cosyvoice-v2

  • Qwen-TTS:

    • Qwen3-TTS-Instruct-Flash-Realtime:qwen3-tts-instruct-flash-realtime(穩定版,當前等同qwen3-tts-instruct-flash-realtime-2026-01-22)、qwen3-tts-instruct-flash-realtime-2026-01-22(最新快照版)
    • Qwen3-TTS-VD-Realtime:qwen3-tts-vd-realtime-2026-01-15(最新快照版)、qwen3-tts-vd-realtime-2025-12-16(快照版)
    • Qwen3-TTS-VC-Realtime:qwen3-tts-vc-realtime-2026-01-15(最新快照版)、qwen3-tts-vc-realtime-2025-11-27(快照版)
    • Qwen3-TTS-Flash-Realtime:qwen3-tts-flash-realtime(穩定版,當前等同qwen3-tts-flash-realtime-2025-11-27)、qwen3-tts-flash-realtime-2025-11-27(最新快照版)、qwen3-tts-flash-realtime-2025-09-18(快照版)
    • Qwen-TTS-Realtime:qwen-tts-realtime(穩定版,當前等同qwen-tts-realtime-2025-07-15)、qwen-tts-realtime-latest(最新版,當前等同qwen-tts-realtime-2025-07-15)、qwen-tts-realtime-2025-07-15(快照版)

支援的音色

不同模型支援的音色不同。將請求參數 voice 設為音色列表中 voice參數 列的值即可。

API參考

常見問題

Q:語音合成發音錯誤怎麼辦?多音字如何控制發音?

  • 將多音字替換為同音的其他漢字,快速解決發音問題。
  • 使用 SSML 標記語言控制發音。

Q:使用複刻音色產生的音頻無聲音如何排查?

  1. 確認音色狀態

    調用CosyVoice聲音複刻/設計API介面,確認音色的 status 是否為 OK。

  2. 檢查模型版本一致性

    確保複刻音色時使用的 target_model 參數與語音合成時的 model 參數完全一致。例如:

    • 複刻時使用 cosyvoice-v3-plus
    • 合成時也必須使用 cosyvoice-v3-plus
  3. 驗證源音頻品質

    檢查複刻音色時使用的源音頻是否符合CosyVoice聲音複刻/設計API:

    • 音頻時間長度:10-20秒
    • 音質清晰
    • 無背景雜音
  4. 檢查請求參數

    確認語音合成請求中的 voice 參數已設定為複刻音色的 ID。

Q:聲音複刻後合成效果不穩定或語音不完整怎麼辦?

如果複刻音色後合成的語音出現以下問題:

  • 語音播放不完整,唯讀出部分文字
  • 合成效果不穩定,時好時壞
  • 語音中包含異常停頓或靜音段

可能原因:源音頻品質不符合要求。

解決方案:請檢查源音頻是否符合錄音操作指南中的音頻要求,建議按照錄音指南重新錄製。

Q:為什麼語音合成的實際時間長度與 WAV 檔案顯示的時間長度不一致?

語音合成採用流式機制,邊合成邊返回資料,因此儲存的 WAV 檔案頭中的時間長度是預估值,存在一定誤差。如需精確時間長度,可將 format 設定為 pcm,待擷取完整合成結果後自行添加 WAV 檔案頭資訊。

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

請按以下情境逐一排查:

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

    1. 音頻格式一致性:請求參數中的音頻格式須與檔案尾碼一致(如參數為 wav 則檔案須為 .wav)。
    2. 播放器相容性:確認播放器支援該音訊格式和採樣率。
  2. 流式播放音訊情況

    1. 將音頻流儲存為完整檔案,嘗試用播放器播放。如果檔案無法播放,請參考情境 1 的排查方法。
    2. 如果檔案可正常播放,則問題在流式播放實現。請確認播放器支援流式播放(如 ffmpeg、pyaudio、AudioFormat、MediaSource 等)。

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

請按以下步驟逐一排查:

  1. 檢查文本發送速度:確保發送間隔合理,避免上段音頻播完後下段文本尚未到達。

  2. 檢查回呼函數效能:

    • 確認回呼函數中無阻塞性商務邏輯。
    • 回調運行在 WebSocket 線程,阻塞會影響資料接收。建議將音頻資料寫入獨立緩衝區,在其他線程中處理。
  3. 檢查網路穩定性:網路波動可能導致音頻傳輸中斷或延遲。

Q:語音合成耗時較長是什麼原因?

請按以下步驟排查:

  1. 檢查輸入間隔

    如果是流式合成,確認文本發送間隔是否過長,過長會導致合成總時間長度增加。

  2. 分析效能指標

    • 首包延遲:正常約 500ms。
    • RTF(即時率 = 合成總耗時 / 音頻時間長度):正常應小於 1.0。

Q:合成的音頻中讀出了文本裡的特殊符號怎麼辦?

Qwen-TTS 系列模型可能將文本中的部分特殊符號(如 Markdown 加粗標記 **)合成為語音。可通過以下方式處理:

  1. 調用前對文本進行預先處理,去除特殊符號。
  2. 改用 CosyVoice 模型。

Q:如何限制 API Key 僅用於語音合成服務(許可權隔離)?

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