All Products
Search
Document Center

Alibaba Cloud Model Studio:Sintesis suara real-time

Last Updated:Jun 26, 2026

Sintesis suara real-time mengonversi teks menjadi suara alami melalui koneksi WebSocket. Fitur ini mendukung input dan output streaming, kloning suara, desain suara, serta kontrol audio detail untuk kasus penggunaan seperti asisten suara, buku audio, dan layanan pelanggan cerdas.

Ikhtisar

Sintesis suara real-time berlatensi rendah melalui WebSocket, dirancang untuk asisten suara, layanan pelanggan cerdas, takarir langsung, dan skenario lain yang memerlukan respons instan.

  • Input dan output streaming (WebSocket full-duplex) dengan waktu ke audio pertama yang rendah, ideal untuk percakapan real-time seperti asisten suara dan layanan pelanggan cerdas

  • Laju ucapan, pitch, volume, dan bitrate yang dapat disesuaikan untuk kontrol suara detail

  • Kompatibel dengan format audio utama (PCM, WAV, MP3, Opus) dan mendukung output laju sampel hingga 48 kHz

  • Mendukung Kontrol berbasis instruksi, memungkinkan instruksi bahasa alami untuk mengatur ekspresivitas suara

  • Mendukung Kloning suara dan Desain Suara untuk kustomisasi suara

Jika Anda tidak memerlukan output real-time, gunakan Sintesis suara non-real-time (API HTTP), yang cocok untuk skenario batch seperti buku audio dan sulih suara materi pembelajaran. Untuk panduan pemilihan model, lihat Sintesis suara.

Prasyarat

Memulai dengan cepat

Contoh berikut menunjukkan sintesis suara untuk setiap model. Untuk contoh dan deskripsi parameter lebih lanjut, lihat Referensi API masing-masing model.

CosyVoice

Penting

cosyvoice-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 menggunakan model ini, buat suara melalui Kloning suara atau Desain Suara, lalu atur voice ke ID suara dan model ke nama model yang sesuai dalam kode Anda.

Contoh berikut menunjukkan cara melakukan sintesis suara dengan suara sistem (lihat Daftar Suara CosyVoice).

Untuk menggunakan kontrol instruksi, konfigurasikan instruksi melalui parameter instructions.

Python

# coding=utf-8

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

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

    # URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda berdasarkan wilayah.
    dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'

    # Model
    # Versi model berbeda memerlukan jenis suara yang sesuai:
    # cosyvoice-v3-flash/cosyvoice-v3-plus: Gunakan suara seperti longanyang.
    # cosyvoice-v2: Gunakan suara seperti longxiaochun_v2.
    # Setiap suara mendukung bahasa berbeda. Saat mensintesis bahasa non-Tiongkok seperti Jepang atau Korea, pilih suara yang mendukung bahasa target. Untuk detailnya, lihat daftar suara CosyVoice.
    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 untuk sintesis 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 pembentukan koneksi
    print('[Metric] requestId: {}, first package delay: {} 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)

Java

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 berbeda memerlukan jenis suara yang sesuai:
        // cosyvoice-v3-flash/cosyvoice-v3-plus: Gunakan suara seperti longanyang.
        // cosyvoice-v2: Gunakan suara seperti longxiaochun_v2.
        // Setiap suara mendukung bahasa berbeda. Saat mensintesis bahasa non-Tiongkok seperti Jepang atau Korea, pilih suara yang mendukung bahasa target. Untuk detailnya, lihat daftar suara CosyVoice.
        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: https://www.alibabacloud.com/help/en/model-studio/get-api-key
                            // Jika variabel lingkungan belum dikonfigurasi, ganti baris berikut dengan Kunci API Studio Model 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 setelah 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 pembentukan koneksi
                // Catatan: getFirstPackageDelay() memerlukan dashscope-sdk-java 2.18.0 atau lebih baru
                System.out.println(
                        "[Metric] requestId: "
                                + synthesizer.getLastRequestId()
                                + ", first package delay (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) {
            // URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda berdasarkan wilayah.
            Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
            streamAudioDataToSpeaker();
            System.exit(0);
        }
    }

Qwen-TTS

Contoh berikut menunjukkan cara melakukan sintesis suara menggunakan suara sistem (lihat Suara yang Didukung).

Untuk menggunakan Kontrol Berbasis Instruksi, atur parameter model ke qwen3-tts-instruct-flash-realtime dan konfigurasikan instruksi melalui parameter instructions.

Python

Mode server commit

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 selengkapnya:
        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('connection opened, init player')

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

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

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


if __name__  == '__main__':
    init_dashscope_api_key()

    print('Initializing ...')

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        # Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime
        model='qwen3-tts-flash-realtime',
        callback=callback,
        # URL ini untuk wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. Jika Anda menggunakan wilayah Beijing, ganti dengan: wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime
        url='wss://{WorkspaceId}.ap-southeast-1.maas.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'send text: {text_chunk}')
        qwen_tts_realtime.append_text(text_chunk)
        time.sleep(0.1)
    qwen_tts_realtime.finish()
    callback.wait_for_finished()
    print('[Metric] session: {}, first audio delay: {}'.format(
                    qwen_tts_realtime.get_session_id(), 
                    qwen_tts_realtime.get_first_audio_delay(),
                    ))

Mode commit

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 selengkapnya:
        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('connection opened, init player')

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

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

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


