All Products
Search
Document Center

Alibaba Cloud Model Studio:Bahasa Markup Sintesis Ucapan (SSML)

Last Updated:Mar 04, 2026

Bahasa Markup Sintesis Ucapan (SSML) adalah bahasa markup berbasis XML untuk sintesis suara. SSML memungkinkan model sintesis suara skala besar memproses teks yang kaya dan memberikan Anda kendali detail halus atas fitur ucapan, seperti laju ucapan, pitch, jeda, dan volume. Anda juga dapat menambahkan musik latar untuk menciptakan efek ucapan yang lebih ekspresif. Topik ini menjelaskan fitur SSML dari CosyVoice dan cara menggunakannya.

Batasan

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

  • Voice: Hanya mendukung voice yang dikloning dan voice sistem yang ditandai di Daftar Voice sebagai pendukung SSML.

  • API: Hanya mendukung beberapa API.

Mulai

Sebelum menjalankan kode, selesaikan langkah-langkah berikut:

  1. Dapatkan Kunci API

  2. Instal SDK (jika Anda berencana menjalankan contoh Java/Python SDK)

Penting

cosyvoice-v3.5-plus dan cosyvoice-v3.5-flash saat ini hanya tersedia di wilayah Beijing dan dirancang khusus untuk skenario kloning suara (tidak termasuk voice sistem). Sebelum menggunakannya untuk sintesis suara, lihat API kloning suara CosyVoice untuk membuat voice. Kemudian, perbarui bidang voice dalam kode Anda dengan ID voice yang telah dikloning dan tentukan model yang sesuai untuk menggunakannya.

Java SDK

Panggilan non-streaming

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;

/**
 * Catatan fitur SSML:
 *     1. SSML hanya didukung untuk panggilan non-streaming dan panggilan streaming unidirectional.
 *     2. SSML hanya didukung untuk voice yang dikloning dari model cosyvoice-v3-flash, cosyvoice-v3-plus, dan cosyvoice-v2,
 *        serta voice sistem yang ditandai sebagai mendukung SSML dalam daftar voice (misalnya, voice longanyang untuk cosyvoice-v3-flash).
 */
public class Main {
    private static String model = "cosyvoice-v3-flash";
    private static String voice = "longanyang";

    public static void main(String[] args) {
        // Jika Anda menggunakan model dari wilayah Beijing, ganti URL dengan: wss://dashscope.aliyuncs.com/api-ws/v1/inference
        Constants.baseWebsocketApiUrl = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference";
        streamAudioDataToSpeaker();
        System.exit(0);
    }

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

        SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
        ByteBuffer audio = null;
        try {
            // Panggilan non-streaming; diblokir hingga audio dikembalikan
            // Escape karakter khusus
            audio = synthesizer.call("<speak rate=\"2\">Laju bicara saya lebih cepat daripada orang normal.</speak>");
        } 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 bernama "output.mp3"
            File file = new File("output.mp3");
            try (FileOutputStream fos = new FileOutputStream(file)) {
                fos.write(audio.array());
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }

        // Latensi paket pertama mencakup waktu yang diperlukan untuk membuat koneksi WebSocket
        System.out.println(
                "[Metric] Request ID: "
                        + synthesizer.getLastRequestId()
                        + ", Latensi paket pertama (ms): "
                        + synthesizer.getFirstPackageDelay());
    }
}

Panggilan streaming unidirectional

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

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;

/**
 * Catatan fitur SSML:
 *     1. SSML hanya didukung untuk panggilan non-streaming dan panggilan streaming unidirectional.
 *     2. SSML hanya didukung untuk voice yang dikloning dari model cosyvoice-v3-flash, cosyvoice-v3-plus, dan cosyvoice-v2,
 *        serta voice sistem yang ditandai sebagai mendukung SSML dalam daftar voice (misalnya, voice longanyang untuk cosyvoice-v3-flash).
 */
public class Main {
    private static String model = "cosyvoice-v3-flash";
    private static String voice = "longanyang";

    public static void main(String[] args) {
        // Jika Anda menggunakan model dari wilayah Beijing, ganti URL dengan: wss://dashscope.aliyuncs.com/api-ws/v1/inference
        Constants.baseWebsocketApiUrl = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference";
        streamAudioDataToSpeaker();
        System.out.println("Audio disimpan ke output.mp3");
        System.exit(0);
    }

    public static void streamAudioDataToSpeaker() {
        CountDownLatch latch = new CountDownLatch(1);
        final FileOutputStream[] fileOutputStream = new FileOutputStream[1];

        try {
            fileOutputStream[0] = new FileOutputStream("output.mp3");
        } catch (IOException e) {
            System.err.println("Gagal membuat file output: " + e.getMessage());
            return;
        }

        // Implementasikan antarmuka ResultCallback
        ResultCallback<SpeechSynthesisResult> callback = new ResultCallback<SpeechSynthesisResult>() {
            @Override
            public void onEvent(SpeechSynthesisResult result) {
                if (result.getAudioFrame() != null) {
                    // Tulis data audio ke file lokal
                    try {
                        byte[] audioData = result.getAudioFrame().array();
                        fileOutputStream[0].write(audioData);
                        fileOutputStream[0].flush();
                    } catch (IOException e) {
                        System.err.println("Gagal menulis data audio: " + e.getMessage());
                    }
                }
            }

            @Override
            public void onComplete() {
                System.out.println("Menerima Complete; sintesis suara selesai");
                closeFileOutputStream(fileOutputStream[0]);
                latch.countDown();
            }

            @Override
            public void onError(Exception e) {
                System.out.println("Terjadi kesalahan: " + e.toString());
                closeFileOutputStream(fileOutputStream[0]);
                latch.countDown();
            }
        };

        SpeechSynthesisParam param =
                SpeechSynthesisParam.builder()
                        // Kunci API untuk wilayah Singapura dan Beijing berbeda. Untuk mendapatkan kunci API, lihat https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                        // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan: .apiKey("sk-xxx")
                        .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                        .model(model)
                        .voice(voice)
                        .format(SpeechSynthesisAudioFormat.MP3_22050HZ_MONO_256KBPS)
                        .build();

        SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, callback);

        try {
            // Panggilan streaming unidirectional; langsung mengembalikan null (hasil dikirim secara asinkron melalui callback)
            // Escape karakter khusus
            synthesizer.call("<speak rate=\"2\">Laju bicara saya lebih cepat daripada orang normal.</speak>");
            // Tunggu hingga sintesis selesai
            latch.await();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // Tutup koneksi WebSocket setelah tugas selesai
            try {
                synthesizer.getDuplexApi().close(1000, "bye");
            } catch (Exception e) {
                System.err.println("Gagal menutup koneksi WebSocket: " + e.getMessage());
            }

            // Pastikan aliran file ditutup
            closeFileOutputStream(fileOutputStream[0]);
        }

        // Latensi paket pertama mencakup waktu yang diperlukan untuk membuat koneksi WebSocket
        System.out.println(
                "[Metric] Request ID: "
                        + synthesizer.getLastRequestId()
                        + ", Latensi paket pertama (ms): "
                        + synthesizer.getFirstPackageDelay());
    }

    private static void closeFileOutputStream(FileOutputStream fileOutputStream) {
        try {
            if (fileOutputStream != null) {
                fileOutputStream.close();
            }
        } catch (IOException e) {
            System.err.println("Gagal menutup aliran file: " + e.getMessage());
        }
    }
}

Python SDK

Panggilan non-streaming

# coding=utf-8
# Catatan fitur SSML:
#     1. SSML hanya didukung untuk panggilan non-streaming dan panggilan streaming unidirectional.
#     2. SSML hanya didukung untuk voice yang dikloning dari model cosyvoice-v3-flash, cosyvoice-v3-plus, dan cosyvoice-v2,
#        serta voice sistem yang ditandai sebagai mendukung SSML dalam daftar voice (misalnya, voice longanyang untuk cosyvoice-v3-flash)

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

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

