Lakukan konversi teks-ke-ucapan secara streaming dengan latensi paket pertama yang rendah. Sintesis suara real-time mendukung input dan output streaming, kloning suara, desain suara, serta kontrol audio detail halus untuk asisten suara, buku audio, dan layanan pelanggan cerdas.
Ikhtisar
Konversi teks ke ucapan secara real-time dengan latensi rendah.
- Input dan output streaming dengan latensi paket pertama yang rendah
- Laju ucapan, pitch, volume, dan bitrate yang dapat disesuaikan untuk kontrol audio detail halus
- Kompatibel dengan format audio utama (PCM, WAV, MP3, Opus) dengan output laju sampel hingga 48 kHz
- Mendukung Kontrol instruksi, yang memungkinkan Anda mengontrol ekspresivitas ucapan melalui instruksi bahasa alami
- Mendukung Kloning suara dan Desain Suara untuk pembuatan suara kustom
- Mendukung Tag emosi dan tag bahasa kaya, yang memungkinkan Anda menyematkan tag emosi atau efek suara dalam teks
Untuk skenario batch seperti buku audio dan sulih suara materi kursus, gunakan Sintesis suara non-real-time. Untuk panduan pemilihan model, lihat Sintesis suara.
Prasyarat
- Konfigurasikan Kunci API dan tetapkan sebagai Variabel lingkungan.
- Jika Anda memanggil API melalui SDK DashScope, instal SDK terbaru.
- Untuk menggunakan protokol AOQ dengan model CosyVoice, unduh dan integrasikan SDK klien AOQ. Untuk detailnya, lihat Ikhtisar SDK.
Memulai cepat
Contoh berikut menunjukkan sintesis suara untuk setiap model. Untuk contoh dan detail parameter lainnya, lihat Referensi API.
Qwen-Audio-TTS
Contoh berikut mensintesis suara menggunakan suara sistem.
Untuk menggunakan fitur Kontrol instruksi, atur instruksi melalui parameter instruction.
# coding=utf-8
import os
import dashscope
from dashscope.audio.tts_v2 import *
# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
# Model
# qwen-audio-3.0-tts-flash/qwen-audio-3.0-tts-plus: Gunakan suara seperti longanhuan_v3.6.
# Setiap suara mendukung bahasa yang berbeda. Untuk mensintesis bahasa non-Cina seperti Jepang atau Korea, pilih suara yang mendukung bahasa target. Lihat daftar suara untuk detailnya.
model = "qwen-audio-3.0-tts-flash"
# Suara
voice = "longanhuan_v3.6"
# Buat instance SpeechSynthesizer dan teruskan parameter permintaan seperti model dan suara di konstruktor
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# Kirim teks yang akan disintesis dan dapatkan audio biner
audio = synthesizer.call("How is the weather today?")
# Pengiriman teks pertama memerlukan pembentukan koneksi WebSocket, sehingga latensi paket pertama mencakup waktu penyiapan koneksi
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
synthesizer.get_last_request_id(),
synthesizer.get_first_package_delay()))
# Simpan audio ke file lokal
with open('output.mp3', 'wb') as f:
f.write(audio)
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.utils.Constants;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class Main {
// Model
// qwen-audio-3.0-tts-flash/qwen-audio-3.0-tts-plus: Gunakan suara seperti longanhuan_v3.6.
// Setiap suara mendukung bahasa yang berbeda. Untuk mensintesis bahasa non-Cina seperti Jepang atau Korea, pilih suara yang mendukung bahasa target. Lihat daftar suara untuk detailnya.
private static String model = "qwen-audio-3.0-tts-flash";
// Suara
private static String voice = "longanhuan_v3.6";
public static void streamAudioDataToSpeaker() {
// Parameter permintaan
SpeechSynthesisParam param =
SpeechSynthesisParam.builder()
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: .apiKey("sk-xxx")
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.model(model) // Model
.voice(voice) // Suara
.build();
// Mode sinkron: nonaktifkan callback (parameter kedua null)
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
ByteBuffer audio = null;
try {
// Blokir hingga audio dikembalikan
audio = synthesizer.call("How is the weather today?");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// Tutup koneksi WebSocket saat tugas selesai
synthesizer.getDuplexApi().close(1000, "bye");
}
if (audio != null) {
// Simpan data audio ke file lokal "output.mp3"
File file = new File("output.mp3");
// Pengiriman teks pertama memerlukan pembentukan koneksi WebSocket, sehingga latensi paket pertama mencakup waktu penyiapan koneksi
// Catatan: getFirstPackageDelay() memerlukan dashscope-sdk-java 2.18.0 atau lebih baru
System.out.println(
"[Metric] requestId: "
+ synthesizer.getLastRequestId()
+ ", first-packet latency (ms): "
+ synthesizer.getFirstPackageDelay());
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(audio.array());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
// Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
streamAudioDataToSpeaker();
System.exit(0);
}
}
CosyVoice
Selain WebSocket, model ini juga mendukung protokol AOQ. Untuk integrasi sisi klien yang memprioritaskan latensi stabil, ketahanan pada jaringan lemah, serta penekanan noise dan pembatalan gema full-duplex bawaan, AOQ direkomendasikan. Untuk perbandingan protokol, lihat Ikhtisar API Realtime.
Pentingcosyvoice-v3.5-plus dan cosyvoice-v3.5-flash hanya tersedia di wilayah Beijing dan hanya mendukung skenario desain suara dan kloning suara (tidak ada suara sistem). Sebelum digunakan, buat suara kustom melalui Kloning suara atau Desain Suara, lalu atur voice ke ID suara dan model ke nama model yang sesuai dalam kode Anda.
Contoh berikut mensintesis suara menggunakan suara sistem (lihat Daftar Suara CosyVoice).
Untuk menggunakan fitur Kontrol instruksi, atur instruksi melalui parameter instruction.
# coding=utf-8
import os
import dashscope
from dashscope.audio.tts_v2 import *
# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
# Model
# Versi model yang berbeda memerlukan suara yang sesuai:
# cosyvoice-v3-flash/cosyvoice-v3-plus: Gunakan suara seperti longanyang.
# cosyvoice-v2: Gunakan suara seperti longxiaochun_v2.
# Setiap suara mendukung bahasa yang berbeda. Untuk mensintesis bahasa non-Cina seperti Jepang atau Korea, pilih suara yang mendukung bahasa target. Lihat daftar suara Qwen-Audio-TTS/CosyVoice untuk detailnya.
model = "cosyvoice-v3-flash"
# Suara
voice = "longanyang"
# Buat instance SpeechSynthesizer dan teruskan parameter permintaan seperti model dan suara di konstruktor
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# Kirim teks yang akan disintesis dan dapatkan audio biner
audio = synthesizer.call("How is the weather today?")
# Pengiriman teks pertama memerlukan pembentukan koneksi WebSocket, sehingga latensi paket pertama mencakup waktu penyiapan koneksi
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
synthesizer.get_last_request_id(),
synthesizer.get_first_package_delay()))
# Simpan audio ke file lokal
with open('output.mp3', 'wb') as f:
f.write(audio)
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.utils.Constants;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
public class Main {
// Model
// Versi model yang berbeda memerlukan suara yang sesuai:
// cosyvoice-v3-flash/cosyvoice-v3-plus: Gunakan suara seperti longanyang.
// cosyvoice-v2: Gunakan suara seperti longxiaochun_v2.
// Setiap suara mendukung bahasa yang berbeda. Untuk mensintesis bahasa non-Cina seperti Jepang atau Korea, pilih suara yang mendukung bahasa target. Lihat daftar suara Qwen-Audio-TTS/CosyVoice untuk detailnya.
private static String model = "cosyvoice-v3-flash";
// Suara
private static String voice = "longanyang";
public static void streamAudioDataToSpeaker() {
// Parameter permintaan
SpeechSynthesisParam param =
SpeechSynthesisParam.builder()
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: .apiKey("sk-xxx")
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.model(model) // Model
.voice(voice) // Suara
.build();
// Mode sinkron: nonaktifkan callback (parameter kedua null)
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
ByteBuffer audio = null;
try {
// Blokir hingga audio dikembalikan
audio = synthesizer.call("How is the weather today?");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// Tutup koneksi WebSocket saat tugas selesai
synthesizer.getDuplexApi().close(1000, "bye");
}
if (audio != null) {
// Simpan data audio ke file lokal "output.mp3"
File file = new File("output.mp3");
// Pengiriman teks pertama memerlukan pembentukan koneksi WebSocket, sehingga latensi paket pertama mencakup waktu penyiapan koneksi
// Catatan: getFirstPackageDelay() memerlukan dashscope-sdk-java 2.18.0 atau lebih baru
System.out.println(
"[Metric] requestId: "
+ synthesizer.getLastRequestId()
+ ", first-packet latency (ms): "
+ synthesizer.getFirstPackageDelay());
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(audio.array());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void main(String[] args) {
// Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
streamAudioDataToSpeaker();
System.exit(0);
}
}
Qwen-TTS
Contoh berikut mensintesis suara menggunakan suara sistem (lihat Suara yang didukung).
Untuk menggunakan fitur Kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime dan atur instruksi melalui parameter instructions.
Python
import os
import base64
import threading
import time
import dashscope
from dashscope.audio.qwen_tts_realtime import *
qwen_tts_realtime: QwenTtsRealtime = None
text_to_synthesize = [
'Right? I love supermarkets like this.',
'Especially during Chinese New Year,',
'I go shopping at supermarkets.',
'And I feel',
'absolutely thrilled!',
'I want to buy so many things!'
]
DO_VIDEO_TEST = False
def init_dashscope_api_key():
"""
Tetapkan Kunci API DashScope Anda. Informasi lebih lanjut:
https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
"""
# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
if 'DASHSCOPE_API_KEY' in os.environ:
dashscope.api_key = os.environ[
'DASHSCOPE_API_KEY'] # Muat Kunci API dari variabel lingkungan DASHSCOPE_API_KEY
else:
dashscope.api_key = 'your-dashscope-api-key' # Tetapkan Kunci API secara manual
class MyCallback(QwenTtsRealtimeCallback):
def __init__(self):
self.complete_event = threading.Event()
self.file = open('result_24k.pcm', 'wb')
def on_open(self) -> None:
print('koneksi dibuka, inisialisasi pemutar')
def on_close(self, close_status_code, close_msg) -> None:
self.file.close()
print('koneksi ditutup dengan kode: {}, pesan: {}, hancurkan pemutar'.format(close_status_code, close_msg))
def on_event(self, response: str) -> None:
try:
global qwen_tts_realtime
type = response['type']
if 'session.created' == type:
print('mulai sesi: {}'.format(response['session']['id']))
if 'response.audio.delta' == type:
recv_audio_b64 = response['delta']
self.file.write(base64.b64decode(recv_audio_b64))
if 'response.done' == type:
print(f'respons {qwen_tts_realtime.get_last_response_id()} selesai')
if 'session.finished' == type:
print('sesi selesai')
self.complete_event.set()
except Exception as e:
print('[Error] {}'.format(e))
return
def wait_for_finished(self):
self.complete_event.wait()
if __name__ == '__main__':
init_dashscope_api_key()
print('Menginisialisasi ...')
callback = MyCallback()
qwen_tts_realtime = QwenTtsRealtime(
# Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime
model='qwen3-tts-flash-realtime',
callback=callback,
# Wilayah Singapura
url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime'
)
qwen_tts_realtime.connect()
qwen_tts_realtime.update_session(
voice = 'Cherry',
response_format = AudioFormat.PCM_24000HZ_MONO_16BIT,
# Untuk menggunakan kontrol instruksi, hapus komentar baris berikut dan ganti model dengan qwen3-tts-instruct-flash-realtime
# instructions='Speak quickly with a rising intonation, suitable for introducing fashion products.',
# optimize_instructions=True,
mode = 'server_commit'
)
for text_chunk in text_to_synthesize:
print(f'kirim teks: {text_chunk}')
qwen_tts_realtime.append_text(text_chunk)
time.sleep(0.1)
qwen_tts_realtime.finish()
callback.wait_for_finished()
print('[Metric] sesi: {}, latensi audio pertama: {}'.format(
qwen_tts_realtime.get_session_id(),
qwen_tts_realtime.get_first_audio_delay(),
))
import base64
import os
import threading
import dashscope
from dashscope.audio.qwen_tts_realtime import *
qwen_tts_realtime: QwenTtsRealtime = None
text_to_synthesize = [
'This is the first sentence.',
'This is the second sentence.',
'This is the third sentence.',
]
DO_VIDEO_TEST = False
def init_dashscope_api_key():
"""
Tetapkan Kunci API DashScope Anda. Informasi lebih lanjut:
https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
"""
# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
if 'DASHSCOPE_API_KEY' in os.environ:
dashscope.api_key = os.environ[
'DASHSCOPE_API_KEY'] # Muat Kunci API dari variabel lingkungan DASHSCOPE_API_KEY
else:
dashscope.api_key = 'your-dashscope-api-key' # Tetapkan Kunci API secara manual
class MyCallback(QwenTtsRealtimeCallback):
def __init__(self):
super().__init__()
self.response_counter = 0
self.complete_event = threading.Event()
self.file = open(f'result_{self.response_counter}_24k.pcm', 'wb')
def reset_event(self):
self.response_counter += 1
self.file = open(f'result_{self.response_counter}_24k.pcm', 'wb')
self.complete_event = threading.Event()
def on_open(self) -> None:
print('koneksi dibuka, inisialisasi pemutar')
def on_close(self, close_status_code, close_msg) -> None:
print('koneksi ditutup dengan kode: {}, pesan: {}, hancurkan pemutar'.format(close_status_code, close_msg))
def on_event(self, response: str) -> None:
try:
global qwen_tts_realtime
type = response['type']
if 'session.created' == type:
print('mulai sesi: {}'.format(response['session']['id']))
if 'response.audio.delta' == type:
recv_audio_b64 = response['delta']
self.file.write(base64.b64decode(recv_audio_b64))
if 'response.done' == type:
print(f'respons {qwen_tts_realtime.get_last_response_id()} selesai')
self.complete_event.set()
self.file.close()
if 'session.finished' == type:
print('sesi selesai')
self.complete_event.set()
except Exception as e:
print('[Error] {}'.format(e))
return
def wait_for_response_done(self):
self.complete_event.wait()
if __name__ == '__main__':
init_dashscope_api_key()
print('Menginisialisasi ...')
callback = MyCallback()
qwen_tts_realtime = QwenTtsRealtime(
# Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime
model='qwen3-tts-flash-realtime',
callback=callback,
# Wilayah Singapura
url='wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime'
)
qwen_tts_realtime.connect()
qwen_tts_realtime.update_session(
voice = 'Cherry',
response_format = AudioFormat.PCM_24000HZ_MONO_16BIT,
# Untuk menggunakan kontrol instruksi, hapus komentar baris berikut dan ganti model dengan qwen3-tts-instruct-flash-realtime
# instructions='Speak quickly with a rising intonation, suitable for introducing fashion products.',
# optimize_instructions=True,
mode = 'commit'
)
print(f'kirim teks: {text_to_synthesize[0]}')
qwen_tts_realtime.append_text(text_to_synthesize[0])
qwen_tts_realtime.commit()
callback.wait_for_response_done()
callback.reset_event()
print(f'kirim teks: {text_to_synthesize[1]}')
qwen_tts_realtime.append_text(text_to_synthesize[1])
qwen_tts_realtime.commit()
callback.wait_for_response_done()
callback.reset_event()
print(f'kirim teks: {text_to_synthesize[2]}')
qwen_tts_realtime.append_text(text_to_synthesize[2])
qwen_tts_realtime.commit()
callback.wait_for_response_done()
qwen_tts_realtime.finish()
print('[Metric] sesi: {}, latensi audio pertama: {}'.format(
qwen_tts_realtime.get_session_id(),
qwen_tts_realtime.get_first_audio_delay(),
))
Java
Mode server commit
appendText()
import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.AudioSystem;
import java.io.*;
import java.util.Base64;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class Main {
static String[] textToSynthesize = {
"Right? I really love this kind of supermarket.",
"Especially during the Chinese New Year.",
"Going to the supermarket.",
"It just makes me feel.",
"Super, super happy!",
"I want to buy so many things!"
};
public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;
// Pemutar audio PCM real-time
public static class RealtimePcmPlayer {
private int sampleRate;
private SourceDataLine line;
private AudioFormat audioFormat;
private Thread decoderThread;
private Thread playerThread;
private AtomicBoolean stopped = new AtomicBoolean(false);
private Queue<String> b64AudioBuffer = new ConcurrentLinkedQueue<>();
private Queue<byte[]> RawAudioBuffer = new ConcurrentLinkedQueue<>();
private ByteArrayOutputStream totalAudioStream = new ByteArrayOutputStream();
// Inisialisasi format audio dan saluran audio.
public RealtimePcmPlayer(int sampleRate) throws LineUnavailableException {
this.sampleRate = sampleRate;
this.audioFormat = new AudioFormat(this.sampleRate, 16, 1, true, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
line = (SourceDataLine) AudioSystem.getLine(info);
line.open(audioFormat);
line.start();
decoderThread = new Thread(new Runnable() {
@Override
public void run() {
while (!stopped.get()) {
String b64Audio = b64AudioBuffer.poll();
if (b64Audio != null) {
byte[] rawAudio = Base64.getDecoder().decode(b64Audio);
RawAudioBuffer.add(rawAudio);
// Tulis data audio ke totalAudioStream.
try {
totalAudioStream.write(rawAudio);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
playerThread = new Thread(new Runnable() {
@Override
public void run() {
while (!stopped.get()) {
byte[] rawAudio = RawAudioBuffer.poll();
if (rawAudio != null) {
try {
playChunk(rawAudio);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
decoderThread.start();
playerThread.start();
}
// Putar potongan audio dan blokir hingga pemutaran selesai.
private void playChunk(byte[] chunk) throws IOException, InterruptedException {
if (chunk == null || chunk.length == 0) return;
int bytesWritten = 0;
while (bytesWritten < chunk.length) {
bytesWritten += line.write(chunk, bytesWritten, chunk.length - bytesWritten);
}
int audioLength = chunk.length / (this.sampleRate*2/1000);
// Tunggu hingga audio yang dibuffer selesai diputar.
Thread.sleep(audioLength - 10);
}
public void write(String b64Audio) {
b64AudioBuffer.add(b64Audio);
}
public void cancel() {
b64AudioBuffer.clear();
RawAudioBuffer.clear();
}
public void waitForComplete() throws InterruptedException {
while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
Thread.sleep(100);
}
line.drain();
}
public void shutdown() throws InterruptedException, IOException {
stopped.set(true);
decoderThread.join();
playerThread.join();
// Simpan file audio lengkap.
File file = new File("TotalAudio_"+ttsFormat.getSampleRate()+"."+ttsFormat.getFormat());
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(totalAudioStream.toByteArray());
}
if (line != null && line.isRunning()) {
line.drain();
line.close();
}
}
}
public static void main(String[] args) throws InterruptedException, LineUnavailableException, IOException {
QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
// Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime.
.model("qwen3-tts-flash-realtime")
// Wilayah China (Beijing)
.url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
// Kunci API berbeda antara Singapura dan China (Beijing). Lihat https://www.alibabacloud.com/help/en/model-studio/get-api-key.
.apikey(System.getenv("DASHSCOPE_API_KEY"))
.build();
AtomicReference<CountDownLatch> completeLatch = new AtomicReference<>(new CountDownLatch(1));
final AtomicReference<QwenTtsRealtime> qwenTtsRef = new AtomicReference<>(null);
// Buat instance pemutar audio real-time.
RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);
QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
@Override
public void onOpen() {
// Tangani pembentukan koneksi.
}
@Override
public void onEvent(JsonObject message) {
String type = message.get("type").getAsString();
switch(type) {
case "session.created":
// Tangani pembuatan sesi.
if (message.has("session")) {
String eventId = message.get("event_id").getAsString();
String sessionId = message.get("session").getAsJsonObject().get("id").getAsString();
System.out.println("[onEvent] session.created, session_id: "
+ sessionId + ", event_id: " + eventId);
}
break;
case "response.audio.delta":
String recvAudioB64 = message.get("delta").getAsString();
// Putar audio secara real-time.
audioPlayer.write(recvAudioB64);
break;
case "response.done":
// Tangani penyelesaian respons.
break;
case "session.finished":
// Tangani penghentian sesi.
completeLatch.get().countDown();
default:
break;
}
}
@Override
public void onClose(int code, String reason) {
// Tangani penutupan koneksi.
}
});
qwenTtsRef.set(qwenTtsRealtime);
try {
qwenTtsRealtime.connect();
} catch (NoApiKeyException e) {
throw new RuntimeException(e);
}
QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
.voice("Cherry")
.responseFormat(ttsFormat)
.mode("server_commit")
// Untuk menggunakan kontrol instruksi, hapus komentar baris berikut dan ganti model dengan qwen3-tts-instruct-flash-realtime.
// .instructions("")
// .optimizeInstructions(true)
.build();
qwenTtsRealtime.updateSession(config);
for (String text:textToSynthesize) {
qwenTtsRealtime.appendText(text);
Thread.sleep(100);
}
qwenTtsRealtime.finish();
completeLatch.get().await();
qwenTtsRealtime.close();
// Tunggu hingga pemutaran audio selesai, lalu matikan pemutar.
audioPlayer.waitForComplete();
audioPlayer.shutdown();
System.exit(0);
}
}
Mode commit
commit()
import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.AudioSystem;
import java.io.*;
import java.util.Base64;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class Main {
public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;
// Pemutar audio PCM real-time
public static class RealtimePcmPlayer {
private int sampleRate;
private SourceDataLine line;
private AudioFormat audioFormat;
private Thread decoderThread;
private Thread playerThread;
private AtomicBoolean stopped = new AtomicBoolean(false);
private Queue<String> b64AudioBuffer = new ConcurrentLinkedQueue<>();
private Queue<byte[]> RawAudioBuffer = new ConcurrentLinkedQueue<>();
private ByteArrayOutputStream totalAudioStream = new ByteArrayOutputStream();
// Inisialisasi format audio dan saluran audio.
public RealtimePcmPlayer(int sampleRate) throws LineUnavailableException {
this.sampleRate = sampleRate;
this.audioFormat = new AudioFormat(this.sampleRate, 16, 1, true, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
line = (SourceDataLine) AudioSystem.getLine(info);
line.open(audioFormat);
line.start();
decoderThread = new Thread(new Runnable() {
@Override
public void run() {
while (!stopped.get()) {
String b64Audio = b64AudioBuffer.poll();
if (b64Audio != null) {
byte[] rawAudio = Base64.getDecoder().decode(b64Audio);
RawAudioBuffer.add(rawAudio);
// Tulis data audio ke totalAudioStream.
try {
totalAudioStream.write(rawAudio);
} catch (IOException e) {
throw new RuntimeException(e);
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
playerThread = new Thread(new Runnable() {
@Override
public void run() {
while (!stopped.get()) {
byte[] rawAudio = RawAudioBuffer.poll();
if (rawAudio != null) {
try {
playChunk(rawAudio);
} catch (IOException e) {
throw new RuntimeException(e);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
} else {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
});
decoderThread.start();
playerThread.start();
}
// Putar potongan audio dan blokir hingga pemutaran selesai.
private void playChunk(byte[] chunk) throws IOException, InterruptedException {
if (chunk == null || chunk.length == 0) return;
int bytesWritten = 0;
while (bytesWritten < chunk.length) {
bytesWritten += line.write(chunk, bytesWritten, chunk.length - bytesWritten);
}
int audioLength = chunk.length / (this.sampleRate*2/1000);
// Tunggu hingga audio yang dibuffer selesai diputar.
Thread.sleep(audioLength - 10);
}
public void write(String b64Audio) {
b64AudioBuffer.add(b64Audio);
}
public void cancel() {
b64AudioBuffer.clear();
RawAudioBuffer.clear();
}
public void waitForComplete() throws InterruptedException {
// Tunggu hingga semua data audio yang dibuffer selesai diputar.
while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
Thread.sleep(100);
}
// Tunggu hingga saluran audio dikosongkan.
line.drain();
}
public void shutdown() throws InterruptedException {
stopped.set(true);
decoderThread.join();
playerThread.join();
// Simpan file audio lengkap.
File file = new File("TotalAudio_"+ttsFormat.getSampleRate()+"."+ttsFormat.getFormat());
try (FileOutputStream fos = new FileOutputStream(file)) {
fos.write(totalAudioStream.toByteArray());
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
if (line != null && line.isRunning()) {
line.drain();
line.close();
}
}
}
public static void main(String[] args) throws InterruptedException, LineUnavailableException, FileNotFoundException {
Scanner scanner = new Scanner(System.in);
QwenTtsRealtimeParam param = QwenTtsRealtimeParam.builder()
// Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime.
.model("qwen3-tts-flash-realtime")
// Wilayah China (Beijing)
.url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
// Kunci API berbeda antara Singapura dan China (Beijing). Lihat https://www.alibabacloud.com/help/en/model-studio/get-api-key.
.apikey(System.getenv("DASHSCOPE_API_KEY"))
.build();
AtomicReference<CountDownLatch> completeLatch = new AtomicReference<>(new CountDownLatch(1));
// Buat instance pemutar real-time.
RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);
final AtomicReference<QwenTtsRealtime> qwenTtsRef = new AtomicReference<>(null);
QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
@Override
public void onOpen() {
System.out.println("koneksi dibuka");
System.out.println("Masukkan teks dan tekan Enter untuk mengirim. Masukkan 'quit' untuk keluar dari program.");
}
@Override
public void onEvent(JsonObject message) {
String type = message.get("type").getAsString();
switch(type) {
case "session.created":
System.out.println("mulai sesi: " + message.get("session").getAsJsonObject().get("id").getAsString());
break;
case "response.audio.delta":
String recvAudioB64 = message.get("delta").getAsString();
byte[] rawAudio = Base64.getDecoder().decode(recvAudioB64);
// Putar audio secara real-time.
audioPlayer.write(recvAudioB64);
break;
case "response.done":
System.out.println("respons selesai");
// Tunggu hingga pemutaran audio selesai.
try {
audioPlayer.waitForComplete();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Siapkan untuk input berikutnya.
completeLatch.get().countDown();
break;
case "session.finished":
System.out.println("sesi selesai");
if (qwenTtsRef.get() != null) {
System.out.println("[Metric] respons: " + qwenTtsRef.get().getResponseId() +
", latensi audio pertama: " + qwenTtsRef.get().getFirstAudioDelay() + " ms");
}
completeLatch.get().countDown();
default:
break;
}
}
@Override
public void onClose(int code, String reason) {
System.out.println("koneksi ditutup kode: " + code + ", alasan: " + reason);
try {
// Tunggu hingga pemutaran selesai, lalu matikan pemutar.
audioPlayer.waitForComplete();
audioPlayer.shutdown();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
qwenTtsRef.set(qwenTtsRealtime);
try {
qwenTtsRealtime.connect();
} catch (NoApiKeyException e) {
throw new RuntimeException(e);
}
QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
.voice("Cherry")
.responseFormat(ttsFormat)
.mode("commit")
// Untuk menggunakan kontrol instruksi, hapus komentar baris berikut dan ganti model dengan qwen3-tts-instruct-flash-realtime.
// .instructions("")
// .optimizeInstructions(true)
.build();
qwenTtsRealtime.updateSession(config);
// Baca input pengguna dalam loop.
while (true) {
System.out.print("Masukkan teks untuk disintesis: ");
String text = scanner.nextLine();
// Keluar saat pengguna memasukkan 'quit'.
if ("quit".equalsIgnoreCase(text.trim())) {
System.out.println("Menutup koneksi...");
qwenTtsRealtime.finish();
completeLatch.get().await();
break;
}
// Lewati input kosong.
if (text.trim().isEmpty()) {
continue;
}
// Inisialisasi ulang latch countdown.
completeLatch.set(new CountDownLatch(1));
// Kirim teks.
qwenTtsRealtime.appendText(text);
qwenTtsRealtime.commit();
// Tunggu hingga sintesis saat ini selesai.
completeLatch.get().await();
}
// Bersihkan sumber daya.
audioPlayer.waitForComplete();
audioPlayer.shutdown();
scanner.close();
System.exit(0);
}
}
Konfigurasi sesi
Mode interaksi Qwen-TTS
API Realtime Qwen-TTS menyediakan dua mode interaksi:
- Mode server_commit: Server secara otomatis menangani segmentasi teks dan penjadwalan sintesis. Mode ini cocok untuk sintesis berkelanjutan pada blok teks besar. Aplikasi klien hanya perlu menambahkan teks tanpa mengelola segmentasi atau pengiriman.
- Mode commit: Klien secara eksplisit mengirim buffer teks untuk memicu sintesis. Mode ini ideal untuk skenario yang memerlukan kontrol presisi atas waktu sintesis, seperti sintesis per giliran dalam percakapan AI.
Beralih mode interaksi:
- WebSocket: Atur bidang
modepada eventsession.update.
{
"type": "session.update",
"session": {
"mode": "server_commit"
}
}
- SDK Python: Atur parameter
modedalam metodeupdate_session.
qwen_tts_realtime.update_session(
voice='Cherry',
response_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
mode='server_commit'
)
- SDK Java: Atur parameter
modemelaluiQwenTtsRealtimeConfig.builder().
QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
.voice("Cherry")
.responseFormat(ttsFormat)
.mode("server_commit")
.build();
qwenTtsRealtime.updateSession(config);
Untuk contoh kode SDK lengkap, lihat SDK Python dan SDK Java. Untuk detail mengenai siklus hidup event WebSocket dan penggunaan kembali koneksi, lihat Referensi API WebSocket.
Fitur lanjutan
Kontrol instruksi
Kontrol instruksi memungkinkan penyesuaian nada, kecepatan, emosi, dan karakteristik timbre ucapan melalui deskripsi bahasa alami, tanpa perlu mengonfigurasi parameter audio yang kompleks.
Spesifikasi instruksi berdasarkan model:
Qwen-Audio-TTS
Model yang didukung: qwen-audio-3.0-tts-plus, qwen-audio-3.0-tts-flash
Suara sistem dan suara kloning: menerima instruksi apa pun.
CosyVoice
Model yang didukung: cosyvoice-v3.5-plus, cosyvoice-v3.5-flash, cosyvoice-v3-plus, cosyvoice-v3-flash
Persyaratan format instruksi bervariasi tergantung pada model:
-
cosyvoice-v3.5-plus, cosyvoice-v3.5-flash:
- Suara kloning/desain: menerima instruksi apa pun.
- Suara sistem: v3.5 tidak mendukung suara sistem.
-
cosyvoice-v3-plus:
- Suara kloning/desain: tidak mendukung kontrol instruksi.
- Suara sistem: instruksi harus menggunakan format dan konten tetap. Lihat Daftar Suara CosyVoice.
-
cosyvoice-v3-flash:
- Suara kloning/desain: menerima instruksi apa pun.
- Suara sistem: instruksi harus menggunakan format dan konten tetap. Lihat Daftar Suara CosyVoice.
Penggunaan: Tentukan konten instruksi melalui parameter instruction.
Bahasa yang didukung untuk teks instruksi:
-
cosyvoice-v3.5-plus, cosyvoice-v3.5-flash:
- Suara kloning/desain: Cina, Inggris, Prancis, Jerman, Jepang, Korea, Rusia, Portugis, Thailand, Indonesia, dan Vietnam.
- Suara sistem: v3.5 tidak mendukung suara sistem.
-
cosyvoice-v3-plus:
- Suara kloning/desain: Cina, Inggris, Prancis, Jerman, Jepang, Korea, dan Rusia.
- Suara sistem: instruksi harus menggunakan format dan konten tetap. Lihat Daftar Suara CosyVoice.
-
cosyvoice-v3-flash:
- Suara kloning/desain: Cina, Inggris, Prancis, Jerman, Jepang, Korea, dan Rusia.
- Suara sistem: hanya Cina.
Batas panjang teks instruksi: Maksimal 100 karakter. Karakter Cina (termasuk Cina sederhana/tradisional, kanji Jepang, dan hanja Korea) dihitung sebagai 2 karakter. Semua karakter lain (seperti tanda baca, huruf, angka, kana Jepang, dan hangul Korea) dihitung sebagai 1 karakter.
Qwen-TTS
Model yang didukung: Hanya model seri Qwen3-TTS-Instruct-Flash-Realtime.
Penggunaan: Tentukan konten instruksi melalui parameter instructions.
Bahasa yang didukung untuk teks instruksi: Hanya Cina dan Inggris.
Batas panjang teks instruksi: Maksimal 1.600 token.
Kasus penggunaan:
- Narasi buku audio dan drama radio
- Narasi iklan dan video promosi
- Suara karakter game dan animasi
- Asisten suara yang ekspresif secara emosional
- Narasi dokumenter dan siaran berita
Menulis deskripsi suara yang efektif:
-
Prinsip dasar:
- Bersifat spesifik, bukan samar: Gunakan kata-kata yang menggambarkan kualitas vokal, seperti "dalam", "jernih", atau "kecepatan sedikit cepat". Hindari istilah subjektif atau samar seperti "bagus" atau "normal".
- Bersifat multidimensi, bukan satu dimensi: Deskripsi yang baik biasanya mencakup beberapa dimensi (seperti jenis kelamin, usia, dan emosi). Menulis hanya "suara perempuan" terlalu umum untuk menghasilkan timbre yang khas.
- Bersifat objektif, bukan subjektif: Fokus pada karakteristik fisik dan perseptual suara. Misalnya, gunakan "nada lebih tinggi dengan intonasi energetik" daripada "suara favorit saya".
- Bersifat orisinal, bukan imitatif: Jelaskan kualitas vokal alih-alih meminta peniruan individu tertentu (seperti selebriti atau aktor). Model tidak mendukung peniruan, dan hal tersebut dapat menimbulkan risiko pelanggaran hak cipta.
- Bersifat ringkas, bukan redundan: Pastikan setiap kata memiliki tujuan. Hindari sinonim berulang atau pengubah yang tidak bermakna.
-
Referensi dimensi deskripsi:
Gabungkan dimensi-dimensi berikut untuk mendeskripsikan suara. Semakin banyak dimensi yang Anda sertakan, semakin akurat output-nya.
Dimension
Contoh deskripsi
Gender
Laki-laki, perempuan, androgini
Age
Anak (5–12), remaja (13–18), dewasa muda (19–35), paruh baya (36–55), lansia (55+)
Pitch
Tinggi, mid-range, rendah, agak tinggi, agak rendah
Speed
Cepat, sedang, lambat, agak cepat, agak lambat
Emotion
Ceria, tenang, lembut, serius, hidup, terkendali, menenangkan
Characteristics
Magnetis, jernih, serak, hangat, manis, kaya, kuat
Use case
Siaran berita, narasi iklan, buku audio, karakter animasi, asisten suara, narasi dokumenter
-
Contoh:
- Gaya siaran standar: artikulasi jelas dan tepat dengan pelafalan sempurna
- Suara perempuan muda yang ceria dengan kecepatan lebih cepat dan intonasi naik yang terasa jelas, cocok untuk memperkenalkan produk fesyen
- Laki-laki paruh baya yang tenang, kecepatan lambat, suara dalam dan magnetis, cocok untuk membacakan berita atau narasi dokumenter
- Perempuan intelektual yang lembut, sekitar usia 30 tahun, dengan nada merata, cocok untuk narasi buku audio
- Suara anak yang lucu, kira-kira anak perempuan berusia 8 tahun, berbicara dengan kualitas yang sedikit kekanak-kanakan, cocok untuk narasi karakter animasi
Dialek
Bagian ini menjelaskan cara menghasilkan ucapan dalam dialek Tiongkok (seperti dialek Henan, dialek Sichuan, dan Kanton). Metode konfigurasi bervariasi tergantung pada model dan jenis voice.
Konfigurasi dialek berdasarkan model:
Qwen-Audio-TTS
-
System voices: Pilih salah satu jenis voice berikut:
- System voice dengan dukungan dialek bawaan, yang menghasilkan dialek yang sesuai tanpa perlu konfigurasi tambahan.
- Voice yang mendukung Instruction control dan dapat dikonfigurasi untuk menghasilkan dialek tertentu melalui teks instruksi.
-
Voice cloning voices: Konfigurasikan melalui fitur Instruction control. Misalnya, atur teks instruksi menjadi
请用河南话表达.
Dialek yang didukung: Lihat kolom "Supported languages" untuk setiap model di Qwen-Audio-TTS.
CosyVoice
-
System voices: Pilih salah satu jenis voice berikut dari daftar Voice CosyVoice:
- System voice dengan dukungan dialek bawaan (seperti
longshange_v3), yang menghasilkan dialek yang sesuai tanpa perlu konfigurasi tambahan. - Voice yang mendukung Instruction control dan dapat dikonfigurasi untuk menghasilkan dialek tertentu (seperti
longanhuan_v3), yang ditentukan melalui teks instruksi.
- System voice dengan dukungan dialek bawaan (seperti
-
Voice cloning voices: Konfigurasikan melalui fitur Instruction control. Misalnya, atur teks instruksi menjadi
请用河南话表达. -
Voice design voices: tidak mendukung dialek.
Dialek yang didukung: Lihat kolom "Supported languages" untuk setiap model di CosyVoice.
Contoh: Gunakan cosyvoice-v3-flash dengan voice longanhuan_v3, dan atur teks instruksi menjadi "请用河南话表达。" untuk menghasilkan ucapan dalam dialek Henan.
# coding=utf-8
import os
import dashscope
from dashscope.audio.tts_v2 import *
# API Key berbeda antara Wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio Tiongkok Anda: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# Berikut adalah konfigurasi untuk Wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi tergantung wilayah.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
# Model
# Versi model yang berbeda memerlukan voice yang sesuai:
# cosyvoice-v3-flash/cosyvoice-v3-plus: Gunakan voice seperti longanyang.
# cosyvoice-v2: Gunakan voice seperti longxiaochun_v2.
# Pilih voice yang sesuai untuk target language Anda
model = "cosyvoice-v3-flash"
# Voice
voice = "longanhuan_v3"
# Buat instance SpeechSynthesizer dan teruskan parameter permintaan seperti model dan voice di konstruktor
synthesizer = SpeechSynthesizer(model=model, voice=voice, instruction="请用河南话表达。")
# Kirim teks yang akan disintesis dan dapatkan audio biner
audio = synthesizer.call("叫你去买盐,你买回来一袋面,这不是弄啥嘞吗!")
# Pengiriman teks pertama memerlukan pembentukan koneksi WebSocket, sehingga latensi paket pertama mencakup waktu pembentukan koneksi
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
synthesizer.get_last_request_id(),
synthesizer.get_first_package_delay()))
# Simpan audio ke file lokal
with open('output.mp3', 'wb') as f:
f.write(audio)
Qwen-TTS
- System voices: Gunakan system voice dengan dukungan dialek bawaan. Lihat daftar voice Qwen-TTS di Voice yang didukung.
- Voice cloning voices: tidak mendukung dialek.
- Voice design voices: tidak mendukung dialek.
Dialek yang didukung: Lihat kolom "Supported languages" untuk setiap model di Qwen3-TTS.
Tag emosi dan bahasa kaya
Model seri Qwen-Audio-TTS mendukung penyematan tag emosi dan bahasa kaya secara langsung dalam teks untuk sintesis suara (parameter text). Tag-tag ini mengontrol ekspresi emosional atau menyisipkan efek vokal (seperti tawa dan desahan) pada posisi tertentu, sehingga menghasilkan ucapan yang lebih ekspresif tanpa perlu mengonfigurasi parameter audio yang kompleks.
PentingModel yang didukung: hanya qwen-audio-3.0-tts-plus dan qwen-audio-3.0-tts-flash.
Batasan: Hanya mode streaming unidirectional yang didukung.
Tag kontrol menetapkan emosi atau gaya ucapan. Tempatkan tag dalam teks untuk memengaruhi seluruh teks berikutnya hingga tag kontrol berikutnya muncul atau kalimat secara otomatis tersegmentasi karena panjangnya.
Tag | Deskripsi |
|---|---|
| Sedih |
| Takjub |
| Teriakan keras dan dalam |
| Gemetar |
| Marah |
| Bersemangat |
| Sarkastik |
| Ingin tahu |
| Gaya Dracula (dalam dan menyeramkan) |
| Bosan |
| Lelah |
| Menghina |
| Berteriak |
| Bisikan lembut ASMR |
| Panik |
| Nakal |
| Empatik |
| Berbisik |
| Enggan |
| Menangis |
| Serius |
| Ucapan sangat lambat |
| Ucapan sangat cepat |
Tag bahasa kaya menyisipkan efek vokal pada posisi saat ini dalam teks tanpa memengaruhi gaya emosional teks di sekitarnya.
Tag | Deskripsi |
|---|---|
| Astaga! |
| Mendesah |
| Membersihkan tenggorokan |
| Tertawa kecil |
| Tertawa |
| Batuk |
| Snort |
Contoh berikut menunjukkan cara menggabungkan tag kontrol dan tag bahasa kaya dalam parameter text:
[excited]What a beautiful day today![laughing]Let's go out and have fun together!
Dalam teks ini, [excited] adalah tag kontrol yang menerapkan emosi bersemangat pada seluruh teks berikutnya. [laughing] adalah tag bahasa kaya yang menyisipkan tawa pada posisi tersebut sebelum melanjutkan sintesis teks sisanya.
Anda juga dapat beralih antar emosi berbeda dalam teks yang sama:
[serious]Please pay attention to the safety precautions.[excited]Alright, let's get started now!
Di sini, [serious] menetapkan nada serius pada kalimat pertama, dan [excited] mengubah nada menjadi bersemangat mulai dari kalimat kedua.
Cancel task
Jika Anda perlu menginterupsi proses sintesis saat ini selama sintesis suara real-time, kirimkan perintah cancel. Setelah pembatalan, server segera mengakhiri task saat ini dan mengembalikan event completion. Anda dapat memulai task sintesis baru pada koneksi WebSocket yang sama tanpa perlu terhubung ulang.
Penggunaan:
- Python SDK: versi 1.26.4 atau lebih baru, panggil
SpeechSynthesizer.streaming_cancel(). - Java SDK: versi 2.22.26 atau lebih baru, panggil
SpeechSynthesizer.streamingCancel(). - Protokol raw WebSocket: Kirim event
finish-taskdan aturdirective=canceldalaminput.
PentingBatasan model:
- China (Beijing): Semua model Qwen-Audio-TTS mendukung fitur ini. Model CosyVoice memerlukan versi v2 atau lebih baru.
- Singapura: Semua model Qwen-Audio-TTS mendukung fitur ini. Model CosyVoice tidak mendukung fitur ini.
Panggilan protokol mentah WebSocket
Contoh berikut menunjukkan cara menghubungkan langsung ke server melalui protokol mentah WebSocket, cocok untuk skenario tanpa SDK DashScope. Ini merupakan implementasi minimal yang dapat dijalankan. Untuk detail protokol WebSocket, lihat referensi API untuk masing-masing model.
Lihat contoh panggilan protokol mentah WebSocket
Qwen-Audio-TTS/CosyVoice
Qwen-Audio-TTS dan CosyVoice menggunakan protokol WebSocket yang sama. Contoh berikut menggunakan qwen-audio-3.0-tts-flash. Untuk menggunakan CosyVoice, ganti parameter model dengan model CosyVoice (seperti cosyvoice-v3-flash) dan parameter voice dengan suara yang diinginkan.
Go
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
const (
// Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi berdasarkan wilayah.
wsURL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"
outputFile = "output.mp3"
)
func main() {
// API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: apiKey := "sk-xxx"
apiKey := os.Getenv("DASHSCOPE_API_KEY")
// Hapus file output
os.Remove(outputFile)
os.Create(outputFile)
// Hubungkan ke WebSocket
header := make(http.Header)
header.Add("X-DashScope-DataInspection", "enable")
header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header)
if err != nil {
if resp != nil {
fmt.Printf("Koneksi gagal, kode status HTTP: %d\n", resp.StatusCode)
}
fmt.Println("Koneksi gagal:", err)
return
}
defer conn.Close()
// Hasilkan ID tugas
taskID := uuid.New().String()
fmt.Printf("ID tugas yang dihasilkan: %s\n", taskID)
// Mengirim event run-task
runTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "run-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"task_group": "audio",
"task": "tts",
"function": "SpeechSynthesizer",
"model": "qwen-audio-3.0-tts-flash",
"parameters": map[string]interface{}{
"text_type": "PlainText",
"voice": "longanhuan_v3.6",
"format": "mp3",
"sample_rate": 22050,
"volume": 50,
"rate": 1,
"pitch": 1,
// Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, error "Text request limit violated, expected 1." akan dikembalikan
"enable_ssml": false,
},
"input": map[string]interface{}{},
},
}
runTaskJSON, _ := json.Marshal(runTaskCmd)
fmt.Printf("Mengirim event run-task: %s\n", string(runTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, runTaskJSON)
if err != nil {
fmt.Println("Gagal mengirim run-task:", err)
return
}
textSent := false
// Proses pesan
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
fmt.Println("Gagal membaca pesan:", err)
break
}
// Proses pesan biner
if messageType == websocket.BinaryMessage {
fmt.Printf("Menerima pesan biner, panjang: %d\n", len(message))
file, _ := os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
file.Write(message)
file.Close()
continue
}
// Proses pesan teks
messageStr := string(message)
fmt.Printf("Menerima pesan teks: %s\n", strings.ReplaceAll(messageStr, "\n", ""))
// Uraikan JSON untuk mendapatkan jenis event
var msgMap map[string]interface{}
if json.Unmarshal(message, &msgMap) == nil {
if header, ok := msgMap["header"].(map[string]interface{}); ok {
if event, ok := header["event"].(string); ok {
fmt.Printf("Jenis event: %s\n", event)
switch event {
case "task-started":
fmt.Println("=== Menerima event task-started ===")
if !textSent {
// Mengirim event continue-task
texts := []string{"Before my bed, moonlight shines bright, I suspect it's frost upon the ground.", "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home."}
for _, text := range texts {
continueTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "continue-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"input": map[string]interface{}{
"text": text,
},
},
}
continueTaskJSON, _ := json.Marshal(continueTaskCmd)
fmt.Printf("Mengirim event continue-task: %s\n", string(continueTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, continueTaskJSON)
if err != nil {
fmt.Println("Gagal mengirim continue-task:", err)
return
}
}
textSent = true
// Tunda sebelum mengirim finish-task
time.Sleep(500 * time.Millisecond)
// Mengirim event finish-task
finishTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "finish-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"input": map[string]interface{}{},
},
}
finishTaskJSON, _ := json.Marshal(finishTaskCmd)
fmt.Printf("Mengirim event finish-task: %s\n", string(finishTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, finishTaskJSON)
if err != nil {
fmt.Println("Gagal mengirim finish-task:", err)
return
}
}
case "task-finished":
fmt.Println("=== Tugas selesai ===")
return
case "task-failed":
fmt.Println("=== Tugas gagal ===")
if header["error_message"] != nil {
fmt.Printf("Pesan error: %s\n", header["error_message"])
}
return
case "result-generated":
fmt.Println("Menerima event result-generated")
}
}
}
}
}
}
C#
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
class Program {
// API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: private static readonly string ApiKey = "sk-xxx"
private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("Variabel lingkungan DASHSCOPE_API_KEY belum diatur.");
// Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi berdasarkan wilayah.
private const string WebSocketUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
// Jalur file output
private const string OutputFilePath = "output.mp3";
// Klien WebSocket
private static ClientWebSocket _webSocket = new ClientWebSocket();
// Sumber token pembatalan
private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
// ID tugas
private static string? _taskId;
// Apakah tugas telah dimulai
private static TaskCompletionSource<bool> _taskStartedTcs = new TaskCompletionSource<bool>();
static async Task Main(string[] args) {
try {
// Hapus file output
ClearOutputFile(OutputFilePath);
// Hubungkan ke layanan WebSocket
await ConnectToWebSocketAsync(WebSocketUrl);
// Mulai tugas penerima pesan
Task receiveTask = ReceiveMessagesAsync();
// Kirim event run-task
_taskId = GenerateTaskId();
await SendRunTaskCommandAsync(_taskId);
// Tunggu event task-started
await _taskStartedTcs.Task;
// Kirim event continue-task
string[] texts = {
"Before my bed, moonlight shines bright,",
"I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,",
"then bow my head, thinking of home."
};
foreach (string text in texts) {
await SendContinueTaskCommandAsync(text);
}
// Kirim event finish-task
await SendFinishTaskCommandAsync(_taskId);
// Tunggu tugas penerima selesai
await receiveTask;
Console.WriteLine("Tugas selesai, koneksi ditutup.");
} catch (OperationCanceledException) {
Console.WriteLine("Tugas dibatalkan.");
} catch (Exception ex) {
Console.WriteLine($"Terjadi error: {ex.Message}");
} finally {
_cancellationTokenSource.Cancel();
_webSocket.Dispose();
}
}
private static void ClearOutputFile(string filePath) {
if (File.Exists(filePath)) {
File.WriteAllText(filePath, string.Empty);
Console.WriteLine("File output dihapus.");
} else {
Console.WriteLine("File output tidak ada, tidak perlu dihapus.");
}
}
private static async Task ConnectToWebSocketAsync(string url) {
var uri = new Uri(url);
if (_webSocket.State == WebSocketState.Connecting || _webSocket.State == WebSocketState.Open) {
return;
}
// Atur header koneksi WebSocket
_webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
_webSocket.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");
try {
await _webSocket.ConnectAsync(uri, _cancellationTokenSource.Token);
Console.WriteLine("Berhasil terhubung ke layanan WebSocket.");
} catch (OperationCanceledException) {
Console.WriteLine("Koneksi WebSocket dibatalkan.");
} catch (Exception ex) {
Console.WriteLine($"Koneksi WebSocket gagal: {ex.Message}");
throw;
}
}
private static async Task SendRunTaskCommandAsync(string taskId) {
var command = CreateCommand("run-task", taskId, "duplex", new {
task_group = "audio",
task = "tts",
function = "SpeechSynthesizer",
model = "qwen-audio-3.0-tts-flash",
parameters = new
{
text_type = "PlainText",
voice = "longanhuan_v3.6",
format = "mp3",
sample_rate = 22050,
volume = 50,
rate = 1,
pitch = 1,
// Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, error "Text request limit violated, expected 1." akan dikembalikan
enable_ssml = false
},
input = new { }
});
await SendJsonMessageAsync(command);
Console.WriteLine("Event run-task dikirim.");
}
private static async Task SendContinueTaskCommandAsync(string text) {
if (_taskId == null) {
throw new InvalidOperationException("ID tugas belum diinisialisasi.");
}
var command = CreateCommand("continue-task", _taskId, "duplex", new {
input = new {
text
}
});
await SendJsonMessageAsync(command);
Console.WriteLine("Event continue-task dikirim.");
}
private static async Task SendFinishTaskCommandAsync(string taskId) {
var command = CreateCommand("finish-task", taskId, "duplex", new {
input = new { }
});
await SendJsonMessageAsync(command);
Console.WriteLine("Event finish-task dikirim.");
}
private static async Task SendJsonMessageAsync(string message) {
var buffer = Encoding.UTF8.GetBytes(message);
try {
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
} catch (OperationCanceledException) {
Console.WriteLine("Pengiriman pesan dibatalkan.");
}
}
private static async Task ReceiveMessagesAsync() {
while (_webSocket.State == WebSocketState.Open) {
var response = await ReceiveMessageAsync();
if (response != null) {
var eventStr = response.RootElement.GetProperty("header").GetProperty("event").GetString();
switch (eventStr) {
case "task-started":
Console.WriteLine("Tugas dimulai.");
_taskStartedTcs.TrySetResult(true);
break;
case "task-finished":
Console.WriteLine("Tugas selesai.");
_cancellationTokenSource.Cancel();
break;
case "task-failed":
Console.WriteLine("Tugas gagal: " + response.RootElement.GetProperty("header").GetProperty("error_message").GetString());
_cancellationTokenSource.Cancel();
break;
default:
// result-generated dapat ditangani di sini
break;
}
}
}
}
private static async Task<JsonDocument?> ReceiveMessageAsync() {
var buffer = new byte[1024 * 4];
var segment = new ArraySegment<byte>(buffer);
try {
WebSocketReceiveResult result = await _webSocket.ReceiveAsync(segment, _cancellationTokenSource.Token);
if (result.MessageType == WebSocketMessageType.Close) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cancellationTokenSource.Token);
return null;
}
if (result.MessageType == WebSocketMessageType.Binary) {
// Proses data biner
Console.WriteLine("Menerima data biner...");
// Simpan data biner ke file
using (var fileStream = new FileStream(OutputFilePath, FileMode.Append)) {
fileStream.Write(buffer, 0, result.Count);
}
return null;
}
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
return JsonDocument.Parse(message);
} catch (OperationCanceledException) {
Console.WriteLine("Penerimaan pesan dibatalkan.");
return null;
}
}
private static string GenerateTaskId() {
return Guid.NewGuid().ToString("N").Substring(0, 32);
}
private static string CreateCommand(string action, string taskId, string streaming, object payload) {
var command = new {
header = new {
action,
task_id = taskId,
streaming
},
payload
};
return JsonSerializer.Serialize(command);
}
}
PHP
Struktur direktori kode contoh:
my-php-project/
├── composer.json
├── vendor/
└── index.php
Isi composer.json (sesuaikan versi dependensi sesuai kebutuhan):
{
"require": {
"react/event-loop": "^1.3",
"react/socket": "^1.11",
"react/stream": "^1.2",
"react/http": "^1.1",
"ratchet/pawl": "^0.4"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Isi index.php:
<?php
require __DIR__ . '/vendor/autoload.php';
use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;
// API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi berdasarkan wilayah.
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'; // URL server WebSocket
$output_file = 'output.mp3'; // Jalur file output
$loop = Loop::get();
if (file_exists($output_file)) {
// Hapus isi file
file_put_contents($output_file, '');
}
// Buat konektor kustom
$socketConnector = new SocketConnector($loop, [
'tcp' => [
'bindto' => '0.0.0.0:0',
],
'tls' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$connector = new Connector($loop, $socketConnector);
$headers = [
'Authorization' => 'bearer ' . $api_key,
'X-DashScope-DataInspection' => 'enable'
];
$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $output_file) {
echo "Terhubung ke server WebSocket\n";
// Hasilkan ID tugas
$taskId = generateTaskId();
// Kirim event run-task
sendRunTaskMessage($conn, $taskId);
// Definisikan fungsi untuk mengirim event continue-task
$sendContinueTask = function() use ($conn, $loop, $taskId) {
// Teks yang akan dikirim
$texts = ["Before my bed, moonlight shines bright,", "I suspect it\'s frost upon the ground.", "I raise my eyes to gaze at the bright moon,", "then bow my head, thinking of home."];
$continueTaskCount = 0;
foreach ($texts as $text) {
$continueTaskMessage = json_encode([
"header" => [
"action" => "continue-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => [
"text" => $text
]
]
]);
echo "Mengirim event continue-task: " . $continueTaskMessage . "\n";
$conn->send($continueTaskMessage);
$continueTaskCount++;
}
echo "Jumlah event continue-task yang dikirim: " . $continueTaskCount . "\n";
// Kirim event finish-task
sendFinishTaskMessage($conn, $taskId);
};
// Bendera apakah event task-started telah diterima
$taskStarted = false;
// Dengarkan pesan
$conn->on('message', function($msg) use ($conn, $sendContinueTask, $loop, &$taskStarted, $taskId, $output_file) {
if ($msg->isBinary()) {
// Tulis data biner ke file lokal
file_put_contents($output_file, $msg->getPayload(), FILE_APPEND);
} else {
// Proses pesan non-biner
$response = json_decode($msg, true);
if (isset($response['header']['event'])) {
handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, $taskStarted);
} else {
echo "Format pesan tidak dikenal\n";
}
}
});
// Dengarkan penutupan koneksi
$conn->on('close', function($code = null, $reason = null) {
echo "Koneksi ditutup\n";
if ($code !== null) {
echo "Kode penutupan: " . $code . "\n";
}
if ($reason !== null) {
echo "Alasan penutupan: " . $reason . "\n";
}
});
}, function ($e) {
echo "Gagal terhubung: {$e->getMessage()}\n";
});
$loop->run();
/**
* Hasilkan ID tugas
* @return string
*/
function generateTaskId(): string {
return bin2hex(random_bytes(16));
}
/**
* Kirim event run-task
* @param $conn
* @param $taskId
*/
function sendRunTaskMessage($conn, $taskId) {
$runTaskMessage = json_encode([
"header" => [
"action" => "run-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"task_group" => "audio",
"task" => "tts",
"function" => "SpeechSynthesizer",
"model" => "qwen-audio-3.0-tts-flash",
"parameters" => [
"text_type" => "PlainText",
"voice" => "longanhuan_v3.6",
"format" => "mp3",
"sample_rate" => 22050,
"volume" => 50,
"rate" => 1,
"pitch" => 1,
// Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, error "Text request limit violated, expected 1." akan dikembalikan
"enable_ssml" => false
],
"input" => (object) []
]
]);
echo "Mengirim event run-task: " . $runTaskMessage . "\n";
$conn->send($runTaskMessage);
echo "Event run-task dikirim\n";
}
/**
* Baca file audio
* @param string $filePath
* @return bool|string
*/
function readAudioFile(string $filePath) {
$voiceData = file_get_contents($filePath);
if ($voiceData === false) {
echo "Gagal membaca file audio\n";
}
return $voiceData;
}
/**
* Pisahkan data audio
* @param string $data
* @param int $chunkSize
* @return array
*/
function splitAudioData(string $data, int $chunkSize): array {
return str_split($data, $chunkSize);
}
/**
* Kirim event finish-task
* @param $conn
* @param $taskId
*/
function sendFinishTaskMessage($conn, $taskId) {
$finishTaskMessage = json_encode([
"header" => [
"action" => "finish-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => (object) []
]
]);
echo "Mengirim event finish-task: " . $finishTaskMessage . "\n";
$conn->send($finishTaskMessage);
echo "Event finish-task dikirim\n";
}
/**
* Tangani event
* @param $conn
* @param $response
* @param $sendContinueTask
* @param $loop
* @param $taskId
* @param $taskStarted
*/
function handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, &$taskStarted) {
switch ($response['header']['event']) {
case 'task-started':
echo "Tugas dimulai, mengirim event continue-task...\n";
$taskStarted = true;
// Kirim event continue-task
$sendContinueTask();
break;
case 'result-generated':
// Menerima event result-generated
break;
case 'task-finished':
echo "Tugas selesai\n";
$conn->close();
break;
case 'task-failed':
echo "Tugas gagal\n";
echo "Kode error: " . $response['header']['error_code'] . "\n";
echo "Pesan error: " . $response['header']['error_message'] . "\n";
$conn->close();
break;
case 'error':
echo "Error: " . $response['payload']['message'] . "\n";
break;
default:
echo "Event tidak dikenal: " . $response['header']['event'] . "\n";
break;
}
// Jika tugas selesai, tutup koneksi
if ($response['header']['event'] == 'task-finished') {
// Tunggu 1 detik untuk memastikan semua data telah dikirim
$loop->addTimer(1, function() use ($conn) {
$conn->close();
echo "Klien menutup koneksi\n";
});
}
// Jika event task-started belum diterima, tutup koneksi
if (!$taskStarted && in_array($response['header']['event'], ['task-failed', 'error'])) {
$conn->close();
}
}
Node.js
Instal dependensi yang diperlukan:
npm install ws
npm install uuid
Kode contoh:
const WebSocket = require('ws');
const fs = require('fs');
const uuid = require('uuid').v4;
// API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi berdasarkan wilayah.
const url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference';
// Jalur file output
const outputFilePath = 'output.mp3';
// Hapus file output
fs.writeFileSync(outputFilePath, '');
// Buat klien WebSocket
const ws = new WebSocket(url, {
headers: {
Authorization: `bearer ${apiKey}`,
'X-DashScope-DataInspection': 'enable'
}
});
let taskStarted = false;
let taskId = uuid();
ws.on('open', () => {
console.log('Terhubung ke server WebSocket');
// Kirim event run-task
const runTaskMessage = JSON.stringify({
header: {
action: 'run-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
task_group: 'audio',
task: 'tts',
function: 'SpeechSynthesizer',
model: 'qwen-audio-3.0-tts-flash',
parameters: {
text_type: 'PlainText',
voice: 'longanhuan_v3.6', // Suara
format: 'mp3', // Format audio
sample_rate: 22050, // Laju sampel
volume: 50, // Volume
rate: 1, // Laju bicara
pitch: 1, // Pitch
enable_ssml: false // Apakah SSML diaktifkan. Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, error "Text request limit violated, expected 1." akan dikembalikan
},
input: {}
}
});
ws.send(runTaskMessage);
console.log('Pesan run-task dikirim');
});
const fileStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
ws.on('message', (data, isBinary) => {
if (isBinary) {
// Tulis data biner ke file
fileStream.write(data);
} else {
const message = JSON.parse(data);
switch (message.header.event) {
case 'task-started':
taskStarted = true;
console.log('Tugas dimulai');
// Kirim event continue-task
sendContinueTasks(ws);
break;
case 'task-finished':
console.log('Tugas selesai');
ws.close();
fileStream.end(() => {
console.log('Aliran file ditutup');
});
break;
case 'task-failed':
console.error('Tugas gagal: ', message.header.error_message);
ws.close();
fileStream.end(() => {
console.log('Aliran file ditutup');
});
break;
default:
// result-generated dapat ditangani di sini
break;
}
}
});
function sendContinueTasks(ws) {
const texts = [
'Before my bed, moonlight shines bright,',
'I suspect it is frost upon the ground.',
'I raise my eyes to gaze at the bright moon,',
'then bow my head, thinking of home.'
];
texts.forEach((text, index) => {
setTimeout(() => {
if (taskStarted) {
const continueTaskMessage = JSON.stringify({
header: {
action: 'continue-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
input: {
text: text
}
}
});
ws.send(continueTaskMessage);
console.log(`continue-task dikirim, teks: ${text}`);
}
}, index * 1000); // Kirim satu per detik
});
// Kirim event finish-task
setTimeout(() => {
if (taskStarted) {
const finishTaskMessage = JSON.stringify({
header: {
action: 'finish-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
input: {}
}
});
ws.send(finishTaskMessage);
console.log('finish-task dikirim');
}
}, texts.length * 1000 + 1000); // Kirim 1 detik setelah semua event continue-task dikirim
}
ws.on('close', () => {
console.log('Terputus dari server WebSocket');
});
Java
Kami merekomendasikan penggunaan SDK Java DashScope untuk pengembangan. Lihat SDK Java.
Berikut adalah contoh koneksi langsung WebSocket Java. Impor dependensi berikut sebelum menjalankan:
Java-WebSocketjackson-databind
Gunakan Maven atau Gradle untuk mengelola dependensi:
<dependencies>
<!-- WebSocket Client -->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>
</dependencies>
// Kode lainnya dihilangkan
dependencies {
// WebSocket Client
implementation 'org.java-websocket:Java-WebSocket:1.5.3'
// JSON Processing
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}
// Kode lainnya dihilangkan
Kode Java:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.*;
public class TTSWebSocketClient extends WebSocketClient {
private final String taskId = UUID.randomUUID().toString();
private final String outputFile = "output_" + System.currentTimeMillis() + ".mp3";
private boolean taskFinished = false;
public TTSWebSocketClient(URI serverUri, Map<String, String> headers) {
super(serverUri, headers);
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("Koneksi berhasil");
// Kirim event run-task
// Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, error "Text request limit violated, expected 1." akan dikembalikan
String runTaskCommand = "{ \"header\": { \"action\": \"run-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"task_group\": \"audio\", \"task\": \"tts\", \"function\": \"SpeechSynthesizer\", \"model\": \"qwen-audio-3.0-tts-flash\", \"parameters\": { \"text_type\": \"PlainText\", \"voice\": \"longanhuan_v3.6\", \"format\": \"mp3\", \"sample_rate\": 22050, \"volume\": 50, \"rate\": 1, \"pitch\": 1, \"enable_ssml\": false }, \"input\": {} }}";
send(runTaskCommand);
}
@Override
public void onMessage(String message) {
System.out.println("Menerima pesan dari server: " + message);
try {
// Uraikan pesan JSON
Map<String, Object> messageMap = new ObjectMapper().readValue(message, Map.class);
if (messageMap.containsKey("header")) {
Map<String, Object> header = (Map<String, Object>) messageMap.get("header");
if (header.containsKey("event")) {
String event = (String) header.get("event");
if ("task-started".equals(event)) {
System.out.println("Menerima event task-started dari server");
List<String> texts = Arrays.asList(
"Before my bed, moonlight shines bright,I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,then bow my head, thinking of home."
);
for (String text : texts) {
// Kirim event continue-task
sendContinueTask(text);
}
// Kirim event finish-task
sendFinishTask();
} else if ("task-finished".equals(event)) {
System.out.println("Menerima event task-finished dari server");
taskFinished = true;
closeConnection();
} else if ("task-failed".equals(event)) {
System.out.println("Tugas gagal: " + message);
closeConnection();
}
}
}
} catch (Exception e) {
System.err.println("Terjadi exception: " + e.getMessage());
}
}
@Override
public void onMessage(ByteBuffer message) {
System.out.println("Menerima data audio biner ukuran: " + message.remaining());
try (FileOutputStream fos = new FileOutputStream(outputFile, true)) {
byte[] buffer = new byte[message.remaining()];
message.get(buffer);
fos.write(buffer);
System.out.println("Data audio ditulis ke file lokal " + outputFile);
} catch (IOException e) {
System.err.println("Gagal menulis data audio ke file lokal: " + e.getMessage());
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("Koneksi ditutup: " + reason + " (" + code + ")");
}
@Override
public void onError(Exception ex) {
System.err.println("Error: " + ex.getMessage());
ex.printStackTrace();
}
private void sendContinueTask(String text) {
String command = "{ \"header\": { \"action\": \"continue-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": { \"text\": \"" + text + "\" } }}";
send(command);
}
private void sendFinishTask() {
String command = "{ \"header\": { \"action\": \"finish-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": {} }}";
send(command);
}
private void closeConnection() {
if (!isClosed()) {
close();
}
}
public static void main(String[] args) {
try {
// API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: String apiKey = "sk-xxx"
String apiKey = System.getenv("DASHSCOPE_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
System.err.println("Harap atur variabel lingkungan DASHSCOPE_API_KEY");
return;
}
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "bearer " + apiKey);
// Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi berdasarkan wilayah.
TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"), headers);
client.connect();
while (!client.isClosed() && !client.taskFinished) {
Thread.sleep(1000);
}
} catch (Exception e) {
System.err.println("Gagal terhubung ke layanan WebSocket: " + e.getMessage());
e.printStackTrace();
}
}
}
Python
Kami merekomendasikan penggunaan SDK Python DashScope untuk pengembangan. Lihat SDK Python.
Berikut adalah contoh koneksi langsung WebSocket Python. Instal dependensi berikut sebelum menjalankan:
pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client
PentingJangan beri nama file Python "websocket.py", karena akan menyebabkan error (AttributeError: module 'websocket' has no attribute 'WebSocketApp'. Did you mean: 'WebSocket'?).
import websocket
import json
import uuid
import os
import time
class TTSClient:
def __init__(self, api_key, uri):
"""
Inisialisasi instans TTSClient
Parameter:
api_key (str): Kunci API untuk otentikasi
uri (str): URL layanan WebSocket
"""
self.api_key = api_key # Ganti dengan Kunci API Anda
self.uri = uri # Ganti dengan URL WebSocket Anda
self.task_id = str(uuid.uuid4()) # Hasilkan ID tugas unik
self.output_file = f"output_{int(time.time())}.mp3" # Jalur file audio keluaran
self.ws = None # Instans WebSocketApp
self.task_started = False # Apakah task-started telah diterima
self.task_finished = False # Apakah task-finished / task-failed telah diterima
def on_open(self, ws):
"""
Callback ketika koneksi WebSocket terbentuk
Mengirim event run-task untuk memulai tugas sintesis suara
"""
print("WebSocket connected")
# Buat event run-task
run_task_cmd = {
"header": {
"action": "run-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"task_group": "audio",
"task": "tts",
"function": "SpeechSynthesizer",
"model": "qwen-audio-3.0-tts-flash",
"parameters": {
"text_type": "PlainText",
"voice": "longanhuan_v3.6",
"format": "mp3",
"sample_rate": 22050,
"volume": 50,
"rate": 1,
"pitch": 1,
# Jika enable_ssml diatur ke true, hanya satu event continue-task yang dapat dikirim; jika tidak, kesalahan akan dikembalikan
"enable_ssml": False
},
"input": {}
}
}
# Kirim event run-task
ws.send(json.dumps(run_task_cmd))
print("Sent run-task event")
def on_message(self, ws, message):
"""
Callback ketika pesan diterima
Menangani pesan teks dan biner secara berbeda
"""
if isinstance(message, str):
# Tangani pesan teks JSON
try:
msg_json = json.loads(message)
print(f"Received JSON message: {msg_json}")
if "header" in msg_json:
header = msg_json["header"]
if "event" in header:
event = header["event"]
if event == "task-started":
print("Task started")
self.task_started = True
# Kirim event continue-task
texts = [
"Before my bed, moonlight shines bright,I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,then bow my head, thinking of home."
]
for text in texts:
self.send_continue_task(text)
# Kirim finish-task setelah semua event continue-task dikirim
self.send_finish_task()
elif event == "task-finished":
print("Task completed")
self.task_finished = True
self.close(ws)
elif event == "task-failed":
error_msg = msg_json.get("error_message", "Unknown error")
print(f"Task failed: {error_msg}")
self.task_finished = True
self.close(ws)
except json.JSONDecodeError as e:
print(f"JSON parsing failed: {e}")
else:
# Tangani pesan biner (data audio)
print(f"Received binary message, size: {len(message)} bytes")
with open(self.output_file, "ab") as f:
f.write(message)
print(f"Audio data written to local file {self.output_file}")
def on_error(self, ws, error):
"""Callback ketika terjadi kesalahan"""
print(f"WebSocket error: {error}")
def on_close(self, ws, close_status_code, close_msg):
"""Callback ketika koneksi ditutup"""
print(f"WebSocket closed: {close_msg} ({close_status_code})")
def send_continue_task(self, text):
"""Kirim event continue-task dengan konten teks yang akan disintesis"""
cmd = {
"header": {
"action": "continue-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"input": {
"text": text
}
}
}
self.ws.send(json.dumps(cmd))
print(f"Sent continue-task event, text content: {text}")
def send_finish_task(self):
"""Kirim event finish-task untuk mengakhiri tugas sintesis suara"""
cmd = {
"header": {
"action": "finish-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"input": {}
}
}
self.ws.send(json.dumps(cmd))
print("Sent finish-task event")
def close(self, ws):
"""Tutup secara aktif koneksi"""
if ws and ws.sock and ws.sock.connected:
ws.close()
print("Connection closed actively")
def run(self):
"""Mulai klien WebSocket"""
# Atur header permintaan (otentikasi)
header = {
"Authorization": f"bearer {self.api_key}",
"X-DashScope-DataInspection": "enable"
}
# Buat instans WebSocketApp
self.ws = websocket.WebSocketApp(
self.uri,
header=header,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
print("Listening for WebSocket messages...")
self.ws.run_forever() # Mulai pendengar koneksi persisten
# Contoh penggunaan
if __name__ == "__main__":
# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikutnya dengan Kunci API Model Studio China Anda: API_KEY = "sk-xxx"
API_KEY = os.environ.get("DASHSCOPE_API_KEY")
# Berikut ini adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
SERVER_URI = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference" # Ganti dengan URL WebSocket Anda
client = TTSClient(API_KEY, SERVER_URI)
client.run()
Qwen-TTS
-
Buat klien
Python
Buat file Python bernama
tts_realtime_client.pydan salin kode berikut ke dalam file tersebut:# -- coding: utf-8 -- import asyncio import websockets import json import base64 import time from typing import Optional, Callable, Dict, Any from enum import Enum class SessionMode(Enum): SERVER_COMMIT = "server_commit" COMMIT = "commit" class TTSRealtimeClient: """ Klien untuk berinteraksi dengan API Realtime TTS. Kelas ini menyediakan metode untuk menghubungkan ke API Realtime TTS, mengirim data teks, menerima output audio, dan mengelola koneksi WebSocket. Atribut: base_url (str): URL dasar API Realtime. api_key (str): API Key untuk otentikasi. voice (str): Suara yang digunakan untuk sintesis suara di sisi server. mode (SessionMode): Mode sesi, baik server_commit maupun commit. audio_callback (Callable[[bytes], None]): Fungsi callback untuk menerima data audio. language_type(str) Bahasa untuk sintesis suara. Opsi: Chinese, English, German, Italian, Portuguese, Spanish, Japanese, Korean, French, Russian, Auto """ def __init__( self, base_url: str, api_key: str, voice: str = "Cherry", mode: SessionMode = SessionMode.SERVER_COMMIT, audio_callback: Optional[Callable[[bytes], None]] = None, language_type: str = "Auto"): self.base_url = base_url self.api_key = api_key self.voice = voice self.mode = mode self.ws = None self.audio_callback = audio_callback self.language_type = language_type // Status respons saat ini self._current_response_id = None self._current_item_id = None self._is_responding = False self._response_done_future = None async def connect(self) -> None: """Buat koneksi WebSocket dengan API Realtime TTS.""" headers = { "Authorization": f"Bearer {self.api_key}" } self.ws = await websockets.connect(self.base_url, additional_headers=headers) // Atur konfigurasi sesi default await self.update_session({ "mode": self.mode.value, "voice": self.voice, // Untuk menggunakan fitur kontrol instruksi, hapus komentar baris di bawah dan ganti model dengan qwen3-tts-instruct-flash-realtime di server_commit.py atau commit.py // "instructions": "Speak quickly with a noticeable rising intonation, suitable for introducing fashion products.", // "optimize_instructions": true "language_type": self.language_type, "response_format": "pcm", "sample_rate": 24000 }) async def send_event(self, event) -> None: """Kirim event ke server.""" event['event_id'] = "event_" + str(int(time.time() * 1000)) print(f"Mengirim event: type={event['type']}, event_id={event['event_id']}") await self.ws.send(json.dumps(event)) async def update_session(self, config: Dict[str, Any]) -> None: """Perbarui konfigurasi sesi.""" event = { "type": "session.update", "session": config } print("Memperbarui konfigurasi sesi: ", event) await self.send_event(event) async def append_text(self, text: str) -> None: """Kirim data teks ke API.""" event = { "type": "input_text_buffer.append", "text": text } await self.send_event(event) async def commit_text_buffer(self) -> None: """Commit buffer teks untuk memicu pemrosesan.""" event = { "type": "input_text_buffer.commit" } await self.send_event(event) async def clear_text_buffer(self) -> None: """Hapus buffer teks.""" event = { "type": "input_text_buffer.clear" } await self.send_event(event) async def finish_session(self) -> None: """Akhiri sesi.""" event = { "type": "session.finish" } await self.send_event(event) async def wait_for_response_done(self): """Tunggu event response.done""" if self._response_done_future: await self._response_done_future async def handle_messages(self) -> None: """Tangani pesan dari server.""" try: async for message in self.ws: event = json.loads(message) event_type = event.get("type") if event_type != "response.audio.delta": print(f"Event diterima: {event_type}") if event_type == "error": print("Error: ", event.get('error', {})) continue elif event_type == "session.created": print("Sesi dibuat, ID: ", event.get('session', {}).get('id')) elif event_type == "session.updated": print("Sesi diperbarui, ID: ", event.get('session', {}).get('id')) elif event_type == "input_text_buffer.committed": print("Buffer teks di-commit, ID item: ", event.get('item_id')) elif event_type == "input_text_buffer.cleared": print("Buffer teks dihapus") elif event_type == "response.created": self._current_response_id = event.get("response", {}).get("id") self._is_responding = True // Buat future baru untuk menunggu response.done self._response_done_future = asyncio.Future() print("Respons dibuat, ID: ", self._current_response_id) elif event_type == "response.output_item.added": self._current_item_id = event.get("item", {}).get("id") print("Item output ditambahkan, ID: ", self._current_item_id) // Tangani delta audio elif event_type == "response.audio.delta" and self.audio_callback: audio_bytes = base64.b64decode(event.get("delta", "")) self.audio_callback(audio_bytes) elif event_type == "response.audio.done": print("Generasi audio selesai") elif event_type == "response.done": self._is_responding = False self._current_response_id = None self._current_item_id = None // Tandai future sebagai selesai if self._response_done_future and not self._response_done_future.done(): self._response_done_future.set_result(True) print("Respons selesai") elif event_type == "session.finished": print("Sesi selesai") except websockets.exceptions.ConnectionClosed: print("Koneksi ditutup") except Exception as e: print("Error menangani pesan: ", str(e)) async def close(self) -> None: """Tutup koneksi WebSocket.""" if self.ws: await self.ws.close()Java
Buat file Java bernama
TTSRealtimeClient.javadan salin kode berikut ke dalam file tersebut:import com.google.gson.Gson; import com.google.gson.JsonObject; import org.java_websocket.client.WebSocketClient; import org.java_websocket.handshake.ServerHandshake; import java.net.URI; import java.util.Base64; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.function.Consumer; /** * Klien untuk berinteraksi dengan API Realtime TTS. * * Kelas ini menyediakan metode untuk menghubungkan ke API Realtime TTS, mengirim data teks, mengambil output audio, dan mengelola koneksi WebSocket. */ public class TTSRealtimeClient { public enum SessionMode { SERVER_COMMIT("server_commit"), COMMIT("commit"); private final String value; SessionMode(String value) { this.value = value; } public String getValue() { return value; } } /** * Antarmuka callback audio */ public interface AudioCallback { void onAudio(byte[] audioData); } private final String baseUrl; private final String apiKey; private final String voice; private final SessionMode mode; private final String languageType; private final AudioCallback audioCallback; private final Gson gson = new Gson(); private WebSocketClient ws; private CountDownLatch responseDoneLatch; private CountDownLatch sessionFinishedLatch; public TTSRealtimeClient(String baseUrl, String apiKey, String voice, SessionMode mode, AudioCallback audioCallback, String languageType) { this.baseUrl = baseUrl; this.apiKey = apiKey; this.voice = voice; this.mode = mode; this.audioCallback = audioCallback; this.languageType = languageType; } public TTSRealtimeClient(String baseUrl, String apiKey, String voice, SessionMode mode, AudioCallback audioCallback) { this(baseUrl, apiKey, voice, mode, audioCallback, "Auto"); } /** * Buat koneksi WebSocket ke API Realtime TTS. */ public void connect() throws Exception { Map<String, String> headers = new HashMap<>(); headers.put("Authorization", "Bearer " + apiKey); responseDoneLatch = new CountDownLatch(0); sessionFinishedLatch = new CountDownLatch(1); ws = new WebSocketClient(new URI(baseUrl), headers) { @Override public void onOpen(ServerHandshake handshake) { System.out.println("Koneksi WebSocket berhasil"); // Kirim konfigurasi sesi default JsonObject session = new JsonObject(); session.addProperty("mode", mode.getValue()); session.addProperty("voice", TTSRealtimeClient.this.voice); // Untuk menggunakan fitur kontrol instruksi, hapus komentar baris di bawah dan ganti model dengan qwen3-tts-instruct-flash-realtime // session.addProperty("instructions", "Speak quickly with a noticeable rising intonation, suitable for introducing fashion products."); // session.addProperty("optimize_instructions", true); session.addProperty("language_type", languageType); session.addProperty("response_format", "pcm"); session.addProperty("sample_rate", 24000); updateSession(session); } @Override public void onMessage(String message) { JsonObject event = gson.fromJson(message, JsonObject.class); String eventType = event.has("type") ? event.get("type").getAsString() : ""; if (!"response.audio.delta".equals(eventType)) { System.out.println("Event diterima: " + eventType); } switch (eventType) { case "error": System.err.println("Error: " + event.get("error")); break; case "session.created": System.out.println("Sesi dibuat, ID: " + event.getAsJsonObject("session").get("id").getAsString()); break; case "session.updated": System.out.println("Sesi diperbarui, ID: " + event.getAsJsonObject("session").get("id").getAsString()); break; case "input_text_buffer.committed": System.out.println("Buffer teks di-commit, ID item: " + event.get("item_id")); break; case "input_text_buffer.cleared": System.out.println("Buffer teks dihapus"); break; case "response.created": System.out.println("Respons dibuat, ID: " + event.getAsJsonObject("response").get("id").getAsString()); responseDoneLatch = new CountDownLatch(1); break; case "response.output_item.added": System.out.println("Item output ditambahkan, ID: " + event.getAsJsonObject("item").get("id").getAsString()); break; case "response.audio.delta": if (audioCallback != null) { byte[] audioBytes = Base64.getDecoder().decode( event.get("delta").getAsString()); audioCallback.onAudio(audioBytes); } break; case "response.audio.done": System.out.println("Generasi audio selesai"); break; case "response.done": System.out.println("Respons selesai"); responseDoneLatch.countDown(); break; case "session.finished": System.out.println("Sesi selesai"); sessionFinishedLatch.countDown(); break; } } @Override public void onClose(int code, String reason, boolean remote) { System.out.println("Koneksi ditutup: " + reason); } @Override public void onError(Exception ex) { System.err.println("Error WebSocket: " + ex.getMessage()); } }; ws.connectBlocking(); } /** * Kirim event ke server. */ public void sendEvent(JsonObject event) { String eventId = "event_" + System.currentTimeMillis(); event.addProperty("event_id", eventId); System.out.println("Mengirim event: type=" + event.get("type").getAsString() + ", event_id=" + eventId); ws.send(gson.toJson(event)); } /** * Perbarui konfigurasi sesi. */ public void updateSession(JsonObject config) { JsonObject event = new JsonObject(); event.addProperty("type", "session.update"); event.add("session", config); System.out.println("Memperbarui konfigurasi sesi: " + event); sendEvent(event); } /** * Kirim data teks ke API. */ public void appendText(String text) { JsonObject event = new JsonObject(); event.addProperty("type", "input_text_buffer.append"); event.addProperty("text", text); sendEvent(event); } /** * Commit buffer teks untuk memicu pemrosesan. */ public void commitTextBuffer() { JsonObject event = new JsonObject(); event.addProperty("type", "input_text_buffer.commit"); sendEvent(event); } /** * Hapus buffer teks. */ public void clearTextBuffer() { JsonObject event = new JsonObject(); event.addProperty("type", "input_text_buffer.clear"); sendEvent(event); } /** * Akhiri sesi. */ public void finishSession() { JsonObject event = new JsonObject(); event.addProperty("type", "session.finish"); sendEvent(event); } /** * Tunggu event response.done. */ public void waitForResponseDone() throws InterruptedException { responseDoneLatch.await(); } /** * Tunggu event session.finished. */ public void waitForSessionFinished() throws InterruptedException { sessionFinishedLatch.await(); } /** * Tutup koneksi WebSocket. */ public void close() { if (ws != null) { ws.close(); } } } -
Pilih mode sintesis suara
API Realtime mendukung dua mode:
-
server_commit mode
Server menangani segmentasi teks dan waktu sintesis secara otomatis. Klien hanya mengirim teks. Cocok untuk skenario latensi rendah (seperti navigasi GPS).
-
commit mode
Klien menambahkan teks ke buffer dan secara eksplisit memicu sintesis. Cocok untuk skenario yang memerlukan kontrol segmentasi kalimat yang tepat (seperti siaran berita).
mode server_commit
Python
Di direktori yang sama dengan
tts_realtime_client.py, buat file Python lain bernamaserver_commit.pydan salin kode berikut ke dalam file tersebut:import os import asyncio import logging import wave from tts_realtime_client import TTSRealtimeClient, SessionMode import pyaudio // Konfigurasi layanan QwenTTS // Untuk menggunakan fitur kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime dan hapus komentar instruksi di tts_realtime_client.py // Berikut ini adalah konfigurasi untuk wilayah Singapura. URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime" // API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: API_KEY="sk-xxx" API_KEY = os.getenv("DASHSCOPE_API_KEY") if not API_KEY: raise ValueError("Harap atur variabel lingkungan DASHSCOPE_API_KEY") // Kumpulkan data audio _audio_chunks = [] // Terkait pemutaran real-time _AUDIO_SAMPLE_RATE = 24000 _audio_pyaudio = pyaudio.PyAudio() _audio_stream = None // Akan dibuka saat runtime def _audio_callback(audio_bytes: bytes): """Callback audio TTSRealtimeClient: pemutaran real-time dan caching""" global _audio_stream if _audio_stream is not None: try: _audio_stream.write(audio_bytes) except Exception as exc: logging.error(f"Error pemutaran PyAudio: {exc}") _audio_chunks.append(audio_bytes) logging.info(f"Chunk audio diterima: {len(audio_bytes)} byte") def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool: """Simpan data audio yang dikumpulkan sebagai file WAV""" if not _audio_chunks: logging.warning("Tidak ada data audio untuk disimpan") return False try: audio_data = b"".join(_audio_chunks) with wave.open(filename, 'wb') as wav_file: wav_file.setnchannels(1) // Mono wav_file.setsampwidth(2) // 16-bit wav_file.setframerate(sample_rate) wav_file.writeframes(audio_data) logging.info(f"Audio disimpan ke: {filename}") return True except Exception as exc: logging.error(f"Gagal menyimpan audio: {exc}") return False async def _produce_text(client: TTSRealtimeClient): """Kirim fragmen teks ke server""" text_fragments = [ "Alibaba Cloud's large language model platform, Model Studio, is an all-in-one platform for developing and building large language model applications.", "Both developers and business users can deeply participate in the design and development of large language model applications.", "You can develop a large language model application in five minutes using a simple interface,", "or train a dedicated model in a few hours, allowing you to focus more energy on application innovation.", ] logging.info("Mengirim fragmen teks…") for text in text_fragments: logging.info(f"Mengirim fragmen: {text}") await client.append_text(text) await asyncio.sleep(0.1) // Penundaan singkat antar fragmen // Tunggu server menyelesaikan pemrosesan internal sebelum mengakhiri sesi await asyncio.sleep(1.0) await client.finish_session() async def _run_demo(): """Jalankan demo lengkap""" global _audio_stream // Buka aliran output PyAudio _audio_stream = _audio_pyaudio.open( format=pyaudio.paInt16, channels=1, rate=_AUDIO_SAMPLE_RATE, output=True, frames_per_buffer=1024 ) client = TTSRealtimeClient( base_url=URL, api_key=API_KEY, voice="Cherry", mode=SessionMode.SERVER_COMMIT, audio_callback=_audio_callback ) // Buat koneksi await client.connect() // Jalankan penanganan pesan dan pengiriman teks secara paralel consumer_task = asyncio.create_task(client.handle_messages()) producer_task = asyncio.create_task(_produce_text(client)) await producer_task // Tunggu pengiriman teks selesai // Tunggu response.done await client.wait_for_response_done() // Tutup koneksi dan batalkan tugas konsumen await client.close() consumer_task.cancel() // Tutup aliran audio if _audio_stream is not None: _audio_stream.stop_stream() _audio_stream.close() _audio_pyaudio.terminate() // Simpan data audio os.makedirs("outputs", exist_ok=True) _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav")) def main(): """Titik masuk sinkron""" logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logging.info("Memulai demo Klien Realtime QwenTTS…") asyncio.run(_run_demo()) if __name__ == "__main__": main()Jalankan
server_commit.pyuntuk mendengar audio yang dihasilkan oleh API Realtime secara real-time.Java
Di direktori yang sama dengan
TTSRealtimeClient.java, buat file Java lain bernamaServerCommit.javadan salin kode berikut ke dalam file tersebut:import javax.sound.sampled.*; import java.io.*; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; public class ServerCommit { // Berikut ini adalah konfigurasi untuk wilayah Singapura. private static final String URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"; // API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: private static final String API_KEY = "sk-xxx"; private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY"); private static final int SAMPLE_RATE = 24000; // Cache data audio private static final List<byte[]> audioChunks = new ArrayList<>(); // Antrian pemutaran real-time private static final ConcurrentLinkedQueue<byte[]> playbackQueue = new ConcurrentLinkedQueue<>(); private static final AtomicBoolean playing = new AtomicBoolean(true); public static void main(String[] args) throws Exception { if (API_KEY == null || API_KEY.isEmpty()) { throw new IllegalStateException("Harap atur variabel lingkungan DASHSCOPE_API_KEY"); } // Inisialisasi pemutaran audio AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false); DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); SourceDataLine audioLine = (SourceDataLine) AudioSystem.getLine(info); audioLine.open(format); audioLine.start(); // Mulai thread pemutaran Thread playerThread = new Thread(() -> { while (playing.get() || !playbackQueue.isEmpty()) { byte[] chunk = playbackQueue.poll(); if (chunk != null) { audioLine.write(chunk, 0, chunk.length); } else { try { Thread.sleep(10); } catch (InterruptedException ignored) {} } } }); playerThread.start(); // Buat klien TTS // Untuk menggunakan fitur kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime dan hapus komentar instruksi di TTSRealtimeClient.java TTSRealtimeClient client = new TTSRealtimeClient( URL, API_KEY, "Cherry", TTSRealtimeClient.SessionMode.SERVER_COMMIT, audioData -> { playbackQueue.add(audioData); audioChunks.add(audioData); System.out.println("Data audio diterima: " + audioData.length + " byte"); } ); client.connect(); // Kirim fragmen teks String[] textFragments = { "Alibaba Cloud's large language model platform, Model Studio, is an all-in-one platform for developing and building large language model applications.", "Both developers and business users can deeply participate in the design and development of large language model applications.", "You can develop a large language model application in five minutes using a simple interface,", "or train a dedicated model in a few hours, allowing you to focus more energy on application innovation." }; System.out.println("Mulai mengirim teks..."); for (String text : textFragments) { System.out.println("Mengirim fragmen: " + text); client.appendText(text); Thread.sleep(100); } Thread.sleep(1000); client.finishSession(); // Tunggu respons selesai client.waitForResponseDone(); client.waitForSessionFinished(); client.close(); // Tunggu pemutaran selesai playing.set(false); playerThread.join(); audioLine.drain(); audioLine.close(); // Simpan file audio saveWav("output.wav"); System.out.println("Selesai"); } private static void saveWav(String filename) throws IOException { if (audioChunks.isEmpty()) { System.out.println("Tidak ada data audio untuk disimpan"); return; } ByteArrayOutputStream bos = new ByteArrayOutputStream(); for (byte[] chunk : audioChunks) { bos.write(chunk); } byte[] allAudio = bos.toByteArray(); AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false); AudioInputStream ais = new AudioInputStream( new ByteArrayInputStream(allAudio), format, allAudio.length / 2); new File("outputs").mkdirs(); AudioSystem.write(ais, AudioFileFormat.Type.WAVE, new File("outputs/" + filename)); System.out.println("Audio disimpan ke: outputs/" + filename); } }Kompilasi dan jalankan
ServerCommit.javauntuk mendengar audio yang dihasilkan oleh API Realtime secara real-time.commit mode
Python
Di direktori yang sama dengan
tts_realtime_client.py, buat file Python lain bernamacommit.pydan salin kode berikut ke dalam file tersebut:import os import asyncio import logging import wave from tts_realtime_client import TTSRealtimeClient, SessionMode import pyaudio # Konfigurasi layanan QwenTTS # Untuk menggunakan fitur kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime dan hapus komentar instruksi di tts_realtime_client.py # Berikut ini adalah konfigurasi untuk wilayah Singapura. URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime" # Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key # Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: API_KEY="sk-xxx" API_KEY = os.getenv("DASHSCOPE_API_KEY") if not API_KEY: raise ValueError("Harap atur variabel lingkungan DASHSCOPE_API_KEY") # Mengumpulkan data audio _audio_chunks = [] _AUDIO_SAMPLE_RATE = 24000 _audio_pyaudio = pyaudio.PyAudio() _audio_stream = None def _audio_callback(audio_bytes: bytes): """Callback audio TTSRealtimeClient: pemutaran real-time dan caching""" global _audio_stream if _audio_stream is not None: try: _audio_stream.write(audio_bytes) except Exception as exc: logging.error(f"Kesalahan pemutaran PyAudio: {exc}") _audio_chunks.append(audio_bytes) logging.info(f"Menerima chunk audio: {len(audio_bytes)} byte") def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool: """Menyimpan data audio yang dikumpulkan sebagai file WAV""" if not _audio_chunks: logging.warning("Tidak ada data audio untuk disimpan") return False try: audio_data = b"".join(_audio_chunks) with wave.open(filename, 'wb') as wav_file: wav_file.setnchannels(1) # Mono wav_file.setsampwidth(2) # 16-bit wav_file.setframerate(sample_rate) wav_file.writeframes(audio_data) logging.info(f"Audio disimpan ke: {filename}") return True except Exception as exc: logging.error(f"Gagal menyimpan audio: {exc}") return False async def _user_input_loop(client: TTSRealtimeClient): """Terus-menerus mendapatkan input pengguna dan mengirim teks. Ketika pengguna memasukkan teks kosong, kirim event commit dan akhiri sesi saat ini""" print("Masukkan teks (tekan Enter langsung untuk mengirim event commit dan mengakhiri sesi saat ini, tekan Ctrl+C atau Ctrl+D untuk keluar dari program):") while True: try: user_text = input("> ") if not user_text: # Input pengguna kosong # Input kosong dianggap sebagai akhir percakapan: buffer commit -> akhiri sesi -> hentikan loop logging.info("Input kosong, mengirim event commit dan mengakhiri sesi saat ini") await client.commit_text_buffer() # Tunggu sebentar agar server memproses commit, mencegah pengakhiran sesi prematur yang dapat menyebabkan kehilangan audio await asyncio.sleep(0.3) await client.finish_session() break # Keluar dari loop input pengguna langsung, tidak perlu menekan Enter lagi else: logging.info(f"Mengirim teks: {user_text}") await client.append_text(user_text) except EOFError: # Pengguna menekan Ctrl+D break except KeyboardInterrupt: # Pengguna menekan Ctrl+C break # Akhiri sesi logging.info("Mengakhiri sesi...") async def _run_demo(): """Menjalankan demo lengkap""" global _audio_stream # Buka aliran output PyAudio _audio_stream = _audio_pyaudio.open( format=pyaudio.paInt16, channels=1, rate=_AUDIO_SAMPLE_RATE, output=True, frames_per_buffer=1024 ) client = TTSRealtimeClient( base_url=URL, api_key=API_KEY, voice="Cherry", mode=SessionMode.COMMIT, # Diubah ke mode COMMIT audio_callback=_audio_callback ) # Membuat koneksi await client.connect() # Menjalankan penanganan pesan dan input pengguna secara paralel consumer_task = asyncio.create_task(client.handle_messages()) producer_task = asyncio.create_task(_user_input_loop(client)) await producer_task # Menunggu input pengguna selesai # Menunggu response.done await client.wait_for_response_done() # Menutup koneksi dan membatalkan tugas consumer await client.close() consumer_task.cancel() # Menutup aliran audio if _audio_stream is not None: _audio_stream.stop_stream() _audio_stream.close() _audio_pyaudio.terminate() # Menyimpan data audio os.makedirs("outputs", exist_ok=True) _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav")) def main(): logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logging.info("Memulai demo Klien Realtime QwenTTS…") asyncio.run(_run_demo()) if __name__ == "__main__": main()Jalankan
commit.py. Anda dapat memasukkan teks untuk disintesis beberapa kali. Tekan Enter tanpa memasukkan teks untuk mendengar audio yang dikembalikan oleh API Realtime melalui speaker.Java
Di direktori yang sama dengan
TTSRealtimeClient.java, buat file Java lain bernamaCommit.javadan salin kode berikut ke dalam file tersebut:import javax.sound.sampled.*; import java.io.*; import java.util.ArrayList; import java.util.List; import java.util.Scanner; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; public class Commit { // Berikut ini adalah konfigurasi untuk wilayah Singapura. private static final String URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"; // API Key berbeda antara wilayah Singapura dan Beijing. Dapatkan API Key Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key // Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan API Key Model Studio China Anda: private static final String API_KEY = "sk-xxx"; private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY"); private static final int SAMPLE_RATE = 24000; private static final List<byte[]> audioChunks = new ArrayList<>(); private static final ConcurrentLinkedQueue<byte[]> playbackQueue = new ConcurrentLinkedQueue<>(); private static final AtomicBoolean playing = new AtomicBoolean(true); public static void main(String[] args) throws Exception { if (API_KEY == null || API_KEY.isEmpty()) { throw new IllegalStateException("Harap atur variabel lingkungan DASHSCOPE_API_KEY"); } // Inisialisasi pemutaran audio AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false); DataLine.Info info = new DataLine.Info(SourceDataLine.class, format); SourceDataLine audioLine = (SourceDataLine) AudioSystem.getLine(info); audioLine.open(format); audioLine.start(); // Mulai thread pemutaran Thread playerThread = new Thread(() -> { while (playing.get() || !playbackQueue.isEmpty()) { byte[] chunk = playbackQueue.poll(); if (chunk != null) { audioLine.write(chunk, 0, chunk.length); } else { try { Thread.sleep(10); } catch (InterruptedException ignored) {} } } }); playerThread.start(); // Buat klien TTS (mode commit) // Untuk menggunakan kontrol instruksi, ganti model dengan qwen3-tts-instruct-flash-realtime dan hapus komentar instruksi di TTSRealtimeClient.java TTSRealtimeClient client = new TTSRealtimeClient( URL, API_KEY, "Cherry", TTSRealtimeClient.SessionMode.COMMIT, audioData -> { playbackQueue.add(audioData); audioChunks.add(audioData); System.out.println("Data audio diterima: " + audioData.length + " byte"); } ); client.connect(); // Input interaktif System.out.println("Masukkan teks (tekan Enter langsung untuk mengirim event commit dan mengakhiri sesi, tekan Ctrl+D untuk keluar dari program):"); Scanner scanner = new Scanner(System.in); while (true) { System.out.print("> "); if (!scanner.hasNextLine()) { client.finishSession(); break; } String userText = scanner.nextLine(); if (userText.isEmpty()) { // Input kosong: commit buffer dan akhiri sesi System.out.println("Input kosong, mengirim event commit dan mengakhiri sesi"); client.commitTextBuffer(); Thread.sleep(300); client.finishSession(); break; } else { System.out.println("Mengirim teks: " + userText); client.appendText(userText); } } scanner.close(); // Tunggu respons selesai client.waitForResponseDone(); client.waitForSessionFinished(); client.close(); // Tunggu pemutaran selesai playing.set(false); playerThread.join(); audioLine.drain(); audioLine.close(); // Simpan file audio saveWav("output.wav"); System.out.println("Selesai"); } private static void saveWav(String filename) throws IOException { if (audioChunks.isEmpty()) { System.out.println("Tidak ada data audio untuk disimpan"); return; } ByteArrayOutputStream bos = new ByteArrayOutputStream(); for (byte[] chunk : audioChunks) { bos.write(chunk); } byte[] allAudio = bos.toByteArray(); AudioFormat format = new AudioFormat(SAMPLE_RATE, 16, 1, true, false); AudioInputStream ais = new AudioInputStream( new ByteArrayInputStream(allAudio), format, allAudio.length / 2); new File("outputs").mkdirs(); AudioSystem.write(ais, AudioFileFormat.Type.WAVE, new File("outputs/" + filename)); System.out.println("Audio disimpan ke: outputs/" + filename); } }Kompilasi dan jalankan
Commit.java. Anda dapat memasukkan teks untuk disintesis beberapa kali. Tekan Enter tanpa memasukkan teks untuk mendengar audio yang dikembalikan oleh API Realtime melalui speaker. -
server_commit mode
Terapkan di produksi
Penggunaan kembali koneksi (WebSocket)
Koneksi WebSocket dapat digunakan kembali: setelah tugas sintesis selesai, Anda dapat memulai tugas berikutnya pada koneksi yang sama tanpa perlu membentuk koneksi baru.
Proses penggunaan kembali:
- Qwen-Audio-TTS / Qwen-Audio-TTS/CosyVoice: Klien mengirim
finish-task, dan setelah server mengembalikantask-finished, klien dapat mengirimrun-taskuntuk memulai tugas baru. - Qwen-TTS: Klien mengirim
session.finish, dan setelah server mengembalikansession.finished, klien dapat membuat sesi baru untuk memulai tugas berikutnya.
Penggunaan kembali setelah pembatalan: Untuk Qwen-Audio-TTS / Qwen-Audio-TTS/CosyVoice, jika Anda membatalkan tugas saat ini menggunakan direktif cancel, Anda juga dapat mengirim run-task baru pada koneksi yang sama setelah server mengembalikan task-finished. Untuk detailnya, lihat Batalkan Tugas.
Penting
- Tunggu server mengembalikan event penyelesaian (
task-finishedatausession.finished) sebelum memulai tugas baru. - Qwen-Audio-TTS, Qwen-Audio-TTS/CosyVoice memerlukan
task_idyang berbeda untuk setiap tugas pada koneksi yang digunakan kembali. - Jika tugas gagal, server mengembalikan event error dan menutup koneksi. Koneksi tersebut tidak dapat digunakan kembali.
- Jika tidak ada tugas baru yang dimulai dalam waktu 60 detik setelah tugas sebelumnya berakhir, koneksi akan ditutup secara otomatis.
Untuk detail event setiap model, lihat Referensi API yang sesuai.
Batas laju
Pemanggilan model tunduk pada batas laju. Ketika batas terlampaui, server mengembalikan error Requests rate limit exceeded, please try again later. Kurangi laju permintaan atau konkurensi Anda, lalu coba lagi.
Untuk batas laju setiap model, lihat Pembatasan Laju.
Praktik terbaik konkurensi tinggi
SDK DashScope memiliki pooling bawaan yang menggunakan kembali koneksi WebSocket dan objek synthesizer, sehingga menghilangkan overhead pembuatan dan penghancuran berulang.
Lihat praktik terbaik konkurensi tinggi
Qwen-Audio-TTS/CosyVoice
Qwen-Audio-TTS dan Qwen-Audio-TTS/CosyVoice menggunakan antarmuka SDK yang sama. Contoh berikut juga berlaku untuk model Qwen-Audio-TTS — cukup ganti parameter model dan voice.
Prasyarat
-
SDK DashScope telah diinstal dan memenuhi persyaratan versi. Kami merekomendasikan menginstal versi terbaru:
- SDK Python: versi >= 1.25.2
- SDK Java: versi >= 2.16.6
SDK Python
SDK Python menggunakan SpeechSynthesizerObjectPool untuk mengelola dan menggunakan kembali objek SpeechSynthesizer.
Pool membuat sejumlah instance SpeechSynthesizer dan membentuk koneksi WebSocket saat inisialisasi. Saat Anda meminjam objek, objek tersebut siap mengirim permintaan segera, sehingga mengurangi latensi paket pertama. Setelah objek dikembalikan, koneksi tetap aktif untuk tugas berikutnya.
Langkah implementasi
-
Instal dependensi: Instal dependensi DashScope (
pip install -U dashscope). -
Buat dan konfigurasikan pool objek
Atur ukuran pool menjadi 1,5–2 kali konkurensi puncak, dan jangan melebihi batas QPS akun Anda.
Buat pool singleton global (pembentukan koneksi saat inisialisasi memakan waktu):
from dashscope.audio.tts_v2 import SpeechSynthesizerObjectPool
synthesizer_object_pool = SpeechSynthesizerObjectPool(max_size=20)
import dashscope
// Berikut adalah konfigurasi untuk wilayah China (Beijing). Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"
Penting
- Dalam skenario pool objek,
SpeechSynthesizerObjectPoolmembentuk koneksi WebSocket dengan server menggunakandashscope.api_keyglobal saat ini saat inisialisasi. Kunci API ditulis ke headerAuthorizationhanya selama handshake WebSocket untuk autentikasi. Pesan tugas berikutnya (sepertirun-task) tidak membawa Kunci API. Memodifikasidashscope.api_keysetelah pembuatan pool tidak memengaruhi koneksi yang ada — objek yang dipinjam melaluiborrow_synthesizer(termasuk yang dikembalikan dan dipinjam ulang) masih menggunakan Kunci API dari handshake awal. Nilai baru diabaikan diam-diam, yang dapat menyebabkan atribusi identitas, kuota, atau penagihan berbeda dari yang diharapkan. Catatan:borrow_synthesizertidak mendukung menentukan Kunci API sebagai parameter. - Untuk menggunakan beberapa Kunci API, pertahankan instance
SpeechSynthesizerObjectPoolterpisah untuk setiap kunci.
-
Pinjam objek
SpeechSynthesizerdari poolJika jumlah objek yang belum dikembalikan melebihi kapasitas pool, sistem membuat objek tambahan.
Objek tambahan ini harus membentuk koneksi baru dan tidak mendapat manfaat dari pooling.
speech_synthesizer = connectionPool.borrow_synthesizer(
model='cosyvoice-v3-flash',
voice='longanyang',
seed=12382,
callback=synthesizer_callback
)
-
Lakukan sintesis ucapan
Panggil metode call atau streaming_call objek
SpeechSynthesizeruntuk mensintesis ucapan. -
Kembalikan objek
SpeechSynthesizerKembalikan objek setelah tugas selesai agar tersedia untuk digunakan kembali.
Jangan mengembalikan objek dengan tugas yang belum lengkap atau gagal.
connectionPool.return_synthesizer(speech_synthesizer)
Kode lengkap
PentingSebelum menggunakan kode ini: SpeechSynthesizerObjectPool membentuk koneksi WebSocket dan melakukan autentikasi menggunakan dashscope.api_key global saat ini saat inisialisasi. Memodifikasi dashscope.api_key setelah pembuatan pool tidak memengaruhi koneksi yang ada — nilai baru diabaikan diam-diam. Untuk beberapa Kunci API, pertahankan instance pool terpisah untuk setiap kunci. Untuk detailnya, lihat catatan penting di atas.
// !/usr/bin/env python3
// Hak Cipta (C) Alibaba Group. Seluruh Hak Dilindungi.
// Lisensi MIT (https://opensource.org/licenses/MIT)
import os
import time
import threading
import dashscope
from dashscope.audio.tts_v2 import *
USE_CONNECTION_POOL = True
text_to_synthesize = [
'Kalimat 1: Selamat datang di layanan sintesis ucapan Alibaba.',
'Kalimat 2: Selamat datang di layanan sintesis ucapan Alibaba.',
'Kalimat 3: Selamat datang di layanan sintesis ucapan Alibaba.',
]
connectionPool = None
def init_dashscope_api_key():
'''
Tetapkan Kunci API DashScope Anda. Informasi lebih lanjut:
https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
'''
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
if 'DASHSCOPE_API_KEY' in os.environ:
dashscope.api_key = os.environ[
'DASHSCOPE_API_KEY'] // muat Kunci API dari variabel lingkungan DASHSCOPE_API_KEY
else:
dashscope.api_key = '<your-dashscope-api-key>' // tetapkan Kunci API secara manual
def synthesis_text_to_speech_and_play_by_streaming_mode(text, task_id):
global USE_CONNECTION_POOL, connectionPool
'''
Sintesis ucapan dengan teks yang diberikan dengan mode streaming, panggilan asinkron dan putar audio yang disintesis secara real-time.
untuk informasi lebih lanjut, silakan merujuk ke https://www.alibabacloud.com/help/document_detail/2712523.html
'''
complete_event = threading.Event()
// Definisikan callback untuk menangani hasil
class Callback(ResultCallback):
def on_open(self):
// saat menggunakan pool objek, on_open akan dipanggil setelah tugas dimulai
self.file = open(f'result_{task_id}.mp3', 'wb')
print(f'[task_{task_id}] mulai')
def on_complete(self):
print(f'[task_{task_id}] tugas sintesis ucapan selesai berhasil.')
complete_event.set()
def on_error(self, message: str):
print(f'[task_{task_id}] tugas sintesis ucapan gagal, {message}')
def on_close(self):
// saat menggunakan pool objek, on_open akan dipanggil setelah tugas selesai
print(f'[task_{task_id}] selesai')
def on_event(self, message):
// print(f'terima pesan sintesis ucapan {message}')
pass
def on_data(self, data: bytes) -> None:
// kirim ke pemutar
// simpan audio ke file
self.file.write(data)
// Panggil callback synthesizer ucapan
synthesizer_callback = Callback()
// Inisialisasi synthesizer ucapan
// Anda dapat menyesuaikan parameter sintesis, seperti suara, format, laju sampel atau parameter lainnya
if USE_CONNECTION_POOL:
speech_synthesizer = connectionPool.borrow_synthesizer(
model='cosyvoice-v3-flash',
voice='longanyang',
seed=12382,
callback=synthesizer_callback
)
else:
speech_synthesizer = SpeechSynthesizer(model='cosyvoice-v3-flash',
voice='longanyang',
seed=12382,
callback=synthesizer_callback)
try:
speech_synthesizer.call(text)
except Exception as e:
print(f'[task_{task_id}] tugas sintesis ucapan gagal, {e}')
if USE_CONNECTION_POOL:
// tutup koneksi synthesizer secara manual jika tugas gagal saat menggunakan pool koneksi.
speech_synthesizer.close()
return
print('[task_{}] Teks yang disintesis: {}'.format(task_id, text))
complete_event.wait()
print('[task_{}][Metric] requestId: {}, latensi paket pertama ms: {}'.format(
task_id,
speech_synthesizer.get_last_request_id(),
speech_synthesizer.get_first_package_delay()))
if USE_CONNECTION_POOL:
connectionPool.return_synthesizer(speech_synthesizer)
// fungsi utama
if __name__ == '__main__':
// Anda harus mengatur dashscope.api_key dan base_websocket_api_url sebelum membuat SpeechSynthesizerObjectPool.
// Pool membentuk koneksi WebSocket menggunakan dashscope.api_key global saat ini saat waktu inisialisasi.
// Memodifikasi dashscope.api_key setelah pembuatan pool tidak akan memengaruhi koneksi yang ada di pool.
// Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
init_dashscope_api_key()
if USE_CONNECTION_POOL:
print('membuat pool koneksi')
start_time = time.time() * 1000
connectionPool = SpeechSynthesizerObjectPool(max_size=3)
end_time = time.time() * 1000
print('pool koneksi dibuat, biaya: {} ms'.format(end_time - start_time))
task_thread_list = []
for task_id in range(3):
thread = threading.Thread(
target=synthesis_text_to_speech_and_play_by_streaming_mode,
args=(text_to_synthesize[task_id], task_id))
task_thread_list.append(thread)
for task_thread in task_thread_list:
task_thread.start()
for task_thread in task_thread_list:
task_thread.join()
if USE_CONNECTION_POOL:
connectionPool.shutdown()
Manajemen sumber daya dan penanganan error
-
Tugas berhasil: Setelah tugas sintesis selesai secara normal, panggil
connectionPool.return_synthesizer(speech_synthesizer)untuk mengembalikan objekSpeechSynthesizerke pool untuk digunakan kembali.PentingJangan mengembalikan objek
SpeechSynthesizerdengan tugas yang belum lengkap atau gagal. -
Tugas gagal: Jika error internal SDK atau exception logika bisnis menyebabkan tugas dibatalkan, tutup koneksi WebSocket dasar:
speech_synthesizer.close() -
Setelah semua tugas sintesis selesai, matikan pool:
connectionPool.shutdown() -
Saat terjadi error TaskFailed di sisi server, tidak diperlukan penanganan tambahan.
SDK Java
SDK Java mencapai kinerja optimal melalui koordinasi pool koneksi bawaan dan pool objek kustom.
- Pool koneksi: Pool koneksi OkHttp3 yang terintegrasi dalam SDK mengelola dan menggunakan kembali koneksi WebSocket dasar, sehingga mengurangi overhead jabat tangan jaringan. Fitur ini diaktifkan secara default.
- Pool objek: Dibangun di atas
commons-pool2, pool ini mempertahankan seperangkat objekSpeechSynthesizeryang telah terhubung sebelumnya. Meminjam dari pool menghilangkan latensi penyiapan koneksi, sehingga secara signifikan mengurangi latensi paket pertama.
Langkah implementasi
-
Tambahkan dependensi
Tambahkan dashscope-sdk-java dan commons-pool2 ke konfigurasi dependensi proyek Anda berdasarkan alat build yang digunakan.
Contoh Maven dan Gradle:
Maven
- Buka file
pom.xmlproyek Maven Anda. - Tambahkan dependensi berikut di dalam tag
<dependencies>.
<dependency> <groupId>com.alibaba</groupId> <artifactId>dashscope-sdk-java</artifactId> <!-- Ganti 'the-latest-version' dengan versi 2.16.9 atau lebih baru. Periksa versi yang tersedia di: https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java --> <version>the-latest-version</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> <!-- Ganti 'the-latest-version' dengan versi terbaru. Periksa versi yang tersedia di: https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 --> <version>the-latest-version</version> </dependency>- Simpan file
pom.xml. - Jalankan perintah Maven seperti
mvn clean installataumvn compileuntuk memperbarui dependensi proyek.
Gradle
- Buka file
build.gradleproyek Gradle Anda. - Tambahkan dependensi berikut di dalam blok
dependencies.
dependencies { // Ganti 'the-latest-version' dengan versi 2.16.6 atau lebih baru. Periksa versi yang tersedia di: https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java implementation group: 'com.alibaba', name: 'dashscope-sdk-java', version: 'the-latest-version' // Ganti 'the-latest-version' dengan versi terbaru. Periksa versi yang tersedia di: https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 implementation group: 'org.apache.commons', name: 'commons-pool2', version: 'the-latest-version' }- Simpan file
build.gradle. - Di terminal, navigasi ke direktori root proyek dan jalankan perintah Gradle berikut untuk memperbarui dependensi.
./gradlew build --refresh-dependenciesDi Windows, gunakan perintah berikut sebagai gantinya:
gradlew build --refresh-dependencies - Buka file
-
Konfigurasikan pool koneksi
Konfigurasikan parameter kunci pool koneksi melalui variabel lingkungan:
Variabel lingkungan
Deskripsi
DASHSCOPE_CONNECTION_POOL_SIZE
Ukuran pool koneksi.
Nilai yang direkomendasikan: minimal 2x konkurensi puncak.
Default: 32.
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS
Jumlah maksimum permintaan asinkron.
Nilai yang direkomendasikan: sama dengan
DASHSCOPE_CONNECTION_POOL_SIZE.Default: 32.
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST
Jumlah maksimum permintaan asinkron per host.
Nilai yang direkomendasikan: sama dengan
DASHSCOPE_CONNECTION_POOL_SIZE.Default: 32.
-
Konfigurasikan pool objek
Konfigurasikan ukuran pool objek melalui variabel lingkungan:
Variabel lingkungan
Deskripsi
COSYVOICE_OBJECTPOOL_SIZE
Ukuran pool objek.
Nilai yang direkomendasikan: 1,5x-2x konkurensi puncak.
Default: 500.
Penting
- Ukuran pool objek (
COSYVOICE_OBJECTPOOL_SIZE) harus kurang dari atau sama dengan ukuran pool koneksi (DASHSCOPE_CONNECTION_POOL_SIZE). Jika tidak, saat pool objek meminta objek dan pool koneksi penuh, thread pemanggil akan memblokir sambil menunggu koneksi yang tersedia. - Ukuran pool objek tidak boleh melebihi batas QPS (queries per second) akun Anda.
Buat pool objek dengan kode berikut:
- Ukuran pool objek (
class CosyvoiceObjectPool {
// ... Kode lain dihilangkan di sini. Untuk contoh lengkap, lihat kode lengkap.
public static GenericObjectPool<SpeechSynthesizer> getInstance() {
lock.lock();
if (synthesizerPool == null) {
// Anda dapat mengatur ukuran pool objek di sini, atau mengaturnya di variabel lingkungan COSYVOICE_OBJECTPOOL_SIZE.
// Disarankan untuk mengaturnya menjadi 1,5 hingga 2 kali koneksi konkuren maksimum server.
int objectPoolSize = getObjectivePoolSize();
SpeechSynthesizerObjectFactory speechSynthesizerObjectFactory =
new SpeechSynthesizerObjectFactory();
GenericObjectPoolConfig<SpeechSynthesizer> config =
new GenericObjectPoolConfig<>();
config.setMaxTotal(objectPoolSize);
config.setMaxIdle(objectPoolSize);
config.setMinIdle(objectPoolSize);
synthesizerPool =
new GenericObjectPool<>(speechSynthesizerObjectFactory, config);
}
lock.unlock();
return synthesizerPool;
}
}
-
Pinjam objek
SpeechSynthesizerdari poolJika jumlah objek yang belum dikembalikan melebihi kapasitas maksimum pool, sistem membuat objek
SpeechSynthesizertambahan.Objek yang baru dibuat ini memerlukan inisialisasi ulang dan koneksi WebSocket baru, sehingga tidak mendapat manfaat dari pooling.
synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
-
Lakukan sintesis ucapan
Setelah meminjam objek
SpeechSynthesizerdari pool, panggilupdateParamAndCallback(param, callback)untuk mengikat parameter dan callback untuk tugas saat ini, lalu panggilstreamingCallataucalluntuk mensintesis ucapan.Penting
- Dalam skenario pool objek,
updateParamAndCallbackdipanggil beberapa kali (sekali setiap kali objek dipinjam, untuk mengatur callback dan parameter tingkat tugas sepertivoicedanformat). KunciapiKeyyang diteruskan dalam setiap panggilan harus tetap sama.updateParamAndCallbackhanya memperbarui bidang lokal instanceSpeechSynthesizersaat ini dan tidak membangun ulang koneksi WebSocket dasar. SDK menulisapiKeyke headerAuthorizationhanya selama handshake WebSocket untuk autentikasi. Pesan tugas berikutnya (sepertirun-task) tidak membawaapiKey. Selama koneksi yang digunakan kembali tidak terputus, meneruskanapiKeybaru tidak akan dikirim ke server — permintaan masih menggunakanapiKeydari handshake awal, yang dapat menyebabkan atribusi identitas, kuota, atau penagihan berbeda dari yang diharapkan. - Untuk menggunakan beberapa Kunci API, pertahankan instance pool objek terpisah untuk setiap kunci.
- Dalam skenario pool objek,
-
Kembalikan objek
SpeechSynthesizerSetelah tugas sintesis selesai, kembalikan objek
SpeechSynthesizeragar tugas berikutnya dapat menggunakannya kembali.Jangan mengembalikan objek dengan tugas yang belum lengkap atau gagal.
CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
Kode lengkap
PentingSebelum menggunakan kode ini: Dalam skenario pool objek, apiKey yang diteruskan ke updateParamAndCallback di beberapa panggilan harus tetap sama — SDK tidak memperbarui Kunci API koneksi yang telah dibentuk, dan meneruskan Kunci API yang berbeda tidak berpengaruh. Untuk beberapa Kunci API, pertahankan instance pool terpisah untuk setiap kunci. Untuk detailnya, lihat catatan penting di atas.
import com.alibaba.dashscope.audio.tts.SpeechSynthesisResult;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisAudioFormat;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesisParam;
import com.alibaba.dashscope.audio.ttsv2.SpeechSynthesizer;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.Constants;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
/**
* Anda perlu menyertakan paket org.apache.commons.pool2 dan DashScope dalam proyek Anda.
*
* SDK DashScope versi 2.16.6 dan lebih baru dioptimalkan untuk skenario konkurensi tinggi.
* Versi SDK DashScope sebelum 2.16.6 tidak direkomendasikan untuk penggunaan konkurensi tinggi.
*
*
* Sebelum melakukan panggilan konkurensi tinggi ke layanan TTS,
* harap konfigurasikan parameter pool koneksi melalui variabel lingkungan berikut.
*
* DASHSCOPE_MAXIMUM_ASYNC_REQUESTS
* DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST
* DASHSCOPE_CONNECTION_POOL_SIZE
*
*/
class SpeechSynthesizerObjectFactory
extends BasePooledObjectFactory<SpeechSynthesizer> {
public SpeechSynthesizerObjectFactory() {
super();
}
@Override
public SpeechSynthesizer create() throws Exception {
return new SpeechSynthesizer();
}
@Override
public PooledObject<SpeechSynthesizer> wrap(SpeechSynthesizer obj) {
return new DefaultPooledObject<>(obj);
}
}
class CosyvoiceObjectPool {
public static GenericObjectPool<SpeechSynthesizer> synthesizerPool;
public static String COSYVOICE_OBJECTPOOL_SIZE_ENV = "COSYVOICE_OBJECTPOOL_SIZE";
public static int DEFAULT_OBJECT_POOL_SIZE = 500;
private static Lock lock = new java.util.concurrent.locks.ReentrantLock();
public static int getObjectivePoolSize() {
try {
Integer n = Integer.parseInt(System.getenv(COSYVOICE_OBJECTPOOL_SIZE_ENV));
System.out.println("Menggunakan Ukuran Pool Objek di Env: "+ n);
return n;
} catch (NumberFormatException e) {
System.out.println("Menggunakan Ukuran Pool Objek Default: "+ DEFAULT_OBJECT_POOL_SIZE);
return DEFAULT_OBJECT_POOL_SIZE;
}
}
public static GenericObjectPool<SpeechSynthesizer> getInstance() {
lock.lock();
if (synthesizerPool == null) {
// Anda dapat mengatur ukuran pool objek di sini atau di variabel lingkungan COSYVOICE_OBJECTPOOL_SIZE.
// Disarankan untuk mengaturnya menjadi 1,5 hingga 2 kali koneksi konkuren maksimum server Anda.
int objectPoolSize = getObjectivePoolSize();
SpeechSynthesizerObjectFactory speechSynthesizerObjectFactory =
new SpeechSynthesizerObjectFactory();
GenericObjectPoolConfig<SpeechSynthesizer> config =
new GenericObjectPoolConfig<>();
config.setMaxTotal(objectPoolSize);
config.setMaxIdle(objectPoolSize);
config.setMinIdle(objectPoolSize);
synthesizerPool =
new GenericObjectPool<>(speechSynthesizerObjectFactory, config);
}
lock.unlock();
return synthesizerPool;
}
}
class SynthesizeTaskWithCallback implements Runnable {
String[] textArray;
String requestId;
long timeCost;
public SynthesizeTaskWithCallback(String[] textArray) {
this.textArray = textArray;
}
@Override
public void run() {
SpeechSynthesizer synthesizer = null;
long startTime = System.currentTimeMillis();
// jika menerima onError
final boolean[] hasError = {false};
try {
class ReactCallback extends ResultCallback<SpeechSynthesisResult> {
ReactCallback() {}
@Override
public void onEvent(SpeechSynthesisResult message) {
if (message.getAudioFrame() != null) {
try {
byte[] bytesArray = message.getAudioFrame().array();
System.out.println("Audio diterima, panjang aliran audio: " + bytesArray.length);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Override
public void onComplete() {}
@Override
public void onError(Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
hasError[0] = true;
}
}
SpeechSynthesisParam param =
SpeechSynthesisParam.builder()
.model("cosyvoice-v3-flash")
.voice("longanyang")
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio China Anda: .apiKey("sk-xxx")
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.format(SpeechSynthesisAudioFormat
.MP3_22050HZ_MONO_256KBPS) // Gunakan PCM atau MP3 untuk sintesis streaming
.build();
try {
synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
// Catatan: Dalam skenario pool objek, apiKey yang diteruskan dalam beberapa panggilan updateParamAndCallback harus selalu sama. SDK tidak memperbarui apiKey untuk koneksi yang telah dibentuk, sehingga meneruskan apiKey yang berbeda tidak berpengaruh. Lihat catatan penting di langkah "Lakukan sintesis ucapan" di atas.
synthesizer.updateParamAndCallback(param, new ReactCallback());
for (String text : textArray) {
synthesizer.streamingCall(text);
}
Thread.sleep(20);
synthesizer.streamingComplete(60000);
requestId = synthesizer.getLastRequestId();
} catch (Exception e) {
System.out.println("Exception e: " + e.toString());
hasError[0] = true;
}
} catch (Exception e) {
hasError[0] = true;
throw new RuntimeException(e);
}
if (synthesizer != null) {
try {
if (hasError[0] == true) {
// Jika terjadi exception, tutup koneksi dan batalkan objek di pool.
synthesizer.getDuplexApi().close(1000, "bye");
CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
} else {
// Jika tugas selesai secara normal, kembalikan objek ke pool.
CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
long endTime = System.currentTimeMillis();
timeCost = endTime - startTime;
System.out.println("[Thread " + Thread.currentThread() + "] Tugas sintesis ucapan selesai. Biaya waktu: " + timeCost + " ms, RequestId " + requestId);
}
}
}
@Slf4j
public class SynthesizeTextToSpeechWithCallbackConcurrently {
public static void checkoutEnv(String envName, int defaultSize) {
if (System.getenv(envName) != null) {
System.out.println("[PERIKSA ENV]: " + envName + " "
+ System.getenv(envName));
} else {
System.out.println("[PERIKSA ENV]: " + envName
+ " Menggunakan Default yaitu " + defaultSize);
}
}
public static void main(String[] args)
throws InterruptedException, NoApiKeyException {
// Berikut adalah konfigurasi untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi bervariasi berdasarkan wilayah.
Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
// Periksa env pool koneksi
checkoutEnv("DASHSCOPE_CONNECTION_POOL_SIZE", 32);
checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS", 32);
checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST", 32);
checkoutEnv(CosyvoiceObjectPool.COSYVOICE_OBJECTPOOL_SIZE_ENV, CosyvoiceObjectPool.DEFAULT_OBJECT_POOL_SIZE);
int runTimes = 3;
// Buat pool objek SpeechSynthesis
ExecutorService executorService = Executors.newFixedThreadPool(runTimes);
for (int i = 0; i < runTimes; i++) {
// Catat waktu pengiriman tugas
LocalDateTime submissionTime = LocalDateTime.now();
executorService.submit(new SynthesizeTaskWithCallback(new String[] {
"Before my bed, moonlight shines bright,", "I wonder if it is frost on the ground,", "I raise my eyes to gaze at the bright moon,", "then bow my head, thinking of home."}));
}
// Matikan ExecutorService dan tunggu semua tugas selesai
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
System.exit(0);
}
}
Konfigurasi yang direkomendasikan
Konfigurasi berikut didasarkan pada hasil pengujian dari menjalankan hanya layanan sintesis ucapan Qwen-Audio-TTS/CosyVoice pada instance ECS Alibaba Cloud dengan spesifikasi yang ditentukan. Konkurensi berlebihan dapat meningkatkan latensi pemrosesan tugas.
"Konkurensi mesin tunggal" mengacu pada jumlah tugas sintesis Qwen-Audio-TTS/CosyVoice yang berjalan secara bersamaan, setara dengan jumlah thread pekerja.
Spesifikasi ECS (Alibaba Cloud) | Maksimum konkurensi mesin tunggal | Ukuran pool objek | Ukuran pool koneksi |
|---|---|---|---|
4 vCPU, 8 GiB | 100 | 500 | 2000 |
8 vCPU, 16 GiB | 150 | 500 | 2000 |
16 vCPU, 32 GiB | 200 | 500 | 2000 |
Manajemen sumber daya dan penanganan error
-
Tugas berhasil: Setelah tugas sintesis selesai secara normal, panggil metode GenericObjectPool.returnObject untuk mengembalikan objek
SpeechSynthesizerke pool untuk digunakan kembali.Dalam kode contoh, ini sesuai dengan
CosyvoiceObjectPool.getInstance().returnObject(synthesizer).PentingJangan mengembalikan objek
SpeechSynthesizerdengan tugas yang belum lengkap atau gagal. -
Tugas gagal: Jika error internal SDK atau exception logika bisnis menyebabkan tugas dibatalkan, lakukan hal berikut:
- Tutup koneksi WebSocket dasar.
- Batalkan objek di pool untuk mencegahnya digunakan kembali.
// Dalam kode saat ini, konten yang sesuai adalah sebagai berikut
// Tutup koneksi
synthesizer.getDuplexApi().close(1000, "bye");
// Batalkan synthesizer di pool objek saat terjadi exception
CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
- Saat terjadi error TaskFailed di sisi server, tidak diperlukan penanganan tambahan.
Pemanasan dan pengukuran latensi
Saat mengevaluasi SDK Java DashScope untuk latensi dan kinerja konkurensi, jalankan operasi pemanasan yang cukup terlebih dahulu. Ini memastikan bahwa pengukuran mencerminkan kinerja keadaan stabil daripada overhead koneksi awal.
Mekanisme penggunaan kembali koneksi
SDK Java DashScope menggunakan pool koneksi singleton global untuk mengelola dan menggunakan kembali koneksi WebSocket secara efisien, sehingga mengurangi overhead pembuatan dan penghancuran koneksi yang sering untuk beban kerja konkurensi tinggi.
Cara kerja mekanisme ini:
-
Pembuatan sesuai permintaan: SDK tidak membuat koneksi WebSocket sebelumnya saat startup. Koneksi dibentuk pada panggilan pertama.
-
Penggunaan kembali berbatas waktu: Setelah permintaan selesai, koneksi tetap berada di pool hingga 60 detik untuk digunakan kembali.
- Jika permintaan baru tiba dalam waktu 60 detik, koneksi yang ada digunakan kembali, sehingga menghindari overhead jabat tangan.
- Jika koneksi menganggur lebih dari 60 detik, koneksi tersebut ditutup secara otomatis untuk membebaskan sumber daya.
Pentingnya pemanasan
Dalam skenario berikut, pool koneksi mungkin tidak memiliki koneksi aktif yang dapat digunakan kembali, sehingga memaksa pembuatan koneksi baru:
- Aplikasi baru saja dimulai dan belum melakukan panggilan apa pun.
- Layanan telah menganggur lebih dari 60 detik, dan koneksi di pool telah timeout serta ditutup.
Dalam skenario ini, permintaan pertama harus menyelesaikan jabat tangan WebSocket penuh (jabat tangan TCP, negosiasi TLS, peningkatan protokol), sehingga menghasilkan latensi yang jauh lebih tinggi daripada permintaan berikutnya yang menggunakan kembali koneksi. Tanpa pemanasan, hasil pengujian kinerja condong oleh overhead koneksi awal ini.
Latensi yang dilaporkan SDK vs. latensi paket pertama aktual
Latensi paket pertama yang dilaporkan oleh SDK (misalnya, nilai dari get_first_package_delay()) mencakup penyiapan koneksi WebSocket dan waktu transmisi jaringan, dan tidak sama dengan latensi paket pertama layanan model aktual.
Latensi paket pertama aktual adalah interval antara saat server menerima instruksi run-task dan saat server mengembalikan event result-generated pertama. Nilai ini dapat ditemukan di log sisi server.
Dalam skenario konkurensi tinggi, karena pembuatan koneksi simultan dan penjadwalan sumber daya, latensi yang dilaporkan SDK mungkin jauh lebih tinggi daripada latensi paket pertama sisi server aktual. Jika Anda mengamati latensi paket pertama yang dilaporkan SDK tinggi:
- Bandingkan dengan latensi paket pertama log sisi server (dari
run-taskkeresult-generatedpertama) untuk memeriksa apakah kinerja inferensi model normal. - Gunakan mekanisme pool objek atau pool koneksi yang dijelaskan di atas untuk pemanasan. Ini menghilangkan overhead jabat tangan WebSocket, sehingga membawa latensi yang dilaporkan SDK lebih dekat ke latensi paket pertama aktual.
Praktik yang direkomendasikan
Untuk data kinerja yang andal, ikuti langkah pemanasan ini sebelum menjalankan pengujian beban atau mengumpulkan metrik latensi:
- Simulasikan tingkat konkurensi target dan kirim sejumlah permintaan pemanasan terlebih dahulu (misalnya, pertahankan lalu lintas selama 1–2 menit) untuk sepenuhnya mengisi pool koneksi.
- Konfirmasi bahwa pool koneksi telah membentuk dan mempertahankan koneksi aktif yang cukup sebelum memulai pengumpulan data kinerja formal.
Pemanasan yang tepat membawa pool koneksi SDK ke keadaan penggunaan kembali yang stabil, sehingga menghasilkan metrik latensi yang secara akurat mencerminkan kinerja keadaan stabil online.
Exception SDK Java umum
Exception 1: Koneksi TCP server terus meningkat meskipun lalu lintas stabil
Setiap objek SDK membuat koneksi saat inisialisasi. Tanpa pool objek, objek dihancurkan setelah setiap tugas selesai. Koneksi kemudian memasuki keadaan tidak direferensikan dan tetap terbuka hingga server menghentikannya setelah 61 detik. Selama periode ini, koneksi tidak dapat digunakan kembali.
Di bawah konkurensi tinggi, tugas baru yang tidak menemukan koneksi yang dapat digunakan kembali membuat yang baru, menyebabkan:
- Jumlah koneksi terus meningkat.
- Kelelahan sumber daya server dan penurunan kinerja karena koneksi berlebihan.
- Pool koneksi jenuh, menyebabkan tugas baru memblokir sambil menunggu koneksi yang tersedia.
MaxIdle pool objek dikonfigurasi lebih rendah dari MaxTotal, menyebabkan objek menganggur di luar MaxIdle dihancurkan, yang menyebabkan kebocoran koneksi mereka. Koneksi yang bocor ini harus menunggu 61 detik untuk timeout sebelum terputus, sama seperti Jenis 1.
Solusi:
Untuk Jenis 1: Gunakan pool objek.
Untuk Jenis 2: Periksa konfigurasi pool objek dan atur MaxIdle sama dengan MaxTotal. Nonaktifkan kebijakan penggantian objek otomatis.
Exception 2: Tugas memakan waktu 60 detik lebih lama dari biasanya
Akar penyebab yang sama dengan Exception 1: Pool koneksi telah mencapai batas koneksi maksimum, dan tugas baru harus menunggu 61 detik agar koneksi yang tidak direferensikan timeout sebelum koneksi tersedia.
Exception 3: Tugas lambat saat startup layanan tetapi secara bertahap pulih
Akar penyebab:
Di bawah konkurensi tinggi, satu objek menggunakan kembali koneksi WebSocket yang sama. Koneksi WebSocket hanya dibuat saat startup layanan. Jika konkurensi tinggi dimulai segera saat peluncuran, membuat terlalu banyak koneksi WebSocket secara simultan menyebabkan pemblokiran.
Solusi:
Tingkatkan konkurensi secara bertahap setelah memulai layanan, atau tambahkan tugas pemanasan.
Exception 4: Server melaporkan "Invalid action('run-task')! Please follow the protocol!"
Akar penyebab:
Terjadi error di sisi klien, tetapi server tidak mendeteksinya. Koneksi tetap dalam keadaan dalam tugas. Saat koneksi dan objek ini digunakan kembali untuk tugas berikutnya, alur protokol rusak dan tugas berikutnya gagal.
Solusi:
Setelah exception dilemparkan, tutup koneksi WebSocket sebelum mengembalikan objek ke pool.
Exception 5: Lonjakan lalu lintas meskipun beban bisnis stabil
Akar penyebab:
Membuat terlalu banyak koneksi WebSocket secara simultan menyebabkan pemblokiran. Saat diblokir, lalu lintas bisnis masuk menumpuk. Setelah kemacetan hilang, semua tugas yang tertunda dijalankan sekaligus, menciptakan lonjakan lalu lintas yang mungkin melebihi batas konkurensi akun Anda, sehingga menyebabkan kegagalan tugas atau penurunan server.
Pemicu umum untuk membuat terlalu banyak koneksi WebSocket secara simultan:
- Fase startup layanan
- Anomali jaringan menyebabkan banyak koneksi WebSocket terputus dan terhubung kembali secara simultan
- Lonjakan error sisi server menyebabkan banyak koneksi WebSocket terhubung kembali. Error umum termasuk melebihi batas konkurensi akun ("Requests rate limit exceeded, please try again later.").
Solusi:
- Periksa kondisi jaringan.
- Investigasi apakah lonjakan error sisi server lainnya mendahului lonjakan tersebut.
- Tingkatkan batas konkurensi akun.
- Kurangi ukuran pool objek dan pool koneksi untuk membatasi konkurensi maksimum melalui batas pool.
- Tingkatkan spesifikasi server atau tambahkan lebih banyak mesin.
Exception 6: Semua tugas melambat saat konkurensi meningkat
Solusi:
- Periksa apakah lebar pita jaringan telah mencapai batasnya.
- Periksa apakah konkurensi aktual terlalu tinggi.
Model dan wilayah yang didukung
Singapura
Untuk memanggil model berikut, pilih Kunci API dari wilayah Singapura:
-
Qwen-Audio-TTS: qwen-audio-3.0-tts-plus, qwen-audio-3.0-tts-flash
-
Qwen-Audio-TTS/CosyVoice: cosyvoice-v3-plus, cosyvoice-v3-flash
-
Qwen-TTS:
- Qwen3-TTS-Instruct-Flash-Realtime: qwen3-tts-instruct-flash-realtime (stabil, saat ini setara dengan qwen3-tts-instruct-flash-realtime-2026-01-22), qwen3-tts-instruct-flash-realtime-2026-01-22 (snapshot terbaru)
- Qwen3-TTS-VD-Realtime: qwen3-tts-vd-realtime-2026-01-15 (snapshot terbaru), qwen3-tts-vd-realtime-2025-12-16 (snapshot)
- Qwen3-TTS-VC-Realtime: qwen3-tts-vc-realtime-2026-01-15 (snapshot terbaru), qwen3-tts-vc-realtime-2025-11-27 (snapshot)
- Qwen3-TTS-Flash-Realtime: qwen3-tts-flash-realtime (stabil, saat ini setara dengan qwen3-tts-flash-realtime-2025-11-27), qwen3-tts-flash-realtime-2025-11-27 (snapshot terbaru), qwen3-tts-flash-realtime-2025-09-18 (snapshot)
China (Beijing)
Untuk memanggil model berikut, pilih Kunci API dari wilayah Beijing:
-
Qwen-Audio-TTS: qwen-audio-3.0-tts-plus, qwen-audio-3.0-tts-flash
-
Qwen-Audio-TTS/CosyVoice: cosyvoice-v3.5-plus, cosyvoice-v3.5-flash, cosyvoice-v3-plus, cosyvoice-v3-flash, cosyvoice-v2
-
Qwen-TTS:
- Qwen3-TTS-Instruct-Flash-Realtime: qwen3-tts-instruct-flash-realtime (stabil, saat ini setara dengan qwen3-tts-instruct-flash-realtime-2026-01-22), qwen3-tts-instruct-flash-realtime-2026-01-22 (snapshot terbaru)
- Qwen3-TTS-VD-Realtime: qwen3-tts-vd-realtime-2026-01-15 (snapshot terbaru), qwen3-tts-vd-realtime-2025-12-16 (snapshot)
- Qwen3-TTS-VC-Realtime: qwen3-tts-vc-realtime-2026-01-15 (snapshot terbaru), qwen3-tts-vc-realtime-2025-11-27 (snapshot)
- Qwen3-TTS-Flash-Realtime: qwen3-tts-flash-realtime (stabil, saat ini setara dengan qwen3-tts-flash-realtime-2025-11-27), qwen3-tts-flash-realtime-2025-11-27 (snapshot terbaru), qwen3-tts-flash-realtime-2025-09-18 (snapshot)
- Qwen-TTS-Realtime: qwen-tts-realtime (stabil, saat ini setara dengan qwen-tts-realtime-2025-07-15), qwen-tts-realtime-latest (terbaru, saat ini setara dengan qwen-tts-realtime-2025-07-15), qwen-tts-realtime-2025-07-15 (snapshot)
Suara yang didukung
Model yang berbeda mendukung suara yang berbeda. Atur parameter permintaan voice ke nilai yang tercantum dalam kolom parameter suara pada daftar suara yang sesuai.
Referensi API
- Sintesis Suara Real-Time – Referensi API Qwen-Audio-TTS/CosyVoice
- Sintesis Suara Real-Time – Referensi API Qwen-TTS
- SDK Klien AOQ (untuk model CosyVoice)
FAQ
T: Bagaimana cara memperbaiki pengucapan yang salah dalam sintesis ucapan? Bagaimana cara mengontrol pengucapan karakter homofon?
- Ganti karakter polifonik dengan homofon untuk memperbaiki masalah pengucapan secara cepat.
- Gunakan markup SSML untuk mengontrol pengucapan .
T: Bagaimana cara memecahkan masalah audio diam saat menggunakan suara kloning?
-
Verifikasi status suara
Panggil antarmuka API kloning/desain suara dan pastikan
statussuara adalahOK. -
Periksa konsistensi versi model
Pastikan parameter
target_modelyang digunakan selama kloning suara cocok dengan parametermodelyang digunakan untuk sintesis ucapan. Misalnya:- Kloning menggunakan
cosyvoice-v3-plus - Sintesis juga harus menggunakan
cosyvoice-v3-plus
- Kloning menggunakan
-
Verifikasi kualitas audio sumber
Periksa apakah audio sumber yang digunakan untuk kloning suara memenuhi persyaratan di API kloning/desain suara:
- Durasi audio: 10–20 detik
- Kualitas audio jernih
- Tidak ada kebisingan latar belakang
-
Periksa parameter permintaan
Konfirmasi bahwa parameter
voicedalam permintaan sintesis ucapan diatur ke ID suara kloning.
T: Apa yang harus saya lakukan jika audio yang disintesis dari suara kloning tidak stabil atau tidak lengkap?
Jika audio yang disintesis dari suara kloning memiliki salah satu masalah berikut:
- Pemutaran audio tidak lengkap, hanya sebagian teks yang diucapkan
- Kualitas sintesis tidak konsisten
- Audio berisi jeda abnormal atau segmen diam
Kemungkinan penyebab: Audio sumber tidak memenuhi persyaratan kualitas.
Solusi: Periksa apakah audio sumber memenuhi persyaratan di Panduan perekaman untuk kloning suara. Rekam ulang audio mengikuti panduan perekaman.
T: Mengapa durasi aktual audio yang disintesis berbeda dari durasi yang ditunjukkan di file WAV?
Sintesis ucapan menggunakan mekanisme streaming yang mengembalikan data saat dihasilkan. Durasi di header file WAV yang disimpan bersifat perkiraan dan mungkin tidak akurat. Untuk durasi yang tepat, atur format ke pcm, tunggu hingga hasil sintesis lengkap, lalu tambahkan header file WAV secara manual.
T: Mengapa file audio tidak dapat diputar?
Pecahkan masalah berdasarkan skenario Anda:
-
Audio disimpan sebagai file lengkap (misalnya, xx.mp3)
- Konsistensi format audio: Format audio dalam parameter permintaan harus sesuai dengan ekstensi file (misalnya, jika parameter adalah wav, file harus berekstensi .wav).
- Kompatibilitas pemutar: Pastikan pemutar mendukung format audio dan laju sampel tersebut.
-
Pemutaran audio streaming
- Simpan aliran audio sebagai file lengkap dan coba putar dengan pemutar media. Jika file tidak dapat diputar, rujuk ke skenario 1 di atas.
- Jika file dapat diputar dengan benar, masalahnya terletak pada implementasi pemutaran streaming. Pastikan pemutar mendukung pemutaran streaming (seperti ffmpeg, pyaudio, AudioFormat, atau MediaSource).
T: Mengapa pemutaran audio tersendat?
Pecahkan masalah dengan langkah-langkah berikut:
-
Periksa laju pengiriman teks: Pastikan interval pengiriman masuk akal agar segmen audio sebelumnya tidak selesai sebelum teks berikutnya tiba.
-
Periksa kinerja fungsi callback:
- Pastikan tidak ada logika pemblokiran di fungsi callback.
- Callback berjalan di thread WebSocket. Operasi pemblokiran akan memengaruhi penerimaan data. Tulis data audio ke buffer terpisah dan proses di thread lain.
-
Periksa stabilitas jaringan: Fluktuasi jaringan dapat menyebabkan gangguan atau penundaan transmisi audio.
T: Mengapa sintesis ucapan memakan waktu lama?
Pecahkan masalah dengan langkah-langkah berikut:
-
Periksa interval input
Untuk sintesis streaming, periksa apakah interval pengiriman teks terlalu lama. Interval yang panjang meningkatkan total waktu sintesis.
-
Analisis metrik kinerja
- Latensi paket pertama: biasanya sekitar 500 ms.
- RTF (Real-Time Factor = total waktu sintesis / durasi audio): harus kurang dari 1,0.
T: Bagaimana cara membatasi Kunci API hanya untuk layanan sintesis ucapan (isolasi izin)?
Buat ruang kerja baru dan berikan akses hanya ke model tertentu. Hal ini membatasi cakupan Kunci API. Untuk detailnya, lihat Kelola ruang kerja.