All Products
Search
Document Center

Alibaba Cloud Model Studio:Sintesis suara real-time

Last Updated:Sep 09, 2026

Lakukan konversi teks-ke-ucapan secara streaming dengan latensi paket pertama yang rendah. Sintesis suara real-time mendukung input dan output streaming, kloning suara, desain suara, serta kontrol audio detail halus untuk asisten suara, buku audio, dan layanan pelanggan cerdas.

Ikhtisar

Konversi teks ke ucapan secara real-time dengan latensi rendah.

  • Input dan output streaming dengan latensi paket pertama yang rendah
  • Laju ucapan, pitch, volume, dan bitrate yang dapat disesuaikan untuk kontrol audio detail halus
  • Kompatibel dengan format audio utama (PCM, WAV, MP3, Opus) dengan output laju sampel hingga 48 kHz
  • Mendukung Kontrol instruksi, yang memungkinkan Anda mengontrol ekspresivitas ucapan melalui instruksi bahasa alami
  • Mendukung Kloning suara dan Desain Suara untuk pembuatan suara kustom
  • Mendukung Tag emosi dan tag bahasa kaya, yang memungkinkan Anda menyematkan tag emosi atau efek suara dalam teks

Untuk skenario batch seperti buku audio dan sulih suara materi kursus, gunakan Sintesis suara non-real-time. Untuk panduan pemilihan model, lihat Sintesis suara.

Prasyarat

Memulai cepat

Contoh berikut menunjukkan sintesis suara untuk setiap model. Untuk contoh dan detail parameter lainnya, lihat Referensi API.

Qwen-Audio-TTS

Contoh berikut mensintesis suara menggunakan suara sistem.

Untuk menggunakan fitur Kontrol instruksi, atur instruksi melalui parameter instruction.

# coding=utf-8

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

# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')

# Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'

# Model
# qwen-audio-3.0-tts-flash/qwen-audio-3.0-tts-plus: Gunakan suara seperti longanhuan_v3.6.
# Setiap suara mendukung bahasa yang berbeda. Untuk mensintesis bahasa non-Cina seperti Jepang atau Korea, pilih suara yang mendukung bahasa target. Lihat daftar suara untuk detailnya.
model = "qwen-audio-3.0-tts-flash"
# Suara
voice = "longanhuan_v3.6"

# Buat instance SpeechSynthesizer dan teruskan parameter permintaan seperti model dan suara di konstruktor
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# Kirim teks yang akan disintesis dan dapatkan audio biner
audio = synthesizer.call("How is the weather today?")
# Pengiriman teks pertama memerlukan pembentukan koneksi WebSocket, sehingga latensi paket pertama mencakup waktu penyiapan koneksi
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
    synthesizer.get_last_request_id(),
    synthesizer.get_first_package_delay()))

# Simpan audio ke file lokal
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 {
    // Model
    // qwen-audio-3.0-tts-flash/qwen-audio-3.0-tts-plus: Gunakan suara seperti longanhuan_v3.6.
    // Setiap suara mendukung bahasa yang berbeda. Untuk mensintesis bahasa non-Cina seperti Jepang atau Korea, pilih suara yang mendukung bahasa target. Lihat daftar suara untuk detailnya.
    private static String model = "qwen-audio-3.0-tts-flash";
    // Suara
    private static String voice = "longanhuan_v3.6";

    public static void streamAudioDataToSpeaker() {
        // Parameter permintaan
        SpeechSynthesisParam param =
                SpeechSynthesisParam.builder()
                        // Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                        // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: .apiKey("sk-xxx")
                        .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                        .model(model) // Model
                        .voice(voice) // Suara
                        .build();

        // Mode sinkron: nonaktifkan callback (parameter kedua null)
        SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
        ByteBuffer audio = null;
        try {
            // Blokir hingga audio dikembalikan
            audio = synthesizer.call("How is the weather today?");
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // Tutup koneksi WebSocket saat tugas selesai
            synthesizer.getDuplexApi().close(1000, "bye");
        }
        if (audio != null) {
            // Simpan data audio ke file lokal "output.mp3"
            File file = new File("output.mp3");
            // Pengiriman teks pertama memerlukan pembentukan koneksi WebSocket, sehingga latensi paket pertama mencakup waktu penyiapan koneksi
            // Catatan: getFirstPackageDelay() memerlukan dashscope-sdk-java 2.18.0 atau lebih baru
            System.out.println(
                    "[Metric] requestId: "
                            + synthesizer.getLastRequestId()
                            + ", first-packet latency (ms): "
                            + 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) {
        // Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
        Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
        streamAudioDataToSpeaker();
        System.exit(0);
    }
}

CosyVoice

Selain WebSocket, model ini juga mendukung protokol AOQ. Untuk integrasi sisi klien yang memprioritaskan latensi stabil, ketahanan pada jaringan lemah, serta penekanan noise dan pembatalan gema full-duplex bawaan, AOQ direkomendasikan. Untuk perbandingan protokol, lihat Ikhtisar API Realtime.

Pentingcosyvoice-v3.5-plus dan cosyvoice-v3.5-flash hanya tersedia di wilayah Beijing dan hanya mendukung skenario desain suara dan kloning suara (tidak ada suara sistem). Sebelum digunakan, buat suara kustom melalui Kloning suara atau Desain Suara, lalu atur voice ke ID suara dan model ke nama model yang sesuai dalam kode Anda.

Contoh berikut mensintesis suara menggunakan suara sistem (lihat Daftar Suara CosyVoice).

Untuk menggunakan fitur Kontrol instruksi, atur instruksi melalui parameter instruction.

# coding=utf-8

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

# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')

# Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'

# Model
# Versi model yang berbeda memerlukan suara yang sesuai:
# cosyvoice-v3-flash/cosyvoice-v3-plus: Gunakan suara seperti longanyang.
# cosyvoice-v2: Gunakan suara seperti longxiaochun_v2.
# Setiap suara mendukung bahasa yang berbeda. Untuk mensintesis bahasa non-Cina seperti Jepang atau Korea, pilih suara yang mendukung bahasa target. Lihat daftar suara Qwen-Audio-TTS/CosyVoice untuk detailnya.
model = "cosyvoice-v3-flash"
# Suara
voice = "longanyang"

# Buat instance SpeechSynthesizer dan teruskan parameter permintaan seperti model dan suara di konstruktor
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# Kirim teks yang akan disintesis dan dapatkan audio biner
audio = synthesizer.call("How is the weather today?")
# Pengiriman teks pertama memerlukan pembentukan koneksi WebSocket, sehingga latensi paket pertama mencakup waktu penyiapan koneksi
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
    synthesizer.get_last_request_id(),
    synthesizer.get_first_package_delay()))

# Simpan audio ke file lokal
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 {
        // Model
        // Versi model yang berbeda memerlukan suara yang sesuai:
        // cosyvoice-v3-flash/cosyvoice-v3-plus: Gunakan suara seperti longanyang.
        // cosyvoice-v2: Gunakan suara seperti longxiaochun_v2.
        // Setiap suara mendukung bahasa yang berbeda. Untuk mensintesis bahasa non-Cina seperti Jepang atau Korea, pilih suara yang mendukung bahasa target. Lihat daftar suara Qwen-Audio-TTS/CosyVoice untuk detailnya.
        private static String model = "cosyvoice-v3-flash";
        // Suara
        private static String voice = "longanyang";

        public static void streamAudioDataToSpeaker() {
            // Parameter permintaan
            SpeechSynthesisParam param =
                    SpeechSynthesisParam.builder()
                            // Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                            // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: .apiKey("sk-xxx")
                            .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                            .model(model) // Model
                            .voice(voice) // Suara
                            .build();

            // Mode sinkron: nonaktifkan callback (parameter kedua null)
            SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
            ByteBuffer audio = null;
            try {
                // Blokir hingga audio dikembalikan
                audio = synthesizer.call("How is the weather today?");
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                // Tutup koneksi WebSocket saat tugas selesai
                synthesizer.getDuplexApi().close(1000, "bye");
            }
            if (audio != null) {
                // Simpan data audio ke file lokal "output.mp3"
                File file = new File("output.mp3");
                // Pengiriman teks pertama memerlukan pembentukan koneksi WebSocket, sehingga latensi paket pertama mencakup waktu penyiapan koneksi
                // Catatan: getFirstPackageDelay() memerlukan dashscope-sdk-java 2.18.0 atau lebih baru
                System.out.println(
                        "[Metric] requestId: "
                                + synthesizer.getLastRequestId()
                                + ", first-packet latency (ms): "
                                + 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) {
            // Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
            Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
            streamAudioDataToSpeaker();
            System.exit(0);
        }
    }

Qwen-TTS

Contoh berikut mensintesis suara menggunakan suara sistem (lihat Suara yang didukung).

Untuk menggunakan fitur Kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime dan atur instruksi melalui parameter 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 = [
    'Right? I love supermarkets like this.',
    'Especially during Chinese New Year,',
    'I go shopping at supermarkets.',
    'And I feel',
    'absolutely thrilled!',
    'I want to buy so many things!'
]

DO_VIDEO_TEST = False

def init_dashscope_api_key():
    """
        Tetapkan Kunci API DashScope Anda. Informasi lebih lanjut:
        https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
    """

    # Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    if 'DASHSCOPE_API_KEY' in os.environ:
        dashscope.api_key = os.environ[
            'DASHSCOPE_API_KEY']  # Muat Kunci API dari variabel lingkungan DASHSCOPE_API_KEY
    else:
        dashscope.api_key = 'your-dashscope-api-key'  # Tetapkan Kunci API secara manual

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

    def on_open(self) -> None:
        print('koneksi dibuka, inisialisasi pemutar')

    def on_close(self, close_status_code, close_msg) -> None:
        self.file.close()
        print('koneksi ditutup dengan kode: {}, pesan: {}, hancurkan pemutar'.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('mulai sesi: {}'.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'respons {qwen_tts_realtime.get_last_response_id()} selesai')
            if 'session.finished' == type:
                print('sesi selesai')
                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('Menginisialisasi ...')

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        # Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime
        model='qwen3-tts-flash-realtime',
        callback=callback,
        # Wilayah Singapura
        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,
        # Untuk menggunakan kontrol instruksi, hapus komentar baris berikut dan ganti model dengan qwen3-tts-instruct-flash-realtime
        # instructions='Speak quickly with a rising intonation, suitable for introducing fashion products.',
        # optimize_instructions=True,
        mode = 'server_commit'
    )
    for text_chunk in text_to_synthesize:
        print(f'kirim teks: {text_chunk}')
        qwen_tts_realtime.append_text(text_chunk)
        time.sleep(0.1)
    qwen_tts_realtime.finish()
    callback.wait_for_finished()
    print('[Metric] sesi: {}, latensi audio pertama: {}'.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 = [
    'This is the first sentence.',
    'This is the second sentence.',
    'This is the third sentence.',
]

DO_VIDEO_TEST = False

def init_dashscope_api_key():
    """
        Tetapkan Kunci API DashScope Anda. Informasi lebih lanjut:
        https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
    """

    # Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    if 'DASHSCOPE_API_KEY' in os.environ:
        dashscope.api_key = os.environ[
            'DASHSCOPE_API_KEY']  # Muat Kunci API dari variabel lingkungan DASHSCOPE_API_KEY
    else:
        dashscope.api_key = 'your-dashscope-api-key'  # Tetapkan Kunci API secara manual

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('koneksi dibuka, inisialisasi pemutar')

    def on_close(self, close_status_code, close_msg) -> None:
        print('koneksi ditutup dengan kode: {}, pesan: {}, hancurkan pemutar'.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('mulai sesi: {}'.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'respons {qwen_tts_realtime.get_last_response_id()} selesai')
                self.complete_event.set()
                self.file.close()
            if 'session.finished' == type:
                print('sesi selesai')
                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('Menginisialisasi ...')

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        # Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime
        model='qwen3-tts-flash-realtime',
        callback=callback,
        # Wilayah Singapura
        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,
        # Untuk menggunakan kontrol instruksi, hapus komentar baris berikut dan ganti model dengan qwen3-tts-instruct-flash-realtime
        # instructions='Speak quickly with a rising intonation, suitable for introducing fashion products.',
        # optimize_instructions=True,
        mode = 'commit'
    )
    print(f'kirim teks: {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'kirim teks: {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'kirim teks: {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] sesi: {}, latensi audio pertama: {}'.format(
                    qwen_tts_realtime.get_session_id(),
                    qwen_tts_realtime.get_first_audio_delay(),
                    ))

Java

Mode server commit

appendText()

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 = {
            "Right? I really love this kind of supermarket.",
            "Especially during the Chinese New Year.",
            "Going to the supermarket.",
            "It just makes me feel.",
            "Super, super happy!",
            "I want to buy so many things!"
    };
    public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;

    // Pemutar audio PCM real-time
    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();

        // Inisialisasi format audio dan saluran audio.
        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);
                            // Tulis data audio ke 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();
        }

        // Putar potongan audio dan blokir hingga pemutaran selesai.
        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);
            // Tunggu hingga audio yang dibuffer selesai diputar.
            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();

            // Simpan file audio lengkap.
            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()
                // Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime.
                .model("qwen3-tts-flash-realtime")
                // Wilayah China (Beijing)
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                // Kunci API berbeda antara Singapura dan China (Beijing). Lihat https://www.alibabacloud.com/help/en/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);

        // Buat instance pemutar audio real-time.
        RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);

        QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
            @Override
            public void onOpen() {
                // Tangani pembentukan koneksi.
            }
            @Override
            public void onEvent(JsonObject message) {
                String type = message.get("type").getAsString();
                switch(type) {
                    case "session.created":
                        // Tangani pembuatan sesi.
                        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();
                        // Putar audio secara real-time.
                        audioPlayer.write(recvAudioB64);
                        break;
                    case "response.done":
                        // Tangani penyelesaian respons.
                        break;
                    case "session.finished":
                        // Tangani penghentian sesi.
                        completeLatch.get().countDown();
                    default:
                        break;
                }
            }
            @Override
            public void onClose(int code, String reason) {
                // Tangani penutupan koneksi.
            }
        });
        qwenTtsRef.set(qwenTtsRealtime);
        try {
            qwenTtsRealtime.connect();
        } catch (NoApiKeyException e) {
            throw new RuntimeException(e);
        }
        QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
                .voice("Cherry")
                .responseFormat(ttsFormat)
                .mode("server_commit")
                // Untuk menggunakan kontrol instruksi, hapus komentar baris berikut dan ganti model dengan 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();

        // Tunggu hingga pemutaran audio selesai, lalu matikan pemutar.
        audioPlayer.waitForComplete();
        audioPlayer.shutdown();
        System.exit(0);
    }
}

Mode commit