# Jika Anda menggunakan model dari wilayah Beijing, ganti URL dengan: wss://dashscope.aliyuncs.com/api-ws/v1/inference
dashscope.base_websocket_api_url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference'

# Model
model = "cosyvoice-v3-flash"
# Voice
voice = "longanyang"

# Buat instance SpeechSynthesizer dan teruskan model, voice, dan parameter permintaan lainnya ke konstruktor
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# Panggilan non-streaming; diblokir hingga audio dikembalikan
# Escape karakter khusus
audio = synthesizer.call("<speak rate=\"2\">Laju bicara saya lebih cepat daripada orang normal.</speak>")

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

# Latensi paket pertama mencakup waktu yang diperlukan untuk membuat koneksi WebSocket
print('[Metric] Request ID: {}, Latensi paket pertama: {} ms'.format(
    synthesizer.get_last_request_id(),
    synthesizer.get_first_package_delay()))

Panggilan streaming unidirectional

# coding=utf-8
# Catatan fitur SSML:
#     1. SSML hanya didukung untuk panggilan non-streaming dan panggilan streaming unidirectional.
#     2. SSML hanya didukung untuk voice yang dikloning dari model cosyvoice-v3-flash, cosyvoice-v3-plus, dan cosyvoice-v2,
#        serta voice sistem yang ditandai sebagai mendukung SSML dalam daftar voice (misalnya, voice longanyang untuk cosyvoice-v3-flash)

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

def get_timestamp():
    now = datetime.now()
    formatted_timestamp = now.strftime("[%Y-%m-%d %H:%M:%S.%f]")
    return formatted_timestamp

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

# Jika Anda menggunakan model dari wilayah Beijing, ganti URL dengan: wss://dashscope.aliyuncs.com/api-ws/v1/inference
dashscope.base_websocket_api_url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference'

# Model
model = "cosyvoice-v3-flash"
# Voice
voice = "longanyang"

# Definisikan antarmuka callback
class Callback(ResultCallback):
    _player = None
    _stream = None

    def on_open(self):
        # Buka file output untuk menulis data audio
        self.file = open("output.mp3", "wb")
        print("Koneksi berhasil dibuat: " + get_timestamp())

    def on_complete(self):
        print("Sintesis suara selesai; semua hasil diterima: " + get_timestamp())
        if hasattr(self, 'file') and self.file:
            self.file.close()
        self
        # Latensi paket pertama mencakup waktu yang diperlukan untuk membuat koneksi WebSocket
        print('[Metric] Request ID: {}, Latensi paket pertama: {} ms'.format(
            self.synthesizer.get_last_request_id(),
            self.synthesizer.get_first_package_delay()))

    def on_error(self, message: str):
        print(f"Kesalahan sintesis suara: {message}")
        if hasattr(self, 'file') and self.file:
            self.file.close()

    def on_close(self):
        print("Koneksi ditutup: " + get_timestamp())
        if hasattr(self, 'file') and self.file:
            self.file.close()

    def on_event(self, message):
        pass

    def on_data(self, data: bytes) -> None:
        print(get_timestamp() + " Panjang audio biner: " + str(len(data)))
        # Tulis data audio ke file
        self.file.write(data)

callback = Callback()

# Buat instance SpeechSynthesizer dan teruskan model, voice, dan parameter permintaan lainnya ke konstruktor
synthesizer = SpeechSynthesizer(
    model=model,
    voice=voice,
    callback=callback,
)

# Tetapkan instance synthesizer ke callback untuk digunakan dalam on_complete
callback.synthesizer = synthesizer

# Panggilan streaming unidirectional; kirim teks untuk disintesis dan terima audio biner secara real-time melalui metode on_data dari callback
# Escape karakter khusus
synthesizer.call("<speak rate=\"2\">Laju bicara saya lebih cepat daripada orang normal.</speak>")

WebSocket API

Go

// Catatan fitur SSML:
//     1. Atur parameter enable_ssml ke true dalam perintah run-task untuk mengaktifkan SSML.
//     2. Kirim teks SSML menggunakan perintah continue-task; Anda hanya dapat mengirim perintah ini sekali.
//     3. SSML hanya didukung untuk voice yang dikloning dari model cosyvoice-v3-flash, cosyvoice-v3-plus, dan cosyvoice-v2,
//        serta voice sistem yang ditandai sebagai mendukung SSML dalam daftar voice (misalnya, voice longanyang untuk cosyvoice-v3-flash)

package main

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

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

const (
    // Jika Anda menggunakan model dari wilayah Beijing, ganti URL dengan: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
    wsURL      = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/"
    outputFile = "output.mp3"
)

func main() {
    // Kunci API untuk wilayah Singapura dan Beijing berbeda. Untuk mendapatkan kunci API, lihat https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan: 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 perintah 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, Anda hanya dapat mengirim perintah continue-task sekali.
                // Jika tidak, Anda akan mendapatkan kesalahan "Text request limit violated, expected 1."
                "enable_ssml": true,
            },
            "input": map[string]interface{}{},
        },
    }

    runTaskJSON, _ := json.Marshal(runTaskCmd)
    fmt.Printf("Mengirim perintah 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
        }

        // Tangani 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
        }

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

        // Uraikan JSON untuk mendapatkan jenis event
        var msgMap map[string]interface{}
        if json.Unmarshal(message, &msgMap) == nil {
            if header, ok := msgMap["header"].(map[string]interface{}); ok {
                if event, ok := header["event"].(string); ok {
                    fmt.Printf("Jenis event: %s\n", event)

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

                        if !textSent {
                            // Kirim perintah continue-task; saat menggunakan SSML, Anda hanya dapat mengirim perintah ini sekali
                            continueTaskCmd := map[string]interface{}{
                                "header": map[string]interface{}{
                                    "action":    "continue-task",
                                    "task_id":   taskID,
                                    "streaming": "duplex",
                                },
                                "payload": map[string]interface{}{
                                    "input": map[string]interface{}{
                                        // Escape karakter khusus
                                        "text": "<speak rate=\"2\">Laju bicara saya lebih cepat daripada orang normal.</speak>",
                                    },
                                },
                            }

                            continueTaskJSON, _ := json.Marshal(continueTaskCmd)
                            fmt.Printf("Mengirim perintah 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 pengiriman finish-task
                            time.Sleep(500 * time.Millisecond)

                            // Kirim perintah 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 perintah 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 kesalahan: %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;

// Catatan fitur SSML:
//     1. Atur parameter enable_ssml ke true dalam perintah run-task untuk mengaktifkan SSML.
//     2. Kirim teks SSML menggunakan perintah continue-task; Anda hanya dapat mengirim perintah ini sekali.
//     3. SSML hanya didukung untuk voice yang dikloning dari model cosyvoice-v3-flash, cosyvoice-v3-plus, dan cosyvoice-v2,
//        serta voice sistem yang ditandai sebagai mendukung SSML dalam daftar voice (misalnya, voice longanyang untuk cosyvoice-v3-flash)
class Program {
    // Kunci API untuk wilayah Singapura dan Beijing berbeda. Untuk mendapatkan kunci API, lihat https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan: 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.");

    // Jika Anda menggunakan model dari wilayah Beijing, ganti URL dengan: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
    private const string WebSocketUrl = "wss://dashscope-intl.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 untuk menerima pesan
            Task receiveTask = ReceiveMessagesAsync();

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

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

            // Kirim perintah continue-task. Saat menggunakan fitur SSML, perintah ini hanya dapat dikirim sekali.
            // Karakter khusus perlu di-escape.
            await SendContinueTaskCommandAsync("<speak rate=\"2\">Laju bicara saya lebih cepat daripada orang normal.</speak>");

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

            // Tunggu hingga 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 telah dihapus.");
        } else {
            Console.WriteLine("File output tidak ada dan 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 untuk 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, Anda hanya dapat mengirim perintah continue-task sekali.
                // Jika tidak, Anda akan mendapatkan kesalahan "Text request limit violated, expected 1."
                enable_ssml = true
            },
            input = new { }
        });

        await SendJsonMessageAsync(command);
        Console.WriteLine("Perintah 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("Perintah 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("Perintah 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) {
                // Tangani 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

Kode contoh memiliki struktur direktori sebagai berikut:

my-php-project/

├── composer.json

├── vendor/

└── index.php

Berikut adalah isi dari composer.json. Tentukan nomor versi dependensi berdasarkan kebutuhan aktual 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/"
        }
    }
}

Berikut adalah isi dari index.php:

<!-- Catatan fitur SSML: -->
<!--     1. Saat mengirim perintah run-task, atur parameter enable_ssml ke true untuk mengaktifkan dukungan SSML. -->
<!--     2. Kirim teks yang berisi SSML dengan menggunakan perintah continue-task. Anda hanya dapat mengirim perintah ini sekali. -->
<!--     3. SSML hanya didukung untuk voice yang dikloning dari model cosyvoice-v3-flash, cosyvoice-v3-plus, dan cosyvoice-v2, serta voice sistem yang ditandai sebagai mendukung SSML dalam daftar voice (misalnya, voice longanyang untuk cosyvoice-v3-flash). -->

<?php

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

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

// Kunci API untuk wilayah Singapura dan Beijing berbeda. Untuk mendapatkan kunci API, lihat https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// URL berikut untuk wilayah Singapura. Jika Anda menggunakan model dari wilayah Beijing, ganti URL dengan: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
$websocket_url = 'wss://dashscope-intl.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 kustom
$socketConnector = new SocketConnector($loop, [
    'tcp' => [
        'bindto' => '0.0.0.0:0',
    ],
    'tls' => [
        'verify_peer' => false,
        'verify_peer_name' => false,
    ],
]);

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

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

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

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

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

    // Definisikan fungsi untuk mengirim perintah continue-task
    $sendContinueTask = function() use ($conn, $loop, $taskId) {
        // Kirim perintah continue-task. Saat menggunakan fitur SSML, perintah ini hanya dapat dikirim sekali.
        $continueTaskMessage = json_encode([
            "header" => [
                "action" => "continue-task",
                "task_id" => $taskId,
                "streaming" => "duplex"
            ],
            "payload" => [
                "input" => [
                    // Karakter khusus perlu di-escape
                    "text" => "<speak rate=\"2\">Laju bicara saya lebih cepat daripada orang normal.</speak>"
                ]
            ]
        ]);
        $conn->send($continueTaskMessage);

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

    // Bendera untuk memeriksa apakah event task-started 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 {
            // Tangani 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 perintah 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, Anda hanya dapat mengirim perintah continue-task sekali.
                // Jika tidak, Anda akan mendapatkan kesalahan "Text request limit violated, expected 1."
                "enable_ssml" => true
            ],
            "input" => (object) []
        ]
    ]);
    echo "Bersiap mengirim perintah run-task: " . $runTaskMessage . "\n";
    $conn->send($runTaskMessage);
    echo "Perintah run-task dikirim\n";
}

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

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