if __name__  == '__main__':
    init_dashscope_api_key()

    print('Initializing ...')

    callback = MyCallback()

    qwen_tts_realtime = QwenTtsRealtime(
        # Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime
        model='qwen3-tts-flash-realtime',
        callback=callback, 
        # URL ini untuk wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. Jika Anda menggunakan wilayah Beijing, ganti dengan: wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime
        url='wss://{WorkspaceId}.ap-southeast-1.maas.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'send text: {text_to_synthesize[0]}')
    qwen_tts_realtime.append_text(text_to_synthesize[0])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    callback.reset_event()
    
    print(f'send text: {text_to_synthesize[1]}')
    qwen_tts_realtime.append_text(text_to_synthesize[1])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    callback.reset_event()

    print(f'send text: {text_to_synthesize[2]}')
    qwen_tts_realtime.append_text(text_to_synthesize[2])
    qwen_tts_realtime.commit()
    callback.wait_for_response_done()
    
    qwen_tts_realtime.finish()
    print('[Metric] session: {}, first audio delay: {}'.format(
                    qwen_tts_realtime.get_session_id(), 
                    qwen_tts_realtime.get_first_audio_delay(),
                    ))

Java

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")
                // Titik akhir Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. Untuk Tiongkok (Beijing), gunakan wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime.
                .url("wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime")
                // Kunci API berbeda antara Singapura dan Tiongkok (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 pengakhiran 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")
                // Titik akhir Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. Untuk Tiongkok (Beijing), gunakan wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/realtime.
                .url("wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime")
                // Kunci API berbeda antara Singapura dan Tiongkok (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("connection opened");
                System.out.println("Enter text and press Enter to send. Enter 'quit' to exit the program.");
            }
            @Override
            public void onEvent(JsonObject message) {
                String type = message.get("type").getAsString();
                switch(type) {
                    case "session.created":
                        System.out.println("start session: " + message.get("session").getAsJsonObject().get("id").getAsString());
                        break;
                    case "response.audio.delta":
                        String recvAudioB64 = message.get("delta").getAsString();
                        byte[] rawAudio = Base64.getDecoder().decode(recvAudioB64);
                        // Putar audio secara real-time.
                        audioPlayer.write(recvAudioB64);
                        break;
                    case "response.done":
                        System.out.println("response done");
                        // 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("session finished");
                        if (qwenTtsRef.get() != null) {
                            System.out.println("[Metric] response: " + qwenTtsRef.get().getResponseId() +
                                    ", first audio delay: " + qwenTtsRef.get().getFirstAudioDelay() + " ms");
                        }
                        completeLatch.get().countDown();
                    default:
                        break;
                }
            }
            @Override
            public void onClose(int code, String reason) {
                System.out.println("connection closed code: " + code + ", reason: " + reason);
                try {
                    // 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("Enter the text to synthesize: ");
            String text = scanner.nextLine();

            // Keluar saat pengguna memasukkan 'quit'.
            if ("quit".equalsIgnoreCase(text.trim())) {
                System.out.println("Closing the connection...");
                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 cerdas menangani segmentasi teks dan waktu sintesis. Mode ini cocok untuk sintesis berkelanjutan dari blok teks besar. Klien hanya perlu menambahkan teks tanpa mengelola segmentasi atau pengiriman.

  • Mode commit: Klien secara manual mengirimkan buffer teks untuk memicu sintesis. Mode ini cocok untuk skenario yang memerlukan kontrol tepat atas waktu sintesis, seperti sintesis bergiliran dalam AI percakapan.

Alihkan mode interaksi:

  • WebSocket: Atur bidang mode dalam 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 siklus hidup event WebSocket dan penggunaan kembali koneksi, lihat Referensi API WebSocket.

Fitur lanjutan

Kontrol berbasis instruksi

Kontrol berbasis instruksi memungkinkan Anda membentuk nada, kecepatan, emosi, dan timbre melalui deskripsi bahasa alami, tanpa menyesuaikan parameter audio yang kompleks.

Spesifikasi instruksi berdasarkan model:

CosyVoice

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

Model berbeda memiliki persyaratan format instruksi berbeda:

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

    • Timbre Kloning/Desain Suara: Menerima instruksi apa pun.

    • Suara sistem: v3.5 tidak mendukung suara sistem.

  • cosyvoice-v3-plus:

    • Timbre Kloning/Desain Suara: Kontrol instruksi tidak didukung.

    • Suara sistem: Instruksi harus mengikuti format tetap. Untuk detailnya, lihat Daftar Suara CosyVoice.

  • cosyvoice-v3-flash:

    • Timbre Kloning/Desain Suara: Menerima instruksi apa pun.

    • Suara sistem: Instruksi harus mengikuti format tetap. Untuk detailnya, lihat Daftar Suara CosyVoice.

Cara menggunakan: Tentukan konten instruksi melalui parameter instructions.

Bahasa yang didukung untuk teks instruksi:

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

    • Timbre Kloning/Desain Suara: Tiongkok, Inggris, Prancis, Jerman, Jepang, Korea, Rusia, Portugis, Thailand, Indonesia, dan Vietnam.

    • Suara sistem: v3.5 tidak mendukung suara sistem.

  • cosyvoice-v3-plus:

    • Timbre Kloning/Desain Suara: Tiongkok, Inggris, Prancis, Jerman, Jepang, Korea, dan Rusia.

    • Suara sistem: Instruksi harus mengikuti format tetap. Untuk detailnya, lihat Daftar Suara CosyVoice.

  • cosyvoice-v3-flash:

    • Kloning Suara/Desain Timbre: Bahasa Tionghoa, Inggris, Prancis, Jerman, Jepang, Korea, dan Rusia.

    • Suara sistem: Tiongkok.

Batas panjang teks instruksi: Hingga 100 karakter. Karakter Tiongkok (termasuk Tiongkok Sederhana/Tradisional, Kanji Jepang, dan Hanja Korea) dihitung sebagai 2 karakter. Karakter lain (tanda baca, huruf, angka, Kana Jepang, Hangul Korea, dll.) dihitung sebagai 1 karakter.

Qwen-TTS

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

Cara menggunakan: Tentukan konten instruksi melalui parameter instruction.

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

Batas panjang teks instruksi: Hingga 1.600 token.

Kasus Penggunaan:

  • Sulih suara buku audio dan drama radio

  • Sulih suara iklan dan promosi

  • Sulih suara karakter game dan animasi

  • Asisten suara dengan ekspresi emosional

  • Narasi dokumenter dan siaran berita

Kiat menulis deskripsi suara berkualitas tinggi:

  • Prinsip inti:

    1. Spesifik, bukan samar: Gunakan kata yang menggambarkan kualitas suara konkret, seperti "dalam," "jernih," atau "sedikit cepat." Hindari istilah subjektif atau samar seperti "bagus" atau "normal."

    2. Multi-dimensi, bukan satu sisi: Deskripsi yang baik mencakup beberapa dimensi (jenis kelamin, usia, emosi, dll.). Menulis hanya "suara perempuan" terlalu luas untuk menghasilkan timbre yang khas.

    3. Objektif, bukan subjektif: Fokus pada kualitas fisik dan persepsi suara. Misalnya, gunakan "pitch sedikit tinggi dengan energi" daripada "suara favorit saya."

    4. Orisinal, bukan meniru: Jelaskan kualitas suara yang Anda inginkan, daripada meminta peniruan tokoh publik tertentu (seperti selebriti atau aktor). Model tidak mendukung peniruan, dan hal ini dapat melibatkan risiko hak cipta.

    5. Ringkas, bukan berlebihan: Setiap kata harus berarti. Hindari mengulang sinonim atau menumpuk pengubah yang tidak berarti.

  • Dimensi deskripsi:

    Menggabungkan dimensi berikut menghasilkan hasil yang lebih akurat. Semakin banyak dimensi yang dijelaskan, semakin tepat outputnya.

    Dimensi

    Contoh deskripsi

    Jenis kelamin

    Laki-laki, perempuan, netral

    Usia

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

    Pitch

    Tinggi, sedang, rendah, sedikit tinggi, sedikit rendah

    Kecepatan

    Cepat, sedang, lambat, sedikit cepat, sedikit lambat

    Emosi

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

    Timbre

    Magnetis, jernih, serak, lembut, manis, kaya, kuat

    Kasus penggunaan

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

  • Contoh:

    • Gaya siaran standar: Artikulasi jelas dan tepat dengan pengucapan standar

    • Suara perempuan muda dan hidup dengan kecepatan sedikit cepat dan intonasi naik yang nyata, cocok untuk memperkenalkan produk fesyen

    • Suara laki-laki paruh baya yang tenang dengan kecepatan lambat, timbre dalam dan magnetis, cocok untuk membaca berita atau narasi dokumenter

    • Suara perempuan intelektual yang lembut, sekitar 30 tahun, dengan nada tenang, cocok untuk membaca buku audio

    • Suara anak lucu, sekitar perempuan 8 tahun, ucapan sedikit kekanak-kanakan, cocok untuk sulih suara karakter animasi

Dialek

Gunakan model untuk menghasilkan ucapan dalam dialek Tiongkok seperti Henan, Sichuan, atau Kanton. Konfigurasi bervariasi berdasarkan model dan jenis suara.

Pengaturan dialek berdasarkan model:

CosyVoice

  • Suara sistem: Pilih salah satu suara berikut dari Daftar Suara CosyVoice:

    • Suara dengan dukungan dialek bawaan (misalnya, longshange_v3) menghasilkan dialek tersebut tanpa konfigurasi tambahan.

    • Suara yang mendukung Kontrol berbasis instruksi dan memungkinkan pemilihan dialek (misalnya, longanhuan_v3): tentukan dialek target dalam teks instruksi.

  • Timbre kloning suara: Gunakan Kontrol berbasis instruksi untuk mengatur dialek — misalnya, atur teks instruksi ke 请用河南话表达.

  • Timbre desain suara: Dialek belum didukung.

Dialek yang didukung per model: Lihat entri "Bahasa yang didukung" untuk setiap model di CosyVoice.

Contoh: Untuk menghasilkan ucapan dialek Henan, gunakan model cosyvoice-v3-flash dengan suara longanhuan_v3 dan atur teks instruksi ke "请用河南话表达。".

# coding=utf-8

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

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

# URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda berdasarkan wilayah.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'

# Model
# Versi model berbeda memerlukan jenis suara yang sesuai:
# cosyvoice-v3-flash/cosyvoice-v3-plus: Gunakan suara seperti longanyang.
# cosyvoice-v2: Gunakan suara seperti longxiaochun_v2.
# Pilih suara yang mendukung dialek
model = "cosyvoice-v3-flash"
# Suara
voice = "longanhuan_v3"

# Buat instance SpeechSynthesizer dan teruskan parameter permintaan seperti model, suara, dan instruksi di konstruktor
synthesizer = SpeechSynthesizer(model=model, voice=voice, instruction="请用河南话表达。")
# Kirim teks untuk sintesis 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 package delay: {} 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

  • Suara sistem: Gunakan suara sistem yang mendukung dialek. Untuk daftar suara Qwen-TTS, lihat Suara yang didukung.

  • Timbre Kloning/Desain Suara: Dialek belum didukung.

Dialek yang didukung per model: Lihat entri "Bahasa yang didukung" untuk setiap model di Qwen3-TTS.

Protokol WebSocket mentah

Contoh berikut menunjukkan cara menghubungkan langsung ke server melalui protokol WebSocket mentah, untuk skenario di mana SDK DashScope tidak digunakan. Ini adalah implementasi kerja minimal. Untuk spesifikasi protokol WebSocket setiap model, lihat referensi API yang sesuai.

Lihat contoh protokol WebSocket mentah

CosyVoice

Go

package main

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

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

const (
	// URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda berdasarkan wilayah.
	wsURL      = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/"
	outputFile = "output.mp3"
)

func main() {
	// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
	// Jika variabel lingkungan belum dikonfigurasi, ganti baris berikut dengan Kunci API Studio Model 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)

	// Kirim 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":      "cosyvoice-v3-flash",
			"parameters": map[string]interface{}{
				"text_type":   "PlainText",
				"voice":       "longanyang",
				"format":      "mp3",
				"sample_rate": 22050,
				"volume":      50,
				"rate":        1,
				"pitch":       1,
				// Jika enable_ssml diatur ke true, hanya satu event continue-task yang diizinkan; jika tidak akan terjadi error "Text request limit violated, expected 1."
				"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", ""))

		// Parsing JSON sederhana 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 {
							// Kirim 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)

							// Kirim 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 {
    // Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    // Jika variabel lingkungan belum dikonfigurasi, ganti baris berikut dengan Kunci API Studio Model 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.");

    // URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda 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 penerimaan pesan
            Task receiveTask = ReceiveMessagesAsync();

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

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

            // Kirim terus-menerus 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 penerimaan selesai
            await receiveTask;

            Console.WriteLine("Tugas selesai, koneksi ditutup.");
        } catch (OperationCanceledException) {
            Console.WriteLine("Tugas dibatalkan.");
        } catch (Exception ex) {
            Console.WriteLine($"Terjadi kesalahan: {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 = "cosyvoice-v3-flash",
            parameters = new
            {
                text_type = "PlainText",
                voice = "longanyang",
                format = "mp3",
                sample_rate = 22050,
                volume = 50,
                rate = 1,
                pitch = 1,
                // Jika enable_ssml diatur ke true, hanya satu event continue-task yang diizinkan; jika tidak akan terjadi error "Text request limit violated, expected 1."
                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 sebagai berikut. Tentukan versi dependensi yang sesuai berdasarkan kebutuhan Anda:

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

// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
// Jika variabel lingkungan belum dikonfigurasi, ganti baris berikut dengan Kunci API Studio Model Anda: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda berdasarkan wilayah.
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/'; // Alamat 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 khusus
$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 untuk 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 tutup: " . $code . "\n";
        }
        if ($reason !== null) {
            echo "Alasan tutup: " . $reason . "\n";
        }
    });
}, function ($e) {
    echo "Tidak dapat 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" => "cosyvoice-v3-flash",
            "parameters" => [
                "text_type" => "PlainText",
                "voice" => "longanyang",
                "format" => "mp3",
                "sample_rate" => 22050,
                "volume" => 50,
                "rate" => 1,
                "pitch" => 1,
                // Jika enable_ssml diatur ke true, hanya satu event continue-task yang diizinkan; jika tidak akan terjadi error "Text request limit violated, expected 1."
                "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 "Tidak dapat 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;

// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
// Jika variabel lingkungan belum dikonfigurasi, ganti baris berikut dengan Kunci API Studio Model Anda: const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda 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: 'cosyvoice-v3-flash',
      parameters: {
        text_type: 'PlainText',
        voice: 'longanyang', // Suara
        format: 'mp3', // Format audio
        sample_rate: 22050, // Laju sampel
        volume: 50, // Volume
        rate: 1, // Laju ucapan
        pitch: 1, // Pitch
        enable_ssml: false // Apakah mengaktifkan SSML. Jika enable_ssml diatur ke true, hanya satu event continue-task yang diizinkan; jika tidak akan terjadi error "Text request limit violated, expected 1."
      },
      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\'s 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(`Mengirim continue-task, 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

Untuk Java, kami merekomendasikan menggunakan SDK Java DashScope. Untuk detailnya, lihat SDK Java.

Berikut adalah contoh WebSocket Java. Sebelum menjalankannya, pastikan Anda telah mengimpor dependensi berikut:

  • Java-WebSocket

  • jackson-databind

Gunakan Maven atau Gradle untuk mengelola dependensi. Konfigurasinya sebagai berikut:

pom.xml

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

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

build.gradle

// Kode lain 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 lain 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 dibentuk");

        // Kirim event run-task
        // Jika enable_ssml diatur ke true, hanya satu event continue-task yang diizinkan; jika tidak akan terjadi error "Text request limit violated, expected 1."
        String runTaskCommand = "{ \"header\": { \"action\": \"run-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"task_group\": \"audio\", \"task\": \"tts\", \"function\": \"SpeechSynthesizer\", \"model\": \"cosyvoice-v3-flash\", \"parameters\": { \"text_type\": \"PlainText\", \"voice\": \"longanyang\", \"format\": \"mp3\", \"sample_rate\": 22050, \"volume\": 50, \"rate\": 1, \"pitch\": 1, \"enable_ssml\": false }, \"input\": {} }}";
        send(runTaskCommand);
    }

    @Override
    public void onMessage(String message) {
        System.out.println("Menerima pesan dari server: " + message);
        try {
            // Parsing 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 pengecualian: " + 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 {
            // Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
            // Jika variabel lingkungan belum dikonfigurasi, ganti baris berikut dengan Kunci API Studio Model 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);
            // URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda 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

Untuk Python, kami merekomendasikan menggunakan SDK Python DashScope. Untuk detailnya, lihat SDK Python.

Berikut adalah contoh WebSocket Python. Sebelum menjalankannya, instal dependensi sebagai berikut:

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

Jangan beri nama file Python Anda "websocket.py". Jika tidak, akan terjadi 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):
        """
    Menginisialisasi instans TTSClient.

    Parameter:
        api_key (str): Kunci API untuk otentikasi
        uri (str): Alamat layanan WebSocket
    """
        self.api_key = api_key  # Ganti dengan Kunci API Anda
        self.uri = uri  # Ganti dengan alamat WebSocket Anda
        self.task_id = str(uuid.uuid4())  # Buat ID tugas yang unik
        self.output_file = f"output_{int(time.time())}.mp3"  # Jalur file audio output
        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 saat koneksi WebSocket dibuat.
    Mengirimkan event run-task untuk memulai tugas sintesis suara.
    """
        print("WebSocket terhubung")

        # 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": "cosyvoice-v3-flash",
                "parameters": {
                    "text_type": "PlainText",
                    "voice": "longanyang",
                    "format": "mp3",
                    "sample_rate": 22050,
                    "volume": 50,
                    "rate": 1,
                    "pitch": 1,
                    # Jika enable_ssml diatur ke True, hanya satu event continue-task yang diizinkan; jika tidak, akan terjadi error "Text request limit violated, expected 1.".
                    "enable_ssml": False
                },
                "input": {}
            }
        }

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

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

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

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

                        if event == "task-started":
                            print("Tugas dimulai")
                            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 terkirim
                            self.send_finish_task()

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

                        elif event == "task-failed":
                            error_msg = msg_json.get("error_message", "Error tidak diketahui")
                            print(f"Tugas gagal: {error_msg}")
                            self.task_finished = True
                            self.close(ws)

            except json.JSONDecodeError as e:
                print(f"Penguraian JSON gagal: {e}")
        else:
            # Proses pesan biner (data audio)
            print(f"Menerima pesan biner, ukuran: {len(message)} byte")
            with open(self.output_file, "ab") as f:
                f.write(message)
            print(f"Data audio ditulis ke file lokal {self.output_file}")

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

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

    def send_continue_task(self, text):
        """Mengirim event continue-task dengan konten teks untuk 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"Mengirim event continue-task, konten teks: {text}")

    def send_finish_task(self):
        """Mengirim 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("event finish-task terkirim")

    def close(self, ws):
        """Menutup koneksi secara aktif"""
        if ws and ws.sock and ws.sock.connected:
            ws.close()
            print("Koneksi ditutup secara aktif")

    def run(self):
        """Memulai 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("Mendengarkan pesan WebSocket...")
        self.ws.run_forever()  # Mulai pendengar koneksi jangka panjang

# Contoh penggunaan
if __name__ == "__main__":
    # Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    # Jika variabel lingkungan tidak dikonfigurasi, ganti baris berikut dengan Kunci API Model Studio Anda: API_KEY = "sk-xxx"
    API_KEY = os.environ.get("DASHSCOPE_API_KEY")
    # URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja Anda yang sebenarnya. URL bervariasi berdasarkan wilayah.
    SERVER_URI = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/"  # Ganti dengan alamat 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 dalamnya:

    # -- 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 menyambungkan ke API Realtime TTS, mengirimkan data teks,
        menerima output audio, dan mengelola koneksi WebSocket.
    
        Atribut:
            base_url (str):
                URL dasar dari API Realtime.
            api_key (str):
                Kunci API untuk otentikasi.
            voice (str):
                Suara yang digunakan oleh server untuk sintesis suara.
            mode (SessionMode):
                Mode sesi, baik server_commit atau commit.
            audio_callback (Callable[[bytes], None]):
                Fungsi callback untuk menerima data audio.
            language_type(str)
                Bahasa ucapan yang disintesis. 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 tanggapan 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:
            """Membuat koneksi WebSocket dengan API Realtime TTS."""
            headers = {
                "Authorization": f"Bearer {self.api_key}"
            }
    
            self.ws = await websockets.connect(self.base_url, additional_headers=headers)
    
            # Mengatur konfigurasi sesi default
            await self.update_session({
                "mode": self.mode.value,
                "voice": self.voice,
                # Untuk menggunakan fitur kontrol instruksi, hapus komentar pada baris berikut 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:
            """Mengirim event ke server."""
            event['event_id'] = "event_" + str(int(time.time() * 1000))
            print(f"Sending 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:
            """Memperbarui konfigurasi sesi."""
            event = {
                "type": "session.update",
                "session": config
            }
            print("Updating session configuration: ", event)
            await self.send_event(event)
    
        async def append_text(self, text: str) -> None:
            """Mengirim data teks ke API."""
            event = {
                "type": "input_text_buffer.append",
                "text": text
            }
            await self.send_event(event)
    
        async def commit_text_buffer(self) -> None:
            """Melakukan commit pada buffer teks untuk memicu pemrosesan."""
            event = {
                "type": "input_text_buffer.commit"
            }
            await self.send_event(event)
    
        async def clear_text_buffer(self) -> None:
            """Menghapus buffer teks."""
            event = {
                "type": "input_text_buffer.clear"
            }
            await self.send_event(event)
    
        async def finish_session(self) -> None:
            """Mengakhiri sesi."""
            event = {
                "type": "session.finish"
            }
            await self.send_event(event)
    
        async def wait_for_response_done(self):
            """Menunggu event response.done"""
            if self._response_done_future:
                await self._response_done_future
    
        async def handle_messages(self) -> None:
            """Memproses paket 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"Received event: {event_type}")
    
                    if event_type == "error":
                        print("Error: ", event.get('error', {}))
                        continue
                    elif event_type == "session.created":
                        print("Session created, ID: ", event.get('session', {}).get('id'))
                    elif event_type == "session.updated":
                        print("Session updated, ID: ", event.get('session', {}).get('id'))
                    elif event_type == "input_text_buffer.committed":
                        print("Text buffer committed, item ID: ", event.get('item_id'))
                    elif event_type == "input_text_buffer.cleared":
                        print("Text buffer cleared")
                    elif event_type == "response.created":
                        self._current_response_id = event.get("response", {}).get("id")
                        self._is_responding = True
                        # Membuat future baru untuk menunggu response.done
                        self._response_done_future = asyncio.Future()
                        print("Response created, ID: ", self._current_response_id)
                    elif event_type == "response.output_item.added":
                        self._current_item_id = event.get("item", {}).get("id")
                        print("Output item added, ID: ", self._current_item_id)
                    # Memproses 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("Audio generation completed")
                    elif event_type == "response.done":
                        self._is_responding = False
                        self._current_response_id = None
                        self._current_item_id = None
                        # Menandai future sebagai selesai
                        if self._response_done_future and not self._response_done_future.done():
                            self._response_done_future.set_result(True)
                        print("Response completed")
                    elif event_type == "session.finished":
                        print("Session ended")
    
            except websockets.exceptions.ConnectionClosed:
                print("Connection closed")
            except Exception as e:
                print("Error processing messages: ", str(e))
    
        async def close(self) -> None:
            """Menutup koneksi WebSocket."""
            if self.ws:
                await self.ws.close()

    Java

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

    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 TTS Realtime.
     *
     * Kelas ini menyediakan metode untuk menghubungkan ke API TTS Realtime, mengirim data teks,
     * menerima 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");
        }
    
        /**
         * Bentuk koneksi WebSocket dengan API TTS Realtime.
         */
        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 dibentuk");
                    // 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 berikut 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("Menerima event: " + 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 dikomit, 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 berakhir");
                            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);
        }
    
        /**
         * Komit 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

    API Realtime mendukung dua mode berikut:

    • mode server_commit

      Klien hanya mengirim teks. Server secara cerdas menentukan segmentasi dan waktu sintesis. Mode ini cocok untuk skenario latensi rendah yang tidak memerlukan kontrol manual atas ritme sintesis, seperti navigasi GPS.

    • commit mode

      Klien menambahkan teks ke buffer lalu memicu server untuk mensintesis teks yang ditentukan. Mode ini cocok untuk skenario yang memerlukan kontrol detail atas jeda kalimat dan jeda, 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 dalamnya:

    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
    // URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda berdasarkan wilayah.
    URL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"
    // Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    // Jika variabel lingkungan belum dikonfigurasi, ganti baris berikut dengan Kunci API Studio Model 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 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"Menerima potongan audio: {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 = [
            "Platform model bahasa besar Alibaba Cloud, Studio Model, adalah platform all-in-one untuk mengembangkan dan membangun aplikasi model bahasa besar.",
            "Baik pengembang maupun pengguna bisnis dapat berpartisipasi secara mendalam dalam desain dan pengembangan aplikasi model bahasa besar.",
            "Anda dapat mengembangkan aplikasi model bahasa besar dalam lima menit menggunakan antarmuka sederhana,",
            "atau melatih model khusus dalam beberapa jam, memungkinkan Anda fokus lebih banyak energi pada inovasi aplikasi."
        ]
    
        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
        )
    
        // Bentuk 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 QwenTTS Realtime…")
        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 dalamnya:

    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 {
        // URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda berdasarkan wilayah.
        private static final String URL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime";
        // Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
        // Jika variabel lingkungan belum dikonfigurasi, ganti baris berikut dengan Kunci API Studio Model 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("Menerima data audio: " + audioData.length + " byte");
                }
            );
    
            client.connect();
    
            // Kirim fragmen teks
            String[] textFragments = {
                "Platform model bahasa besar Alibaba Cloud, Studio Model, adalah platform all-in-one untuk mengembangkan dan membangun aplikasi model bahasa besar.",
                "Baik pengembang maupun pengguna bisnis dapat berpartisipasi secara mendalam dalam desain dan pengembangan aplikasi model bahasa besar.",
                "Anda dapat mengembangkan aplikasi model bahasa besar dalam lima menit menggunakan antarmuka sederhana,",
                "atau melatih model khusus dalam beberapa jam, memungkinkan Anda fokus lebih banyak energi pada inovasi aplikasi."
            };
    
            System.out.println("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 dalamnya:

    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 tanda komentar pada instruksi di tts_realtime_client.py
    # URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja Anda yang sebenarnya. URL bervariasi berdasarkan wilayah.
    URL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"
    # Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    # Jika variabel lingkungan tidak dikonfigurasi, ganti baris berikut dengan Kunci API Model Studio 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 = []
    _AUDIO_SAMPLE_RATE = 24000
    _audio_pyaudio = pyaudio.PyAudio()
    _audio_stream = None
    
    def _audio_callback(audio_bytes: bytes):
        """Callback audio TTSRealtimeClient: pemutaran dan caching real-time"""
        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:
        """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 _user_input_loop(client: TTSRealtimeClient):
        """Terus-menerus mendapatkan input pengguna dan mengirim teks. Saat pengguna memasukkan teks kosong, kirim event commit dan akhiri sesi."""
        print("Masukkan teks (tekan Enter secara langsung untuk mengirim event commit dan mengakhiri sesi, 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 menandai akhir percakapan: commit buffer -> akhiri sesi -> keluar dari loop
                    logging.info("Input kosong, mengirim event commit dan mengakhiri sesi")
                    await client.commit_text_buffer()
                    # Tunggu sebentar hingga server memproses commit, untuk mencegah penghentian sesi prematur yang dapat menyebabkan hilangnya audio
                    await asyncio.sleep(0.3)
                    await client.finish_session()
                    break  # Keluar dari loop input pengguna secara 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():
        """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.COMMIT,  # Diubah ke mode COMMIT
            audio_callback=_audio_callback
        )
    
        # Buat koneksi
        await client.connect()
    
        # Jalankan penanganan paket 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  # Tunggu hingga input pengguna selesai
    
        # Tunggu tanggapan.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():
        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 beberapa kali. Tekan Enter tanpa memasukkan teks untuk mendengar audio yang dikembalikan oleh API Realtime melalui speaker Anda.

    Java

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

    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 {
        // URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda berdasarkan wilayah.
        private static final String URL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime";
        // Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
        // Jika variabel lingkungan belum dikonfigurasi, ganti baris berikut dengan Kunci API Studio Model 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 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.COMMIT,
                audioData -> {
                    playbackQueue.add(audioData);
                    audioChunks.add(audioData);
                    System.out.println("Menerima data audio: " + 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: komit 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 beberapa kali. Tekan Enter tanpa memasukkan teks untuk mendengar audio yang dikembalikan oleh API Realtime melalui speaker Anda.

Sambert

Go

package main

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

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

const (
	wsURL      = "wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference/" // Alamat server WebSocket
	outputFile = "output.mp3"                                        // Jalur file output
)

func main() {
	// Jika Kunci API belum dikonfigurasi di variabel lingkungan, ganti baris berikut dengan: apiKey := "your_api_key". Tidak disarankan untuk menyematkan Kunci API secara langsung dalam kode produksi untuk mengurangi risiko kebocoran Kunci API.
	apiKey := os.Getenv("DASHSCOPE_API_KEY")

	// Periksa dan hapus file output
	if err := clearOutputFile(outputFile); err != nil {
		fmt.Println("Gagal menghapus file output:", err)
		return
	}

	// Hubungkan ke layanan WebSocket
	conn, err := connectWebSocket(apiKey)
	if err != nil {
		fmt.Println("Gagal terhubung ke WebSocket:", err)
		return
	}
	defer closeConnection(conn)

	// Buat channel untuk menerima notifikasi penyelesaian tugas
	done := make(chan struct{})

	// Mulai goroutine async untuk menerima pesan
	go receiveMessage(conn, done)

	// Kirim perintah run-task
	if err := sendRunTaskMsg(conn); err != nil {
		fmt.Println("Gagal mengirim perintah run-task:", err)
		return
	}

	// Tunggu penyelesaian tugas atau timeout
	select {
	case <-done:
		fmt.Println("Tugas berakhir")
	case <-time.After(5 * time.Minute):
		fmt.Println("Tugas timeout")
	}
}

// Definisi struktur pesan
type Message struct {
	Header  Header  `json:"header"`
	Payload Payload `json:"payload"`
}

// Definisi Header
type Header struct {
	Action       string                 `json:"action,omitempty"`
	TaskID       string                 `json:"task_id"`
	Streaming    string                 `json:"streaming,omitempty"`
	Event        string                 `json:"event,omitempty"`
	ErrorCode    string                 `json:"error_code,omitempty"`
	ErrorMessage string                 `json:"error_message,omitempty"`
	Attributes   map[string]interface{} `json:"attributes"`
}

// Definisi Payload
type Payload struct {
	Model      string     `json:"model,omitempty"`
	TaskGroup  string     `json:"task_group,omitempty"`
	Task       string     `json:"task,omitempty"`
	Function   string     `json:"function,omitempty"`
	Input      Input      `json:"input,omitempty"`
	Parameters Parameters `json:"parameters,omitempty"`
	Output     Output     `json:"output,omitempty"`
	Usage      Usage      `json:"usage,omitempty"`
}

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

// Definisi Parameters
type Parameters struct {
	TextType                string  `json:"text_type"`
	Format                  string  `json:"format"`
	SampleRate              int     `json:"sample_rate"`
	Volume                  int     `json:"volume"`
	Rate                    float64 `json:"rate"`
	Pitch                   float64 `json:"pitch"`
	WordTimestampEnabled    bool    `json:"word_timestamp_enabled"`
	PhonemeTimestampEnabled bool    `json:"phoneme_timestamp_enabled"`
}

// Definisi Output
type Output struct {
	Sentence Sentence `json:"sentence"`
}

// Definisi Sentence
type Sentence struct {
	BeginTime int    `json:"begin_time"`
	EndTime   int    `json:"end_time"`
	Words     []Word `json:"words"`
}

// Definisi Word
type Word struct {
	Text      string    `json:"text"`
	BeginTime int       `json:"begin_time"`
	EndTime   int       `json:"end_time"`
	Phonemes  []Phoneme `json:"phonemes"`
}

// Definisi Phoneme
type Phoneme struct {
	BeginTime int    `json:"begin_time"`
	EndTime   int    `json:"end_time"`
	Text      string `json:"text"`
	Tone      int    `json:"tone"`
}

// Definisi Usage
type Usage struct {
	Characters int `json:"characters"`
}

func receiveMessage(conn *websocket.Conn, done chan struct{}) {
	for {
		msgType, message, err := conn.ReadMessage()
		if err != nil {
			fmt.Println("Gagal mengurai pesan server:", err)
			close(done)
			break
		}

		if msgType == websocket.BinaryMessage {
			// Proses aliran audio biner
			if err := writeBinaryDataToFile(message, outputFile); err != nil {
				fmt.Println("Gagal menulis data biner:", err)
				close(done)
				break
			}
			fmt.Println("Potongan audio ditulis ke file lokal")
		} else {
			// Proses pesan teks
			var msg Message
			if err := json.Unmarshal(message, &msg); err != nil {
				fmt.Println("Gagal mengurai event:", err)
				continue
			}
			if handleMessage(conn, msg, done) {
				break
			}
		}
	}
}

func handleMessage(conn *websocket.Conn, msg Message, done chan struct{}) bool {
	switch msg.Header.Event {
	case "task-started":
		fmt.Println("Tugas dimulai")

	case "result-generated":
	// Tambahkan kode di sini jika Anda perlu mengambil pesan tambahan

	case "task-finished":
		fmt.Println("Tugas selesai")
		close(done)
		return true

	case "task-failed":
		if msg.Header.ErrorMessage != "" {
			fmt.Printf("Tugas gagal: %s\n", msg.Header.ErrorMessage)
		} else {
			fmt.Println("Tugas gagal karena alasan tidak dikenal")
		}
		close(done)
		return true

	default:
		fmt.Printf("Event tak terduga: %v\n", msg)
		close(done)
	}

	return false
}

func sendRunTaskMsg(conn *websocket.Conn) error {
	runTaskMsg, err := generateRunTaskMsg()
	if err != nil {
		return err
	}
	if err := conn.WriteMessage(websocket.TextMessage, []byte(runTaskMsg)); err != nil {
		return err
	}
	return nil
}

func generateRunTaskMsg() (string, error) {
	runTaskMessage := Message{
		Header: Header{
			Action:    "run-task",
			TaskID:    uuid.New().String(),
			Streaming: "out",
		},
		Payload: Payload{
			Model:     "sambert-zhichu-v1",
			TaskGroup: "audio",
			Task:      "tts",
			Function:  "SpeechSynthesizer",
			Input: Input{
				Text: "Matahari putih tenggelam di balik gunung, dan Sungai Kuning mengalir ke laut. Untuk melihat seribu mil lebih jauh, Anda harus naik satu lantai lagi.",
			},
			Parameters: Parameters{
				TextType:                "PlainText",
				Format:                  "mp3",
				SampleRate:              16000,
				Volume:                  50,
				Rate:                    1.0,
				Pitch:                   1.0,
				WordTimestampEnabled:    true,
				PhonemeTimestampEnabled: true,
			},
		},
	}

	runTaskMsgJSON, err := json.Marshal(runTaskMessage)
	return string(runTaskMsgJSON), err
}

func connectWebSocket(apiKey string) (*websocket.Conn, error) {
	header := make(http.Header)
	header.Add("X-DashScope-DataInspection", "enable")
	header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
	conn, _, err := websocket.DefaultDialer.Dial(wsURL, header)
	if err != nil {
		fmt.Println("Gagal terhubung ke WebSocket:", err)
		return nil, err
	}
	return conn, nil
}

func writeBinaryDataToFile(data []byte, filePath string) error {
	file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer file.Close()
	_, err = file.Write(data)
	return err
}

func closeConnection(conn *websocket.Conn) {
	if conn != nil {
		conn.Close()
	}
}

func clearOutputFile(filePath string) error {
	file, err := os.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	file.Close()
	return nil
}

C#

Kode contoh:

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

class Program {
    // Jika Kunci API belum dikonfigurasi di variabel lingkungan, ganti baris berikut dengan: private const string ApiKey="your_api_key". Tidak disarankan untuk menyematkan Kunci API secara langsung dalam kode produksi untuk mengurangi risiko kebocoran Kunci API.
    private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("Variabel lingkungan DASHSCOPE_API_KEY belum diatur.");

    private const string WebSocketUrl = "wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference/"; // Alamat server WebSocket
    private const string OutputFilePath = "output.mp3"; // Jalur file output

    static async Task Main(string[] args) {
        var ws = new ClientWebSocket();
        try {
            // 1. Hubungkan ke layanan WebSocket dan autentikasi
            await ConnectWithAuth(ws, WebSocketUrl);

            // 2. Mulai thread penerimaan pesan
            var receiveTask = ReceiveMessages(ws);

            // 3. Kirim perintah run-task
            string textToSynthesize = "Matahari putih tenggelam di balik gunung, dan Sungai Kuning mengalir ke laut. Untuk melihat seribu mil lebih jauh, Anda harus naik satu lantai lagi.";
            string taskId = GenerateTaskId();
            await SendRunTaskCommand(ws, textToSynthesize, taskId);

            // 4. Tunggu tugas penerimaan selesai
            await receiveTask;
        } catch (Exception ex) {
            Console.WriteLine($"Error: {ex.Message}");
        } finally {
            if (ws.State == WebSocketState.Open) {
                await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "Menutup koneksi", CancellationToken.None);
            }
        }
    }

    private static async Task ConnectWithAuth(ClientWebSocket ws, string url) {
        var uri = new Uri(url);
        ws.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
        ws.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");
        await ws.ConnectAsync(uri, CancellationToken.None);
        Console.WriteLine("Terhubung ke server WebSocket.");
    }

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

    private static async Task SendRunTaskCommand(ClientWebSocket ws, string text, string taskId) {
        var command = CreateRunTaskCommand(text, taskId);
        var buffer = Encoding.UTF8.GetBytes(command);
        await ws.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, CancellationToken.None);
        Console.WriteLine("Perintah run-task dikirim.");
    }

    private static string CreateRunTaskCommand(string text, string taskId) {
        var command = new {
            header = new {
                action = "run-task",
                task_id = taskId,
                streaming = "out"
            },
            payload = new {
                model = "sambert-zhichu-v1",
                task_group = "audio",
                task = "tts",
                function = "SpeechSynthesizer",
                input = new {
                    text = text
                },
                parameters = new {
                    text_type = "PlainText",
                    format = "mp3",
                    sample_rate = 16000,
                    volume = 50,
                    rate = 1,
                    pitch = 1,
                    word_timestamp_enabled = true,
                    phoneme_timestamp_enabled = true
                }
            }
        };
        return JsonSerializer.Serialize(command);
    }

    private static async Task ReceiveMessages(ClientWebSocket ws) {
        var buffer = new byte[1024 * 4];
        var fs = new FileStream(OutputFilePath, FileMode.Create, FileAccess.Write);
        bool taskStarted = false;
        bool taskFinished = false;

        while (ws.State == WebSocketState.Open && !taskFinished) {
            var result = await ws.ReceiveAsync(new ArraySegment<byte>(buffer), CancellationToken.None);

            switch (result.MessageType) {
                case WebSocketMessageType.Text:
                    var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
                    var jsonMessage = JsonSerializer.Deserialize<JsonElement>(message);

                    ProcessTextMessage(jsonMessage, ref taskStarted, ref taskFinished);
                    break;
                case WebSocketMessageType.Binary:
                    if (taskStarted) {
                        await fs.WriteAsync(buffer, 0, result.Count);
                        Console.WriteLine("Menerima data audio.");
                    }
                    break;
                case WebSocketMessageType.Close:
                    Console.WriteLine("Server menutup koneksi.");
                    taskFinished = true;
                    break;
            }
        }
        fs.Close();
    }

    private static void ProcessTextMessage(JsonElement jsonMessage, ref bool taskStarted, ref bool taskFinished) {
        if (jsonMessage.TryGetProperty("header", out JsonElement header) && header.TryGetProperty("event", out JsonElement eventToken)) {
            var eventType = eventToken.GetString();
            switch (eventType) {
                case "task-started":
                    taskStarted = true;
                    Console.WriteLine("Tugas dimulai.");
                    break;
                case "result-generated":
                    // Tambahkan kode di sini jika Anda perlu mengambil pesan tambahan
                    break;
                case "task-finished":
                    taskFinished = true;
                    Console.WriteLine("Tugas selesai.");
                    break;
                case "task-failed":
                    taskFinished = true;
                    Console.WriteLine("Tugas gagal.");
                    break;
            }
        }
    }
}

PHP

Struktur direktori kode contoh:

my-php-project/

├── composer.json

├── vendor/

└── index.php

Isi composer.json sebagai berikut. Tentukan versi dependensi yang sesuai berdasarkan kebutuhan Anda:

{
    "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 'vendor/autoload.php';

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

# Jika Kunci API belum dikonfigurasi di variabel lingkungan, ganti baris berikut dengan: $api_key="your_api_key". Tidak disarankan untuk menyematkan Kunci API secara langsung dalam kode produksi untuk mengurangi risiko kebocoran Kunci API.
$api_key = getenv("DASHSCOPE_API_KEY");
$websocket_url = 'wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference/'; // Alamat 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, '');
    echo "File dihapus\n";
}

// Buat konektor khusus
$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'
];

// Hubungkan ke layanan WebSocket
$connector($websocket_url, [], $headers)
    ->then(function ($conn) use ($output_file) {
        echo "Koneksi dibentuk\n";

        // Terima pesan WebSocket secara asinkron
        $conn->on('message', function ($msg) use ($conn, $output_file) {
            if ($msg->isBinary()) {
                // Tulis data biner ke file lokal
                file_put_contents($output_file, $msg->getPayload(), FILE_APPEND);
                echo "Data biner ditulis ke file\n";
            } else {
                $data = json_decode($msg, true);
                switch ($data['header']['event']) {
                    case 'task-started':
                        echo "Tugas dimulai\n";
                        break;
                    case 'result-generated':
                        // Tambahkan kode di sini jika Anda perlu mengambil pesan tambahan
                        break;
                    case 'task-finished':
                        echo "Tugas selesai\n";
                        $conn->close();
                        break;
                    case 'task-failed':
                        echo "Tugas gagal: " . $data['header']['error_message'] . "\n";
                        $conn->close();
                        break;
                    default:
                        echo "Event tidak dikenal: " . $msg . "\n";
                }
            }
        });

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

        // Kirim perintah run-task
        $conn->send(json_encode([
            'header' => [
                'action' => 'run-task',
                'task_id' => bin2hex(random_bytes(16)),
                'streaming' => 'out'
            ],
            'payload' => [
                'model' => 'sambert-zhichu-v1',
                'task_group' => 'audio',
                'task' => 'tts',
                'function' => 'SpeechSynthesizer',
                'input' => [
                    'text' => 'Cahaya bulan terang di depan tempat tidurku, aku bertanya-tanya apakah itu embun beku di tanah. Aku mengangkat kepalaku untuk menatap bulan terang, dan menundukkan kepalaku untuk memikirkan kampung halamanku.'
                ],
                'parameters' => [
                    'text_type' => 'PlainText',
                    'format' => 'mp3',
                    'sample_rate' => 16000,
                    'volume' => 50,
                    'rate' => 1,
                    'pitch' => 1,
                    'word_timestamp_enabled' => true,
                    'phoneme_timestamp_enabled' => true
                ]
            ]
        ]));
        echo "Perintah run-task dikirim\n";
    }, function (Exception $e) {
        echo "Koneksi gagal: {$e->getMessage()}\n";
        file_put_contents('error.log', $e->getMessage() . "\n", FILE_APPEND);
    });

$loop->run();

Node.js

Instal dependensi yang diperlukan:

npm install ws
npm install uuid

Kode contoh:

const WebSocket = require('ws');
const fs = require('fs');
const { v4: uuidv4 } = require('uuid');

// Jika Kunci API belum dikonfigurasi di variabel lingkungan, ganti baris berikut dengan: apiKey = 'your_api_key'. Tidak disarankan untuk menyematkan Kunci API secara langsung dalam kode produksi untuk mengurangi risiko kebocoran Kunci API.
const apiKey = process.env.DASHSCOPE_API_KEY;
const wsUrl = 'wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference/'; // Alamat server WebSocket
const outputFilePath = 'output.mp3'; // Ganti dengan jalur file audio Anda

async function main() {
  await checkAndClearOutputFile(outputFilePath);
  createWebSocketConnection();
}

const fileStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
function createWebSocketConnection() {
  const ws = new WebSocket(wsUrl, {
    headers: {
      Authorization: `bearer ${apiKey}`,
      'X-DashScope-DataInspection': 'enable'
    }
  });

  ws.on('open', () => {
    console.log('Terhubung ke server WebSocket');
    sendRunTaskMessage(ws);
  });

  ws.on('message', (data, isBinary) => handleWebSocketMessage(data, isBinary, ws));
  ws.on('error', (error) => console.error('Error WebSocket:', error));
  ws.on('close', () => console.log('Koneksi WebSocket ditutup'));

  return ws;
}

function sendRunTaskMessage(ws) {
  const taskId = uuidv4();
  const runTaskMessage = {
    header: {
      action: 'run-task',
      task_id: taskId,
      streaming: 'out'
    },
    payload: {
      model: 'sambert-zhichu-v1',
      task_group: 'audio',
      task: 'tts',
      function: 'SpeechSynthesizer',
      input: {
        text: 'Matahari putih tenggelam di balik gunung, dan Sungai Kuning mengalir ke laut. Untuk melihat seribu mil lebih jauh, Anda harus naik satu lantai lagi.'
      },
      parameters: {
        text_type: 'PlainText',
        format: 'mp3',
        sample_rate: 16000,
        volume: 50,
        rate: 1,
        pitch: 1,
        word_timestamp_enabled: true,
        phoneme_timestamp_enabled: true
      }
    }
  };
  ws.send(JSON.stringify(runTaskMessage));
  console.log('Perintah run-task dikirim');
}

function handleWebSocketMessage(data, isBinary, ws) {
  if (isBinary) {
    fileStream.write(data);
  } else {
    const message = JSON.parse(data);
    handleWebSocketEvent(message, ws);
  }
}

function handleWebSocketEvent(message, ws) {
  switch (message.header.event) {
    case 'task-started':
      console.log('Tugas dimulai');
      break;
    case 'result-generated':
      console.log('Hasil dihasilkan');
      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:
      console.log('Event tidak dikenal:', message.header.event);
  }
}

function checkAndClearOutputFile(filePath) {
  return new Promise((resolve,reject) => {
    fs.access(filePath, fs.F_OK, (err) => {
      if (!err) {
        fs.truncate(filePath, 0, (truncateErr) => {
          if (truncateErr) return reject(truncateErr);
          console.log('File dihapus');
          resolve();
        });
      } else {
        fs.open(filePath, 'w', (openErr) => {
          if (openErr) return reject(openErr);
          console.log('File dibuat');
          resolve();
        });
      }
    });
  });
}

main().catch(console.error);

Penerapan produksi

Penggunaan kembali koneksi (WebSocket)

Koneksi WebSocket mendukung penggunaan kembali: setelah tugas sintesis selesai, tugas berikutnya dapat dimulai pada koneksi yang sama tanpa perlu menghubungkan ulang.

Alur penggunaan kembali:

  • CosyVoice: Klien mengirim finish-task. Setelah server mengembalikan event task-finished, klien dapat mengirim event run-task untuk memulai tugas baru.

  • Qwen-TTS: Klien mengirim session.finish. Setelah server mengembalikan session.finished, klien dapat membuat sesi baru untuk tugas berikutnya.

Penting
  1. Tunggu hingga server mengembalikan event penyelesaian (task-finished atau session.finished) sebelum memulai tugas baru.

  2. 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 tidak dapat digunakan kembali.

  4. Jika tidak ada tugas baru yang dimulai dalam waktu 60 detik, koneksi akan terputus secara otomatis.

Untuk detail event, lihat Referensi API CosyVoice, Referensi API Qwen-TTS.

Praktik terbaik konkurensi tinggi

SDK DashScope mencakup mekanisme pooling bawaan untuk menggunakan kembali koneksi WebSocket dan objek sintesis, mengurangi overhead pembuatan dan penghancuran yang sering terjadi.

Lihat praktik terbaik konkurensi tinggi

CosyVoice

Prasyarat

  • Dapatkan Kunci API

  • Versi SDK DashScope yang memenuhi persyaratan telah diinstal. Kami merekomendasikan menginstal versi terbaru:

    • SDK Python: Versi 1.25.2 atau lebih baru

    • SDK Java: Versi 2.16.6 atau lebih baru

SDK Python

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

Kolam objek membuat jumlah tertentu dari  SpeechSynthesizer  instance dan membentuk koneksi WebSocket saat inisialisasi. Objek yang diperoleh dari kolam dapat segera memulai permintaan, mengurangi waktu ke audio pertama. Saat dikembalikan, koneksi tetap aktif untuk tugas berikutnya.

Langkah implementasi

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

  2. Buat dan konfigurasikan kolam objek

    Atur ukuran kolam menjadi 1,5–2 kali konkurensi puncak Anda. Ukuran kolam tidak boleh melebihi batas QPS akun Anda.

    Buat instance singleton global kolam objek. Koneksi dibentuk selama inisialisasi, yang membutuhkan waktu:

    from dashscope.audio.tts_v2 import SpeechSynthesizerObjectPool
    
    synthesizer_object_pool = SpeechSynthesizerObjectPool(max_size=20)
    

    SpeechSynthesizerObjectPool membentuk koneksi WebSocket menggunakan nilai global saat ini dashscope.api_key saat inisialisasi. Kunci API ditulis ke header Authorization hanya selama handshake WebSocket; pesan tugas selanjutnya (seperti run-task) tidak membawanya. Mengubah dashscope.api_key setelah kolam dibuat tidak memengaruhi koneksi yang ada. Objek yang dikembalikan oleh borrow_synthesizer—termasuk objek yang digunakan kembali setelah dikembalikan—terus menggunakan kunci API dari handshake asli. Nilai baru diabaikan diam-diam, yang dapat menyebabkan atribusi identitas, kuota, atau penagihan berbeda dari yang diharapkan. borrow_synthesizer tidak mendukung menentukan kunci API melalui parameternya.

    Jika Anda perlu menggunakan beberapa kunci API berbeda, pertahankan instance terpisah SpeechSynthesizerObjectPool untuk setiap kunci API.

  3. Pinjam objek SpeechSynthesizer dari kolam

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

    Objek seperti itu memerlukan koneksi baru dan tidak memberikan manfaat penggunaan kembali.

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

    Panggil metode call atau streaming_call objek SpeechSynthesizer untuk melakukan sintesis suara.

  5. Kembalikan objek SpeechSynthesizer

    Setelah tugas selesai, kembalikan objek ke kolam untuk digunakan kembali.

    Jangan mengembalikan objek dengan tugas yang belum lengkap atau gagal.

    connectionPool.return_synthesizer(speech_synthesizer)
    
Kode lengkap

SpeechSynthesizerObjectPool membentuk koneksi WebSocket dan mengotentikasi menggunakan nilai global saat ini dashscope.api_key saat inisialisasi. Mengubah dashscope.api_key setelah kolam dibuat tidak memengaruhi koneksi yang ada; nilai baru diabaikan diam-diam. Untuk skenario multi-kunci, pertahankan instance kolam terpisah per kunci API. Untuk detailnya, lihat catatan penting di atas.

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

import os
import time
import threading

import dashscope
from dashscope.audio.tts_v2 import *

USE_CONNECTION_POOL = True
text_to_synthesize = [
    'Kalimat pertama: Selamat datang di sintesis suara Alibaba Cloud.',
    'Kalimat kedua: Selamat datang di sintesis suara Alibaba Cloud.',
    'Kalimat ketiga: Selamat datang di sintesis suara Alibaba Cloud.',
]
connectionPool = None

def init_dashscope_api_key():
    '''
    Tetapkan Kunci API DashScope Anda. Informasi selengkapnya:
    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

def synthesis_text_to_speech_and_play_by_streaming_mode(text, task_id):
    global USE_CONNECTION_POOL, connectionPool
    '''
    Sintesis suara dengan teks yang diberikan dengan mode streaming, panggilan async 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 kolam 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 suara selesai berhasil.')
            complete_event.set()

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

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

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

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

    // Panggil callback synthesizer suara
    synthesizer_callback = Callback()

    // Inisialisasi synthesizer suara
    // 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 suara gagal, {e}')
        if USE_CONNECTION_POOL:
            // tutup koneksi synthesizer secara manual jika tugas gagal saat menggunakan kolam koneksi.
            speech_synthesizer.close()
        return

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

// fungsi utama
if __name__ == '__main__':
    // Anda harus menyetel dashscope.api_key dan base_websocket_api_url sebelum membuat SpeechSynthesizerObjectPool.
    // Kolam membentuk koneksi WebSocket berdasarkan dashscope.api_key global saat ini saat inisialisasi.
    // Mengubah dashscope.api_key setelah kolam dibuat tidak memengaruhi koneksi yang ada di kolam.
    // URL di bawah ini mengarah ke wilayah Singapura. Jika Anda menggunakan model di wilayah Tiongkok (Beijing), ganti dengan: wss://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api-ws/v1/inference
    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 kolam koneksi')
        start_time = time.time() * 1000
        connectionPool = SpeechSynthesizerObjectPool(max_size=3)
        end_time = time.time() * 1000
        print('kolam 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 sukses: Saat tugas sintesis suara selesai secara normal, panggil  connectionPool.return_synthesizer(speech_synthesizer)  untuk mengembalikan objek  SpeechSynthesizer  ke kolam untuk digunakan kembali.

    Penting

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

  • Tugas gagal: Saat error internal SDK atau pengecualian logika bisnis mengganggu tugas, tutup koneksi WebSocket yang mendasari: speech_synthesizer.close()

  • Setelah semua tugas sintesis suara selesai, matikan kolam objek: connectionPool.shutdown() .

  • Saat layanan mengembalikan error TaskFailed, tidak diperlukan penanganan tambahan.

SDK Java

SDK Java mencapai kinerja optimal melalui kolam koneksi bawaan dan kolam objek khusus yang bekerja bersama.

  • Kolam koneksi: Kolam koneksi OkHttp3 yang terintegrasi dalam SDK mengelola dan menggunakan kembali koneksi WebSocket yang mendasari untuk mengurangi overhead handshake jaringan. Fitur ini diaktifkan secara default.

  • Kolam objek: Diimplementasikan berdasarkan  commons-pool2  , mempertahankan sekelompok objek  SpeechSynthesizer  dengan koneksi yang telah dibentuk sebelumnya. Mendapatkan objek dari kolam menghilangkan latensi pembentukan koneksi dan secara signifikan mengurangi waktu ke audio pertama.

Langkah implementasi

  1. Tambahkan dependensi

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

    Contoh berikut menunjukkan konfigurasi untuk Maven dan Gradle:

    Maven

    1. Buka file pom.xml proyek Maven Anda.

    2. Tambahkan informasi dependensi berikut 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. Anda dapat menemukan nomor versi 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. Anda dapat menemukan nomor versi 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 informasi dependensi berikut dalam blok dependencies .

      dependencies {
          // Ganti 'the-latest-version' dengan versi 2.16.6 atau lebih baru. Anda dapat menemukan nomor versi 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. Anda dapat menemukan nomor versi di: https://mvnrepository.com/artifact/org.apache.commons/commons-pool2
          implementation group: 'org.apache.commons', name: 'commons-pool2', version: 'the-latest-version'
      }
      
    3. Simpan file build.gradle .

    4. Di command line, navigasi ke direktori root proyek Anda dan jalankan perintah Gradle berikut untuk memperbarui dependensi proyek.

      ./gradlew build --refresh-dependencies
      

      Atau, jika Anda menggunakan Windows, perintahnya adalah:

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

    Konfigurasikan parameter kunci kolam koneksi dengan menggunakan variabel lingkungan:

    Variabel lingkungan

    Deskripsi

    DASHSCOPE_CONNECTION_POOL_SIZE

    Ukuran kolam koneksi.

    Nilai yang direkomendasikan: setidaknya dua kali konkurensi puncak per server.

    Nilai default: 32.

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS

    Jumlah maksimum permintaan asinkron.

    Nilai yang direkomendasikan: sama dengan  DASHSCOPE_CONNECTION_POOL_SIZE  .

    Nilai default: 32.

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST

    Jumlah maksimum permintaan asinkron per host.

    Nilai yang direkomendasikan: sama dengan  DASHSCOPE_CONNECTION_POOL_SIZE  .

    Nilai default: 32.

  3. Konfigurasikan kolam objek

    Konfigurasikan ukuran kolam objek dengan menggunakan variabel lingkungan:

    Variabel lingkungan

    Deskripsi

    COSYVOICE_OBJECTPOOL_SIZE

    Ukuran kolam objek.

    Nilai yang direkomendasikan: 1,5 hingga 2 kali konkurensi puncak per server.

    Nilai default: 500.

    Penting
    • Ukuran kolam objek ( COSYVOICE_OBJECTPOOL_SIZE ) harus kurang dari atau sama dengan ukuran kolam koneksi ( DASHSCOPE_CONNECTION_POOL_SIZE ). Jika tidak, saat kolam objek meminta objek sementara kolam koneksi penuh, thread pemanggil akan diblokir menunggu koneksi yang tersedia.

    • Ukuran kolam objek tidak boleh melebihi batas QPS (queries per second) akun Anda.

    Buat kolam objek dengan menggunakan kode berikut:

    class CosyvoiceObjectPool {
        // ...Kode lain dihilangkan di sini. Untuk contoh lengkap, lihat bagian kode lengkap.
        public static GenericObjectPool<SpeechSynthesizer> getInstance() {
            lock.lock();
            if (synthesizerPool == null) {
                // Anda dapat mengatur ukuran kolam objek di sini, atau mengonfigurasinya di variabel lingkungan COSYVOICE_OBJECTPOOL_SIZE.
                // Kami merekomendasikan mengatur nilai ini menjadi 1,5 hingga 2 kali konkurensi maksimum per 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;
        }
    }
    
  4. Dapatkan objek SpeechSynthesizer dari kolam objek

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

    Objek yang baru dibuat seperti itu perlu diinisialisasi ulang dan membentuk koneksi WebSocket baru. Mereka tidak dapat memanfaatkan koneksi yang ada di kolam objek dan karena itu tidak memberikan manfaat penggunaan kembali.

    synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
    
  5. Lakukan sintesis suara

    Setelah meminjam objek SpeechSynthesizer dari kolam, panggil updateParamAndCallback(param, callback) untuk mengikat parameter dan callback untuk tugas ini, lalu panggil streamingCall atau call untuk memulai sintesis.

    updateParamAndCallback dipanggil sekali per pinjaman untuk memperbarui callback dan parameter tingkat tugas (seperti voice dan format). Kunci API yang diteruskan dalam setiap panggilan harus tetap sama. updateParamAndCallback hanya memperbarui bidang lokal instance SpeechSynthesizer saat ini; tidak membangun ulang koneksi WebSocket yang mendasari. SDK menulis apiKey ke header Authorization hanya selama handshake WebSocket; pesan tugas selanjutnya (seperti run-task) tidak membawanya. Karena koneksi yang digunakan kembali tetap terbuka, apiKey yang berbeda tidak pernah dikirim ke server—permintaan terus menggunakan apiKey dari handshake asli, yang dapat menyebabkan atribusi identitas, kuota, atau penagihan berbeda dari yang diharapkan.

    Jika Anda perlu menggunakan beberapa kunci API berbeda, pertahankan instance kolam objek terpisah untuk setiap kunci API.

  6. Kembalikan objek SpeechSynthesizer

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

    Jangan mengembalikan objek yang tugasnya belum lengkap atau gagal.

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

Dalam skenario kolam objek, apiKey yang diteruskan ke setiap panggilan updateParamAndCallback harus tetap sama. SDK tidak memperbarui apiKey koneksi yang telah dibentuk; meneruskan apiKey yang berbeda tidak berpengaruh. Untuk skenario multi-kunci, pertahankan instance kolam objek terpisah per kunci API. Untuk detailnya, lihat catatan penting di bagian Lakukan sintesis suara.

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 mengimpor paket org.apache.commons.pool2 dan DashScope terkait 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 skenario konkurensi tinggi.
 *
 *
 * Sebelum melakukan panggilan konkurensi tinggi ke layanan TTS,
 * konfigurasikan parameter kolam koneksi dengan menggunakan 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 Kolam Objek di Env: "+ n);
            return n;
        } catch (NumberFormatException e) {
            System.out.println("Menggunakan Ukuran Kolam 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 kolam objek di sini, atau mengonfigurasinya di variabel lingkungan COSYVOICE_OBJECTPOOL_SIZE.
            // Kami merekomendasikan mengatur nilai ini menjadi 1,5 hingga 2 kali konkurensi maksimum per 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;
    }
}

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 untuk wilayah Singapura dan Beijing berbeda. Untuk mendapatkan Kunci API, kunjungi: https://www.alibabacloud.com/help/en/model-studio/get-api-key
                            // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Studio Model 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();
                // Penting: Dalam skenario kolam objek, kunci API yang diteruskan ke setiap panggilan updateParamAndCallback harus tetap sama. SDK tidak memperbarui kunci API untuk koneksi yang telah dibentuk; meneruskan kunci API yang berbeda tidak berpengaruh. Lihat bagian Lakukan sintesis suara untuk detailnya.
                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 error, tutup koneksi dan batalkan objek di kolam objek.
                    synthesizer.getDuplexApi().close(1000, "bye");
                    CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
                } else {
                    // Jika tugas selesai secara normal, kembalikan objek ke kolam.
                    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 suara selesai. Waktu yang berlalu: " + timeCost + " ms, RequestId " + requestId);
        }
    }
}

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

    public static void main(String[] args)
            throws InterruptedException, NoApiKeyException {
        // URL wilayah Singapura. Ganti WorkspaceId dengan ID ruang kerja aktual Anda. URL berbeda berdasarkan wilayah.
        Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
        // Periksa env kolam 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 kolam 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[] {
                    "Di depan tempat tidurku, cahaya bulan bersinar,", "Sepertinya embun beku di tanah.", "Aku mengangkat pandanganku untuk menonton bulan terang,", "Lalu menundukkan kepalaku, memikirkan rumah."}));
        }

        // 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 suara CosyVoice pada server Alibaba Cloud dengan spesifikasi tertentu. Konkurensi yang terlalu tinggi dapat menyebabkan penundaan pemrosesan tugas.

Di sini, konkurensi per server mengacu pada jumlah tugas sintesis suara CosyVoice yang berjalan secara bersamaan pada saat tertentu, yang juga dapat dipahami sebagai jumlah thread pekerja.

Spesifikasi server (Alibaba Cloud)

Konkurensi maksimum per server

Ukuran kolam objek

Ukuran kolam 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 sukses: Saat tugas sintesis suara selesai secara normal, Anda harus memanggil metode returnObject GenericObjectPool untuk mengembalikan objek SpeechSynthesizer kembali ke kolam untuk digunakan kembali.

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

    Penting

    Jangan mengembalikan objek SpeechSynthesizer yang tugasnya belum lengkap atau gagal.

  • Tugas gagal: Saat SDK secara internal atau logika bisnis melemparkan pengecualian yang mengganggu tugas, Anda harus melakukan dua operasi berikut:

    1. Tutup koneksi WebSocket yang mendasari secara proaktif

    2. Batalkan objek dari kolam objek untuk mencegahnya digunakan kembali

    // Dalam kode saat ini, ini sesuai dengan yang berikut
    // Tutup koneksi
    synthesizer.getDuplexApi().close(1000, "bye");
    // Batalkan synthesizer yang mengalami pengecualian dari kolam objek
    CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
    
  • Saat layanan melaporkan error TaskFailed, tidak diperlukan penanganan tambahan.

Pedoman pemanasan dan pengukuran latensi

Saat mengevaluasi kinerja SDK Java DashScope (seperti latensi panggilan konkuren), lakukan operasi pemanasan yang cukup terlebih dahulu. Ini memastikan bahwa pengukuran mencerminkan kinerja kondisi stabil dan menghindari kesenjangan data yang disebabkan oleh overhead pembentukan koneksi awal.

Mekanisme penggunaan kembali koneksi

SDK Java DashScope secara efisien mengelola dan menggunakan kembali koneksi WebSocket melalui kolam koneksi singleton global, dirancang untuk mengurangi overhead pembentukan dan pemutusan koneksi yang sering terjadi dan meningkatkan kapasitas pemrosesan dalam skenario konkurensi tinggi.

Mekanisme ini bekerja sebagai berikut:

  • Dibuat sesuai permintaan: SDK tidak membuat koneksi WebSocket sebelumnya saat startup layanan. Sebaliknya, koneksi dibentuk sesuai permintaan saat panggilan pertama dilakukan.

  • Penggunaan kembali berbatas waktu: Setelah permintaan selesai, koneksi dipertahankan di kolam hingga 60 detik untuk digunakan kembali.

    • Jika permintaan baru tiba dalam waktu 60 detik, koneksi yang ada digunakan kembali, menghindari overhead handshake berulang.

    • Jika koneksi tetap menganggur lebih dari 60 detik, koneksi tersebut ditutup secara otomatis untuk membebaskan sumber daya.

Pentingnya pemanasan

Dalam skenario berikut, kolam koneksi mungkin tidak memiliki koneksi aktif yang dapat digunakan kembali, menyebabkan permintaan membentuk koneksi baru:

  • Aplikasi baru saja dimulai dan belum ada panggilan yang dilakukan.

  • Layanan telah menganggur lebih dari 60 detik, dan koneksi di kolam telah ditutup karena timeout.

Dalam skenario ini, permintaan pertama harus menyelesaikan koneksi WebSocket penuh (handshake TCP, negosiasi TLS, dan peningkatan protokol), menghasilkan latensi yang jauh lebih tinggi daripada permintaan berikutnya pada koneksi yang digunakan kembali. Tanpa pemanasan, hasil pengujian kinerja condong oleh inklusi overhead pembentukan koneksi.

Perbedaan antara latensi sisi SDK dan waktu aktual ke audio pertama

Waktu ke audio pertama yang dilaporkan di sisi SDK (seperti nilai yang diperoleh melalui get_first_package_delay() ) mencakup waktu yang dihabiskan untuk pembentukan koneksi WebSocket dan transmisi jaringan, dan tidak sama dengan waktu aktual ke audio pertama layanan model.

Waktu aktual ke audio pertama mengacu pada interval waktu dari saat server menerima perintah run-task hingga mengembalikan event result-generated pertama. Nilai ini dapat dilihat di log sisi server.

Dalam skenario konkurensi tinggi, karena pembentukan sejumlah besar koneksi dan penjadwalan sumber daya, latensi yang dilaporkan di sisi SDK mungkin jauh lebih tinggi daripada waktu aktual ke audio pertama di sisi server. Jika SDK melaporkan waktu ke audio pertama yang tinggi:

  • Bandingkan waktu ke audio pertama di log sisi server (dari run-task ke result-generated pertama) untuk memverifikasi apakah kinerja inferensi model normal.

  • Gunakan mekanisme kolam objek atau kolam koneksi yang dijelaskan di atas untuk pemanasan guna menghilangkan overhead pembentukan koneksi WebSocket, sehingga latensi yang dilaporkan di sisi SDK lebih mencerminkan waktu aktual ke audio pertama.

Praktik yang direkomendasikan

Untuk mendapatkan data kinerja yang andal, ikuti langkah-langkah pemanasan ini sebelum melakukan pengujian tolok ukur kinerja formal atau pengukuran latensi:

  1. Simulasikan tingkat konkurensi pengujian formal Anda dengan mengirimkan sejumlah panggilan terlebih dahulu (misalnya, terus-menerus selama 1–2 menit) untuk sepenuhnya mengisi kolam koneksi.

  2. Setelah memastikan bahwa kolam koneksi telah membentuk dan mempertahankan jumlah koneksi aktif yang cukup, mulailah pengumpulan data kinerja formal.

Dengan pemanasan yang tepat, kolam koneksi SDK memasuki kondisi stabil penggunaan kembali, memungkinkan Anda mengukur metrik latensi yang lebih representatif yang secara akurat mencerminkan kinerja layanan selama operasi online kondisi stabil.

Pengecualian umum SDK Java

Pengecualian 1: Koneksi TCP server terus meningkat meskipun lalu lintas bisnis stabil

Penyebab:

Jenis 1:

Setiap objek SDK membuat koneksi saat diinisialisasi. Jika kolam objek tidak digunakan, setiap objek dihancurkan setelah tugasnya selesai. Koneksi kemudian memasuki kondisi tidak direferensikan dan tetap terbuka hingga server memicu timeout koneksi setelah 61 detik, yang berarti koneksi tidak dapat digunakan kembali selama 61 detik tersebut.

Dalam skenario konkurensi tinggi, tugas baru membuat koneksi baru saat tidak ada koneksi yang dapat digunakan kembali, yang menyebabkan konsekuensi berikut:

  1. Jumlah koneksi terus meningkat.

  2. Koneksi berlebihan menghabiskan sumber daya server, menyebabkan server tidak responsif.

  3. Kolam koneksi mencapai batasnya, dan tugas baru diblokir sambil menunggu koneksi yang tersedia.

Jenis 2:

Saat MaxIdle diatur ke nilai lebih kecil dari MaxTotal dalam konfigurasi kolam objek, objek menganggur yang melebihi MaxIdle dihancurkan, menyebabkan kebocoran koneksi. Koneksi yang bocor tetap terbuka hingga timeout 61 detik memicu pemutusan. Mirip dengan Jenis 1, ini menyebabkan jumlah koneksi terus meningkat.

Solusi:

Untuk Jenis 1, gunakan kolam objek.

Untuk Jenis 2, periksa parameter konfigurasi kolam objek. Atur MaxIdle sama dengan MaxTotal, dan nonaktifkan kebijakan penghancuran objek otomatis kolam objek.

Pengecualian 2: Tugas memakan waktu 60 detik lebih lama daripada panggilan normal

Sama dengan "Pengecualian 1". Kolam koneksi telah mencapai batas koneksi maksimum, dan tugas baru harus menunggu 61 detik agar koneksi yang tidak direferensikan timeout sebelum koneksi tersedia.

Pengecualian 3: Tugas lambat saat startup layanan tetapi secara bertahap kembali normal

Penyebab:

Dalam skenario konkurensi tinggi, objek yang sama menggunakan kembali koneksi WebSocket yang sama, sehingga koneksi WebSocket hanya dibuat saat startup layanan. Perhatikan bahwa jika panggilan konkurensi tinggi dimulai segera selama fase startup, membuat terlalu banyak koneksi WebSocket secara bersamaan dapat menyebabkan pemblokiran.

Solusi:

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

Pengecualian 4: Error server "Invalid action('run-task')! Please follow the protocol!"

Penyebab:

Ini terjadi saat error sisi klien terjadi tetapi server tidak mengetahuinya, meninggalkan koneksi dalam kondisi tugas aktif. Saat koneksi dan objek digunakan kembali untuk tugas berikutnya, terjadi error protokol dan tugas berikutnya gagal.

Solusi:

Tutup koneksi WebSocket secara aktif setelah pengecualian dilemparkan, lalu kembalikan objek ke kolam objek.

Pengecualian 5: Lonjakan volume panggilan abnormal meskipun lalu lintas bisnis stabil

Penyebab:

Membuat terlalu banyak koneksi WebSocket secara bersamaan menyebabkan pemblokiran, tetapi lalu lintas bisnis terus tiba, menyebabkan penumpukan tugas jangka pendek. Setelah pemblokiran terselesaikan, semua tugas yang tertunda dieksekusi segera. Ini menyebabkan lonjakan volume panggilan dan mungkin sesaat melebihi batas konkurensi akun, mengakibatkan kegagalan tugas parsial, server tidak responsif, dan masalah lainnya.

Situasi membuat terlalu banyak koneksi WebSocket secara bersamaan sering terjadi selama:

  • Fase startup layanan

  • Anomali jaringan yang menyebabkan sejumlah besar koneksi WebSocket terputus dan terhubung kembali secara bersamaan

  • Sejumlah besar error sisi server terjadi pada saat yang sama, memicu reconnect WebSocket massal. Error umum adalah melebihi batas konkurensi akun ("Requests rate limit exceeded, please try again later.").

Solusi:

  1. Periksa kondisi jaringan.

  2. Investigasi apakah sejumlah besar error sisi server lainnya terjadi sebelum lonjakan.

  3. Tingkatkan batas konkurensi akun.

  4. Kurangi ukuran kolam objek dan kolam koneksi untuk membatasi tingkat konkurensi maksimum melalui batas atas kolam objek.

  5. Tingkatkan konfigurasi server atau tambahkan lebih banyak mesin.

Pengecualian 6: Semua tugas melambat saat tingkat konkurensi meningkat

Solusi:

  1. Periksa apakah batas lebar pita jaringan telah tercapai.

  2. Periksa apakah tingkat konkurensi aktual terlalu tinggi.

Sambert

Sambert hanya mendukung pooling bawaan di SDK Java. SDK Python tidak mendukung pooling.

Prasyarat

Konfigurasi yang direkomendasikan

Ukuran kolam koneksi dan kolam objek bukan "semakin besar semakin baik." Kolam yang terlalu kecil atau terlalu besar dapat menurunkan kinerja. Konfigurasikan berdasarkan spesifikasi aktual server Anda.

Konfigurasi yang direkomendasikan berikut didasarkan pada pengujian dengan hanya menjalankan layanan sintesis suara Sambert di server:

Konfigurasi mesin (Alibaba Cloud)

Konkurensi maksimum per server

Ukuran kolam objek

Ukuran kolam koneksi

4 vCPU, 8 GiB

600

1200

2000

Konkurensi per server mengacu pada jumlah tugas sintesis suara Sambert yang berjalan secara bersamaan, setara dengan jumlah thread pekerja.
Penting

Dalam panggilan konkurensi tinggi, objek yang sama menggunakan kembali koneksi WebSocket yang sama, sehingga koneksi WebSocket hanya dibuat saat startup layanan.

Membuat terlalu banyak koneksi WebSocket secara bersamaan menyebabkan pemblokiran. Tingkatkan secara bertahap konkurensi per server saat memulai layanan.

Parameter yang dapat dikonfigurasi

Kolam koneksi

SDK Java DashScope menggunakan kolam koneksi OkHttp3 untuk menggunakan kembali koneksi WebSocket, mengurangi overhead dan latensi pembuatan koneksi yang sering terjadi.

Kolam koneksi diaktifkan secara default di SDK DashScope. Konfigurasikan ukuran kolam berdasarkan kasus penggunaan Anda.

Sebelum menjalankan layanan Java, konfigurasikan parameter kolam koneksi melalui variabel lingkungan. Parameter konfigurasi kolam koneksi adalah sebagai berikut:

DASHSCOPE_CONNECTION_POOL_SIZE

Ukuran kolam koneksi. Default: 32.

Nilai yang direkomendasikan: setidaknya 2 kali konkurensi puncak Anda.

DASHSCOPE_MAXIMUM_ASYNC_REQUESTS

Jumlah maksimum permintaan async. Default: 32.

Nilai yang direkomendasikan: sama dengan ukuran kolam koneksi.

Untuk informasi lebih lanjut, lihat dokumentasi referensi.

DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST

Jumlah maksimum permintaan async per host. Default: 32.

Nilai yang direkomendasikan: sama dengan ukuran kolam koneksi.

Untuk informasi lebih lanjut, lihat dokumentasi referensi.

Kolam objek

Gunakan kolam objek untuk menggunakan kembali objek SpeechSynthesizer . Ini lebih lanjut mengurangi overhead memori dan waktu pembuatan dan penghancuran objek yang berulang.

Sebelum menjalankan layanan Java, konfigurasikan ukuran kolam objek melalui variabel lingkungan atau kode. Parameter konfigurasi kolam objek adalah sebagai berikut:

SAMBERT_OBJECTPOOL_SIZE

Ukuran kolam objek.

Nilai yang direkomendasikan: 1,5 hingga 2 kali konkurensi puncak Anda.

Ukuran kolam objek harus kurang dari atau sama dengan ukuran kolam koneksi. Jika tidak, objek yang menunggu koneksi menyebabkan pemblokiran panggilan.

Untuk detail tentang mengonfigurasi variabel lingkungan, lihat Konfigurasikan kunci API sebagai variabel lingkungan.

Kode contoh

Contoh berikut menggunakan kolam sumber daya. Kolam objek adalah singleton global.

  • Setiap akun utama dapat mengirimkan hingga 3 tugas sintesis suara Sambert per detik secara default.

    Untuk meminta QPS yang lebih tinggi, hubungi kami.

  • Impor paket DashScope dan org.apache.commons.pool2. DashScope memerlukan versi 2.16.9 atau lebih baru.

    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.9 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'
      }
      
    3. Simpan file build.gradle .

    4. Di terminal, navigasi ke direktori root proyek Anda dan jalankan perintah Gradle berikut untuk memperbarui dependensi proyek.

      ./gradlew build --refresh-dependencies
      

      Di Windows, jalankan:

      gradlew build --refresh-dependencies
      
      Catatan

      Jika proyek Anda tidak memiliki file wrapper Gradle ( gradlew atau gradlew.bat ), Anda dapat:

      • Jalankan gradle build --refresh-dependencies langsung dengan Gradle yang diinstal.

      • Atau jalankan gradle wrapper terlebih dahulu untuk menghasilkan file wrapper, lalu jalankan perintah di atas.

  • Dalam kode contoh, thread berbeda menunggu durasi acak untuk menghindari membuat terlalu banyak koneksi WebSocket secara bersamaan.

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

import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;

/**
 * Sebelum melakukan panggilan konkurensi tinggi ke layanan TTS,
 * harap konfigurasikan ukuran kolam koneksi melalui variabel lingkungan berikut.
 *
 * DASHSCOPE_MAXIMUM_ASYNC_REQUESTS=2000
 * DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST=2000
 * DASHSCOPE_CONNECTION_POOL_SIZE=2000
 *
 * Default adalah 32, dan disarankan untuk mengaturnya menjadi 2 kali koneksi konkuren maksimum server tunggal.
 */

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

    public static void main(String[] args)
            throws InterruptedException, NoApiKeyException {

        // Periksa env kolam koneksi
        checkoutEnv("DASHSCOPE_CONNECTION_POOL_SIZE", 32);
        checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS", 32);
        checkoutEnv(SambertObjectPool.SAMBERT_OBJECTPOOL_SIZE_ENV, SambertObjectPool.DEFAULT_CONNECTION_POOL_SIZE);
        checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST", 32);

        // Catat waktu mulai tugas
        int runTimes = 1;

        // Buat kolam objek SpeechSynthesis
        ExecutorService executorService = Executors.newFixedThreadPool(runTimes);

        for (int i = 0; i < runTimes; i++) {
            executorService.submit(new SynthesizeTask(new String[]{
                    "Di depan tempat tidurku, cahaya bulan bersinar,",
                    "Sepertinya embun beku di tanah.",
                    "Aku mengangkat pandanganku untuk menonton bulan terang,",
                    "Lalu menundukkan kepalaku, memikirkan rumah."
            }));
        }

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

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 SambertObjectPool {
    public static GenericObjectPool<SpeechSynthesizer> synthesizerPool;
    public static String SAMBERT_OBJECTPOOL_SIZE_ENV = "SAMBERT_OBJECTPOOL_SIZE";
    public static int DEFAULT_CONNECTION_POOL_SIZE = 500;
    private static Lock lock = new java.util.concurrent.locks.ReentrantLock();
    public static int getObjectivePoolSize() {
        try {
            Integer n = Integer.parseInt(System.getenv(SAMBERT_OBJECTPOOL_SIZE_ENV));
            return n;
        } catch (NumberFormatException e) {
            return DEFAULT_CONNECTION_POOL_SIZE;
        }
    }
    public static GenericObjectPool<SpeechSynthesizer> getInstance() {
        lock.lock();
        if (synthesizerPool == null) {
            // Anda dapat mengatur ukuran kolam objek di sini. atau di variabel lingkungan
            // SAMBERT_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 SynthesizeTask implements Runnable {
    String[] textList;
    String requestId;
    long timeCost;
    public SynthesizeTask(String[] textList) {
        this.textList = textList;
    }
    @Override
    public void run() {
        // tidur waktu acak sebelum memulai tugas, hindari membuat terlalu banyak websocket pada saat yang sama.
        Random random = new Random();
        try {
            Thread.sleep(random.nextInt(30*1000));
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        for (String text:textList) {
            SpeechSynthesizer synthesizer = null;
            long startTime = System.currentTimeMillis();

            try {
                CountDownLatch latch = new CountDownLatch(1);
                class ReactCallback extends ResultCallback<SpeechSynthesisResult> {
                    ReactCallback() {}

                    @Override
                    public void onEvent(SpeechSynthesisResult message) {
                        if (message.getAudioFrame() != null) {
                            try {
                                byte[] bytesArray = message.getAudioFrame().array();
                            } catch (Exception e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }

                    @Override
                    public void onComplete() {
                        latch.countDown();
                    }

                    @Override
                    public void onError(Exception e) {
                        System.out.println(e.getMessage());
                        e.printStackTrace();
                        latch.countDown();
                    }
                }

                SpeechSynthesisParam param =
                        SpeechSynthesisParam.builder()
                                .model("sambert-zhichu-v1")
                                .format(SpeechSynthesisAudioFormat.MP3) // Gunakan PCM atau MP3
                                .text(text)
                                .enablePhonemeTimestamp(true)
                                .enableWordTimestamp(true)
                                // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Studio Model Anda: .apiKey("sk-xxx")
                                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                                .build();

                try {
                    synthesizer = SambertObjectPool.getInstance().borrowObject();
                    synthesizer.call(param, new ReactCallback());
                    try {
                        latch.await();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    requestId = synthesizer.getLastRequestId();
                } catch (Exception e) {
                    System.out.println("Exception e: " + e.toString());
                    synthesizer.getSyncApi().close(1000, "bye");
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                if (synthesizer != null) {
                    try {
                        // Kembalikan objek SpeechSynthesizer ke kolam
                        SambertObjectPool.getInstance().returnObject(synthesizer);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
            long endTime = System.currentTimeMillis();
            timeCost = endTime - startTime;
            System.out.println("[Thread " + Thread.currentThread() + "] Tugas sintesis suara: (" + text + ") selesai. Biaya waktu: " + timeCost + " ms, RequestId " + requestId);
        }
    }
}
Penanganan error
  • Saat layanan mengembalikan error TaskFailed, tidak diperlukan penanganan tambahan.

  • Jika error sisi klien (seperti pengecualian internal SDK atau pengecualian logika bisnis) mengganggu tugas sintesis sebelum selesai, tutup koneksi secara manual.

    Tutup koneksi sebagai berikut:

    // Tempatkan kode berikut dalam blok try-catch
    synthesizer.getSyncApi().close(1000, "bye");
    

Pengecualian umum

Pengecualian 1: Koneksi TCP server terus meningkat meskipun lalu lintas bisnis stabil

Penyebab:

Jenis 1:

Setiap objek SDK membuat koneksi saat diinisialisasi. Jika kolam objek tidak digunakan, setiap objek dihancurkan setelah tugasnya selesai. Koneksi kemudian memasuki kondisi tidak direferensikan dan tetap terbuka hingga server memicu timeout koneksi setelah 61 detik, yang berarti koneksi tidak dapat digunakan kembali selama 61 detik tersebut.

Dalam skenario konkurensi tinggi, tugas baru membuat koneksi baru saat tidak ada koneksi yang dapat digunakan kembali, yang menyebabkan konsekuensi berikut:

  1. Jumlah koneksi terus meningkat.

  2. Koneksi berlebihan menghabiskan sumber daya server, menyebabkan server tidak responsif.

  3. Kolam koneksi mencapai batasnya, dan tugas baru diblokir sambil menunggu koneksi yang tersedia.

Jenis 2:

Saat MaxIdle diatur ke nilai lebih kecil dari MaxTotal dalam konfigurasi kolam objek, objek menganggur yang melebihi MaxIdle dihancurkan, menyebabkan kebocoran koneksi. Koneksi yang bocor tetap terbuka hingga timeout 61 detik memicu pemutusan. Mirip dengan Jenis 1, ini menyebabkan jumlah koneksi terus meningkat.

Solusi:

Untuk Jenis 1, gunakan kolam objek.

Untuk Jenis 2, periksa parameter konfigurasi kolam objek. Atur MaxIdle sama dengan MaxTotal, dan nonaktifkan kebijakan penghancuran objek otomatis kolam objek.

Pengecualian 2: Tugas memakan waktu 60 detik lebih lama daripada panggilan normal

Sama dengan "Pengecualian 1". Kolam koneksi telah mencapai batas koneksi maksimum, dan tugas baru harus menunggu 61 detik agar koneksi yang tidak direferensikan timeout sebelum koneksi tersedia.

Pengecualian 3: Tugas lambat saat startup layanan tetapi secara bertahap kembali normal

Penyebab:

Dalam skenario konkurensi tinggi, objek yang sama menggunakan kembali koneksi WebSocket yang sama, sehingga koneksi WebSocket hanya dibuat saat startup layanan. Perhatikan bahwa jika panggilan konkurensi tinggi dimulai segera selama fase startup, membuat terlalu banyak koneksi WebSocket secara bersamaan dapat menyebabkan pemblokiran.

Solusi:

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

Pengecualian 4: Error server "Invalid action('run-task')! Please follow the protocol!"

Penyebab:

Ini terjadi saat error sisi klien terjadi tetapi server tidak mengetahuinya, meninggalkan koneksi dalam kondisi tugas aktif. Saat koneksi dan objek digunakan kembali untuk tugas berikutnya, terjadi error protokol dan tugas berikutnya gagal.

Solusi:

Tutup koneksi WebSocket secara aktif setelah pengecualian dilemparkan, lalu kembalikan objek ke kolam objek.

Pengecualian 5: Lonjakan volume panggilan abnormal meskipun lalu lintas bisnis stabil

Penyebab:

Membuat terlalu banyak koneksi WebSocket secara bersamaan menyebabkan pemblokiran, tetapi lalu lintas bisnis terus tiba, menyebabkan penumpukan tugas jangka pendek. Setelah pemblokiran terselesaikan, semua tugas yang tertunda dieksekusi segera. Ini menyebabkan lonjakan volume panggilan dan mungkin sesaat melebihi batas konkurensi akun, mengakibatkan kegagalan tugas parsial, server tidak responsif, dan masalah lainnya.

Situasi membuat terlalu banyak koneksi WebSocket secara bersamaan sering terjadi selama:

  • Fase startup layanan

  • Anomali jaringan yang menyebabkan sejumlah besar koneksi WebSocket terputus dan terhubung kembali secara bersamaan

  • Sejumlah besar error sisi server terjadi pada saat yang sama, memicu reconnect WebSocket massal. Error umum adalah melebihi batas konkurensi akun ("Requests rate limit exceeded, please try again later.").

Solusi:

  1. Periksa kondisi jaringan.

  2. Investigasi apakah sejumlah besar error sisi server lainnya terjadi sebelum lonjakan.

  3. Tingkatkan batas konkurensi akun.

  4. Kurangi ukuran kolam objek dan kolam koneksi untuk membatasi tingkat konkurensi maksimum melalui batas atas kolam objek.

  5. Tingkatkan konfigurasi server atau tambahkan lebih banyak mesin.

Pengecualian 6: Semua tugas melambat saat tingkat konkurensi meningkat

Solusi:

  1. Periksa apakah batas lebar pita jaringan telah tercapai.

  2. Periksa apakah tingkat konkurensi aktual terlalu tinggi.

Model dan wilayah yang didukung

Singapura

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

  • 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)

Tiongkok (Beijing)

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

  • 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.

Referensi API

FAQ

P: Bagaimana cara memperbaiki pengucapan salah dalam sintesis suara? Bagaimana cara mengontrol pengucapan karakter homofon?

  • Ganti karakter polifonik dengan homofon untuk memperbaiki masalah pelafalan secara cepat.

  • Gunakan bahasa markup SSML untuk mengontrol pengucapan .

P: Bagaimana cara mengatasi audio diam saat menggunakan suara yang dikloning?

  1. Verifikasi status suara

    Panggil API Kloning/Desain Suara dan pastikan status suara adalah OK.

  2. Periksa konsistensi versi model

    Pastikan parameter target_model yang digunakan saat kloning suara cocok dengan parameter model yang digunakan saat sintesis suara. Misalnya:

    • Jika Anda menggunakan cosyvoice-v3-plus untuk kloning,

    • Anda juga harus menggunakan cosyvoice-v3-plus untuk sintesis.

  3. Verifikasi kualitas audio sumber

    Periksa apakah audio sumber yang digunakan untuk kloning suara memenuhi persyaratan audio dan praktik terbaik:

    • Durasi audio: 10–20 detik

    • Kualitas audio jernih

    • Tidak ada kebisingan latar belakang

  4. Periksa parameter permintaan

    Pastikan parameter voice dalam permintaan sintesis suara Anda diatur ke ID suara yang dikloning.

P: Apa yang harus saya lakukan jika suara yang dikloning menghasilkan ucapan yang tidak stabil atau tidak lengkap?

Jika ucapan yang disintesis dari suara yang dikloning menunjukkan masalah berikut:

  • Pemutaran tidak lengkap yang hanya membaca bagian dari teks

  • Kualitas sintesis tidak konsisten

  • Jeda abnormal atau segmen diam dalam ucapan

Kemungkinan penyebab: Kualitas audio sumber tidak memenuhi persyaratan.

Solusi: Periksa apakah audio sumber memenuhi persyaratan Panduan perekaman untuk kloning suara. Kami merekomendasikan merekam ulang sesuai panduan tersebut.

P: Mengapa durasi aktual berbeda dari durasi yang ditampilkan di header file WAV?

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

P: Mengapa audio tidak diputar?

Atasi berdasarkan skenario berikut:

  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 format diatur ke wav, file harus disimpan sebagai .wav).

    2. Kompatibilitas pemutar: Pastikan pemutar Anda 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 langkah pemecahan masalah pada Skenario 1.

    2. Jika file dapat diputar dengan benar, masalahnya terletak pada implementasi pemutaran streaming. Pastikan pemutar Anda mendukung pemutaran streaming (seperti ffmpeg, pyaudio, AudioFormat, atau MediaSource).

P: Mengapa pemutaran audio tersendat?

Atasi dengan langkah-langkah berikut:

  1. Periksa laju pengiriman teks: Pastikan interval antar segmen teks cukup singkat sehingga segmen berikutnya tiba sebelum audio sebelumnya selesai diputar.

  2. Periksa kinerja fungsi callback:

    • Pastikan fungsi callback tidak menjalankan logika bisnis yang memblokir.

    • Callback berjalan di thread WebSocket. Memblokirnya akan menunda penerimaan data. Tulis data audio ke buffer terpisah dan proses di thread terpisah.

  3. Periksa stabilitas jaringan: Fluktuasi jaringan dapat menyebabkan gangguan atau penundaan transmisi audio.

P: Mengapa sintesis suara memakan waktu lama?

Atasi dengan langkah-langkah berikut:

  1. Periksa interval input

    Untuk sintesis streaming, pastikan interval antar segmen teks tidak terlalu lama. Interval yang panjang akan 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 dalam kondisi normal.

P: Bagaimana cara membatasi Kunci API hanya untuk sintesis suara saja (isolasi izin)?

Buat ruang kerja baru dan otorisasi hanya model tertentu untuk membatasi cakupan Kunci API. Untuk detailnya, lihat Kelola ruang kerja.