commit()

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;
    // Pemutar audio PCM real-time
    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();

        // Inisialisasi format audio dan saluran audio.
        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);
                            // Tulis data audio ke 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();
        }

        // Putar potongan audio dan blokir hingga pemutaran selesai.
        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);
            // Tunggu hingga audio yang dibuffer selesai diputar.
            Thread.sleep(audioLength - 10);
        }

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

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

        public void waitForComplete() throws InterruptedException {
            // Tunggu hingga semua data audio yang dibuffer selesai diputar.
            while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
                Thread.sleep(100);
            }
            // Tunggu hingga saluran audio dikosongkan.
            line.drain();
        }

        public void shutdown() throws InterruptedException {
            stopped.set(true);
            decoderThread.join();
            playerThread.join();
            // Simpan file audio lengkap.
            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()
                // Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime.
                .model("qwen3-tts-flash-realtime")
                // Wilayah China (Beijing)
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                // Kunci API berbeda antara Singapura dan China (Beijing). Lihat https://www.alibabacloud.com/help/en/model-studio/get-api-key.
                .apikey(System.getenv("DASHSCOPE_API_KEY"))
                .build();

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

        // Buat instance pemutar real-time.
        RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);

        final AtomicReference<QwenTtsRealtime> qwenTtsRef = new AtomicReference<>(null);
        QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
            @Override
            public void onOpen() {
                System.out.println("koneksi dibuka");
                System.out.println("Masukkan teks dan tekan Enter untuk mengirim. Masukkan 'quit' untuk keluar dari program.");
            }
            @Override
            public void onEvent(JsonObject message) {
                String type = message.get("type").getAsString();
                switch(type) {
                    case "session.created":
                        System.out.println("mulai sesi: " + message.get("session").getAsJsonObject().get("id").getAsString());
                        break;
                    case "response.audio.delta":
                        String recvAudioB64 = message.get("delta").getAsString();
                        byte[] rawAudio = Base64.getDecoder().decode(recvAudioB64);
                        // Putar audio secara real-time.
                        audioPlayer.write(recvAudioB64);
                        break;
                    case "response.done":
                        System.out.println("respons selesai");
                        // Tunggu hingga pemutaran audio selesai.
                        try {
                            audioPlayer.waitForComplete();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                        // Siapkan untuk input berikutnya.
                        completeLatch.get().countDown();
                        break;
                    case "session.finished":
                        System.out.println("sesi selesai");
                        if (qwenTtsRef.get() != null) {
                            System.out.println("[Metric] respons: " + qwenTtsRef.get().getResponseId() +
                                    ", latensi audio pertama: " + qwenTtsRef.get().getFirstAudioDelay() + " ms");
                        }
                        completeLatch.get().countDown();
                    default:
                        break;
                }
            }
            @Override
            public void onClose(int code, String reason) {
                System.out.println("koneksi ditutup kode: " + code + ", alasan: " + reason);
                try {
                    // Tunggu hingga pemutaran selesai, lalu matikan pemutar.
                    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")
                // Untuk menggunakan kontrol instruksi, hapus komentar baris berikut dan ganti model dengan qwen3-tts-instruct-flash-realtime.
                // .instructions("")
                // .optimizeInstructions(true)
                .build();
        qwenTtsRealtime.updateSession(config);

        // Baca input pengguna dalam loop.
        while (true) {
            System.out.print("Masukkan teks untuk disintesis: ");
            String text = scanner.nextLine();

            // Keluar saat pengguna memasukkan 'quit'.
            if ("quit".equalsIgnoreCase(text.trim())) {
                System.out.println("Menutup koneksi...");
                qwenTtsRealtime.finish();
                completeLatch.get().await();
                break;
            }

            // Lewati input kosong.
            if (text.trim().isEmpty()) {
                continue;
            }

            // Inisialisasi ulang latch countdown.
            completeLatch.set(new CountDownLatch(1));

            // Kirim teks.
            qwenTtsRealtime.appendText(text);
            qwenTtsRealtime.commit();

            // Tunggu hingga sintesis saat ini selesai.
            completeLatch.get().await();
        }

        // Bersihkan sumber daya.
        audioPlayer.waitForComplete();
        audioPlayer.shutdown();
        scanner.close();
        System.exit(0);
    }
}

Konfigurasi sesi

Mode interaksi Qwen-TTS

API Realtime Qwen-TTS menyediakan dua mode interaksi:

  • Mode server_commit: Server secara otomatis menangani segmentasi teks dan penjadwalan sintesis. Mode ini cocok untuk sintesis berkelanjutan pada blok teks besar. Aplikasi klien hanya perlu menambahkan teks tanpa mengelola segmentasi atau pengiriman.
  • Mode commit: Klien secara eksplisit mengirim buffer teks untuk memicu sintesis. Mode ini ideal untuk skenario yang memerlukan kontrol presisi atas waktu sintesis, seperti sintesis per giliran dalam percakapan AI.

Beralih mode interaksi:

  • WebSocket: Atur bidang mode pada event session.update.
{
    "type": "session.update",
    "session": {
        "mode": "server_commit"
    }
}
  • SDK Python: Atur parameter mode dalam metode update_session.
qwen_tts_realtime.update_session(
    voice='Cherry',
    response_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
    mode='server_commit'
)
  • SDK Java: Atur parameter mode melalui QwenTtsRealtimeConfig.builder().
QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
        .voice("Cherry")
        .responseFormat(ttsFormat)
        .mode("server_commit")
        .build();
qwenTtsRealtime.updateSession(config);

Untuk contoh kode SDK lengkap, lihat SDK Python dan SDK Java. Untuk detail mengenai siklus hidup event WebSocket dan penggunaan kembali koneksi, lihat Referensi API WebSocket.

Fitur lanjutan

Kontrol instruksi

Kontrol instruksi memungkinkan penyesuaian nada, kecepatan, emosi, dan karakteristik timbre ucapan melalui deskripsi bahasa alami, tanpa perlu mengonfigurasi parameter audio yang kompleks.

Spesifikasi instruksi berdasarkan model:

Qwen-Audio-TTS

Model yang didukung: qwen-audio-3.0-tts-plus, qwen-audio-3.0-tts-flash

Suara sistem dan suara kloning: menerima instruksi apa pun.

CosyVoice

Model yang didukung: cosyvoice-v3.5-plus, cosyvoice-v3.5-flash, cosyvoice-v3-plus, cosyvoice-v3-flash

Persyaratan format instruksi bervariasi tergantung pada model:

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

    • Suara kloning/desain: menerima instruksi apa pun.
    • Suara sistem: v3.5 tidak mendukung suara sistem.
  • cosyvoice-v3-plus:

    • Suara kloning/desain: tidak mendukung kontrol instruksi.
    • Suara sistem: instruksi harus menggunakan format dan konten tetap. Lihat Daftar Suara CosyVoice.
  • cosyvoice-v3-flash:

    • Suara kloning/desain: menerima instruksi apa pun.
    • Suara sistem: instruksi harus menggunakan format dan konten tetap. Lihat Daftar Suara CosyVoice.

Penggunaan: Tentukan konten instruksi melalui parameter instruction.

Bahasa yang didukung untuk teks instruksi:

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

    • Suara kloning/desain: Cina, Inggris, Prancis, Jerman, Jepang, Korea, Rusia, Portugis, Thailand, Indonesia, dan Vietnam.
    • Suara sistem: v3.5 tidak mendukung suara sistem.
  • cosyvoice-v3-plus:

    • Suara kloning/desain: Cina, Inggris, Prancis, Jerman, Jepang, Korea, dan Rusia.
    • Suara sistem: instruksi harus menggunakan format dan konten tetap. Lihat Daftar Suara CosyVoice.
  • cosyvoice-v3-flash:

    • Suara kloning/desain: Cina, Inggris, Prancis, Jerman, Jepang, Korea, dan Rusia.
    • Suara sistem: hanya Cina.

Batas panjang teks instruksi: Maksimal 100 karakter. Karakter Cina (termasuk Cina sederhana/tradisional, kanji Jepang, dan hanja Korea) dihitung sebagai 2 karakter. Semua karakter lain (seperti tanda baca, huruf, angka, kana Jepang, dan hangul Korea) dihitung sebagai 1 karakter.

Qwen-TTS

Model yang didukung: Hanya model seri Qwen3-TTS-Instruct-Flash-Realtime.

Penggunaan: Tentukan konten instruksi melalui parameter instructions.

Bahasa yang didukung untuk teks instruksi: Hanya Cina dan Inggris.

Batas panjang teks instruksi: Maksimal 1.600 token.

Kasus penggunaan:

  • Narasi buku audio dan drama radio
  • Narasi iklan dan video promosi
  • Suara karakter game dan animasi
  • Asisten suara yang ekspresif secara emosional
  • Narasi dokumenter dan siaran berita

Menulis deskripsi suara yang efektif:

  • Prinsip dasar:

    1. Bersifat spesifik, bukan samar: Gunakan kata-kata yang menggambarkan kualitas vokal, seperti "dalam", "jernih", atau "kecepatan sedikit cepat". Hindari istilah subjektif atau samar seperti "bagus" atau "normal".
    2. Bersifat multidimensi, bukan satu dimensi: Deskripsi yang baik biasanya mencakup beberapa dimensi (seperti jenis kelamin, usia, dan emosi). Menulis hanya "suara perempuan" terlalu umum untuk menghasilkan timbre yang khas.
    3. Bersifat objektif, bukan subjektif: Fokus pada karakteristik fisik dan perseptual suara. Misalnya, gunakan "nada lebih tinggi dengan intonasi energetik" daripada "suara favorit saya".
    4. Bersifat orisinal, bukan imitatif: Jelaskan kualitas vokal alih-alih meminta peniruan individu tertentu (seperti selebriti atau aktor). Model tidak mendukung peniruan, dan hal tersebut dapat menimbulkan risiko pelanggaran hak cipta.
    5. Bersifat ringkas, bukan redundan: Pastikan setiap kata memiliki tujuan. Hindari sinonim berulang atau pengubah yang tidak bermakna.
  • Referensi dimensi deskripsi:

    Gabungkan dimensi-dimensi berikut untuk mendeskripsikan suara. Semakin banyak dimensi yang Anda sertakan, semakin akurat output-nya.

    Dimension

    Contoh deskripsi

    Gender

    Laki-laki, perempuan, androgini

    Age

    Anak (5–12), remaja (13–18), dewasa muda (19–35), paruh baya (36–55), lansia (55+)

    Pitch

    Tinggi, mid-range, rendah, agak tinggi, agak rendah

    Speed

    Cepat, sedang, lambat, agak cepat, agak lambat

    Emotion

    Ceria, tenang, lembut, serius, hidup, terkendali, menenangkan

    Characteristics

    Magnetis, jernih, serak, hangat, manis, kaya, kuat

    Use case

    Siaran berita, narasi iklan, buku audio, karakter animasi, asisten suara, narasi dokumenter

  • Contoh:

    • Gaya siaran standar: artikulasi jelas dan tepat dengan pelafalan sempurna
    • Suara perempuan muda yang ceria dengan kecepatan lebih cepat dan intonasi naik yang terasa jelas, cocok untuk memperkenalkan produk fesyen
    • Laki-laki paruh baya yang tenang, kecepatan lambat, suara dalam dan magnetis, cocok untuk membacakan berita atau narasi dokumenter
    • Perempuan intelektual yang lembut, sekitar usia 30 tahun, dengan nada merata, cocok untuk narasi buku audio
    • Suara anak yang lucu, kira-kira anak perempuan berusia 8 tahun, berbicara dengan kualitas yang sedikit kekanak-kanakan, cocok untuk narasi karakter animasi

Dialek

Bagian ini menjelaskan cara menghasilkan ucapan dalam dialek Tiongkok (seperti dialek Henan, dialek Sichuan, dan Kanton). Metode konfigurasi bervariasi tergantung pada model dan jenis voice.

Konfigurasi dialek berdasarkan model:

Qwen-Audio-TTS

  • System voices: Pilih salah satu jenis voice berikut:

    • System voice dengan dukungan dialek bawaan, yang menghasilkan dialek yang sesuai tanpa perlu konfigurasi tambahan.
    • Voice yang mendukung Instruction control dan dapat dikonfigurasi untuk menghasilkan dialek tertentu melalui teks instruksi.
  • Voice cloning voices: Konfigurasikan melalui fitur Instruction control. Misalnya, atur teks instruksi menjadi 请用河南话表达.

Dialek yang didukung: Lihat kolom "Supported languages" untuk setiap model di Qwen-Audio-TTS.

CosyVoice

  • System voices: Pilih salah satu jenis voice berikut dari daftar Voice CosyVoice:

    • System voice dengan dukungan dialek bawaan (seperti longshange_v3), yang menghasilkan dialek yang sesuai tanpa perlu konfigurasi tambahan.
    • Voice yang mendukung Instruction control dan dapat dikonfigurasi untuk menghasilkan dialek tertentu (seperti longanhuan_v3), yang ditentukan melalui teks instruksi.
  • Voice cloning voices: Konfigurasikan melalui fitur Instruction control. Misalnya, atur teks instruksi menjadi 请用河南话表达.

  • Voice design voices: tidak mendukung dialek.

Dialek yang didukung: Lihat kolom "Supported languages" untuk setiap model di CosyVoice.

Contoh: Gunakan cosyvoice-v3-flash dengan voice longanhuan_v3, dan atur teks instruksi menjadi "请用河南话表达。" untuk menghasilkan ucapan dalam dialek Henan.

# coding=utf-8

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

# API Key berbeda antara Wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio Tiongkok Anda: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')

# Berikut adalah konfigurasi untuk Wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi tergantung wilayah.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'

# Model
# Versi model yang berbeda memerlukan voice yang sesuai:
# cosyvoice-v3-flash/cosyvoice-v3-plus: Gunakan voice seperti longanyang.
# cosyvoice-v2: Gunakan voice seperti longxiaochun_v2.
# Pilih voice yang sesuai untuk target language Anda
model = "cosyvoice-v3-flash"
# Voice
voice = "longanhuan_v3"

# Buat instance SpeechSynthesizer dan teruskan parameter permintaan seperti model dan voice di konstruktor
synthesizer = SpeechSynthesizer(model=model, voice=voice, instruction="请用河南话表达。")
# Kirim teks yang akan disintesis dan dapatkan audio biner
audio = synthesizer.call("叫你去买盐,你买回来一袋面,这不是弄啥嘞吗!")
# Pengiriman teks pertama memerlukan pembentukan koneksi WebSocket, sehingga latensi paket pertama mencakup waktu pembentukan koneksi
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
    synthesizer.get_last_request_id(),
    synthesizer.get_first_package_delay()))

# Simpan audio ke file lokal
with open('output.mp3', 'wb') as f:
    f.write(audio)

Qwen-TTS

  • System voices: Gunakan system voice dengan dukungan dialek bawaan. Lihat daftar voice Qwen-TTS di Voice yang didukung.
  • Voice cloning voices: tidak mendukung dialek.
  • Voice design voices: tidak mendukung dialek.

Dialek yang didukung: Lihat kolom "Supported languages" untuk setiap model di Qwen3-TTS.

Tag emosi dan bahasa kaya