/**
 * Kirim perintah 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 "Bersiap mengirim perintah finish-task: " . $finishTaskMessage . "\n";
    $conn->send($finishTaskMessage);
    echo "Perintah 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 perintah continue-task...\n";
            $taskStarted = true;
            // Kirim perintah continue-task
            $sendContinueTask();
            break;
        case 'result-generated':
            // Abaikan event result-generated
            break;
        case 'task-finished':
            echo "Tugas selesai\n";
            $conn->close();
            break;
        case 'task-failed':
            echo "Tugas gagal\n";
            echo "Kode kesalahan: " . $response['header']['error_code'] . "\n";
            echo "Pesan kesalahan: " . $response['header']['error_message'] . "\n";
            $conn->close();
            break;
        case 'error':
            echo "Kesalahan: " . $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 ditransfer
        $loop->addTimer(1, function() use ($conn) {
            $conn->close();
            echo "Klien menutup koneksi\n";
        });
    }

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

Node.js

Anda perlu menginstal dependensi yang diperlukan:

npm install ws
npm install uuid

Kode contohnya sebagai berikut:

// Catatan fitur SSML:
//     1. Atur parameter enable_ssml ke true dalam perintah run-task untuk mengaktifkan SSML.
//     2. Kirim teks SSML menggunakan perintah continue-task; Anda hanya dapat mengirim perintah ini sekali.
//     3. SSML hanya didukung untuk voice yang dikloning dari model cosyvoice-v3-flash, cosyvoice-v3-plus, dan cosyvoice-v2,
//        serta voice sistem yang ditandai sebagai mendukung SSML dalam daftar voice (misalnya, voice longanyang untuk cosyvoice-v3-flash)

import fs from 'fs';
import WebSocket from 'ws';
import { v4 as uuid } from 'uuid'; // Digunakan untuk menghasilkan UUID

// Kunci API untuk wilayah Singapura dan Beijing berbeda. Untuk mendapatkan kunci API, lihat https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan: const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// URL berikut untuk wilayah Singapura. Jika Anda menggunakan model dari wilayah Beijing, ganti URL dengan: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
const url = 'wss://dashscope-intl.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 perintah 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', // Voice
        format: 'mp3', // Format audio
        sample_rate: 22050, // Laju sampel
        volume: 50, // Volume
        rate: 1, // Laju ucapan
        pitch: 1, // Pitch
        enable_ssml: true // Apakah akan mengaktifkan fitur SSML. Jika enable_ssml diatur ke true, Anda hanya dapat mengirim perintah continue-task sekali. Jika tidak, kesalahan "Text request limit violated, expected 1." dilaporkan.
      },
      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 telah dimulai');
        // Kirim perintah continue-task
        sendContinueTasks(ws);
        break;
      case 'task-finished':
        console.log('Tugas telah selesai');
        ws.close();
        fileStream.end(() => {
          console.log('Aliran file telah ditutup');
        });
        break;
      case 'task-failed':
        console.error('Tugas gagal: ', message.header.error_message);
        ws.close();
        fileStream.end(() => {
          console.log('Aliran file telah ditutup');
        });
        break;
      default:
        // Anda dapat menangani result-generated di sini
        break;
    }
  }
});

function sendContinueTasks(ws) {
  
  if (taskStarted) {
    // Kirim perintah continue-task. Saat menggunakan fitur SSML, perintah ini hanya dapat dikirim sekali.
    const continueTaskMessage = JSON.stringify({
      header: {
        action: 'continue-task',
        task_id: taskId,
        streaming: 'duplex'
      },
      payload: {
        input: {
          // Karakter khusus perlu di-escape
          text: '<speak rate="2">Laju bicara saya lebih cepat daripada orang normal.</speak>'
        }
      }
    });
    ws.send(continueTaskMessage);
    
    // Kirim perintah finish-task
    const finishTaskMessage = JSON.stringify({
      header: {
        action: 'finish-task',
        task_id: taskId,
        streaming: 'duplex'
      },
      payload: {
        input: {}
      }
    });
    ws.send(finishTaskMessage);
  }
}

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

Java

Jika Anda menggunakan bahasa pemrograman Java, kami menyarankan Anda menggunakan Java DashScope SDK untuk pengembangan. Untuk informasi selengkapnya, lihat Java SDK.

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

  • Java-WebSocket

  • jackson-databind

Kami menyarankan Anda menggunakan Maven atau Gradle untuk mengelola paket 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

// Omit other code
dependencies {
  // WebSocket Client
  implementation 'org.java-websocket:Java-WebSocket:1.5.3'
  // JSON Processing
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}
// Omit other code

Kode Java-nya sebagai berikut:

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.*;

/**
 * Catatan fitur SSML:
 *     1. Saat mengirim perintah run-task, atur parameter enable_ssml ke true untuk mengaktifkan dukungan SSML.
 *     2. Kirim teks yang berisi SSML dengan menggunakan perintah continue-task. Anda hanya dapat mengirim perintah ini sekali.
 *     3. SSML hanya didukung untuk voice yang dikloning dari model cosyvoice-v3-flash, cosyvoice-v3-plus, dan cosyvoice-v2, serta voice sistem yang ditandai sebagai mendukung SSML dalam daftar voice (misalnya, voice longanyang untuk cosyvoice-v3-flash).
 */