Model seri Qwen-Audio-TTS mendukung penyematan tag emosi dan bahasa kaya secara langsung dalam teks untuk sintesis suara (parameter text). Tag-tag ini mengontrol ekspresi emosional atau menyisipkan efek vokal (seperti tawa dan desahan) pada posisi tertentu, sehingga menghasilkan ucapan yang lebih ekspresif tanpa perlu mengonfigurasi parameter audio yang kompleks.

PentingModel yang didukung: hanya qwen-audio-3.0-tts-plus dan qwen-audio-3.0-tts-flash.

Batasan: Hanya mode streaming unidirectional yang didukung.

Tag kontrol

Tag kontrol menetapkan emosi atau gaya ucapan. Tempatkan tag dalam teks untuk memengaruhi seluruh teks berikutnya hingga tag kontrol berikutnya muncul atau kalimat secara otomatis tersegmentasi karena panjangnya.

Tag

Deskripsi

[sad]

Sedih

[amazed]

Takjub

[deep and loud shouting]

Teriakan keras dan dalam

[trembling]

Gemetar

[angry]

Marah

[excited]

Bersemangat

[sarcastic]

Sarkastik

[curious]

Ingin tahu

[like dracula]

Gaya Dracula (dalam dan menyeramkan)

[bored]

Bosan

[tired]

Lelah

[scornful]

Menghina

[shouting]

Berteriak

[asmr]

Bisikan lembut ASMR

[panicked]

Panik

[mischievously]

Nakal

[empathetic]

Empatik

[whispers]

Berbisik

[reluctantly]

Enggan

[crying]

Menangis

[serious]

Serius

[very slowly]

Ucapan sangat lambat

[very fast]

Ucapan sangat cepat

Tag bahasa kaya

Tag bahasa kaya menyisipkan efek vokal pada posisi saat ini dalam teks tanpa memengaruhi gaya emosional teks di sekitarnya.

Tag

Deskripsi

[gasp]

Astaga!

[sighing]

Mendesah

[clears throat]

Membersihkan tenggorokan

[giggles]

Tertawa kecil

[laughing]

Tertawa

[cough]

Batuk

[snorts]

Snort

Contoh penggunaan

Contoh berikut menunjukkan cara menggabungkan tag kontrol dan tag bahasa kaya dalam parameter text:

[excited]What a beautiful day today![laughing]Let's go out and have fun together!

Dalam teks ini, [excited] adalah tag kontrol yang menerapkan emosi bersemangat pada seluruh teks berikutnya. [laughing] adalah tag bahasa kaya yang menyisipkan tawa pada posisi tersebut sebelum melanjutkan sintesis teks sisanya.

Anda juga dapat beralih antar emosi berbeda dalam teks yang sama:

[serious]Please pay attention to the safety precautions.[excited]Alright, let's get started now!

Di sini, [serious] menetapkan nada serius pada kalimat pertama, dan [excited] mengubah nada menjadi bersemangat mulai dari kalimat kedua.

Cancel task

Jika Anda perlu menginterupsi proses sintesis saat ini selama sintesis suara real-time, kirimkan perintah cancel. Setelah pembatalan, server segera mengakhiri task saat ini dan mengembalikan event completion. Anda dapat memulai task sintesis baru pada koneksi WebSocket yang sama tanpa perlu terhubung ulang.

Penggunaan:

  • Python SDK: versi 1.26.4 atau lebih baru, panggil SpeechSynthesizer.streaming_cancel().
  • Java SDK: versi 2.22.26 atau lebih baru, panggil SpeechSynthesizer.streamingCancel().
  • Protokol raw WebSocket: Kirim event finish-task dan atur directive=cancel dalam input.

PentingBatasan model:

  • China (Beijing): Semua model Qwen-Audio-TTS mendukung fitur ini. Model CosyVoice memerlukan versi v2 atau lebih baru.
  • Singapura: Semua model Qwen-Audio-TTS mendukung fitur ini. Model CosyVoice tidak mendukung fitur ini.

Panggilan protokol mentah WebSocket

Contoh berikut menunjukkan cara menghubungkan langsung ke server melalui protokol mentah WebSocket, cocok untuk skenario tanpa SDK DashScope. Ini merupakan implementasi minimal yang dapat dijalankan. Untuk detail protokol WebSocket, lihat referensi API untuk masing-masing model.

Lihat contoh panggilan protokol mentah WebSocket

Qwen-Audio-TTS/CosyVoice

Qwen-Audio-TTS dan CosyVoice menggunakan protokol WebSocket yang sama. Contoh berikut menggunakan qwen-audio-3.0-tts-flash. Untuk menggunakan CosyVoice, ganti parameter model dengan model CosyVoice (seperti cosyvoice-v3-flash) dan parameter voice dengan suara yang diinginkan.

Go

package main

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

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

const (
	// Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi berdasarkan wilayah.
	wsURL      = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"
	outputFile = "output.mp3"
)

func main() {
	// API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
	// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: apiKey := "sk-xxx"
	apiKey := os.Getenv("DASHSCOPE_API_KEY")

	// Hapus file output
	os.Remove(outputFile)
	os.Create(outputFile)

	// Hubungkan ke 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("Koneksi gagal, kode status HTTP: %d\n", resp.StatusCode)
		}
		fmt.Println("Koneksi gagal:", err)
		return
	}
	defer conn.Close()

	// Hasilkan ID tugas
	taskID := uuid.New().String()
	fmt.Printf("ID tugas yang dihasilkan: %s\n", taskID)

	// Mengirim event 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,
				// Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, error "Text request limit violated, expected 1." akan dikembalikan
				"enable_ssml": false,
			},
			"input": map[string]interface{}{},
		},
	}

	runTaskJSON, _ := json.Marshal(runTaskCmd)
	fmt.Printf("Mengirim event run-task: %s\n", string(runTaskJSON))

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

	textSent := false

	// Proses pesan
	for {
		messageType, message, err := conn.ReadMessage()
		if err != nil {
			fmt.Println("Gagal membaca pesan:", err)
			break
		}

		// Proses pesan biner
		if messageType == websocket.BinaryMessage {
			fmt.Printf("Menerima pesan biner, panjang: %d\n", len(message))
			file, _ := os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
			file.Write(message)
			file.Close()
			continue
		}

		// Proses pesan teks
		messageStr := string(message)
		fmt.Printf("Menerima pesan teks: %s\n", strings.ReplaceAll(messageStr, "\n", ""))

		// Uraikan JSON untuk mendapatkan jenis 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("Jenis event: %s\n", event)

					switch event {
					case "task-started":
						fmt.Println("=== Menerima event task-started ===")

						if !textSent {
							// Mengirim event continue-task

							texts := []string{"Before my bed, moonlight shines bright, I suspect it's frost upon the ground.", "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home."}

							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("Mengirim event continue-task: %s\n", string(continueTaskJSON))

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

							textSent = true

							// Tunda sebelum mengirim finish-task
							time.Sleep(500 * time.Millisecond)

							// Mengirim event 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("Mengirim event finish-task: %s\n", string(finishTaskJSON))

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

					case "task-finished":
						fmt.Println("=== Tugas selesai ===")
						return

					case "task-failed":
						fmt.Println("=== Tugas gagal ===")
						if header["error_message"] != nil {
							fmt.Printf("Pesan error: %s\n", header["error_message"])
						}
						return

					case "result-generated":
						fmt.Println("Menerima event result-generated")
					}
				}
			}
		}
	}
}

C#

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

class Program {
    // API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: private static readonly string ApiKey = "sk-xxx"
    private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("Variabel lingkungan DASHSCOPE_API_KEY belum diatur.");

    // Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi berdasarkan wilayah.
    private const string WebSocketUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
    // Jalur file output
    private const string OutputFilePath = "output.mp3";

    // Klien WebSocket
    private static ClientWebSocket _webSocket = new ClientWebSocket();
    // Sumber token pembatalan
    private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
    // ID tugas
    private static string? _taskId;
    // Apakah tugas telah dimulai
    private static TaskCompletionSource<bool> _taskStartedTcs = new TaskCompletionSource<bool>();

    static async Task Main(string[] args) {
        try {
            // Hapus file output
            ClearOutputFile(OutputFilePath);

            // Hubungkan ke layanan WebSocket
            await ConnectToWebSocketAsync(WebSocketUrl);

            // Mulai tugas penerima pesan
            Task receiveTask = ReceiveMessagesAsync();

            // Kirim event run-task
            _taskId = GenerateTaskId();
            await SendRunTaskCommandAsync(_taskId);

            // Tunggu event task-started
            await _taskStartedTcs.Task;

            // Kirim event continue-task
            string[] texts = {
                "Before my bed, moonlight shines bright,",
                "I suspect it\'s frost upon the ground.",
                "I raise my eyes to gaze at the bright moon,",
                "then bow my head, thinking of home."
            };
            foreach (string text in texts) {
                await SendContinueTaskCommandAsync(text);
            }

            // Kirim event finish-task
            await SendFinishTaskCommandAsync(_taskId);

            // Tunggu tugas penerima selesai
            await receiveTask;

            Console.WriteLine("Tugas selesai, koneksi ditutup.");
        } catch (OperationCanceledException) {
            Console.WriteLine("Tugas dibatalkan.");
        } catch (Exception ex) {
            Console.WriteLine($"Terjadi error: {ex.Message}");
        } finally {
            _cancellationTokenSource.Cancel();
            _webSocket.Dispose();
        }
    }

    private static void ClearOutputFile(string filePath) {
        if (File.Exists(filePath)) {
            File.WriteAllText(filePath, string.Empty);
            Console.WriteLine("File output dihapus.");
        } else {
            Console.WriteLine("File output tidak ada, tidak perlu dihapus.");
        }
    }

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

        // Atur header koneksi WebSocket
        _webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
        _webSocket.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");

        try {
            await _webSocket.ConnectAsync(uri, _cancellationTokenSource.Token);
            Console.WriteLine("Berhasil terhubung ke layanan WebSocket.");
        } catch (OperationCanceledException) {
            Console.WriteLine("Koneksi WebSocket dibatalkan.");
        } catch (Exception ex) {
            Console.WriteLine($"Koneksi WebSocket gagal: {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,
                // Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, error "Text request limit violated, expected 1." akan dikembalikan
                enable_ssml = false
            },
            input = new { }
        });

        await SendJsonMessageAsync(command);
        Console.WriteLine("Event run-task dikirim.");
    }

    private static async Task SendContinueTaskCommandAsync(string text) {
        if (_taskId == null) {
            throw new InvalidOperationException("ID tugas belum diinisialisasi.");
        }

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

        await SendJsonMessageAsync(command);
        Console.WriteLine("Event continue-task dikirim.");
    }

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

        await SendJsonMessageAsync(command);
        Console.WriteLine("Event finish-task dikirim.");
    }

    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("Pengiriman pesan dibatalkan.");
        }
    }

    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("Tugas dimulai.");
                        _taskStartedTcs.TrySetResult(true);
                        break;
                    case "task-finished":
                        Console.WriteLine("Tugas selesai.");
                        _cancellationTokenSource.Cancel();
                        break;
                    case "task-failed":
                        Console.WriteLine("Tugas gagal: " + response.RootElement.GetProperty("header").GetProperty("error_message").GetString());
                        _cancellationTokenSource.Cancel();
                        break;
                    default:
                        // result-generated dapat ditangani di sini
                        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) {
                // Proses data biner
                Console.WriteLine("Menerima data biner...");

                // Simpan data biner ke file
                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("Penerimaan pesan dibatalkan.");
            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

Struktur direktori kode contoh:

my-php-project/

├── composer.json

├── vendor/

└── index.php

Isi composer.json (sesuaikan versi dependensi sesuai kebutuhan):

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

Isi index.php:

<?php

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

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

// API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi berdasarkan wilayah.
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'; // URL server WebSocket
$output_file = 'output.mp3'; // Jalur file output

$loop = Loop::get();

if (file_exists($output_file)) {
    // Hapus isi file
    file_put_contents($output_file, '');
}

// Buat konektor kustom
$socketConnector = new SocketConnector($loop, [
    'tcp' => [
        'bindto' => '0.0.0.0:0',
    ],
    'tls' => [
        'verify_peer' => false,
        'verify_peer_name' => false,
    ],
]);

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

$headers = [
    'Authorization' => 'bearer ' . $api_key,
    'X-DashScope-DataInspection' => 'enable'
];

$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $output_file) {
    echo "Terhubung ke server WebSocket\n";

    // Hasilkan ID tugas
    $taskId = generateTaskId();

    // Kirim event run-task
    sendRunTaskMessage($conn, $taskId);

    // Definisikan fungsi untuk mengirim event continue-task
    $sendContinueTask = function() use ($conn, $loop, $taskId) {
        // Teks yang akan dikirim
        $texts = ["Before my bed, moonlight shines bright,", "I suspect it\'s frost upon the ground.", "I raise my eyes to gaze at the bright moon,", "then bow my head, thinking of home."];
        $continueTaskCount = 0;
        foreach ($texts as $text) {
            $continueTaskMessage = json_encode([
                "header" => [
                    "action" => "continue-task",
                    "task_id" => $taskId,
                    "streaming" => "duplex"
                ],
                "payload" => [
                    "input" => [
                        "text" => $text
                    ]
                ]
            ]);
            echo "Mengirim event continue-task: " . $continueTaskMessage . "\n";
            $conn->send($continueTaskMessage);
            $continueTaskCount++;
        }
        echo "Jumlah event continue-task yang dikirim: " . $continueTaskCount . "\n";

        // Kirim event finish-task
        sendFinishTaskMessage($conn, $taskId);
    };

    // Bendera apakah event task-started telah diterima
    $taskStarted = false;

    // Dengarkan pesan
    $conn->on('message', function($msg) use ($conn, $sendContinueTask, $loop, &$taskStarted, $taskId, $output_file) {
        if ($msg->isBinary()) {
            // Tulis data biner ke file lokal
            file_put_contents($output_file, $msg->getPayload(), FILE_APPEND);
        } else {
            // Proses pesan non-biner
            $response = json_decode($msg, true);

            if (isset($response['header']['event'])) {
                handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, $taskStarted);
            } else {
                echo "Format pesan tidak dikenal\n";
            }
        }
    });

    // Dengarkan penutupan koneksi
    $conn->on('close', function($code = null, $reason = null) {
        echo "Koneksi ditutup\n";
        if ($code !== null) {
            echo "Kode penutupan: " . $code . "\n";
        }
        if ($reason !== null) {
            echo "Alasan penutupan: " . $reason . "\n";
        }
    });
}, function ($e) {
    echo "Gagal terhubung: {$e->getMessage()}\n";
});

$loop->run();

/**
 * Hasilkan ID tugas
 * @return string
 */
function generateTaskId(): string {
    return bin2hex(random_bytes(16));
}

/**
 * Kirim event 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,
                // Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, error "Text request limit violated, expected 1." akan dikembalikan
                "enable_ssml" => false
            ],
            "input" => (object) []
        ]
    ]);
    echo "Mengirim event run-task: " . $runTaskMessage . "\n";
    $conn->send($runTaskMessage);
    echo "Event run-task dikirim\n";
}

/**
 * Baca file audio
 * @param string $filePath
 * @return bool|string
 */
function readAudioFile(string $filePath) {
    $voiceData = file_get_contents($filePath);
    if ($voiceData === false) {
        echo "Gagal membaca file audio\n";
    }
    return $voiceData;
}

/**
 * Pisahkan data audio
 * @param string $data
 * @param int $chunkSize
 * @return array
 */
function splitAudioData(string $data, int $chunkSize): array {
    return str_split($data, $chunkSize);
}

/**
 * Kirim event 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 "Mengirim event finish-task: " . $finishTaskMessage . "\n";
    $conn->send($finishTaskMessage);
    echo "Event finish-task dikirim\n";
}

/**
 * Tangani event
 * @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 "Tugas dimulai, mengirim event continue-task...\n";
            $taskStarted = true;
            // Kirim event continue-task
            $sendContinueTask();
            break;
        case 'result-generated':
            // Menerima event result-generated
            break;
        case 'task-finished':
            echo "Tugas selesai\n";
            $conn->close();
            break;
        case 'task-failed':
            echo "Tugas gagal\n";
            echo "Kode error: " . $response['header']['error_code'] . "\n";
            echo "Pesan error: " . $response['header']['error_message'] . "\n";
            $conn->close();
            break;
        case 'error':
            echo "Error: " . $response['payload']['message'] . "\n";
            break;
        default:
            echo "Event tidak dikenal: " . $response['header']['event'] . "\n";
            break;
    }

    // Jika tugas selesai, tutup koneksi
    if ($response['header']['event'] == 'task-finished') {
        // Tunggu 1 detik untuk memastikan semua data telah dikirim
        $loop->addTimer(1, function() use ($conn) {
            $conn->close();
            echo "Klien menutup koneksi\n";
        });
    }

    // Jika event task-started belum diterima, tutup koneksi
    if (!$taskStarted && in_array($response['header']['event'], ['task-failed', 'error'])) {
        $conn->close();
    }
}

Node.js

Instal dependensi yang diperlukan:

npm install ws
npm install uuid

Kode contoh:

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

// API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi berdasarkan wilayah.
const url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference';
// Jalur file output
const outputFilePath = 'output.mp3';

// Hapus file output
fs.writeFileSync(outputFilePath, '');

// Buat klien 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('Terhubung ke server WebSocket');

  // Kirim event 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', // Suara
        format: 'mp3', // Format audio
        sample_rate: 22050, // Laju sampel
        volume: 50, // Volume
        rate: 1, // Laju bicara
        pitch: 1, // Pitch
        enable_ssml: false // Apakah SSML diaktifkan. Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, error "Text request limit violated, expected 1." akan dikembalikan
      },
      input: {}
    }
  });
  ws.send(runTaskMessage);
  console.log('Pesan run-task dikirim');
});

const fileStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
ws.on('message', (data, isBinary) => {
  if (isBinary) {
    // Tulis data biner ke file
    fileStream.write(data);
  } else {
    const message = JSON.parse(data);

    switch (message.header.event) {
      case 'task-started':
        taskStarted = true;
        console.log('Tugas dimulai');
        // Kirim event continue-task
        sendContinueTasks(ws);
        break;
      case 'task-finished':
        console.log('Tugas selesai');
        ws.close();
        fileStream.end(() => {
          console.log('Aliran file ditutup');
        });
        break;
      case 'task-failed':
        console.error('Tugas gagal: ', message.header.error_message);
        ws.close();
        fileStream.end(() => {
          console.log('Aliran file ditutup');
        });
        break;
      default:
        // result-generated dapat ditangani di sini
        break;
    }
  }
});

function sendContinueTasks(ws) {
  const texts = [
    'Before my bed, moonlight shines bright,',
    'I suspect it is frost upon the ground.',
    'I raise my eyes to gaze at the bright moon,',
    'then bow my head, thinking of home.'
  ];

  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 dikirim, teks: ${text}`);
      }
    }, index * 1000); // Kirim satu per detik
  });

  // Kirim event 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 dikirim');
    }
  }, texts.length * 1000 + 1000); // Kirim 1 detik setelah semua event continue-task dikirim
}

ws.on('close', () => {
  console.log('Terputus dari server WebSocket');
});

Java

Kami merekomendasikan penggunaan SDK Java DashScope untuk pengembangan. Lihat SDK Java.

Berikut adalah contoh koneksi langsung WebSocket Java. Impor dependensi berikut sebelum menjalankan:

  • Java-WebSocket
  • jackson-databind

Gunakan Maven atau Gradle untuk mengelola dependensi:

<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>
// Kode lainnya dihilangkan
dependencies {
  // WebSocket Client
  implementation 'org.java-websocket:Java-WebSocket:1.5.3'
  // JSON Processing
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}
// Kode lainnya dihilangkan

Kode 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("Koneksi berhasil");

        // Kirim event run-task
        // Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, error "Text request limit violated, expected 1." akan dikembalikan
        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("Menerima pesan dari server: " + message);
        try {
            // Uraikan pesan JSON
            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("Menerima event task-started dari server");

                        List<String> texts = Arrays.asList(
                                "Before my bed, moonlight shines bright,I suspect it\'s frost upon the ground.",
                                "I raise my eyes to gaze at the bright moon,then bow my head, thinking of home."
                        );

                        for (String text : texts) {
                            // Kirim event continue-task
                            sendContinueTask(text);
                        }

                        // Kirim event finish-task
                        sendFinishTask();
                    } else if ("task-finished".equals(event)) {
                        System.out.println("Menerima event task-finished dari server");
                        taskFinished = true;
                        closeConnection();
                    } else if ("task-failed".equals(event)) {
                        System.out.println("Tugas gagal: " + message);
                        closeConnection();
                    }
                }
            }
        } catch (Exception e) {
            System.err.println("Terjadi exception: " + e.getMessage());
        }
    }

    @Override
    public void onMessage(ByteBuffer message) {
        System.out.println("Menerima data audio biner ukuran: " + message.remaining());

        try (FileOutputStream fos = new FileOutputStream(outputFile, true)) {
            byte[] buffer = new byte[message.remaining()];
            message.get(buffer);
            fos.write(buffer);
            System.out.println("Data audio ditulis ke file lokal " + outputFile);
        } catch (IOException e) {
            System.err.println("Gagal menulis data audio ke file lokal: " + e.getMessage());
        }
    }

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

    @Override
    public void onError(Exception ex) {
        System.err.println("Error: " + 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 berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
            // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: String apiKey = "sk-xxx"
            String apiKey = System.getenv("DASHSCOPE_API_KEY");
            if (apiKey == null || apiKey.isEmpty()) {
                System.err.println("Harap atur variabel lingkungan DASHSCOPE_API_KEY");
                return;
            }

            Map<String, String> headers = new HashMap<>();
            headers.put("Authorization", "bearer " + apiKey);
            // Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi berdasarkan wilayah.
            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("Gagal terhubung ke layanan WebSocket: " + e.getMessage());
            e.printStackTrace();
        }
    }
}

Python

Kami merekomendasikan penggunaan SDK Python DashScope untuk pengembangan. Lihat SDK Python.

Berikut adalah contoh koneksi langsung WebSocket Python. Instal dependensi berikut sebelum menjalankan:

pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client

PentingJangan beri nama file Python "websocket.py", karena akan menyebabkan error (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):
        """
    Inisialisasi instans TTSClient

    Parameter:
        api_key (str): Kunci API untuk otentikasi
        uri (str): URL layanan WebSocket
    """
        self.api_key = api_key  # Ganti dengan Kunci API Anda
        self.uri = uri  # Ganti dengan URL WebSocket Anda
        self.task_id = str(uuid.uuid4())  # Hasilkan ID tugas unik
        self.output_file = f"output_{int(time.time())}.mp3"  # Jalur file audio keluaran
        self.ws = None  # Instans WebSocketApp
        self.task_started = False  # Apakah task-started telah diterima
        self.task_finished = False  # Apakah task-finished / task-failed telah diterima

    def on_open(self, ws):
        """
    Callback ketika koneksi WebSocket terbentuk
    Mengirim event run-task untuk memulai tugas sintesis suara
    """
        print("WebSocket connected")

        # Buat event 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,
                    # Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, kesalahan akan dikembalikan
                    "enable_ssml": False
                },
                "input": {}
            }
        }

        # Kirim event run-task
        ws.send(json.dumps(run_task_cmd))
        print("Sent run-task event")

    def on_message(self, ws, message):
        """
    Callback ketika pesan diterima
    Menangani pesan teks dan biner secara berbeda
    """
        if isinstance(message, str):
            # Tangani pesan teks JSON
            try:
                msg_json = json.loads(message)
                print(f"Received JSON message: {msg_json}")

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

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

                        if event == "task-started":
                            print("Task started")
                            self.task_started = True

                            # Kirim event continue-task
                            texts = [
                                "Before my bed, moonlight shines bright,I suspect it\'s frost upon the ground.",
                                "I raise my eyes to gaze at the bright moon,then bow my head, thinking of home."
                            ]

                            for text in texts:
                                self.send_continue_task(text)

                            # Kirim finish-task setelah semua event continue-task dikirim
                            self.send_finish_task()

                        elif event == "task-finished":
                            print("Task completed")
                            self.task_finished = True
                            self.close(ws)

                        elif event == "task-failed":
                            error_msg = msg_json.get("error_message", "Unknown error")
                            print(f"Task failed: {error_msg}")
                            self.task_finished = True
                            self.close(ws)

            except json.JSONDecodeError as e:
                print(f"JSON parsing failed: {e}")
        else:
            # Tangani pesan biner (data audio)
            print(f"Received binary message, size: {len(message)} bytes")
            with open(self.output_file, "ab") as f:
                f.write(message)
            print(f"Audio data written to local file {self.output_file}")

    def on_error(self, ws, error):
        """Callback ketika terjadi kesalahan"""
        print(f"WebSocket error: {error}")

    def on_close(self, ws, close_status_code, close_msg):
        """Callback ketika koneksi ditutup"""
        print(f"WebSocket closed: {close_msg} ({close_status_code})")

    def send_continue_task(self, text):
        """Kirim event continue-task dengan konten teks yang akan disintesis"""
        cmd = {
            "header": {
                "action": "continue-task",
                "task_id": self.task_id,
                "streaming": "duplex"
            },
            "payload": {
                "input": {
                    "text": text
                }
            }
        }

        self.ws.send(json.dumps(cmd))
        print(f"Sent continue-task event, text content: {text}")

    def send_finish_task(self):
        """Kirim event finish-task untuk mengakhiri tugas sintesis suara"""
        cmd = {
            "header": {
                "action": "finish-task",
                "task_id": self.task_id,
                "streaming": "duplex"
            },
            "payload": {
                "input": {}
            }
        }

        self.ws.send(json.dumps(cmd))
        print("Sent finish-task event")

    def close(self, ws):
        """Tutup secara aktif koneksi"""
        if ws and ws.sock and ws.sock.connected:
            ws.close()
            print("Connection closed actively")

    def run(self):
        """Mulai klien WebSocket"""
        # Atur header permintaan (otentikasi)
        header = {
            "Authorization": f"bearer {self.api_key}",
            "X-DashScope-DataInspection": "enable"
        }

        # Buat instans 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("Listening for WebSocket messages...")
        self.ws.run_forever()  # Mulai pendengar koneksi persisten

# Contoh penggunaan
if __name__ == "__main__":
    # Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikutnya dengan Kunci API Model Studio China Anda: API_KEY = "sk-xxx"
    API_KEY = os.environ.get("DASHSCOPE_API_KEY")
    # Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
    SERVER_URI = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"  # Ganti dengan URL WebSocket Anda

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

Qwen-TTS

  1. Buat klien

    Python

    Buat file Python bernama tts_realtime_client.py dan salin kode berikut ke dalam file tersebut:

    # -- 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:
        """
        Klien untuk berinteraksi dengan API Realtime TTS.
    
        Kelas ini menyediakan metode untuk menghubungkan ke API Realtime TTS, mengirim data teks,
        menerima output audio, dan mengelola koneksi WebSocket.
    
        Atribut:
            base_url (str):
                URL dasar API Realtime.
            api_key (str):
                API Key untuk otentikasi.
            voice (str):
                Suara yang digunakan untuk sintesis suara di sisi server.
            mode (SessionMode):
                Mode sesi, baik server_commit maupun commit.
            audio_callback (Callable[[bytes], None]):
                Fungsi callback untuk menerima data audio.
            language_type(str)
                Bahasa untuk sintesis suara. Opsi: 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
    
            // Status respons saat ini
            self._current_response_id = None
            self._current_item_id = None
            self._is_responding = False
            self._response_done_future = None
    
        async def connect(self) -> None:
            """Buat koneksi WebSocket dengan API Realtime TTS."""
            headers = {
                "Authorization": f"Bearer {self.api_key}"
            }
    
            self.ws = await websockets.connect(self.base_url, additional_headers=headers)
    
            // Atur konfigurasi sesi default
            await self.update_session({
                "mode": self.mode.value,
                "voice": self.voice,
                // Untuk menggunakan fitur kontrol instruksi, hapus komentar baris di bawah dan ganti model dengan qwen3-tts-instruct-flash-realtime di server_commit.py atau commit.py
                // "instructions": "Speak quickly with a noticeable rising intonation, suitable for introducing fashion products.",
                // "optimize_instructions": true
                "language_type": self.language_type,
                "response_format": "pcm",
                "sample_rate": 24000
            })
    
        async def send_event(self, event) -> None:
            """Kirim event ke server."""
            event['event_id'] = "event_" + str(int(time.time() * 1000))
            print(f"Mengirim event: 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:
            """Perbarui konfigurasi sesi."""
            event = {
                "type": "session.update",
                "session": config
            }
            print("Memperbarui konfigurasi sesi: ", event)
            await self.send_event(event)
    
        async def append_text(self, text: str) -> None:
            """Kirim data teks ke API."""
            event = {
                "type": "input_text_buffer.append",
                "text": text
            }
            await self.send_event(event)
    
        async def commit_text_buffer(self) -> None:
            """Commit buffer teks untuk memicu pemrosesan."""
            event = {
                "type": "input_text_buffer.commit"
            }
            await self.send_event(event)
    
        async def clear_text_buffer(self) -> None:
            """Hapus buffer teks."""
            event = {
                "type": "input_text_buffer.clear"
            }
            await self.send_event(event)
    
        async def finish_session(self) -> None:
            """Akhiri sesi."""
            event = {
                "type": "session.finish"
            }
            await self.send_event(event)
    
        async def wait_for_response_done(self):
            """Tunggu event response.done"""
            if self._response_done_future:
                await self._response_done_future
    
        async def handle_messages(self) -> None:
            """Tangani pesan dari server."""
            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 diterima: {event_type}")
    
                    if event_type == "error":
                        print("Error: ", event.get('error', {}))
                        continue
                    elif event_type == "session.created":
                        print("Sesi dibuat, ID: ", event.get('session', {}).get('id'))
                    elif event_type == "session.updated":
                        print("Sesi diperbarui, ID: ", event.get('session', {}).get('id'))
                    elif event_type == "input_text_buffer.committed":
                        print("Buffer teks di-commit, ID item: ", event.get('item_id'))
                    elif event_type == "input_text_buffer.cleared":
                        print("Buffer teks dihapus")
                    elif event_type == "response.created":
                        self._current_response_id = event.get("response", {}).get("id")
                        self._is_responding = True
                        // Buat future baru untuk menunggu response.done
                        self._response_done_future = asyncio.Future()
                        print("Respons dibuat, ID: ", self._current_response_id)
                    elif event_type == "response.output_item.added":
                        self._current_item_id = event.get("item", {}).get("id")
                        print("Item output ditambahkan, ID: ", self._current_item_id)
                    // Tangani delta audio
                    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("Generasi audio selesai")
                    elif event_type == "response.done":
                        self._is_responding = False
                        self._current_response_id = None
                        self._current_item_id = None
                        // Tandai future sebagai selesai
                        if self._response_done_future and not self._response_done_future.done():
                            self._response_done_future.set_result(True)
                        print("Respons selesai")
                    elif event_type == "session.finished":
                        print("Sesi selesai")
    
            except websockets.exceptions.ConnectionClosed:
                print("Koneksi ditutup")
            except Exception as e:
                print("Error menangani pesan: ", str(e))
    
        async def close(self) -> None:
            """Tutup koneksi WebSocket."""
            if self.ws:
                await self.ws.close()
    

    Java

    Buat file Java bernama TTSRealtimeClient.java dan salin kode berikut ke dalam file tersebut:

    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;
    
    /**
     * Klien untuk berinteraksi dengan API Realtime TTS.
     *
     * Kelas ini menyediakan metode untuk menghubungkan ke API Realtime TTS, mengirim data teks, mengambil output audio, dan mengelola koneksi 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; }
        }
    
        /**
         * Antarmuka callback audio
         */
        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");
        }
    
        /**
         * Buat koneksi WebSocket ke API Realtime TTS.
         */
        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("Koneksi WebSocket berhasil");
                    // Kirim konfigurasi sesi default
                    JsonObject session = new JsonObject();
                    session.addProperty("mode", mode.getValue());
                    session.addProperty("voice", TTSRealtimeClient.this.voice);
                    // Untuk menggunakan fitur kontrol instruksi, hapus komentar baris di bawah dan ganti model dengan qwen3-tts-instruct-flash-realtime
                    // session.addProperty("instructions", "Speak quickly with a noticeable rising intonation, suitable for introducing fashion products.");
                    // 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("Event diterima: " + eventType);
                    }
    
                    switch (eventType) {
                        case "error":
                            System.err.println("Error: " + event.get("error"));
                            break;
                        case "session.created":
                            System.out.println("Sesi dibuat, ID: " +
                                event.getAsJsonObject("session").get("id").getAsString());
                            break;
                        case "session.updated":
                            System.out.println("Sesi diperbarui, ID: " +
                                event.getAsJsonObject("session").get("id").getAsString());
                            break;
                        case "input_text_buffer.committed":
                            System.out.println("Buffer teks di-commit, ID item: " + event.get("item_id"));
                            break;
                        case "input_text_buffer.cleared":
                            System.out.println("Buffer teks dihapus");
                            break;
                        case "response.created":
                            System.out.println("Respons dibuat, ID: " +
                                event.getAsJsonObject("response").get("id").getAsString());
                            responseDoneLatch = new CountDownLatch(1);
                            break;
                        case "response.output_item.added":
                            System.out.println("Item output ditambahkan, 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("Generasi audio selesai");
                            break;
                        case "response.done":
                            System.out.println("Respons selesai");
                            responseDoneLatch.countDown();
                            break;
                        case "session.finished":
                            System.out.println("Sesi selesai");
                            sessionFinishedLatch.countDown();
                            break;
                    }
                }
    
                @Override
                public void onClose(int code, String reason, boolean remote) {
                    System.out.println("Koneksi ditutup: " + reason);
                }
    
                @Override
                public void onError(Exception ex) {
                    System.err.println("Error WebSocket: " + ex.getMessage());
                }
            };
            ws.connectBlocking();
        }
    
        /**
         * Kirim event ke server.
         */
        public void sendEvent(JsonObject event) {
            String eventId = "event_" + System.currentTimeMillis();
            event.addProperty("event_id", eventId);
            System.out.println("Mengirim event: type=" + event.get("type").getAsString()
                + ", event_id=" + eventId);
            ws.send(gson.toJson(event));
        }
    
        /**
         * Perbarui konfigurasi sesi.
         */
        public void updateSession(JsonObject config) {
            JsonObject event = new JsonObject();
            event.addProperty("type", "session.update");
            event.add("session", config);
            System.out.println("Memperbarui konfigurasi sesi: " + event);
            sendEvent(event);
        }
    
        /**
         * Kirim data teks ke API.
         */
        public void appendText(String text) {
            JsonObject event = new JsonObject();
            event.addProperty("type", "input_text_buffer.append");
            event.addProperty("text", text);
            sendEvent(event);
        }
    
        /**
         * Commit buffer teks untuk memicu pemrosesan.
         */
        public void commitTextBuffer() {
            JsonObject event = new JsonObject();
            event.addProperty("type", "input_text_buffer.commit");
            sendEvent(event);
        }
    
        /**
         * Hapus buffer teks.
         */
        public void clearTextBuffer() {
            JsonObject event = new JsonObject();
            event.addProperty("type", "input_text_buffer.clear");
            sendEvent(event);
        }
    
        /**
         * Akhiri sesi.
         */
        public void finishSession() {
            JsonObject event = new JsonObject();
            event.addProperty("type", "session.finish");
            sendEvent(event);
        }
    
        /**
         * Tunggu event response.done.
         */
        public void waitForResponseDone() throws InterruptedException {
            responseDoneLatch.await();
        }
    
        /**
         * Tunggu event session.finished.
         */
        public void waitForSessionFinished() throws InterruptedException {
            sessionFinishedLatch.await();
        }
    
        /**
         * Tutup koneksi WebSocket.
         */
        public void close() {
            if (ws != null) {
                ws.close();
            }
        }
    }
    
  2. Pilih mode sintesis suara

    API Realtime mendukung dua mode:

    • server_commit mode

      Server menangani segmentasi teks dan waktu sintesis secara otomatis. Klien hanya mengirim teks. Cocok untuk skenario latensi rendah (seperti navigasi GPS).

    • commit mode

      Klien menambahkan teks ke buffer dan secara eksplisit memicu sintesis. Cocok untuk skenario yang memerlukan kontrol segmentasi kalimat yang tepat (seperti siaran berita).

    mode server_commit

    Python

    Di direktori yang sama dengan tts_realtime_client.py, buat file Python lain bernama server_commit.py dan salin kode berikut ke dalam file tersebut:

    import os
    import asyncio
    import logging
    import wave
    from tts_realtime_client import TTSRealtimeClient, SessionMode
    import pyaudio
    
    // Konfigurasi layanan QwenTTS
    // Untuk menggunakan fitur kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime dan hapus komentar instruksi di tts_realtime_client.py
    // Berikut ini adalah konfigurasi untuk wilayah Singapura.
    URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"
    // API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: API_KEY="sk-xxx"
    API_KEY = os.getenv("DASHSCOPE_API_KEY")
    
    if not API_KEY:
        raise ValueError("Harap atur variabel lingkungan DASHSCOPE_API_KEY")
    
    // Kumpulkan data audio
    _audio_chunks = []
    // Terkait pemutaran real-time
    _AUDIO_SAMPLE_RATE = 24000
    _audio_pyaudio = pyaudio.PyAudio()
    _audio_stream = None  // Akan dibuka saat runtime
    
    def _audio_callback(audio_bytes: bytes):
        """Callback audio TTSRealtimeClient: pemutaran real-time dan caching"""
        global _audio_stream
        if _audio_stream is not None:
            try:
                _audio_stream.write(audio_bytes)
            except Exception as exc:
                logging.error(f"Error pemutaran PyAudio: {exc}")
        _audio_chunks.append(audio_bytes)
        logging.info(f"Chunk audio diterima: {len(audio_bytes)} byte")
    
    def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool:
        """Simpan data audio yang dikumpulkan sebagai file WAV"""
        if not _audio_chunks:
            logging.warning("Tidak ada data audio untuk disimpan")
            return False
    
        try:
            audio_data = b"".join(_audio_chunks)
            with wave.open(filename, 'wb') as wav_file:
                wav_file.setnchannels(1)  // Mono
                wav_file.setsampwidth(2)  // 16-bit
                wav_file.setframerate(sample_rate)
                wav_file.writeframes(audio_data)
            logging.info(f"Audio disimpan ke: {filename}")
            return True
        except Exception as exc:
            logging.error(f"Gagal menyimpan audio: {exc}")
            return False
    
    async def _produce_text(client: TTSRealtimeClient):
        """Kirim fragmen teks ke server"""
        text_fragments = [
            "Alibaba Cloud's large language model platform, Model Studio, is an all-in-one platform for developing and building large language model applications.",
            "Both developers and business users can deeply participate in the design and development of large language model applications.",
            "You can develop a large language model application in five minutes using a simple interface,",
            "or train a dedicated model in a few hours, allowing you to focus more energy on application innovation.",
        ]
    
        logging.info("Mengirim fragmen teks…")
        for text in text_fragments:
            logging.info(f"Mengirim fragmen: {text}")
            await client.append_text(text)
            await asyncio.sleep(0.1)  // Penundaan singkat antar fragmen
    
        // Tunggu server menyelesaikan pemrosesan internal sebelum mengakhiri sesi
        await asyncio.sleep(1.0)
        await client.finish_session()
    
    async def _run_demo():
        """Jalankan demo lengkap"""
        global _audio_stream
        // Buka aliran output 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
        )
    
        // Buat koneksi
        await client.connect()
    
        // Jalankan penanganan pesan dan pengiriman teks secara paralel
        consumer_task = asyncio.create_task(client.handle_messages())
        producer_task = asyncio.create_task(_produce_text(client))
    
        await producer_task  // Tunggu pengiriman teks selesai
    
        // Tunggu response.done
        await client.wait_for_response_done()
    
        // Tutup koneksi dan batalkan tugas konsumen
        await client.close()
        consumer_task.cancel()
    
        // Tutup aliran audio
        if _audio_stream is not None:
            _audio_stream.stop_stream()
            _audio_stream.close()
        _audio_pyaudio.terminate()
    
        // Simpan data audio
        os.makedirs("outputs", exist_ok=True)
        _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav"))
    
    def main():
        """Titik masuk sinkron"""
        logging.basicConfig(
            level=logging.INFO,
            format='%(asctime)s [%(levelname)s] %(message)s',
            datefmt='%Y-%m-%d %H:%M:%S'
        )
        logging.info("Memulai demo Klien Realtime QwenTTS…")
        asyncio.run(_run_demo())
    
    if __name__ == "__main__":
        main()
    

    Jalankan server_commit.py untuk mendengar audio yang dihasilkan oleh API Realtime secara real-time.

    Java

    Di direktori yang sama dengan TTSRealtimeClient.java, buat file Java lain bernama ServerCommit.java dan salin kode berikut ke dalam file tersebut:

    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 {
        // Berikut ini adalah konfigurasi untuk wilayah Singapura.
        private static final String URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime";
        // API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
        // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: 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;
    
        // Cache data audio
        private static final List<byte[]> audioChunks = new ArrayList<>();
        // Antrian pemutaran real-time
        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("Harap atur variabel lingkungan DASHSCOPE_API_KEY");
            }
    
            // Inisialisasi pemutaran audio
            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();
    
            // Mulai thread pemutaran
            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();
    
            // Buat klien TTS
            // Untuk menggunakan fitur kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime dan hapus komentar instruksi di TTSRealtimeClient.java
            TTSRealtimeClient client = new TTSRealtimeClient(
                URL, API_KEY, "Cherry",
                TTSRealtimeClient.SessionMode.SERVER_COMMIT,
                audioData -> {
                    playbackQueue.add(audioData);
                    audioChunks.add(audioData);
                    System.out.println("Data audio diterima: " + audioData.length + " byte");
                }
            );
    
            client.connect();
    
            // Kirim fragmen teks
            String[] textFragments = {
                "Alibaba Cloud's large language model platform, Model Studio, is an all-in-one platform for developing and building large language model applications.",
                "Both developers and business users can deeply participate in the design and development of large language model applications.",
                "You can develop a large language model application in five minutes using a simple interface,",
                "or train a dedicated model in a few hours, allowing you to focus more energy on application innovation."
            };
    
            System.out.println("Mulai mengirim teks...");
            for (String text : textFragments) {
                System.out.println("Mengirim fragmen: " + text);
                client.appendText(text);
                Thread.sleep(100);
            }
    
            Thread.sleep(1000);
            client.finishSession();
    
            // Tunggu respons selesai
            client.waitForResponseDone();
            client.waitForSessionFinished();
            client.close();
    
            // Tunggu pemutaran selesai
            playing.set(false);
            playerThread.join();
            audioLine.drain();
            audioLine.close();
    
            // Simpan file audio
            saveWav("output.wav");
            System.out.println("Selesai");
        }
    
        private static void saveWav(String filename) throws IOException {
            if (audioChunks.isEmpty()) {
                System.out.println("Tidak ada data audio untuk disimpan");
                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("Audio disimpan ke: outputs/" + filename);
        }
    }
    

    Kompilasi dan jalankan ServerCommit.java untuk mendengar audio yang dihasilkan oleh API Realtime secara real-time.

    commit mode

    Python

    Di direktori yang sama dengan tts_realtime_client.py, buat file Python lain bernama commit.py dan salin kode berikut ke dalam file tersebut:

    import os
    import asyncio
    import logging
    import wave
    from tts_realtime_client import TTSRealtimeClient, SessionMode
    import pyaudio
    
    # Konfigurasi layanan QwenTTS
    # Untuk menggunakan fitur kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime dan hapus komentar instruksi di tts_realtime_client.py
    # Berikut ini adalah konfigurasi untuk wilayah Singapura.
    URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"
    # Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: API_KEY="sk-xxx"
    API_KEY = os.getenv("DASHSCOPE_API_KEY")
    
    if not API_KEY:
        raise ValueError("Harap atur variabel lingkungan DASHSCOPE_API_KEY")
    
    # Mengumpulkan data audio
    _audio_chunks = []
    _AUDIO_SAMPLE_RATE = 24000
    _audio_pyaudio = pyaudio.PyAudio()
    _audio_stream = None
    
    def _audio_callback(audio_bytes: bytes):
        """Callback audio TTSRealtimeClient: pemutaran real-time dan caching"""
        global _audio_stream
        if _audio_stream is not None:
            try:
                _audio_stream.write(audio_bytes)
            except Exception as exc:
                logging.error(f"Kesalahan pemutaran PyAudio: {exc}")
        _audio_chunks.append(audio_bytes)
        logging.info(f"Menerima chunk audio: {len(audio_bytes)} byte")
    
    def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool:
        """Menyimpan data audio yang dikumpulkan sebagai file WAV"""
        if not _audio_chunks:
            logging.warning("Tidak ada data audio untuk disimpan")
            return False
    
        try:
            audio_data = b"".join(_audio_chunks)
            with wave.open(filename, 'wb') as wav_file:
                wav_file.setnchannels(1)  # Mono
                wav_file.setsampwidth(2)  # 16-bit
                wav_file.setframerate(sample_rate)
                wav_file.writeframes(audio_data)
            logging.info(f"Audio disimpan ke: {filename}")
            return True
        except Exception as exc:
            logging.error(f"Gagal menyimpan audio: {exc}")
            return False
    
    async def _user_input_loop(client: TTSRealtimeClient):
        """Terus-menerus mendapatkan input pengguna dan mengirim teks. Ketika pengguna memasukkan teks kosong, kirim event commit dan akhiri sesi saat ini"""
        print("Masukkan teks (tekan Enter langsung untuk mengirim event commit dan mengakhiri sesi saat ini, tekan Ctrl+C atau Ctrl+D untuk keluar dari program):")
    
        while True:
            try:
                user_text = input("> ")
                if not user_text:  # Input pengguna kosong
                    # Input kosong dianggap sebagai akhir percakapan: buffer commit -> akhiri sesi -> hentikan loop
                    logging.info("Input kosong, mengirim event commit dan mengakhiri sesi saat ini")
                    await client.commit_text_buffer()
                    # Tunggu sebentar agar server memproses commit, mencegah pengakhiran sesi prematur yang dapat menyebabkan kehilangan audio
                    await asyncio.sleep(0.3)
                    await client.finish_session()
                    break  # Keluar dari loop input pengguna langsung, tidak perlu menekan Enter lagi
                else:
                    logging.info(f"Mengirim teks: {user_text}")
                    await client.append_text(user_text)
    
            except EOFError:  # Pengguna menekan Ctrl+D
                break
            except KeyboardInterrupt:  # Pengguna menekan Ctrl+C
                break
    
        # Akhiri sesi
        logging.info("Mengakhiri sesi...")
    async def _run_demo():
        """Menjalankan demo lengkap"""
        global _audio_stream
        # Buka aliran output 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,  # Diubah ke mode COMMIT
            audio_callback=_audio_callback
        )
    
        # Membuat koneksi
        await client.connect()
    
        # Menjalankan penanganan pesan dan input pengguna secara paralel
        consumer_task = asyncio.create_task(client.handle_messages())
        producer_task = asyncio.create_task(_user_input_loop(client))
    
        await producer_task  # Menunggu input pengguna selesai
    
        # Menunggu response.done
        await client.wait_for_response_done()
    
        # Menutup koneksi dan membatalkan tugas consumer
        await client.close()
        consumer_task.cancel()
    
        # Menutup aliran audio
        if _audio_stream is not None:
            _audio_stream.stop_stream()
            _audio_stream.close()
        _audio_pyaudio.terminate()
    
        # Menyimpan data audio
        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("Memulai demo Klien Realtime QwenTTS…")
        asyncio.run(_run_demo())
    
    if __name__ == "__main__":
        main()
    

    Jalankan commit.py. Anda dapat memasukkan teks untuk disintesis beberapa kali. Tekan Enter tanpa memasukkan teks untuk mendengar audio yang dikembalikan oleh API Realtime melalui speaker.

    Java

    Di direktori yang sama dengan TTSRealtimeClient.java, buat file Java lain bernama Commit.java dan salin kode berikut ke dalam file tersebut:

    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 {
        // Berikut ini adalah konfigurasi untuk wilayah Singapura.
        private static final String URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime";
        // API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
        // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: 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("Harap atur variabel lingkungan DASHSCOPE_API_KEY");
            }
    
            // Inisialisasi pemutaran audio
            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();
    
            // Mulai thread pemutaran
            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();
    
            // Buat klien TTS (mode commit)
            // Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime dan hapus komentar instruksi di TTSRealtimeClient.java
            TTSRealtimeClient client = new TTSRealtimeClient(
                URL, API_KEY, "Cherry",
                TTSRealtimeClient.SessionMode.COMMIT,
                audioData -> {
                    playbackQueue.add(audioData);
                    audioChunks.add(audioData);
                    System.out.println("Data audio diterima: " + audioData.length + " byte");
                }
            );
    
            client.connect();
    
            // Input interaktif
            System.out.println("Masukkan teks (tekan Enter langsung untuk mengirim event commit dan mengakhiri sesi, tekan Ctrl+D untuk keluar dari program):");
            Scanner scanner = new Scanner(System.in);
            while (true) {
                System.out.print("> ");
                if (!scanner.hasNextLine()) {
                    client.finishSession();
                    break;
                }
                String userText = scanner.nextLine();
                if (userText.isEmpty()) {
                    // Input kosong: commit buffer dan akhiri sesi
                    System.out.println("Input kosong, mengirim event commit dan mengakhiri sesi");
                    client.commitTextBuffer();
                    Thread.sleep(300);
                    client.finishSession();
                    break;
                } else {
                    System.out.println("Mengirim teks: " + userText);
                    client.appendText(userText);
                }
            }
            scanner.close();
    
            // Tunggu respons selesai
            client.waitForResponseDone();
            client.waitForSessionFinished();
            client.close();
    
            // Tunggu pemutaran selesai
            playing.set(false);
            playerThread.join();
            audioLine.drain();
            audioLine.close();
    
            // Simpan file audio
            saveWav("output.wav");
            System.out.println("Selesai");
        }
    
        private static void saveWav(String filename) throws IOException {
            if (audioChunks.isEmpty()) {
                System.out.println("Tidak ada data audio untuk disimpan");
                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("Audio disimpan ke: outputs/" + filename);
        }
    }
    

    Kompilasi dan jalankan Commit.java. Anda dapat memasukkan teks untuk disintesis beberapa kali. Tekan Enter tanpa memasukkan teks untuk mendengar audio yang dikembalikan oleh API Realtime melalui speaker.