public class TTSWebSocketClient extends WebSocketClient {
    private final String taskId = UUID.randomUUID().toString();
    private final String outputFile = "output_" + System.currentTimeMillis() + ".mp3";
    private boolean taskFinished = false;

    public TTSWebSocketClient(URI serverUri, Map<String, String> headers) {
        super(serverUri, headers);
    }

    @Override
    public void onOpen(ServerHandshake serverHandshake) {
        System.out.println("Koneksi berhasil");

        // Kirim perintah run-task
        // Jika enable_ssml diatur ke true, Anda hanya dapat mengirim perintah continue-task sekali.
        // Jika tidak, Anda akan mendapatkan kesalahan "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\": true }, \"input\": {} }}";
        send(runTaskCommand);
    }

    @Override
    public void onMessage(String message) {
        System.out.println("Menerima pesan dari server: " + message);
        try {
            // Uraikan pesan JSON
            Map<String, Object> messageMap = new ObjectMapper().readValue(message, Map.class);

            if (messageMap.containsKey("header")) {
                Map<String, Object> header = (Map<String, Object>) messageMap.get("header");

                if (header.containsKey("event")) {
                    String event = (String) header.get("event");

                    if ("task-started".equals(event)) {
                        System.out.println("Menerima event task-started dari server");

                        // Kirim perintah continue-task. Saat menggunakan fitur SSML, perintah ini hanya dapat dikirim sekali.
                        // Karakter khusus perlu di-escape.
                        sendContinueTask("<speak rate=\\\"2\\\">Laju bicara saya lebih cepat daripada orang normal.</speak>");

                        // Kirim perintah 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("Ukuran data audio biner yang diterima: " + 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 telah 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("Kesalahan: " + 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 untuk wilayah Singapura dan Beijing berbeda. Untuk mendapatkan kunci API, lihat https://www.alibabacloud.com/help/zh/model-studio/get-api-key
            // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan: 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);
            // Jika Anda menggunakan model dari wilayah Beijing, ganti URL dengan: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
            TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://dashscope-intl.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

Jika Anda menggunakan Python, kami merekomendasikan agar Anda menggunakan SDK DashScope Python untuk pengembangan; lihat SDK Python.

Berikut ini adalah contoh WebSocket Python. Sebelum menjalankan contoh ini, impor dependensi dengan cara berikut:

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

Jangan beri nama file Python yang menjalankan kode contoh ini sebagai "websocket.py". Jika tidak, error berikut akan dilaporkan: AttributeError: module 'websocket' has no attribute 'WebSocketApp'. Apakah maksud Anda: 'WebSocket'?

# Catatan fitur SSML:
#     1. Saat mengirim perintah run-task, atur parameter enable_ssml ke true untuk mengaktifkan dukungan SSML.
#     2. Kirim teks yang berisi SSML dengan menggunakan perintah continue-task. Anda hanya dapat mengirim perintah ini sekali.
#     3. SSML didukung hanya untuk suara kloning dari model cosyvoice-v3-flash, cosyvoice-v3-plus, dan cosyvoice-v2, serta suara sistem yang ditandai sebagai didukung SSML dalam daftar suara (misalnya, suara longanyang untuk cosyvoice-v3-flash).

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())  # Hasilkan ID tugas unik.
        self.output_file = f"output_{int(time.time())}.mp3"  # Jalur file audio keluaran.
        self.ws = None  # Instans WebSocketApp.
        self.task_started = False  # Apakah event task-started telah diterima.
        self.task_finished = False  # Apakah event task-finished/task-failed telah diterima.

    def on_open(self, ws):
        """
    Fungsi callback saat koneksi WebSocket terbentuk.
    Mengirim perintah run-task untuk memulai tugas sintesis suara.
    """
        print("Koneksi WebSocket terbentuk")

        # Bangun perintah 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, Anda hanya dapat mengirim perintah continue-task sekali.
                    # Jika tidak, Anda akan mendapatkan kesalahan "Text request limit violated, expected 1."
                    "enable_ssml": True
                },
                "input": {}
            }
        }

        # Kirim perintah run-task.
        ws.send(json.dumps(run_task_cmd))
        print("Perintah run-task dikirim")

    def on_message(self, ws, message):
        """
    Fungsi callback saat pesan diterima.
    Menangani pesan teks dan pesan biner secara terpisah.
    """
        if isinstance(message, str):
            # Proses pesan teks JSON.
            try:
                msg_json = json.loads(message)
                print(f"Pesan JSON diterima: {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 perintah continue-task. Saat menggunakan fitur SSML, perintah ini hanya dapat dikirim sekali.
                            # Karakter khusus harus di-escape.
                            self.send_continue_task("<speak rate=\"2\">Laju berbicara saya lebih cepat daripada orang biasa.</speak>")

                            # Kirim finish-task setelah continue-task dikirim.
                            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", "Kesalahan tidak diketahui")
                            print(f"Tugas gagal: {error_msg}")
                            self.task_finished = True
                            self.close(ws)

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

    def on_error(self, ws, error):
        """Callback pada kesalahan."""
        print(f"Kesalahan WebSocket: {error}")

    def on_close(self, ws, close_status_code, close_msg):
        """Callback pada penutupan."""
        print(f"WebSocket ditutup: {close_msg} ({close_status_code})")

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

        self.ws.send(json.dumps(cmd))
        print(f"Perintah continue-task dikirim, konten teks: {text}")

    def send_finish_task(self):
        """Mengirim perintah 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("Perintah finish-task dikirim")

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


# Contoh penggunaan
if __name__ == "__main__":
    # Kunci API untuk wilayah Singapura dan Beijing berbeda. Untuk mendapatkan kunci API, lihat https://www.alibabacloud.com/help/zh/model-studio/get-api-key
    # Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan: API_KEY = "sk-xxx"
    API_KEY = os.environ.get("DASHSCOPE_API_KEY")
    # URL berikut untuk wilayah Singapura. Jika Anda menggunakan model dari wilayah Beijing, ganti URL dengan: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
    SERVER_URI = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference/"

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

Tag

Catatan

Implementasi SSML untuk layanan sintesis suara didasarkan pada spesifikasi W3C SSML 1.0. Namun, untuk mengakomodasi berbagai skenario bisnis, tidak semua tag standar didukung. Sebagai gantinya, kami mendukung kumpulan tag yang paling praktis.

  • Seluruh konten teks yang menggunakan fitur SSML harus diapit oleh tag <speak></speak>.

  • Anda dapat menggunakan beberapa tag <speak> secara berurutan, seperti <speak></speak><speak></speak>. Anda tidak boleh menumpuknya secara bersarang, seperti <speak><speak></speak></speak>.

  • Anda harus melakukan escape karakter khusus XML dalam konten teks tag. Karakter khusus umum dan bentuk escapenya adalah sebagai berikut:

    • " (tanda kutip ganda) → &quot;

    • ' (tanda kutip tunggal/apostrof) → &apos;

    • & (ampersand) → &amp;

    • < (tanda kurang dari) → &lt;

    • > (tanda lebih dari) → &gt;

<speak>: Node root

  • Deskripsi

    Tag <speak> adalah node root untuk semua dokumen SSML. Seluruh teks yang menggunakan fitur SSML harus diapit oleh tag <speak></speak>.

  • Sintaks

     <speak>Teks yang memerlukan fitur SSML</speak>
  • Properti

    Properti

    Tipe

    Wajib

    Deskripsi

    voice

    String

    Tidak

    Menentukan voice.

    Properti ini memiliki prioritas lebih tinggi daripada parameter voice dalam permintaan API.

    • Nilai yang valid: Untuk informasi lebih lanjut mengenai voice tertentu, lihat voice cosyvoice-v2.

    • Contoh:

      <speak voice="longcheng_v2">
        I am a male voice.
      </speak>

    rate

    String

    Tidak

    Menentukan laju ucapan. Properti ini memiliki prioritas lebih tinggi daripada parameter speech_rate dalam permintaan API.

    • Nilai yang valid: angka desimal dari 0,5 hingga 2.

    • Nilai default: 1

      • Nilai lebih besar dari 1 menunjukkan laju ucapan lebih cepat.

      • Nilai kurang dari 1 menunjukkan laju ucapan lebih lambat.

    • Contoh:

      <speak rate="2">
        My speech rate is faster than normal.
      </speak>

    pitch

    String

    Tidak

    Menentukan pitch. Properti ini memiliki prioritas lebih tinggi daripada parameter pitch_rate dalam permintaan API.

    • Nilai yang valid: angka desimal dari 0,5 hingga 2.

    • Nilai default: 1

      • Nilai lebih besar dari 1 menunjukkan pitch lebih tinggi.

      • Nilai kurang dari 1 menunjukkan pitch lebih rendah.

    • Contoh:

      <speak pitch="0.5">
        However, my pitch is lower than others.
      </speak>

    volume

    String

    Tidak

    Menentukan volume. Properti ini memiliki prioritas lebih tinggi daripada parameter volume dalam permintaan API.

    • Nilai yang valid: bilangan bulat dari 0 hingga 100.

    • Nilai default: 50

      • Nilai lebih besar dari 50 menunjukkan volume lebih tinggi.

      • Nilai kurang dari 50 menunjukkan volume lebih rendah.

    • Contoh:

      <speak volume="80">
        My volume is also very high.
      </speak>

    effect

    String

    Tidak

    Menentukan efek suara.

    • Nilai yang valid:

      • robot: efek suara robot

      • lolita: lively female voice effect

      • lowpass: efek suara low-pass

      • echo: efek gema

      • eq: equalizer (advanced)

      • lpfilter: low-pass filter (advanced)

      • hpfilter: high-pass filter (advanced)

      Catatan
      • eq, lpfilter, dan hpfilter adalah jenis efek suara advanced. Anda dapat menggunakan parameter effectValue untuk menyesuaikan efek spesifiknya.

      • Setiap tag SSML hanya mendukung satu efek suara. Beberapa atribut effect tidak dapat digunakan bersamaan.

      • Penggunaan efek suara meningkatkan latency sistem.

    • Contoh:

      <speak effect="robot">
        Do you like the robot WALL-E?
      </speak>

    effectValue

    String

    Tidak

    Menentukan efek spesifik dari efek suara (parameter effect).

    • Nilai yang valid:

      • eq (equalizer): Sistem mendukung delapan tingkat frekuensi secara default:

        ["40 Hz", "100 Hz", "200 Hz", "400 Hz", "800 Hz", "1600 Hz", "4000 Hz", "12000 Hz"].

        Bandwidth setiap rentang frekuensi adalah 1,0 q.

        Saat menggunakan efek ini, Anda harus menggunakan parameter effectValue untuk menentukan nilai gain pada setiap rentang frekuensi. Parameter ini berupa string delapan bilangan bulat yang dipisahkan spasi. Nilai setiap bilangan bulat berkisar antara -20 hingga 20. Nilai 0 menunjukkan bahwa gain frekuensi terkait tidak disesuaikan.

        Contoh: effectValue="1 1 1 1 1 1 1 1"

      • lpfilter (low-pass filter): Masukkan nilai frekuensi filter low-pass. Nilainya adalah bilangan bulat dalam rentang (0, target sample rate/2]. Contoh: effectValue="800".

      • hpfilter (high-pass filter): Masukkan nilai frekuensi filter high-pass. Nilainya adalah bilangan bulat dalam rentang (0, target sample rate/2]. Contoh: effectValue="1200".

    • Contoh:

      <speak effect="eq" effectValue="1 -20 1 1 1 1 20 1">
        Do you like the robot WALL-E?
      </speak>
      
      <speak effect="lpfilter" effectValue="1200">
        Do you like the robot WALL-E?
      </speak>
      
      <speak effect="hpfilter" effectValue="1200">
        Do you like the robot WALL-E?
      </speak>

    bgm

    String

    Tidak

    Menambahkan musik latar yang ditentukan ke ucapan hasil sintesis. File musik latar harus disimpan di Alibaba Cloud OSS (lihat Upload files), dan bucket-nya harus memiliki izin minimal public-read.

    Jika URL musik latar berisi karakter khusus XML, seperti &, <, dan >, Anda harus melakukan escape terhadap karakter tersebut.

    • Persyaratan audio:

      Tidak ada batas maksimum ukuran file audio, tetapi file yang lebih besar dapat meningkatkan waktu unduh. Jika durasi konten hasil sintesis melebihi durasi musik latar, musik latar akan secara otomatis di-loop agar sesuai dengan panjang audio hasil sintesis.

      • Sample rate: 16 kHz

      • Jumlah saluran suara: mono

      • Format file: WAV

        Jika audio asli tidak dalam format WAV, gunakan tool ffmpeg untuk mengonversinya:

        ffmpeg -i input_audio -acodec pcm_s16le -ac 1 -ar 16000 output.wav
      • Kedalaman bit: 16-bit

    • Contoh:

      <speak bgm="http://nls.alicdn.com/bgm/2.wav" backgroundMusicVolume="30" rate="-500" volume="40">
        <break time="2s"/>
        The old trees on the shady cliff are shrouded in mist
        <break time="700ms"/>
        The sound of rain is still in the bamboo forest
        <break time="700ms"/>
        I know that cotton contributes to the country's plan
        <break time="700ms"/>
        The scenery of Mianzhou is always pitiable
        <break time="2s"/>
      </speak>
    Penting

    Anda bertanggung jawab secara hukum atas hak cipta audio yang diunggah.

    backgroundMusicVolume

    String

    Tidak

    Mengontrol volume musik latar. Ini dikonfigurasi menggunakan properti backgroundMusicVolume.

  • Relasi tag

    Tag <speak> dapat berisi teks dan tag-tag berikut:

  • Contoh lainnya

    • Atribut kosong

      <speak>
        Text that requires SSML tags
      </speak>
    • Kombinasi atribut (dipisahkan spasi)

      <speak rate="200" pitch="-100" volume="80">
        So when put together, my voice sounds like this.
      </speak>

<break>: Mengontrol durasi jeda

  • Deskripsi

    Menambahkan periode diam selama sintesis suara untuk mensimulasikan jeda alami. Anda dapat mengatur durasinya dalam detik (s) atau milidetik (ms). Tag ini bersifat opsional.

  • Sintaks

    # Atribut kosong
    <break/>
    # Dengan atribut time
    <break time="string"/>
  • Properti

    Catatan

    Jika Anda menggunakan tag <break> tanpa atribut, durasi jeda default-nya adalah 1 s.

    Property

    Type

    Required

    Description

    time

    String

    No

    Mengatur durasi jeda dalam detik atau milidetik, seperti "2s" atau "50ms".

    • Nilai yang valid:

      • Dalam detik (s): bilangan bulat dari 1 hingga 10.

      • Dalam milidetik (ms): bilangan bulat dari 50 hingga 10000.

    • Contoh:

      <speak>
        Please close your eyes and take a rest.<break time="500ms"/>Okay, please open your eyes.
      </speak>
    Penting

    Jika Anda menggunakan multiple tag <break> secara berurutan, total durasi jeda adalah jumlah dari waktu yang ditentukan di setiap tag. Jika total durasi melebihi 10 detik, hanya 10 detik pertama yang diterapkan.

    Sebagai contoh, pada segmen SSML berikut, durasi kumulatif dari tag <break> adalah 15 detik, yang melebihi batas 10 detik. Durasi jeda akhir akan dipotong menjadi 10 detik:

    <speak>
      Please close your eyes and take a rest.<break time="5s"/><break time="5s"/><break time="5s"/>Okay, please open your eyes.
    </speak>
  • Hubungan tag

    <break> adalah tag kosong dan tidak dapat berisi tag lain.

<sub>: Mengganti teks

  • Deskripsi

    Mengganti string teks dengan alternatif yang ditentukan yang akan dibacakan sebagai gantinya. Misalnya, teks "W3C" dapat dibacakan sebagai "network protocol". Tag ini bersifat opsional.

  • Sintaksis

    <sub alias="string"></sub>
  • Properti

    Properti

    Jenis

    Wajib

    Deskripsi

    alias

    String

    Ya

    Mengganti sebagian teks dengan teks yang lebih sesuai untuk dibacakan.

    Contoh:

     <speak>
       <sub alias="network protocol">W3C</sub>
     </speak>
  • Hubungan tag

    Tag <sub> hanya dapat berisi teks.

<phoneme>: Menentukan pelafalan (Pinyin/alfabet fonetik)

  • Deskripsi

    Mengontrol pelafalan string teks tertentu. Anda dapat menggunakan Pinyin untuk bahasa Tiongkok dan alfabet fonetik, seperti CMU, untuk bahasa Inggris. Tag ini cocok untuk skenario yang memerlukan pelafalan tepat dan bersifat opsional.

  • Sintaks

    <phoneme alphabet="string" ph="string">text</phoneme>
  • Properti

    Properti

    Jenis

    Wajib

    Deskripsi

    alphabet

    String

    Ya

    Menentukan jenis pelafalan: Pinyin (untuk bahasa Tiongkok) atau alfabet fonetik (untuk bahasa Inggris).

    Nilai yang valid:

    ph

    String

    Ya

    Menentukan Pinyin atau alfabet fonetik spesifik:

    • Pinyin untuk setiap karakter dipisahkan dengan spasi, dan jumlah suku kata Pinyin harus sesuai dengan jumlah karakter.

    • Setiap suku kata Pinyin terdiri dari bagian pelafalan dan nada. Nada dinyatakan sebagai bilangan bulat dari 1 hingga 5, di mana 5 menunjukkan nada netral.

    • Contoh:

      <speak>
        去<phoneme alphabet="py" ph="dian3 dang4 hang2">典当行</phoneme>把这个玩意<phoneme alphabet="py" ph="dang4 diao4">当掉</phoneme>
      </speak>
      
      <speak>
        How to spell <phoneme alphabet="cmu" ph="S AY N">sin</phoneme>?
      </speak>
  • Relasi tag

    Tag <phoneme> hanya dapat berisi teks.

<soundEvent>: Menyisipkan suara eksternal (seperti nada dering atau suara kucing)

  • Deskripsi

    Memungkinkan Anda menyisipkan file efek suara, seperti nada prompt atau suara latar, ke dalam ucapan sintesis untuk memperkaya output audio. Tag ini bersifat opsional.

  • Sintaks

     <soundEvent src="URL"/>
  • Properti

    Property

    Tipe

    Wajib

    Deskripsi

    src

    String

    Ya

    Menetapkan URL audio eksternal.

    File audio harus disimpan di OSS (lihat Upload files), dan bucket-nya harus memiliki setidaknya izin baca-publik. Jika URL berisi karakter khusus XML, seperti &, <, dan >, Anda harus melakukan escape terhadap karakter tersebut.

    • Persyaratan audio:

      • Laju sampel: 16 kHz

      • Jumlah saluran suara: mono

      • Format file: WAV

        Jika audio asli tidak dalam format WAV, gunakan tool ffmpeg untuk mengonversinya:

        ffmpeg -i input_audio -acodec pcm_s16le -ac 1 -ar 16000 output.wav
      • Ukuran file: maksimal 2 MB

      • Kedalaman bit: 16-bit

    • Contoh:

      <speak>
        Seekor kuda ketakutan<soundEvent src="http://nls.alicdn.com/sound-event/horse-neigh.wav"/>dan orang-orang berhamburan menghindarinya.
      </speak>
    Penting

    Anda bertanggung jawab secara hukum atas hak cipta audio yang diunggah.

  • Relasi tag

    <soundEvent> adalah tag kosong dan tidak dapat berisi tag lain.

<say-as>: Menentukan cara teks dibaca (seperti angka, tanggal, dan nomor telepon)

  • Deskripsi

    Menunjukkan tipe konten dari string teks, sehingga model dapat membaca teks dalam format yang sesuai. Tag ini bersifat opsional.

  • Sintaksis

     <say-as interpret-as="string">text</say-as>
  • Properti

    Properti

    Tipe

    Wajib

    Deskripsi

    interpret-as

    String

    Ya

    Menunjukkan tipe informasi dari teks di dalam tag.

    Nilai yang valid:

    • cardinal: Dibaca sebagai angka kardinal (bilangan bulat atau desimal).

    • digits: Dibaca sebagai digit individual. Misalnya, 123 dibaca sebagai one two three.

    • telephone: Dibaca sebagai nomor telepon.

    • name: Dibaca sebagai nama.

    • address: Dibaca sebagai alamat.

    • id: Cocok untuk nama akun dan nickname. Dibaca dengan cara konvensional.

    • characters: Membaca teks di dalam tag karakter per karakter.

    • punctuation: Membaca teks di dalam tag sebagai tanda baca.

    • date: Dibaca sebagai tanggal.

    • time: Dibaca sebagai waktu.

    • currency: Dibaca sebagai jumlah mata uang.

    • measure: Dibaca sebagai satuan ukuran.

  • Format yang didukung untuk setiap tipe <say-as>

    • cardinal

      Format

      Contoh

      English output

      Deskripsi

      String angka

      145

      one hundred forty five

      Rentang input bilangan bulat: bilangan bulat positif atau negatif hingga 13 digit, [-999999999999, 999999999999].

      Rentang input desimal: Tidak ada batasan khusus pada jumlah tempat desimal, tetapi disarankan tidak melebihi 10.

      String angka diawali nol

      0145

      one hundred forty five

      Tanda negatif + string angka

      -145

      minus hundred forty five

      String angka 3 digit dipisahkan koma

      60,000

      sixty thousand

      Tanda negatif + string angka 3 digit dipisahkan koma

      -208,000

      minus two hundred eight thousand

      String angka + titik desimal + nol

      12.00

      twelve

      String angka + titik desimal + string angka

      12.34

      twelve point three four

      String angka 3 digit dipisahkan koma + titik desimal + string angka

      1,000.1

      one thousand point one

      Tanda negatif + string angka + titik desimal + string angka

      -12.34

      minus twelve point three four

      Tanda negatif + string angka 3 digit dipisahkan koma + titik desimal + string angka

      -1,000.1

      minus one thousand point one

      String angka (dipisahkan koma 3 digit) + tanda hubung + angka (dipisahkan koma 3 digit)

      1-1,000

      one to one thousand

      Pembacaan default lainnya

      012.34

      twelve point three four

      Tidak ada

      1/2

      one half

      -3/4

      minus three quarters

      5.1/6

      five point one over six

      -3 1/2

      minus three and a half

      1,000.3^3

      one thousand point three to the power of three

      3e9.1

      three times ten to the power of nine point one

      23.10%

      twenty three point one percent

    • digits

      Format

      Contoh

      Output Bahasa Inggris

      Deskripsi

      String angka

      12034

      one two zero three four

      Tidak ada batasan khusus pada panjang string angka, tetapi disarankan tidak melebihi 20 digit.

      Jika string angka dikelompokkan menggunakan spasi atau tanda hubung, koma dimasukkan di antara kelompok untuk menciptakan jeda yang sesuai. Mendukung hingga 5 kelompok.

      String angka + spasi atau tanda hubung + string angka + spasi atau tanda hubung + string angka + spasi atau tanda hubung + string angka

      1-23-456 7890

      one, two three, four five six, seven eight nine zero

    • telephone

      Format

      Contoh

      Output Bahasa Inggris

      Deskripsi

      String angka

      12034

      one two oh three four

      Tidak ada batasan khusus pada panjang string angka, tetapi disarankan tidak melebihi 20 digit. Jika string angka dikelompokkan menggunakan spasi atau tanda hubung, koma dimasukkan di antara kelompok untuk menciptakan jeda yang sesuai. Mendukung hingga 5 kelompok.

      String angka + spasi atau tanda hubung + string angka + spasi atau tanda hubung + string angka

      1-23-456 7890

      one, two three, four five six, seven eight nine oh

      Tanda plus + string angka + spasi atau tanda hubung + string angka

      +43-211-0567

      plus four three, two one one, oh five six seven

      Tanda kurung kiri + string angka + tanda kurung kanan + spasi + string angka + spasi atau tanda hubung + string angka

      (21) 654-3210

      (two one) six five four, three two one oh

    • address

      Tag ini tidak didukung untuk teks Bahasa Inggris.

    • id

      Untuk teks Bahasa Inggris, tag ini berfungsi sama seperti tag characters.

    • characters

      Format

      Contoh

      Output Bahasa Inggris

      Deskripsi

      string

      *b+3$.c-0'=α

      asterisk B plus three dollar dot C dash zero apostrophe equals alpha

      Mendukung karakter Mandarin, huruf Inggris besar dan kecil, angka Arab 0-9, serta beberapa karakter full-width dan half-width.

      Spasi dalam output menunjukkan bahwa jeda dimasukkan di antara setiap karakter, artinya karakter dibaca satu per satu.

      Jika teks di dalam tag berisi karakter khusus XML, Anda harus melakukan escape terhadapnya.

    • punctuation

      Untuk teks Bahasa Inggris, tag ini berfungsi sama seperti tag characters.

    • date

      Format

      Contoh

      English output

      Deskripsi

      Empat digit/dua digit atau empat digit-dua digit

      2000/01

      two thousand, oh one

      Mencakup beberapa tahun.

      1900-01

      nineteen hundred, oh one

      2001-02

      twenty oh one, oh two

      2019-20

      twenty nineteen, twenty

      1998-99

      nineteen ninety eight, ninety nine

      1999-00

      nineteen ninety nine, oh oh

      Angka empat digit yang diawali 1 atau 2

      2000

      two thousand

      Tahun empat digit.

      1900

      nineteen hundred

      1905

      nineteen oh five

      2021

      twenty twenty one

      Hari dalam Seminggu – Hari dalam Seminggu

      atau

      Hari dalam seminggu~Hari dalam seminggu

      atau

      Hari dalam seminggu&Hari dalam seminggu

      mon-wed

      monday to wednesday

      Jika teks dalam tag rentang hari dalam minggu berisi karakter XML khusus, lakukan escape terhadap karakter tersebut.

      tue~fri

      tuesday to friday

      sat&sun

      saturday and sunday

      DD-DD MMM YYYY

      atau

      DD~DD MMM, YYYY

      atau

      DD&DD MMM, YYYY

      19-20 Jan, 2000

      the nineteen to the twentieth of january two thousand

      DD menunjukkan hari dua digit. MMM menunjukkan singkatan tiga huruf atau nama lengkap bulan. YYYY menunjukkan tahun empat digit yang diawali 1 atau 2.

      01 ~ 10 Jul, 2020

      the first to the tenth of july twenty twenty

      05&06 Apr, 2009

      the fifth and the sixth of april two thousand nine

      MMM DD–DD

      atau

      MMM DD~DD

      atau

      MMM DD&DD

      Feb 01 - 03

      feburary the first to the third

      MMM menunjukkan singkatan tiga huruf atau nama lengkap bulan. DD menunjukkan hari dua digit.

      Aug 10–20

      august the tenth to the twentieth

      Dec 11&12

      december the eleventh and the twelfth

      MMM-MMM

      atau

      MMM~MMM

      atau

      MMM&MMM

      Jan-Jun

      january to june

      MMM menunjukkan singkatan tiga huruf atau nama lengkap bulan.

      Jul - Dec

      july to dcember

      sep&oct

      september and october

      YYYY–YYYY

      atau

      YYYY~YYYY

      1990 - 2000

      nineteen ninety to two thousand

      YYYY menunjukkan tahun empat digit yang diawali 1 atau 2.

      2001–2021

      two thousand one to twenty twenty one

      WWW DD MMM YYYY

      Sun 20 Nov 2011

      sunday the twentieth of november twenty eleven

      WWW adalah singkatan tiga huruf atau nama lengkap hari dalam minggu. DD adalah hari dua digit. MMM adalah singkatan tiga huruf atau nama lengkap bulan. MM adalah bulan dua digit (atau singkatan tiga huruf atau nama lengkap bulan). YYYY adalah tahun empat digit yang diawali 1 atau 2.

      WWW DD MMM

      Sun 20 Nov

      sunday the twentieth of november

      WWW MMM DD YYYY

      Sun Nov 20 2011

      sunday november the twentieth twenty eleven

      WWW MMM DD

      Sun Nov 20

      sunday november the twentieth

      WWW YYYY-MM-DD

      Sat 2010-10-01

      saturday october the first twenty ten

      WWW YYYY/MM/DD

      Sat 2010/10/01

      saturday october the first twenty ten

      WWW MM/DD/YYYY

      Sun 11/20/2011

      sunday november the twentieth twenty eleven

      MM/DD/YYYY

      11/20/2011

      november the twentieth twenty eleven

      YYYY

      1998

      nineteen ninety eight

      Pembacaan default lainnya

      10 Mar, 2001

      the tenth of march two thousand one

      Tidak ada

      10 Mar

      the tenth of march

      Mar 2001

      march two thousand one

      Fri. 10/Mar/2001

      friday the tenth of march two thousand one

      Mar 10th, 2001

      march the tenth two thousand one

      Mar 10

      march the tenth

      2001/03/10

      march the tenth two thousand one

      2001-03-10

      march the tenth two thousand one

      2000s

      two thousands

      2010's

      twenty tens

      1900's

      nineteen hundreds

      1990s

      nineteen nineties

    • time

      Format

      Contoh

      English output

      Deskripsi

      HH:MM AM atau PM

      09:00 AM

      nine A M

      HH merepresentasikan jam satu atau dua digit. MM merepresentasikan menit dua digit. AM/PM merepresentasikan pagi atau sore.

      09:03 PM

      nine oh three P M

      09:13 p.m.

      nine thirteen p m

      HH:MM

      21:00

      twenty one hundred

      HHMM

      100

      one oclock

      Titik waktu-Titik waktu

      8:00 am - 05:30 pm

      eight a m to five p m

      Mendukung format waktu dan rentang waktu umum.

      7:05~10:15 AM

      seven oh five to ten fifteen A M

      09:00-13:00

      nine oclock to thirteen hundred

    • currency

      Format

      Contoh

      Output Bahasa Inggris

      Deskripsi

      Angka + Identifikasi mata uang

      1.00 RMB

      one yuan

      Format angka yang didukung: bilangan bulat, desimal, dan format internasional yang menggunakan koma sebagai pemisah ribuan.

      Identifikasi mata uang yang didukung:

      CN¥ (yuan)

      CNY (yuan)

      RMB (yuan)

      AUD (australian dollar)

      CAD (canadian dollar)

      CHF (swiss franc)

      DKK (danish krone)

      EUR (euro)

      GBP (british pound)

      HKD (Hong Kong(China) dollar)

      JPY (japanese yen)

      NOK (norwegian krone)

      SEK (swedish krona)

      SGD (singapore dollar)

      USD (united states dollar)

      2.02 CNY

      two point zero two yuan

      1,000.23 CN¥

      one thousand point two three yuan

      1.01 SGD

      one singapore dollar and one cent

      2.01 CAD

      two canadian dollars and one cent

      3.1 HKD

      three hong kong dollars and ten cents

      1,000.00 EUR

      one thousand euros

      Identifikasi mata uang + Angka

      US$ 1.00

      one US dollar

      Format angka yang didukung: bilangan bulat, desimal, dan format internasional yang menggunakan koma sebagai pemisah ribuan.

      Identifikasi mata uang yang didukung:

      US$ (US dollar)

      CA$ (Canadian dollar)

      AU$ (Australian dollar)

      SG$ (Singapore dollar)

      HK$ (Hong Kong(China) dollar)

      C$ (Canadian dollar)

      A$ (Australian dollar)

      $ (dollar)

      £ (pound)

      € (euro)

      CN¥ (yuan)

      CNY (yuan)

      RMB (yuan)

      AUD (australian dollar)

      CAD (canadian dollar)

      CHF (swiss franc)

      DKK (danish krone)

      EUR (euro)

      GBP (british pound)

      HKD (Hong Kong (China) dollar)

      JPY (japanese yen)

      NOK (norwegian krone)

      SEK (swedish krona)

      SGD (singapore dollar)

      USD (united states dollar)

      $0.01

      one cent

      JPY 1.01

      one japanese yen and one sen

      £1.1

      one pound and ten pence

      €2.01

      two euros and one cent

      USD 1,000

      one thousand united states dollars

      Angka + Kuantifier + Identifikasi mata uang
      atau
      Identifikasi mata uang + Angka + Kuantifier

      atau

      Identifier mata uang + Angka + Kuantifier

      1.23 Tn RMB

      one point two three trillion yuan

      Format kuantifier yang didukung meliputi:

      thousand

      million

      billion

      trillion

      Mil (million)

      mil (million)

      Bil (billion)

      bil (billion)

      MM (million)

      Bn (billion)

      bn (billion)

      Tn (trillion)

      tn (trillion)

      K(thousand)

      k (thousand)

      M (million)

      m (million)

      $1.2 K

      one point two thousand dollars

    • measure

      Format

      Contoh

      Output Bahasa Inggris

      Deskripsi

      Angka + Satuan pengukuran

      1.0 kg

      one kilogram

      Mendukung bilangan bulat, desimal, dan notasi internasional dengan pemisah koma.

      Mendukung singkatan satuan umum.

      1,234.01 km

      one thousand two hundred thirty-four point zero one kilometers

      Satuan pengukuran

      mm2

      square millimeter

    • Tabel berikut mencantumkan pelafalan simbol umum untuk <say-as>.

      Simbol

      Pelafalan Bahasa Inggris

      !

      exclamation mark

      double quote

      #

      pound

      $

      dollar

      %

      percent

      &

      and

      left quote

      left parenthesis

      right parenthesis

      *

      asterisk

      +

      plus

      ,

      comma

      -

      dash

      .

      dot

      /

      slash

      :

      Solon

      semicolon

      <

      less than

      =

      equals

      >

      greater than

      ?

      question mark

      @

      at

      [

      left bracket

      \

      backslash

      ]

      right bracket

      ^

      caret

      _

      underscore

      `

      backtick

      {

      left brace

      |

      vertical bar

      }

      right brace

      ~

      tilde

      exclamation mark

      left double quote

      right double quote

      left quote

      right quote

      left parenthesis

      right parenthesis

      comma

      full stop

      em dash

      :

      colon

      semicolon

      question mark

      enumeration comma

      ellipsis

      ……

      ellipsis

      left guillemet

      right guillemet

      yuan

      greater than or equal to

      less than or equal to

      not equal

      approximately equal

      ±

      plus or minus

      ×

      times

      π

      pi

      Α

      alpha

      Β

      beta

      Γ

      gamma

      Δ

      delta

      Ε

      epsilon

      Ζ

      zeta

      Θ

      theta

      Ι

      iota

      Κ

      kappa

      lambda

      Μ

      mu

      Ν

      nu

      Ξ

      ksi

      Ο

      omicron

      pi

      Ρ

      rho

      sigma

      Τ

      tau

      Υ

      upsilon

      Φ

      phi

      Χ

      chi

      Ψ

      psi

      Ω

      omega

      α

      alpha

      β

      beta

      γ

      gamma

      δ

      delta

      ε

      epsilon

      ζ

      zeta

      η

      eta

      θ

      theta

      ι

      iota

      κ

      kappa

      λ

      lambda

      μ

      mu

      ν

      nu

      ξ

      ksi

      ο

      omicron

      π

      pi

      ρ

      rho

      σ

      sigma

      τ

      tau

      υ

      upsilon

      φ

      phi

      χ

      chi

      ψ

      psi

      ω

      omega

    • Tabel berikut mencantumkan satuan pengukuran umum untuk <say-as>.

      Format

      Kategori

      Contoh Bahasa Inggris

      Singkatan

      Panjang

      nm (nanometer), μm (micrometer), mm (millimeter), cm (centimeter), m (meter), km (kilometer), ft (foot), in (inch)

      Area

      cm² (square centimeter), m² (square meter), km² (square kilometer), SqFt (square foot)

      Volume

      cm³ (cubic centimeter), m³ (cubic meter), km3 (cubic kilometer), mL (milliliter), L (liter), gal (gallon)

      Berat

      μg (microgram), mg (milligram), g (gram), kg (kilogram)

      Waktu

      min (minute), sec (second), ms (millisecond)

      Elektromagnetisme

      μA (microamp), mA (milliamp), Hz (hertz), kHz (kilohertz), MHz (megahertz), GHz (gigahertz), V (volt), kV (kilovolt), kWh (kilowatt hour)

      Suara

      dB (decibel)

      Tekanan atmosfer

      Pa (pascal), kPa (kilopascal), MPa (megapascal)

      Satuan umum lainnya

      Mendukung satuan pengukuran yang tidak terbatas pada kategori sebelumnya, seperti tsp (teaspoon), rpm (revolutions per minute), KB (kilobyte), dan mmHg (millimetre of mercury).

  • Hubungan

    Tag <say-as> dapat berisi teks dan tag <vhml/>.

  • Contoh

    • cardinal

      <speak>
        <say-as interpret-as="cardinal">12345</say-as>
      </speak>
      <speak>
        <say-as interpret-as="cardinal">10234</say-as>
      </speak>
    • digits

      <speak>
        <say-as interpret-as="digits">12345</say-as>
      </speak>
      <speak>
        <say-as interpret-as="digits">10234</say-as>
      </speak>
    • telephone

      <speak>
        <say-as interpret-as="telephone">12345</say-as>
      </speak>
      <speak>
        <say-as interpret-as="telephone">10234</say-as>
      </speak>
    • name

      <speak>
        Her former name is <say-as interpret-as="name">Zeng Xiaofan</say-as>
      </speak>
    • address

      <speak>
        <say-as interpret-as="address">Fulu International, Building 1, Unit 3, Room 304</say-as>
      </speak>
    • id

      <speak>
        <say-as interpret-as="id">myid_1998</say-as>
      </speak>
    • characters

      <speak>
        <say-as interpret-as="characters">Greek letters αβ</say-as>
      </speak>
      <speak>
        <say-as interpret-as="characters">*b+3.c$=α</say-as>
      </speak>
    • punctuation

      <speak>
        <say-as interpret-as="punctuation"> -./:;</say-as>
      </speak>
    • date

      <speak>
        <say-as interpret-as="date">1000-10-10</say-as>
      </speak>
      <speak>
        <say-as interpret-as="date">10-01-2020</say-as>
      </speak>
    • time

      <speak>
        <say-as interpret-as="time">5:00am</say-as>
      </speak>
      <speak>
        <say-as interpret-as="time">0500</say-as>
      </speak>
    • currency

      <speak>
        <say-as interpret-as="currency">13,000,000.00RMB</say-as>
      </speak>
      <speak>
        <say-as interpret-as="currency">$1,000.01</say-as>
      </speak>
    • measure

      <speak>
        <say-as interpret-as="measure">100m12cm6mm</say-as>
      </speak>
      <speak>
        <say-as interpret-as="measure">1,000.01kg</say-as>
      </speak>