Terapkan di produksi

Penggunaan kembali koneksi (WebSocket)

Koneksi WebSocket dapat digunakan kembali: setelah tugas sintesis selesai, Anda dapat memulai tugas berikutnya pada koneksi yang sama tanpa perlu membentuk koneksi baru.

Proses penggunaan kembali:

  • Qwen-Audio-TTS / Qwen-Audio-TTS/CosyVoice: Klien mengirim finish-task, dan setelah server mengembalikan task-finished, klien dapat mengirim run-task untuk memulai tugas baru.
  • Qwen-TTS: Klien mengirim session.finish, dan setelah server mengembalikan session.finished, klien dapat membuat sesi baru untuk memulai tugas berikutnya.

Penggunaan kembali setelah pembatalan: Untuk Qwen-Audio-TTS / Qwen-Audio-TTS/CosyVoice, jika Anda membatalkan tugas saat ini menggunakan direktif cancel, Anda juga dapat mengirim run-task baru pada koneksi yang sama setelah server mengembalikan task-finished. Untuk detailnya, lihat Batalkan Tugas.

Penting

  1. Tunggu server mengembalikan event penyelesaian (task-finished atau session.finished) sebelum memulai tugas baru.
  2. Qwen-Audio-TTS, Qwen-Audio-TTS/CosyVoice memerlukan task_id yang berbeda untuk setiap tugas pada koneksi yang digunakan kembali.
  3. Jika tugas gagal, server mengembalikan event error dan menutup koneksi. Koneksi tersebut tidak dapat digunakan kembali.
  4. Jika tidak ada tugas baru yang dimulai dalam waktu 60 detik setelah tugas sebelumnya berakhir, koneksi akan ditutup secara otomatis.

Untuk detail event setiap model, lihat Referensi API yang sesuai.

Batas laju

Pemanggilan model tunduk pada batas laju. Ketika batas terlampaui, server mengembalikan error Requests rate limit exceeded, please try again later. Kurangi laju permintaan atau konkurensi Anda, lalu coba lagi.

Untuk batas laju setiap model, lihat Pembatasan Laju.

Praktik terbaik konkurensi tinggi

SDK DashScope memiliki pooling bawaan yang menggunakan kembali koneksi WebSocket dan objek synthesizer, sehingga menghilangkan overhead pembuatan dan penghancuran berulang.

Lihat praktik terbaik konkurensi tinggi

Qwen-Audio-TTS/CosyVoice

Qwen-Audio-TTS dan Qwen-Audio-TTS/CosyVoice menggunakan antarmuka SDK yang sama. Contoh berikut juga berlaku untuk model Qwen-Audio-TTS — cukup ganti parameter model dan voice.

Prasyarat

SDK Python

SDK Python menggunakan SpeechSynthesizerObjectPool untuk mengelola dan menggunakan kembali objek SpeechSynthesizer.

Pool membuat sejumlah instance SpeechSynthesizer dan membentuk koneksi WebSocket saat inisialisasi. Saat Anda meminjam objek, objek tersebut siap mengirim permintaan segera, sehingga mengurangi latensi paket pertama. Setelah objek dikembalikan, koneksi tetap aktif untuk tugas berikutnya.

Langkah implementasi

  1. Instal dependensi: Instal dependensi DashScope (pip install -U dashscope).

  2. Buat dan konfigurasikan pool objek

    Atur ukuran pool menjadi 1,5–2 kali konkurensi puncak, dan jangan melebihi batas QPS akun Anda.

    Buat pool singleton global (pembentukan koneksi saat inisialisasi memakan waktu):

from dashscope.audio.tts_v2 import SpeechSynthesizerObjectPool

synthesizer_object_pool = SpeechSynthesizerObjectPool(max_size=20)
import dashscope
// Berikut adalah konfigurasi untuk wilayah China (Beijing). Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"

Penting

  • Dalam skenario pool objek, SpeechSynthesizerObjectPool membentuk koneksi WebSocket dengan server menggunakan dashscope.api_key global saat ini saat inisialisasi. Kunci API ditulis ke header Authorization hanya selama handshake WebSocket untuk autentikasi. Pesan tugas berikutnya (seperti run-task) tidak membawa Kunci API. Memodifikasi dashscope.api_key setelah pembuatan pool tidak memengaruhi koneksi yang ada — objek yang dipinjam melalui borrow_synthesizer (termasuk yang dikembalikan dan dipinjam ulang) masih menggunakan Kunci API dari handshake awal. Nilai baru diabaikan diam-diam, yang dapat menyebabkan atribusi identitas, kuota, atau penagihan berbeda dari yang diharapkan. Catatan: borrow_synthesizer tidak mendukung menentukan Kunci API sebagai parameter.
  • Untuk menggunakan beberapa Kunci API, pertahankan instance SpeechSynthesizerObjectPool terpisah untuk setiap kunci.
  1. Pinjam objek SpeechSynthesizer dari pool

    Jika jumlah objek yang belum dikembalikan melebihi kapasitas pool, sistem membuat objek tambahan.

    Objek tambahan ini harus membentuk koneksi baru dan tidak mendapat manfaat dari pooling.

speech_synthesizer = connectionPool.borrow_synthesizer(
    model='cosyvoice-v3-flash',
    voice='longanyang',
    seed=12382,
    callback=synthesizer_callback
)
  1. Lakukan sintesis ucapan

    Panggil metode call atau streaming_call objek SpeechSynthesizer untuk mensintesis ucapan.

  2. Kembalikan objek SpeechSynthesizer

    Kembalikan objek setelah tugas selesai agar tersedia untuk digunakan kembali.

    Jangan mengembalikan objek dengan tugas yang belum lengkap atau gagal.

connectionPool.return_synthesizer(speech_synthesizer)
Kode lengkap

PentingSebelum menggunakan kode ini: SpeechSynthesizerObjectPool membentuk koneksi WebSocket dan melakukan autentikasi menggunakan dashscope.api_key global saat ini saat inisialisasi. Memodifikasi dashscope.api_key setelah pembuatan pool tidak memengaruhi koneksi yang ada — nilai baru diabaikan diam-diam. Untuk beberapa Kunci API, pertahankan instance pool terpisah untuk setiap kunci. Untuk detailnya, lihat catatan penting di atas.

// !/usr/bin/env python3
// Hak Cipta (C) Alibaba Group. Seluruh Hak Dilindungi.
// Lisensi MIT (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 = [
    'Kalimat 1: Selamat datang di layanan sintesis ucapan Alibaba.',
    'Kalimat 2: Selamat datang di layanan sintesis ucapan Alibaba.',
    'Kalimat 3: Selamat datang di layanan sintesis ucapan Alibaba.',
]
connectionPool = None

def init_dashscope_api_key():
    '''
    Tetapkan Kunci API DashScope Anda. Informasi lebih lanjut:
    https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
    '''
    // Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: 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']  // muat Kunci API dari variabel lingkungan DASHSCOPE_API_KEY
    else:
        dashscope.api_key = '<your-dashscope-api-key>'  // tetapkan Kunci API secara manual

def synthesis_text_to_speech_and_play_by_streaming_mode(text, task_id):
    global USE_CONNECTION_POOL, connectionPool
    '''
    Sintesis ucapan dengan teks yang diberikan dengan mode streaming, panggilan asinkron dan putar audio yang disintesis secara real-time.
    untuk informasi lebih lanjut, silakan merujuk ke https://www.alibabacloud.com/help/document_detail/2712523.html
    '''

    complete_event = threading.Event()

    // Definisikan callback untuk menangani hasil

    class Callback(ResultCallback):
        def on_open(self):
            // saat menggunakan pool objek, on_open akan dipanggil setelah tugas dimulai
            self.file = open(f'result_{task_id}.mp3', 'wb')
            print(f'[task_{task_id}] mulai')

        def on_complete(self):
            print(f'[task_{task_id}] tugas sintesis ucapan selesai berhasil.')
            complete_event.set()

        def on_error(self, message: str):
            print(f'[task_{task_id}] tugas sintesis ucapan gagal, {message}')

        def on_close(self):
            // saat menggunakan pool objek, on_open akan dipanggil setelah tugas selesai
            print(f'[task_{task_id}] selesai')

        def on_event(self, message):
            // print(f'terima pesan sintesis ucapan {message}')
            pass

        def on_data(self, data: bytes) -> None:
            // kirim ke pemutar
            // simpan audio ke file
            self.file.write(data)

    // Panggil callback synthesizer ucapan
    synthesizer_callback = Callback()

    // Inisialisasi synthesizer ucapan
    // Anda dapat menyesuaikan parameter sintesis, seperti suara, format, laju sampel atau parameter lainnya
    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}] tugas sintesis ucapan gagal, {e}')
        if USE_CONNECTION_POOL:
            // tutup koneksi synthesizer secara manual jika tugas gagal saat menggunakan pool koneksi.
            speech_synthesizer.close()
        return

    print('[task_{}] Teks yang disintesis: {}'.format(task_id, text))
    complete_event.wait()
    print('[task_{}][Metric] requestId: {}, latensi paket pertama 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)

// fungsi utama
if __name__ == '__main__':
    // Anda harus mengatur dashscope.api_key dan base_websocket_api_url sebelum membuat SpeechSynthesizerObjectPool.
    // Pool membentuk koneksi WebSocket menggunakan dashscope.api_key global saat ini saat waktu inisialisasi.
    // Memodifikasi dashscope.api_key setelah pembuatan pool tidak akan memengaruhi koneksi yang ada di pool.
    // Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
    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('membuat pool koneksi')
        start_time = time.time() * 1000
        connectionPool = SpeechSynthesizerObjectPool(max_size=3)
        end_time = time.time() * 1000
        print('pool koneksi dibuat, biaya: {} 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()

Manajemen sumber daya dan penanganan error

  • Tugas berhasil: Setelah tugas sintesis selesai secara normal, panggil connectionPool.return_synthesizer(speech_synthesizer) untuk mengembalikan objek SpeechSynthesizer ke pool untuk digunakan kembali.

    PentingJangan mengembalikan objek SpeechSynthesizer dengan tugas yang belum lengkap atau gagal.

  • Tugas gagal: Jika error internal SDK atau exception logika bisnis menyebabkan tugas dibatalkan, tutup koneksi WebSocket dasar: speech_synthesizer.close()

  • Setelah semua tugas sintesis selesai, matikan pool: connectionPool.shutdown()

  • Saat terjadi error TaskFailed di sisi server, tidak diperlukan penanganan tambahan.

SDK Java

SDK Java mencapai kinerja optimal melalui koordinasi pool koneksi bawaan dan pool objek kustom.

  • Pool koneksi: Pool koneksi OkHttp3 yang terintegrasi dalam SDK mengelola dan menggunakan kembali koneksi WebSocket dasar, sehingga mengurangi overhead jabat tangan jaringan. Fitur ini diaktifkan secara default.
  • Pool objek: Dibangun di atas commons-pool2, pool ini mempertahankan seperangkat objek SpeechSynthesizer yang telah terhubung sebelumnya. Meminjam dari pool menghilangkan latensi penyiapan koneksi, sehingga secara signifikan mengurangi latensi paket pertama.

Langkah implementasi

  1. Tambahkan dependensi

    Tambahkan dashscope-sdk-java dan commons-pool2 ke konfigurasi dependensi proyek Anda berdasarkan alat build yang digunakan.

    Contoh Maven dan Gradle:

    Maven

    1. Buka file pom.xml proyek Maven Anda.
    2. Tambahkan dependensi berikut di dalam tag <dependencies>.
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dashscope-sdk-java</artifactId>
        <!-- Ganti 'the-latest-version' dengan versi 2.16.9 atau lebih baru. Periksa versi yang tersedia di: 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>
        <!-- Ganti 'the-latest-version' dengan versi terbaru. Periksa versi yang tersedia di: https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
        <version>the-latest-version</version>
    </dependency>
    
    1. Simpan file pom.xml.
    2. Jalankan perintah Maven seperti mvn clean install atau mvn compile untuk memperbarui dependensi proyek.

    Gradle

    1. Buka file build.gradle proyek Gradle Anda.
    2. Tambahkan dependensi berikut di dalam blok dependencies.
    dependencies {
        // Ganti 'the-latest-version' dengan versi 2.16.6 atau lebih baru. Periksa versi yang tersedia di: https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java
        implementation group: 'com.alibaba', name: 'dashscope-sdk-java', version: 'the-latest-version'
    
        // Ganti 'the-latest-version' dengan versi terbaru. Periksa versi yang tersedia di: https://mvnrepository.com/artifact/org.apache.commons/commons-pool2
        implementation group: 'org.apache.commons', name: 'commons-pool2', version: 'the-latest-version'
    }
    
    1. Simpan file build.gradle.
    2. Di terminal, navigasi ke direktori root proyek dan jalankan perintah Gradle berikut untuk memperbarui dependensi.
    ./gradlew build --refresh-dependencies
    

    Di Windows, gunakan perintah berikut sebagai gantinya:

    gradlew build --refresh-dependencies
    
  2. Konfigurasikan pool koneksi

    Konfigurasikan parameter kunci pool koneksi melalui variabel lingkungan:

    Variabel lingkungan

    Deskripsi

    DASHSCOPE_CONNECTION_POOL_SIZE

    Ukuran pool koneksi.

    Nilai yang direkomendasikan: minimal 2x konkurensi puncak.

    Default: 32.

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS

    Jumlah maksimum permintaan asinkron.

    Nilai yang direkomendasikan: sama dengan DASHSCOPE_CONNECTION_POOL_SIZE.

    Default: 32.

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST

    Jumlah maksimum permintaan asinkron per host.

    Nilai yang direkomendasikan: sama dengan DASHSCOPE_CONNECTION_POOL_SIZE.

    Default: 32.

  3. Konfigurasikan pool objek

    Konfigurasikan ukuran pool objek melalui variabel lingkungan:

    Variabel lingkungan

    Deskripsi

    COSYVOICE_OBJECTPOOL_SIZE

    Ukuran pool objek.

    Nilai yang direkomendasikan: 1,5x-2x konkurensi puncak.

    Default: 500.

    Penting

    • Ukuran pool objek (COSYVOICE_OBJECTPOOL_SIZE) harus kurang dari atau sama dengan ukuran pool koneksi (DASHSCOPE_CONNECTION_POOL_SIZE). Jika tidak, saat pool objek meminta objek dan pool koneksi penuh, thread pemanggil akan memblokir sambil menunggu koneksi yang tersedia.
    • Ukuran pool objek tidak boleh melebihi batas QPS (queries per second) akun Anda.

    Buat pool objek dengan kode berikut:

class CosyvoiceObjectPool {
    // ... Kode lain dihilangkan di sini. Untuk contoh lengkap, lihat kode lengkap.
    public static GenericObjectPool<SpeechSynthesizer> getInstance() {
        lock.lock();
        if (synthesizerPool == null) {
            // Anda dapat mengatur ukuran pool objek di sini, atau mengaturnya di variabel lingkungan COSYVOICE_OBJECTPOOL_SIZE.
            // Disarankan untuk mengaturnya menjadi 1,5 hingga 2 kali koneksi konkuren maksimum server.
            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. Pinjam objek SpeechSynthesizer dari pool

    Jika jumlah objek yang belum dikembalikan melebihi kapasitas maksimum pool, sistem membuat objek SpeechSynthesizer tambahan.

    Objek yang baru dibuat ini memerlukan inisialisasi ulang dan koneksi WebSocket baru, sehingga tidak mendapat manfaat dari pooling.

synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
  1. Lakukan sintesis ucapan

    Setelah meminjam objek SpeechSynthesizer dari pool, panggil updateParamAndCallback(param, callback) untuk mengikat parameter dan callback untuk tugas saat ini, lalu panggil streamingCall atau call untuk mensintesis ucapan.

    Penting

    • Dalam skenario pool objek, updateParamAndCallback dipanggil beberapa kali (sekali setiap kali objek dipinjam, untuk mengatur callback dan parameter tingkat tugas seperti voice dan format). Kunci apiKey yang diteruskan dalam setiap panggilan harus tetap sama. updateParamAndCallback hanya memperbarui bidang lokal instance SpeechSynthesizer saat ini dan tidak membangun ulang koneksi WebSocket dasar. SDK menulis apiKey ke header Authorization hanya selama handshake WebSocket untuk autentikasi. Pesan tugas berikutnya (seperti run-task) tidak membawa apiKey. Selama koneksi yang digunakan kembali tidak terputus, meneruskan apiKey baru tidak akan dikirim ke server — permintaan masih menggunakan apiKey dari handshake awal, yang dapat menyebabkan atribusi identitas, kuota, atau penagihan berbeda dari yang diharapkan.
    • Untuk menggunakan beberapa Kunci API, pertahankan instance pool objek terpisah untuk setiap kunci.
  2. Kembalikan objek SpeechSynthesizer

    Setelah tugas sintesis selesai, kembalikan objek SpeechSynthesizer agar tugas berikutnya dapat menggunakannya kembali.

    Jangan mengembalikan objek dengan tugas yang belum lengkap atau gagal.

CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
Kode lengkap

PentingSebelum menggunakan kode ini: Dalam skenario pool objek, apiKey yang diteruskan ke updateParamAndCallback di beberapa panggilan harus tetap sama — SDK tidak memperbarui Kunci API koneksi yang telah dibentuk, dan meneruskan Kunci API yang berbeda tidak berpengaruh. Untuk beberapa Kunci API, pertahankan instance pool terpisah untuk setiap kunci. Untuk detailnya, lihat catatan penting di atas.

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 com.alibaba.dashscope.utils.Constants;
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;

/**
 * Anda perlu menyertakan paket org.apache.commons.pool2 dan DashScope dalam proyek Anda.
 *
 * SDK DashScope versi 2.16.6 dan lebih baru dioptimalkan untuk skenario konkurensi tinggi.
 * Versi SDK DashScope sebelum 2.16.6 tidak direkomendasikan untuk penggunaan konkurensi tinggi.
 *
 *
 * Sebelum melakukan panggilan konkurensi tinggi ke layanan TTS,
 * harap konfigurasikan parameter pool koneksi melalui variabel lingkungan berikut.
 *
 * 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("Menggunakan Ukuran Pool Objek di Env: "+ n);
            return n;
        } catch (NumberFormatException e) {
            System.out.println("Menggunakan Ukuran Pool Objek Default: "+ DEFAULT_OBJECT_POOL_SIZE);
            return DEFAULT_OBJECT_POOL_SIZE;
        }
    }
    public static GenericObjectPool<SpeechSynthesizer> getInstance() {
        lock.lock();
        if (synthesizerPool == null) {
            // Anda dapat mengatur ukuran pool objek di sini atau di variabel lingkungan COSYVOICE_OBJECTPOOL_SIZE.
            // Disarankan untuk mengaturnya menjadi 1,5 hingga 2 kali koneksi konkuren maksimum server Anda.
            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();
        // jika menerima 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("Audio diterima, panjang aliran audio: " + 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")
                            // Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                            // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: .apiKey("sk-xxx")
                            .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                            .format(SpeechSynthesisAudioFormat
                                    .MP3_22050HZ_MONO_256KBPS) // Gunakan PCM atau MP3 untuk sintesis streaming
                            .build();

            try {
                synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
                // Catatan: Dalam skenario pool objek, apiKey yang diteruskan dalam beberapa panggilan updateParamAndCallback harus selalu sama. SDK tidak memperbarui apiKey untuk koneksi yang telah dibentuk, sehingga meneruskan apiKey yang berbeda tidak berpengaruh. Lihat catatan penting di langkah "Lakukan sintesis ucapan" di atas.
                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) {
                    // Jika terjadi exception, tutup koneksi dan batalkan objek di pool.
                    synthesizer.getDuplexApi().close(1000, "bye");
                    CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
                } else {
                    // Jika tugas selesai secara normal, kembalikan objek ke pool.
                    CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            long endTime = System.currentTimeMillis();
            timeCost = endTime - startTime;
            System.out.println("[Thread " + Thread.currentThread() + "] Tugas sintesis ucapan selesai. Biaya waktu: " + timeCost + " ms, RequestId " + requestId);
        }
    }
}

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

    public static void main(String[] args)
            throws InterruptedException, NoApiKeyException {
        // Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
        Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
        // Periksa env pool koneksi
        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;
        // Buat pool objek SpeechSynthesis
        ExecutorService executorService = Executors.newFixedThreadPool(runTimes);

        for (int i = 0; i < runTimes; i++) {
            // Catat waktu pengiriman tugas
            LocalDateTime submissionTime = LocalDateTime.now();
            executorService.submit(new SynthesizeTaskWithCallback(new String[] {
                    "Before my bed, moonlight shines bright,", "I wonder if it is frost on the ground,", "I raise my eyes to gaze at the bright moon,", "then bow my head, thinking of home."}));
        }

        // Matikan ExecutorService dan tunggu semua tugas selesai
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);
        System.exit(0);
    }
}

Konfigurasi yang direkomendasikan

Konfigurasi berikut didasarkan pada hasil pengujian dari menjalankan hanya layanan sintesis ucapan Qwen-Audio-TTS/CosyVoice pada instance ECS Alibaba Cloud dengan spesifikasi yang ditentukan. Konkurensi berlebihan dapat meningkatkan latensi pemrosesan tugas.

"Konkurensi mesin tunggal" mengacu pada jumlah tugas sintesis Qwen-Audio-TTS/CosyVoice yang berjalan secara bersamaan, setara dengan jumlah thread pekerja.

Spesifikasi ECS (Alibaba Cloud)

Maksimum konkurensi mesin tunggal

Ukuran pool objek

Ukuran pool koneksi

4 vCPU, 8 GiB

100

500

2000

8 vCPU, 16 GiB

150

500

2000

16 vCPU, 32 GiB

200

500

2000

Manajemen sumber daya dan penanganan error

  • Tugas berhasil: Setelah tugas sintesis selesai secara normal, panggil metode GenericObjectPool.returnObject untuk mengembalikan objek SpeechSynthesizer ke pool untuk digunakan kembali.

    Dalam kode contoh, ini sesuai dengan CosyvoiceObjectPool.getInstance().returnObject(synthesizer).

    PentingJangan mengembalikan objek SpeechSynthesizer dengan tugas yang belum lengkap atau gagal.

  • Tugas gagal: Jika error internal SDK atau exception logika bisnis menyebabkan tugas dibatalkan, lakukan hal berikut:

    1. Tutup koneksi WebSocket dasar.
    2. Batalkan objek di pool untuk mencegahnya digunakan kembali.
// Dalam kode saat ini, konten yang sesuai adalah sebagai berikut
// Tutup koneksi
synthesizer.getDuplexApi().close(1000, "bye");
// Batalkan synthesizer di pool objek saat terjadi exception
CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
  • Saat terjadi error TaskFailed di sisi server, tidak diperlukan penanganan tambahan.

Pemanasan dan pengukuran latensi

Saat mengevaluasi SDK Java DashScope untuk latensi dan kinerja konkurensi, jalankan operasi pemanasan yang cukup terlebih dahulu. Ini memastikan bahwa pengukuran mencerminkan kinerja keadaan stabil daripada overhead koneksi awal.

Mekanisme penggunaan kembali koneksi

SDK Java DashScope menggunakan pool koneksi singleton global untuk mengelola dan menggunakan kembali koneksi WebSocket secara efisien, sehingga mengurangi overhead pembuatan dan penghancuran koneksi yang sering untuk beban kerja konkurensi tinggi.

Cara kerja mekanisme ini:

  • Pembuatan sesuai permintaan: SDK tidak membuat koneksi WebSocket sebelumnya saat startup. Koneksi dibentuk pada panggilan pertama.

  • Penggunaan kembali berbatas waktu: Setelah permintaan selesai, koneksi tetap berada di pool hingga 60 detik untuk digunakan kembali.

    • Jika permintaan baru tiba dalam waktu 60 detik, koneksi yang ada digunakan kembali, sehingga menghindari overhead jabat tangan.
    • Jika koneksi menganggur lebih dari 60 detik, koneksi tersebut ditutup secara otomatis untuk membebaskan sumber daya.
Pentingnya pemanasan

Dalam skenario berikut, pool koneksi mungkin tidak memiliki koneksi aktif yang dapat digunakan kembali, sehingga memaksa pembuatan koneksi baru:

  • Aplikasi baru saja dimulai dan belum melakukan panggilan apa pun.
  • Layanan telah menganggur lebih dari 60 detik, dan koneksi di pool telah timeout serta ditutup.

Dalam skenario ini, permintaan pertama harus menyelesaikan jabat tangan WebSocket penuh (jabat tangan TCP, negosiasi TLS, peningkatan protokol), sehingga menghasilkan latensi yang jauh lebih tinggi daripada permintaan berikutnya yang menggunakan kembali koneksi. Tanpa pemanasan, hasil pengujian kinerja condong oleh overhead koneksi awal ini.

Latensi yang dilaporkan SDK vs. latensi paket pertama aktual

Latensi paket pertama yang dilaporkan oleh SDK (misalnya, nilai dari get_first_package_delay()) mencakup penyiapan koneksi WebSocket dan waktu transmisi jaringan, dan tidak sama dengan latensi paket pertama layanan model aktual.

Latensi paket pertama aktual adalah interval antara saat server menerima instruksi run-task dan saat server mengembalikan event result-generated pertama. Nilai ini dapat ditemukan di log sisi server.

Dalam skenario konkurensi tinggi, karena pembuatan koneksi simultan dan penjadwalan sumber daya, latensi yang dilaporkan SDK mungkin jauh lebih tinggi daripada latensi paket pertama sisi server aktual. Jika Anda mengamati latensi paket pertama yang dilaporkan SDK tinggi:

  • Bandingkan dengan latensi paket pertama log sisi server (dari run-task ke result-generated pertama) untuk memeriksa apakah kinerja inferensi model normal.
  • Gunakan mekanisme pool objek atau pool koneksi yang dijelaskan di atas untuk pemanasan. Ini menghilangkan overhead jabat tangan WebSocket, sehingga membawa latensi yang dilaporkan SDK lebih dekat ke latensi paket pertama aktual.
Praktik yang direkomendasikan

Untuk data kinerja yang andal, ikuti langkah pemanasan ini sebelum menjalankan pengujian beban atau mengumpulkan metrik latensi:

  1. Simulasikan tingkat konkurensi target dan kirim sejumlah permintaan pemanasan terlebih dahulu (misalnya, pertahankan lalu lintas selama 1–2 menit) untuk sepenuhnya mengisi pool koneksi.
  2. Konfirmasi bahwa pool koneksi telah membentuk dan mempertahankan koneksi aktif yang cukup sebelum memulai pengumpulan data kinerja formal.

Pemanasan yang tepat membawa pool koneksi SDK ke keadaan penggunaan kembali yang stabil, sehingga menghasilkan metrik latensi yang secara akurat mencerminkan kinerja keadaan stabil online.

Exception SDK Java umum

Exception 1: Koneksi TCP server terus meningkat meskipun lalu lintas stabil

Akar penyebab:Jenis 1:

Setiap objek SDK membuat koneksi saat inisialisasi. Tanpa pool objek, objek dihancurkan setelah setiap tugas selesai. Koneksi kemudian memasuki keadaan tidak direferensikan dan tetap terbuka hingga server menghentikannya setelah 61 detik. Selama periode ini, koneksi tidak dapat digunakan kembali.

Di bawah konkurensi tinggi, tugas baru yang tidak menemukan koneksi yang dapat digunakan kembali membuat yang baru, menyebabkan:

  1. Jumlah koneksi terus meningkat.
  2. Kelelahan sumber daya server dan penurunan kinerja karena koneksi berlebihan.
  3. Pool koneksi jenuh, menyebabkan tugas baru memblokir sambil menunggu koneksi yang tersedia.
Jenis 2:

MaxIdle pool objek dikonfigurasi lebih rendah dari MaxTotal, menyebabkan objek menganggur di luar MaxIdle dihancurkan, yang menyebabkan kebocoran koneksi mereka. Koneksi yang bocor ini harus menunggu 61 detik untuk timeout sebelum terputus, sama seperti Jenis 1.

Solusi:

Untuk Jenis 1: Gunakan pool objek.

Untuk Jenis 2: Periksa konfigurasi pool objek dan atur MaxIdle sama dengan MaxTotal. Nonaktifkan kebijakan penggantian objek otomatis.

Exception 2: Tugas memakan waktu 60 detik lebih lama dari biasanya

Akar penyebab yang sama dengan Exception 1: Pool koneksi telah mencapai batas koneksi maksimum, dan tugas baru harus menunggu 61 detik agar koneksi yang tidak direferensikan timeout sebelum koneksi tersedia.

Exception 3: Tugas lambat saat startup layanan tetapi secara bertahap pulih

Akar penyebab:

Di bawah konkurensi tinggi, satu objek menggunakan kembali koneksi WebSocket yang sama. Koneksi WebSocket hanya dibuat saat startup layanan. Jika konkurensi tinggi dimulai segera saat peluncuran, membuat terlalu banyak koneksi WebSocket secara simultan menyebabkan pemblokiran.

Solusi:

Tingkatkan konkurensi secara bertahap setelah memulai layanan, atau tambahkan tugas pemanasan.

Exception 4: Server melaporkan "Invalid action('run-task')! Please follow the protocol!"

Akar penyebab:

Terjadi error di sisi klien, tetapi server tidak mendeteksinya. Koneksi tetap dalam keadaan dalam tugas. Saat koneksi dan objek ini digunakan kembali untuk tugas berikutnya, alur protokol rusak dan tugas berikutnya gagal.

Solusi:

Setelah exception dilemparkan, tutup koneksi WebSocket sebelum mengembalikan objek ke pool.

Exception 5: Lonjakan lalu lintas meskipun beban bisnis stabil

Akar penyebab:

Membuat terlalu banyak koneksi WebSocket secara simultan menyebabkan pemblokiran. Saat diblokir, lalu lintas bisnis masuk menumpuk. Setelah kemacetan hilang, semua tugas yang tertunda dijalankan sekaligus, menciptakan lonjakan lalu lintas yang mungkin melebihi batas konkurensi akun Anda, sehingga menyebabkan kegagalan tugas atau penurunan server.

Pemicu umum untuk membuat terlalu banyak koneksi WebSocket secara simultan:

  • Fase startup layanan
  • Anomali jaringan menyebabkan banyak koneksi WebSocket terputus dan terhubung kembali secara simultan
  • Lonjakan error sisi server menyebabkan banyak koneksi WebSocket terhubung kembali. Error umum termasuk melebihi batas konkurensi akun ("Requests rate limit exceeded, please try again later.").

Solusi:

  1. Periksa kondisi jaringan.
  2. Investigasi apakah lonjakan error sisi server lainnya mendahului lonjakan tersebut.
  3. Tingkatkan batas konkurensi akun.
  4. Kurangi ukuran pool objek dan pool koneksi untuk membatasi konkurensi maksimum melalui batas pool.
  5. Tingkatkan spesifikasi server atau tambahkan lebih banyak mesin.

Exception 6: Semua tugas melambat saat konkurensi meningkat

Solusi:

  1. Periksa apakah lebar pita jaringan telah mencapai batasnya.
  2. Periksa apakah konkurensi aktual terlalu tinggi.

Model dan wilayah yang didukung

Singapura

Untuk memanggil model berikut, pilih Kunci API dari wilayah Singapura:

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

  • Qwen-Audio-TTS/CosyVoice: cosyvoice-v3-plus, cosyvoice-v3-flash

  • Qwen-TTS:
    • Qwen3-TTS-Instruct-Flash-Realtime: qwen3-tts-instruct-flash-realtime (stabil, saat ini setara dengan qwen3-tts-instruct-flash-realtime-2026-01-22), qwen3-tts-instruct-flash-realtime-2026-01-22 (snapshot terbaru)
    • Qwen3-TTS-VD-Realtime: qwen3-tts-vd-realtime-2026-01-15 (snapshot terbaru), qwen3-tts-vd-realtime-2025-12-16 (snapshot)
    • Qwen3-TTS-VC-Realtime: qwen3-tts-vc-realtime-2026-01-15 (snapshot terbaru), qwen3-tts-vc-realtime-2025-11-27 (snapshot)
    • Qwen3-TTS-Flash-Realtime: qwen3-tts-flash-realtime (stabil, saat ini setara dengan qwen3-tts-flash-realtime-2025-11-27), qwen3-tts-flash-realtime-2025-11-27 (snapshot terbaru), qwen3-tts-flash-realtime-2025-09-18 (snapshot)

China (Beijing)

Untuk memanggil model berikut, pilih Kunci API dari wilayah Beijing:

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

  • Qwen-Audio-TTS/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 (stabil, saat ini setara dengan qwen3-tts-instruct-flash-realtime-2026-01-22), qwen3-tts-instruct-flash-realtime-2026-01-22 (snapshot terbaru)
    • Qwen3-TTS-VD-Realtime: qwen3-tts-vd-realtime-2026-01-15 (snapshot terbaru), qwen3-tts-vd-realtime-2025-12-16 (snapshot)
    • Qwen3-TTS-VC-Realtime: qwen3-tts-vc-realtime-2026-01-15 (snapshot terbaru), qwen3-tts-vc-realtime-2025-11-27 (snapshot)
    • Qwen3-TTS-Flash-Realtime: qwen3-tts-flash-realtime (stabil, saat ini setara dengan qwen3-tts-flash-realtime-2025-11-27), qwen3-tts-flash-realtime-2025-11-27 (snapshot terbaru), qwen3-tts-flash-realtime-2025-09-18 (snapshot)
    • Qwen-TTS-Realtime: qwen-tts-realtime (stabil, saat ini setara dengan qwen-tts-realtime-2025-07-15), qwen-tts-realtime-latest (terbaru, saat ini setara dengan qwen-tts-realtime-2025-07-15), qwen-tts-realtime-2025-07-15 (snapshot)

Suara yang didukung

Model yang berbeda mendukung suara yang berbeda. Atur parameter permintaan voice ke nilai yang tercantum dalam kolom parameter suara pada daftar suara yang sesuai.

Referensi API

FAQ

T: Bagaimana cara memperbaiki pengucapan yang salah dalam sintesis ucapan? Bagaimana cara mengontrol pengucapan karakter homofon?

  • Ganti karakter polifonik dengan homofon untuk memperbaiki masalah pengucapan secara cepat.
  • Gunakan markup SSML untuk mengontrol pengucapan .

T: Bagaimana cara memecahkan masalah audio diam saat menggunakan suara kloning?

  1. Verifikasi status suara

    Panggil antarmuka API kloning/desain suara dan pastikan status suara adalah OK.

  2. Periksa konsistensi versi model

    Pastikan parameter target_model yang digunakan selama kloning suara cocok dengan parameter model yang digunakan untuk sintesis ucapan. Misalnya:

    • Kloning menggunakan cosyvoice-v3-plus
    • Sintesis juga harus menggunakan cosyvoice-v3-plus
  3. Verifikasi kualitas audio sumber

    Periksa apakah audio sumber yang digunakan untuk kloning suara memenuhi persyaratan di API kloning/desain suara:

    • Durasi audio: 10–20 detik
    • Kualitas audio jernih
    • Tidak ada kebisingan latar belakang
  4. Periksa parameter permintaan

    Konfirmasi bahwa parameter voice dalam permintaan sintesis ucapan diatur ke ID suara kloning.

T: Apa yang harus saya lakukan jika audio yang disintesis dari suara kloning tidak stabil atau tidak lengkap?

Jika audio yang disintesis dari suara kloning memiliki salah satu masalah berikut:

  • Pemutaran audio tidak lengkap, hanya sebagian teks yang diucapkan
  • Kualitas sintesis tidak konsisten
  • Audio berisi jeda abnormal atau segmen diam

Kemungkinan penyebab: Audio sumber tidak memenuhi persyaratan kualitas.

Solusi: Periksa apakah audio sumber memenuhi persyaratan di Panduan perekaman untuk kloning suara. Rekam ulang audio mengikuti panduan perekaman.

T: Mengapa durasi aktual audio yang disintesis berbeda dari durasi yang ditunjukkan di file WAV?

Sintesis ucapan menggunakan mekanisme streaming yang mengembalikan data saat dihasilkan. Durasi di header file WAV yang disimpan bersifat perkiraan dan mungkin tidak akurat. Untuk durasi yang tepat, atur format ke pcm, tunggu hingga hasil sintesis lengkap, lalu tambahkan header file WAV secara manual.

T: Mengapa file audio tidak dapat diputar?

Pecahkan masalah berdasarkan skenario Anda:

  1. Audio disimpan sebagai file lengkap (misalnya, xx.mp3)

    1. Konsistensi format audio: Format audio dalam parameter permintaan harus sesuai dengan ekstensi file (misalnya, jika parameter adalah wav, file harus berekstensi .wav).
    2. Kompatibilitas pemutar: Pastikan pemutar mendukung format audio dan laju sampel tersebut.
  2. Pemutaran audio streaming

    1. Simpan aliran audio sebagai file lengkap dan coba putar dengan pemutar media. Jika file tidak dapat diputar, rujuk ke skenario 1 di atas.
    2. Jika file dapat diputar dengan benar, masalahnya terletak pada implementasi pemutaran streaming. Pastikan pemutar mendukung pemutaran streaming (seperti ffmpeg, pyaudio, AudioFormat, atau MediaSource).

T: Mengapa pemutaran audio tersendat?

Pecahkan masalah dengan langkah-langkah berikut:

  1. Periksa laju pengiriman teks: Pastikan interval pengiriman masuk akal agar segmen audio sebelumnya tidak selesai sebelum teks berikutnya tiba.

  2. Periksa kinerja fungsi callback:

    • Pastikan tidak ada logika pemblokiran di fungsi callback.
    • Callback berjalan di thread WebSocket. Operasi pemblokiran akan memengaruhi penerimaan data. Tulis data audio ke buffer terpisah dan proses di thread lain.
  3. Periksa stabilitas jaringan: Fluktuasi jaringan dapat menyebabkan gangguan atau penundaan transmisi audio.

T: Mengapa sintesis ucapan memakan waktu lama?

Pecahkan masalah dengan langkah-langkah berikut:

  1. Periksa interval input

    Untuk sintesis streaming, periksa apakah interval pengiriman teks terlalu lama. Interval yang panjang meningkatkan total waktu sintesis.

  2. Analisis metrik kinerja

    • Latensi paket pertama: biasanya sekitar 500 ms.
    • RTF (Real-Time Factor = total waktu sintesis / durasi audio): harus kurang dari 1,0.

T: Bagaimana cara membatasi Kunci API hanya untuk layanan sintesis ucapan (isolasi izin)?

Buat ruang kerja baru dan berikan akses hanya ke model tertentu. Hal ini membatasi cakupan Kunci API. Untuk detailnya, lihat Kelola ruang kerja.