Converta texto em fala com baixa latência no primeiro pacote. A síntese de fala em tempo real oferece entrada e saída em streaming, clonagem de voz, design de voz e controles de áudio refinados para assistentes de voz, audiolivros e atendimento ao cliente inteligente.
Visão geral
Transforme texto em fala instantaneamente e com baixa latência.
- Entrada e saída em streaming com baixa latência no primeiro pacote
- Controle granular de áudio com ajustes de velocidade da fala, tom, volume e taxa de bits
- Compatibilidade com os principais formatos de áudio (PCM, WAV, MP3, Opus) e taxa de amostragem de até 48 kHz
- Suporte a Instruction control, que permite controlar a expressividade da fala por meio de instruções em linguagem natural
- Recursos de Voice cloning e Voice Design para criação de vozes personalizadas
- Integração com Emotion and rich language tags, possibilitando a inserção de tags de emoção ou efeitos sonoros diretamente no texto
Para cenários em lote, como narração de audiolivros e materiais didáticos, utilize Non-real-time speech synthesis. Para orientações sobre a escolha do modelo, consulte Speech synthesis.
Pré-requisitos
- Configure an API key e set it as an environment variable.
- Caso pretenda chamar a API pelo DashScope SDK, install the latest SDK.
- Para utilizar o protocolo AOQ com modelos CosyVoice, baixe e integre o SDK do cliente AOQ. Para mais detalhes, consulte SDK overview.
Início rápido
Os exemplos a seguir demonstram a síntese de fala para cada modelo. Para ver mais exemplos e detalhes dos parâmetros, acesse API reference.
Qwen-Audio-TTS
Este exemplo realiza a síntese de fala utilizando uma voz do sistema.
Para aproveitar o recurso Instruction control, defina as instruções por meio do parâmetro instruction.
# coding=utf-8
import os
import dashscope
from dashscope.audio.tts_v2 import *
# The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
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: Use voices such as longanhuan_v3.6.
# Each voice supports different languages. To synthesize non-Chinese languages such as Japanese or Korean, select a voice that supports the target language. See the voice list for details.
model = "qwen-audio-3.0-tts-flash"
# Voice
voice = "longanhuan_v3.6"
# Instantiate SpeechSynthesizer and pass request parameters such as model and voice in the constructor
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# Send the text to be synthesized and get the binary audio
audio = synthesizer.call("How is the weather today?")
# The first text submission requires establishing a WebSocket connection, so the first-packet latency includes connection setup time
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
synthesizer.get_last_request_id(),
synthesizer.get_first_package_delay()))
# Save the audio to a local file
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: Use voices such as longanhuan_v3.6.
// Each voice supports different languages. To synthesize non-Chinese languages such as Japanese or Korean, select a voice that supports the target language. See the voice list for details.
private static String model = "qwen-audio-3.0-tts-flash";
// Voice
private static String voice = "longanhuan_v3.6";
public static void streamAudioDataToSpeaker() {
// Request parameters
SpeechSynthesisParam param =
SpeechSynthesisParam.builder()
// The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: .apiKey("sk-xxx")
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.model(model) // Model
.voice(voice) // Voice
.build();
// Synchronous mode: disable callback (second parameter is null)
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
ByteBuffer audio = null;
try {
// Block until audio is returned
audio = synthesizer.call("How is the weather today?");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// Close the WebSocket connection when the task is done
synthesizer.getDuplexApi().close(1000, "bye");
}
if (audio != null) {
// Save the audio data to the local file "output.mp3"
File file = new File("output.mp3");
// The first text submission requires establishing a WebSocket connection, so the first-packet latency includes connection setup time
// Note: getFirstPackageDelay() requires dashscope-sdk-java 2.18.0 or later
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) {
// The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
streamAudioDataToSpeaker();
System.exit(0);
}
}
CosyVoice
Além do WebSocket, este modelo também suporta o protocolo AOQ. Recomendamos o uso do AOQ para integrações no lado do cliente que exigem latência estável, resiliência em redes instáveis e supressão de ruído full-duplex com cancelamento de eco integrados. Para comparar os protocolos, veja Realtime API overview.
ImportanteOs modelos cosyvoice-v3.5-plus e cosyvoice-v3.5-flash estão disponíveis apenas na região de Beijing e suportam exclusivamente cenários de design de voz e clonagem de voz (não incluem vozes do sistema). Antes de utilizá-los, crie uma voz personalizada por meio de Voice cloning ou Voice Design. Em seguida, no seu código, defina voice com o ID da voz criada e model com o nome do modelo correspondente.
O exemplo abaixo sintetiza fala usando uma voz do sistema (consulte CosyVoice Voice list).
Para habilitar o recurso Instruction control, configure as instruções através do parâmetro instruction.
# coding=utf-8
import os
import dashscope
from dashscope.audio.tts_v2 import *
# The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
# Model
# Different model versions require their corresponding voices:
# cosyvoice-v3-flash/cosyvoice-v3-plus: Use voices such as longanyang.
# cosyvoice-v2: Use voices such as longxiaochun_v2.
# Each voice supports different languages. To synthesize non-Chinese languages such as Japanese or Korean, select a voice that supports the target language. See the Qwen-Audio-TTS/CosyVoice voice list for details.
model = "cosyvoice-v3-flash"
# Voice
voice = "longanyang"
# Instantiate SpeechSynthesizer and pass request parameters such as model and voice in the constructor
synthesizer = SpeechSynthesizer(model=model, voice=voice)
# Send the text to be synthesized and get the binary audio
audio = synthesizer.call("How is the weather today?")
# The first text submission requires establishing a WebSocket connection, so the first-packet latency includes connection setup time
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
synthesizer.get_last_request_id(),
synthesizer.get_first_package_delay()))
# Save the audio to a local file
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
// Different model versions require their corresponding voices:
// cosyvoice-v3-flash/cosyvoice-v3-plus: Use voices such as longanyang.
// cosyvoice-v2: Use voices such as longxiaochun_v2.
// Each voice supports different languages. To synthesize non-Chinese languages such as Japanese or Korean, select a voice that supports the target language. See the Qwen-Audio-TTS/CosyVoice voice list for details.
private static String model = "cosyvoice-v3-flash";
// Voice
private static String voice = "longanyang";
public static void streamAudioDataToSpeaker() {
// Request parameters
SpeechSynthesisParam param =
SpeechSynthesisParam.builder()
// The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: .apiKey("sk-xxx")
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.model(model) // Model
.voice(voice) // Voice
.build();
// Synchronous mode: disable callback (second parameter is null)
SpeechSynthesizer synthesizer = new SpeechSynthesizer(param, null);
ByteBuffer audio = null;
try {
// Block until audio is returned
audio = synthesizer.call("How is the weather today?");
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// Close the WebSocket connection when the task is done
synthesizer.getDuplexApi().close(1000, "bye");
}
if (audio != null) {
// Save the audio data to the local file "output.mp3"
File file = new File("output.mp3");
// The first text submission requires establishing a WebSocket connection, so the first-packet latency includes connection setup time
// Note: getFirstPackageDelay() requires dashscope-sdk-java 2.18.0 or later
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) {
// The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
streamAudioDataToSpeaker();
System.exit(0);
}
}
Qwen-TTS
O exemplo a seguir sintetiza fala usando uma voz do sistema (veja Supported voices).
Para utilizar o recurso Instruction control, substitua model por qwen3-tts-instruct-flash-realtime e especifique as instruções no parâmetro 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():
"""
Set your DashScope API key. More information:
https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
"""
# API keys differ between the Singapore and Beijing regions. Get an API key: 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'] # Load API key from environment variable DASHSCOPE_API_KEY
else:
dashscope.api_key = 'your-dashscope-api-key' # Set API key manually
class MyCallback(QwenTtsRealtimeCallback):
def __init__(self):
self.complete_event = threading.Event()
self.file = open('result_24k.pcm', 'wb')
def on_open(self) -> None:
print('connection opened, init player')
def on_close(self, close_status_code, close_msg) -> None:
self.file.close()
print('connection closed with code: {}, msg: {}, destroy player'.format(close_status_code, close_msg))
def on_event(self, response: str) -> None:
try:
global qwen_tts_realtime
type = response['type']
if 'session.created' == type:
print('start session: {}'.format(response['session']['id']))
if 'response.audio.delta' == type:
recv_audio_b64 = response['delta']
self.file.write(base64.b64decode(recv_audio_b64))
if 'response.done' == type:
print(f'response {qwen_tts_realtime.get_last_response_id()} done')
if 'session.finished' == type:
print('session finished')
self.complete_event.set()
except Exception as e:
print('[Error] {}'.format(e))
return
def wait_for_finished(self):
self.complete_event.wait()
if __name__ == '__main__':
init_dashscope_api_key()
print('Initializing ...')
callback = MyCallback()
qwen_tts_realtime = QwenTtsRealtime(
# To use instruction control, replace the model with qwen3-tts-instruct-flash-realtime
model='qwen3-tts-flash-realtime',
callback=callback,
# Singapore region
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,
# To use instruction control, uncomment the following lines and replace the model with qwen3-tts-instruct-flash-realtime
# instructions='Speak quickly with a rising intonation, suitable for introducing fashion products.',
# optimize_instructions=True,
mode = 'server_commit'
)
for text_chunk in text_to_synthesize:
print(f'send text: {text_chunk}')
qwen_tts_realtime.append_text(text_chunk)
time.sleep(0.1)
qwen_tts_realtime.finish()
callback.wait_for_finished()
print('[Metric] session: {}, first audio delay: {}'.format(
qwen_tts_realtime.get_session_id(),
qwen_tts_realtime.get_first_audio_delay(),
))
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():
"""
Set your DashScope API key. More information:
https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
"""
# API keys differ between the Singapore and Beijing regions. Get an API key: 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'] # Load API key from environment variable DASHSCOPE_API_KEY
else:
dashscope.api_key = 'your-dashscope-api-key' # Set API key manually
class MyCallback(QwenTtsRealtimeCallback):
def __init__(self):
super().__init__()
self.response_counter = 0
self.complete_event = threading.Event()
self.file = open(f'result_{self.response_counter}_24k.pcm', 'wb')
def reset_event(self):
self.response_counter += 1
self.file = open(f'result_{self.response_counter}_24k.pcm', 'wb')
self.complete_event = threading.Event()
def on_open(self) -> None:
print('connection opened, init player')
def on_close(self, close_status_code, close_msg) -> None:
print('connection closed with code: {}, msg: {}, destroy player'.format(close_status_code, close_msg))
def on_event(self, response: str) -> None:
try:
global qwen_tts_realtime
type = response['type']
if 'session.created' == type:
print('start session: {}'.format(response['session']['id']))
if 'response.audio.delta' == type:
recv_audio_b64 = response['delta']
self.file.write(base64.b64decode(recv_audio_b64))
if 'response.done' == type:
print(f'response {qwen_tts_realtime.get_last_response_id()} done')
self.complete_event.set()
self.file.close()
if 'session.finished' == type:
print('session finished')
self.complete_event.set()
except Exception as e:
print('[Error] {}'.format(e))
return
def wait_for_response_done(self):
self.complete_event.wait()
if __name__ == '__main__':
init_dashscope_api_key()
print('Initializing ...')
callback = MyCallback()
qwen_tts_realtime = QwenTtsRealtime(
# To use instruction control, replace the model with qwen3-tts-instruct-flash-realtime
model='qwen3-tts-flash-realtime',
callback=callback,
# Singapore region
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,
# To use instruction control, uncomment the following lines and replace the model with qwen3-tts-instruct-flash-realtime
# instructions='Speak quickly with a rising intonation, suitable for introducing fashion products.',
# optimize_instructions=True,
mode = 'commit'
)
print(f'send text: {text_to_synthesize[0]}')
qwen_tts_realtime.append_text(text_to_synthesize[0])
qwen_tts_realtime.commit()
callback.wait_for_response_done()
callback.reset_event()
print(f'send text: {text_to_synthesize[1]}')
qwen_tts_realtime.append_text(text_to_synthesize[1])
qwen_tts_realtime.commit()
callback.wait_for_response_done()
callback.reset_event()
print(f'send text: {text_to_synthesize[2]}')
qwen_tts_realtime.append_text(text_to_synthesize[2])
qwen_tts_realtime.commit()
callback.wait_for_response_done()
qwen_tts_realtime.finish()
print('[Metric] session: {}, first audio delay: {}'.format(
qwen_tts_realtime.get_session_id(),
qwen_tts_realtime.get_first_audio_delay(),
))
Java
Modo 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;
// Real-time PCM audio player
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();
// Initialize the audio format and audio line.
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);
// Write audio data to 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();
}
// Play an audio chunk and block until playback completes.
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);
// Wait for the buffered audio to finish playing.
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();
// Save the complete audio file.
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()
// To use instruction control, replace the model with qwen3-tts-instruct-flash-realtime.
.model("qwen3-tts-flash-realtime")
// China (Beijing) region
.url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
// API keys differ between Singapore and China (Beijing). See 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);
// Create a real-time audio player instance.
RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);
QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
@Override
public void onOpen() {
// Handle connection establishment.
}
@Override
public void onEvent(JsonObject message) {
String type = message.get("type").getAsString();
switch(type) {
case "session.created":
// Handle session creation.
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();
// Play audio in real time.
audioPlayer.write(recvAudioB64);
break;
case "response.done":
// Handle response completion.
break;
case "session.finished":
// Handle session termination.
completeLatch.get().countDown();
default:
break;
}
}
@Override
public void onClose(int code, String reason) {
// Handle connection closure.
}
});
qwenTtsRef.set(qwenTtsRealtime);
try {
qwenTtsRealtime.connect();
} catch (NoApiKeyException e) {
throw new RuntimeException(e);
}
QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
.voice("Cherry")
.responseFormat(ttsFormat)
.mode("server_commit")
// To use instruction control, uncomment the following lines and replace the model with 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();
// Wait for audio playback to complete, then shut down the player.
audioPlayer.waitForComplete();
audioPlayer.shutdown();
System.exit(0);
}
}
Modo 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;
// Real-time PCM audio player
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();
// Initialize the audio format and audio line.
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);
// Write audio data to 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();
}
// Play an audio chunk and block until playback completes.
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);
// Wait for the buffered audio to finish playing.
Thread.sleep(audioLength - 10);
}
public void write(String b64Audio) {
b64AudioBuffer.add(b64Audio);
}
public void cancel() {
b64AudioBuffer.clear();
RawAudioBuffer.clear();
}
public void waitForComplete() throws InterruptedException {
// Wait for all buffered audio data to finish playing.
while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
Thread.sleep(100);
}
// Wait for the audio line to drain.
line.drain();
}
public void shutdown() throws InterruptedException {
stopped.set(true);
decoderThread.join();
playerThread.join();
// Save the complete audio file.
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()
// To use instruction control, replace the model with qwen3-tts-instruct-flash-realtime.
.model("qwen3-tts-flash-realtime")
// China (Beijing) region
.url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
// API keys differ between Singapore and China (Beijing). See 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));
// Create a real-time player instance.
RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);
final AtomicReference<QwenTtsRealtime> qwenTtsRef = new AtomicReference<>(null);
QwenTtsRealtime qwenTtsRealtime = new QwenTtsRealtime(param, new QwenTtsRealtimeCallback() {
@Override
public void onOpen() {
System.out.println("connection opened");
System.out.println("Enter text and press Enter to send. Enter 'quit' to exit the program.");
}
@Override
public void onEvent(JsonObject message) {
String type = message.get("type").getAsString();
switch(type) {
case "session.created":
System.out.println("start session: " + message.get("session").getAsJsonObject().get("id").getAsString());
break;
case "response.audio.delta":
String recvAudioB64 = message.get("delta").getAsString();
byte[] rawAudio = Base64.getDecoder().decode(recvAudioB64);
// Play audio in real time.
audioPlayer.write(recvAudioB64);
break;
case "response.done":
System.out.println("response done");
// Wait for audio playback to complete.
try {
audioPlayer.waitForComplete();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
// Prepare for the next input.
completeLatch.get().countDown();
break;
case "session.finished":
System.out.println("session finished");
if (qwenTtsRef.get() != null) {
System.out.println("[Metric] response: " + qwenTtsRef.get().getResponseId() +
", first audio delay: " + qwenTtsRef.get().getFirstAudioDelay() + " ms");
}
completeLatch.get().countDown();
default:
break;
}
}
@Override
public void onClose(int code, String reason) {
System.out.println("connection closed code: " + code + ", reason: " + reason);
try {
// Wait for playback to complete, then shut down the player.
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")
// To use instruction control, uncomment the following lines and replace the model with qwen3-tts-instruct-flash-realtime.
// .instructions("")
// .optimizeInstructions(true)
.build();
qwenTtsRealtime.updateSession(config);
// Read user input in a loop.
while (true) {
System.out.print("Enter the text to synthesize: ");
String text = scanner.nextLine();
// Exit when the user enters 'quit'.
if ("quit".equalsIgnoreCase(text.trim())) {
System.out.println("Closing the connection...");
qwenTtsRealtime.finish();
completeLatch.get().await();
break;
}
// Skip empty input.
if (text.trim().isEmpty()) {
continue;
}
// Re-initialize the countdown latch.
completeLatch.set(new CountDownLatch(1));
// Send the text.
qwenTtsRealtime.appendText(text);
qwenTtsRealtime.commit();
// Wait for the current synthesis to complete.
completeLatch.get().await();
}
// Clean up resources.
audioPlayer.waitForComplete();
audioPlayer.shutdown();
scanner.close();
System.exit(0);
}
}
Configuração de sessão
Modos de interação do Qwen-TTS
A API em tempo real do Qwen-TTS oferece dois modos de interação:
- Modo server_commit: O servidor gerencia automaticamente a segmentação de texto e o tempo de síntese. Ideal para síntese contínua de grandes blocos de texto. O cliente adiciona texto sem precisar gerenciar a segmentação ou o envio.
- Modo commit: O cliente envia explicitamente o buffer de texto para acionar a síntese. Recomendado para cenários que exigem controle preciso sobre o momento da síntese, como a síntese por turno em IA conversacional.
Alternar entre modos de interação:
- WebSocket: Defina o campo
modeno eventosession.update.
{
"type": "session.update",
"session": {
"mode": "server_commit"
}
}
- SDK Python: Configure o parâmetro
modeno métodoupdate_session.
qwen_tts_realtime.update_session(
voice='Cherry',
response_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
mode='server_commit'
)
- SDK Java: Especifique o parâmetro
modepor meio deQwenTtsRealtimeConfig.builder().
QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
.voice("Cherry")
.responseFormat(ttsFormat)
.mode("server_commit")
.build();
qwenTtsRealtime.updateSession(config);
Para exemplos completos de código dos SDKs, consulte Python SDK e Java SDK. Para detalhes sobre o ciclo de vida de eventos WebSocket e reutilização de conexões, veja WebSocket API reference.
Recursos avançados
Controle por instruções
O controle por instruções utiliza descrições em linguagem natural para ajustar o tom, a velocidade, a emoção e as características do timbre da fala, sem a necessidade de configurar parâmetros de áudio complexos.
Especificações de instruções por modelo:
Qwen-Audio-TTS
Modelos suportados: qwen-audio-3.0-tts-plus, qwen-audio-3.0-tts-flash
Vozes do sistema e vozes clonadas: aceitam qualquer instrução.
CosyVoice
Modelos suportados: cosyvoice-v3.5-plus, cosyvoice-v3.5-flash, cosyvoice-v3-plus, cosyvoice-v3-flash
Os requisitos de formato de instrução variam conforme o modelo:
-
cosyvoice-v3.5-plus, cosyvoice-v3.5-flash:
- Vozes clonadas ou personalizadas: aceitam qualquer instrução.
- Vozes do sistema: a versão v3.5 não oferece suporte a vozes do sistema.
-
cosyvoice-v3-plus:
- Vozes clonadas ou personalizadas: não oferecem suporte ao controle por instruções.
- Vozes do sistema: as instruções devem seguir um formato e conteúdo fixos. Consulte CosyVoice Voice list.
-
cosyvoice-v3-flash:
- Vozes clonadas ou personalizadas: aceitam qualquer instrução.
- Vozes do sistema: as instruções devem seguir um formato e conteúdo fixos. Consulte CosyVoice Voice list.
Uso: Especifique o conteúdo da instrução por meio do parâmetro instruction.
Idiomas suportados para o texto da instrução:
-
cosyvoice-v3.5-plus, cosyvoice-v3.5-flash:
- Vozes clonadas ou personalizadas: chinês, inglês, francês, alemão, japonês, coreano, russo, português, tailandês, indonésio e vietnamita.
- Vozes do sistema: a versão v3.5 não oferece suporte a vozes do sistema.
-
cosyvoice-v3-plus:
- Vozes clonadas ou personalizadas: chinês, inglês, francês, alemão, japonês, coreano e russo.
- Vozes do sistema: as instruções devem seguir um formato e conteúdo fixos. Consulte CosyVoice Voice list.
-
cosyvoice-v3-flash:
- Vozes clonadas ou personalizadas: chinês, inglês, francês, alemão, japonês, coreano e russo.
- Vozes do sistema: apenas chinês.
Limite de comprimento do texto da instrução: máximo de 100 caracteres. Caracteres chineses (incluindo chinês simplificado/tradicional, kanji japonês e hanja coreano) contam como 2 caracteres cada. Todos os demais caracteres (como pontuação, letras, dígitos, kana japonês e hangul coreano) contam como 1 caractere cada.
Qwen-TTS
Modelos suportados: Apenas modelos da série Qwen3-TTS-Instruct-Flash-Realtime.
Uso: Especifique o conteúdo da instrução por meio do parâmetro instructions.
Idiomas suportados para o texto da instrução: Apenas chinês e inglês.
Limite de comprimento do texto da instrução: Máximo de 1.600 tokens.
Casos de uso:
- Narração de audiolivros e radionovelas
- Locução para publicidade e vídeos promocionais
- Dublagem de personagens de jogos e animações
- Assistentes de voz com expressividade emocional
- Narração de documentários e telejornais
Como escrever descrições de voz eficazes:
-
Princípios fundamentais:
- Seja específico, não vago: Utilize termos que descrevam qualidades vocais, como "profundo", "nítido" ou "ritmo levemente acelerado". Evite palavras subjetivas ou imprecisas como "bonito" ou "normal".
- Aborde múltiplas dimensões: Uma boa descrição geralmente abrange vários aspectos (como gênero, idade e emoção). Escrever apenas "voz feminina" é amplo demais para gerar um timbre distinto.
- Mantenha a objetividade: Concentre-se nas características físicas e perceptivas da voz. Por exemplo, use "tom mais agudo com energia" em vez de "minha voz favorita".
- Priorize a originalidade: Descreva as qualidades vocais em vez de solicitar a imitação de pessoas específicas (como celebridades ou atores). O modelo não suporta imitações e isso pode acarretar riscos de direitos autorais.
- Seja conciso: Garanta que cada palavra tenha um propósito. Evite sinônimos repetitivos ou modificadores sem significado.
-
Referência de dimensões de descrição:
Combine as dimensões abaixo para descrever uma voz. Quanto mais dimensões você incluir, mais precisa será a saída.
Dimensão
Exemplos de descrições
Gênero
Masculino, feminino, andrógino
Idade
Criança (5-12), adolescente (13-18), jovem adulto (19-35), meia-idade (36-55), idoso (55+)
Tom
Agudo, médio, grave, levemente agudo, levemente grave
Velocidade
Rápida, moderada, lenta, levemente rápida, levemente lenta
Emoção
Alegre, calmo, gentil, sério, animado, sereno, suave
Características
Magnético, nítido, rouco, aveludado, doce, encorpado, potente
Caso de uso
Telejornal, locução publicitária, audiolivro, personagem de animação, assistente de voz, narração de documentário
-
Exemplos:
- Estilo de transmissão padrão: articulação clara e precisa com pronúncia perfeita
- Uma voz feminina jovem e animada, com ritmo mais rápido e entonação ascendente notável, adequada para apresentações de produtos de moda
- Um homem de meia-idade calmo, ritmo lento, voz profunda e magnética, adequado para leitura de notícias ou narração de documentários
- Uma mulher gentil e intelectual, por volta dos 30 anos, com tom uniforme, adequada para narração de audiolivros
- Uma voz infantil fofa, aproximadamente de uma menina de 8 anos, falando com uma qualidade levemente pueril, adequada para dublagem de personagens de animação
Dialetos
Esta seção descreve como produzir fala em dialetos chineses (como o dialeto de Henan, dialeto de Sichuan e cantonês). Os métodos de configuração variam conforme o modelo e o tipo de voz.
Configuração de dialeto por modelo:
Qwen-Audio-TTS
-
Vozes do sistema: Selecione um dos seguintes tipos de voz:
- Uma voz do sistema com suporte nativo a dialeto, que gera o dialeto correspondente sem configuração adicional.
- Uma voz compatível com Instruction control, configurável para gerar um dialeto específico por meio de texto de instrução.
-
Vozes clonadas: Configure por meio do recurso Instruction control. Por exemplo, defina o texto da instrução como
请用河南话表达.
Dialetos suportados: Consulte a coluna "Idiomas suportados" de cada modelo em Qwen-Audio-TTS.
CosyVoice
-
Vozes do sistema: Selecione um dos seguintes tipos de voz em CosyVoice Voice list:
- Uma voz do sistema com suporte nativo a dialeto (como
longshange_v3), que gera o dialeto correspondente sem configuração adicional. - Uma voz compatível com Instruction control, configurável para gerar um dialeto específico (como
longanhuan_v3), especificado por meio de texto de instrução.
- Uma voz do sistema com suporte nativo a dialeto (como
-
Vozes clonadas: Configure por meio do recurso Instruction control. Por exemplo, defina o texto da instrução como
请用河南话表达. -
Vozes personalizadas: não oferecem suporte a dialetos.
Dialetos suportados: Consulte a coluna "Idiomas suportados" de cada modelo em CosyVoice.
Exemplo: Utilize o modelo cosyvoice-v3-flash com a voz longanhuan_v3 e defina o texto da instrução como "请用河南话表达。" para gerar fala no dialeto de Henan.
# coding=utf-8
import os
import dashscope
from dashscope.audio.tts_v2 import *
# The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
# Model
# Different model versions require their corresponding voices:
# cosyvoice-v3-flash/cosyvoice-v3-plus: Use voices such as longanyang.
# cosyvoice-v2: Use voices such as longxiaochun_v2.
# Select the appropriate voice for your target language
model = "cosyvoice-v3-flash"
# Voice
voice = "longanhuan_v3"
# Instantiate SpeechSynthesizer and pass request parameters such as model and voice in the constructor
synthesizer = SpeechSynthesizer(model=model, voice=voice, instruction="请用河南话表达。")
# Send the text to be synthesized and get the binary audio
audio = synthesizer.call("叫你去买盐,你买回来一袋面,这不是弄啥嘞吗!")
# The first text submission requires establishing a WebSocket connection, so the first-packet latency includes connection setup time
print('[Metric] requestId: {}, first-packet latency: {} ms'.format(
synthesizer.get_last_request_id(),
synthesizer.get_first_package_delay()))
# Save the audio to a local file
with open('output.mp3', 'wb') as f:
f.write(audio)
Qwen-TTS
- Vozes do sistema: Utilize uma voz do sistema com suporte nativo a dialeto. Consulte a lista de vozes do Qwen-TTS em Supported voices.
- Vozes clonadas: não oferecem suporte a dialetos.
- Vozes personalizadas: não oferecem suporte a dialetos.
Dialetos suportados: Consulte a coluna "Idiomas suportados" de cada modelo em Qwen3-TTS.
Tags de emoção e linguagem rica
Os modelos da série Qwen-Audio-TTS permitem incorporar tags de emoção e linguagem rica diretamente no texto a ser sintetizado (parâmetro text). Essas tags controlam a expressão emocional ou inserem efeitos vocais (como risadas e suspiros) em posições específicas, produzindo uma fala mais expressiva sem a necessidade de configurar parâmetros de áudio complexos.
ImportanteModelos suportados: Apenas qwen-audio-3.0-tts-plus e qwen-audio-3.0-tts-flash.
Limitação: Apenas o modo de streaming unidirecional é suportado.
As tags de controle definem a emoção ou o estilo da fala. Insira uma tag no texto para afetar todo o conteúdo subsequente até que outra tag de controle apareça ou a frase seja segmentada automaticamente devido ao comprimento.
Tag | Descrição |
|---|---|
| Triste |
| Espantado |
| Grito profundo e alto |
| Trêmulo |
| Com raiva |
| Empolgado |
| Sarcástico |
| Curioso |
| Estilo Drácula (profundo, sombrio) |
| Entediado |
| Cansado |
| Desdenhoso |
| Gritando |
| Sussurro suave ASMR |
| Em pânico |
| Malicioso |
| Empático |
| Sussurro |
| Relutante |
| Chorando |
| Sério |
| Fala muito lenta |
| Fala muito rápida |
As tags de linguagem rica inserem um efeito vocal na posição atual do texto, sem alterar o estilo emocional do conteúdo ao redor.
Tag | Descrição |
|---|---|
| Arquejo |
| Suspiro |
| Limpeza de garganta |
| Risadinha |
| Risada |
| Tosse |
| Bufada |
O exemplo a seguir demonstra como combinar tags de controle e tags de linguagem rica no parâmetro text:
[excited]What a beautiful day today![laughing]Let's go out and have fun together!
Neste texto, [excited] é uma tag de controle que aplica emoção de empolgação a todo o texto subsequente. Já [laughing] é uma tag de linguagem rica que insere uma risada naquela posição antes de continuar a síntese do restante do texto.
Também é possível alternar entre diferentes emoções no mesmo texto:
[serious]Please pay attention to the safety precautions.[excited]Alright, let's get started now!
Aqui, [serious] define a primeira frase com um tom sério, enquanto [excited] alterna para um tom empolgado a partir da segunda frase.
Cancelar tarefa
Caso precise interromper a rodada atual de síntese durante a geração de fala em tempo real, envie um comando de cancelamento. Após o cancelamento, o servidor encerra imediatamente a tarefa atual e retorna um evento de conclusão. É possível iniciar uma nova tarefa de síntese na mesma conexão WebSocket sem precisar reconectar.
Uso:
- Python SDK: Versão 1.26.4 ou posterior, chame
SpeechSynthesizer.streaming_cancel(). - Java SDK: Versão 2.22.26 ou posterior, chame
SpeechSynthesizer.streamingCancel(). - Protocolo bruto WebSocket: Envie um evento
finish-taske definadirective=canceleminput.
ImportanteLimitações de modelo:
- China (Beijing): Todos os modelos Qwen-Audio-TTS suportam este recurso. Os modelos CosyVoice exigem a versão v2 ou posterior.
- Singapore: Todos os modelos Qwen-Audio-TTS suportam este recurso. Os modelos CosyVoice não oferecem suporte a este recurso.
Chamadas diretas ao protocolo WebSocket
Os exemplos a seguir demonstram como se conectar diretamente ao servidor usando o protocolo nativo do WebSocket, ideal para cenários em que o DashScope SDK não está disponível. Estas são implementações mínimas e executáveis. Para obter detalhes sobre o protocolo WebSocket, consulte a referência da API de cada modelo.
Visualizar exemplos de chamada direta ao protocolo WebSocket
Qwen-TTS
-
Crie o cliente
Python
Crie um arquivo Python chamado
tts_realtime_client.pye copie o código a seguir para o arquivo:# -- 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: """ Client for interacting with the TTS Realtime API. This class provides methods for connecting to the TTS Realtime API, sending text data, receiving audio output, and managing WebSocket connections. Attributes: base_url (str): Base URL of the Realtime API. api_key (str): API Key for authentication. voice (str): Voice used for server-side speech synthesis. mode (SessionMode): Session mode, either server_commit or commit. audio_callback (Callable[[bytes], None]): Callback function for receiving audio data. language_type(str) Language for synthesized speech. Options: 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 # Current response state self._current_response_id = None self._current_item_id = None self._is_responding = False self._response_done_future = None async def connect(self) -> None: """Establish WebSocket connection with the TTS Realtime API.""" headers = { "Authorization": f"Bearer {self.api_key}" } self.ws = await websockets.connect(self.base_url, additional_headers=headers) # Set default session configuration await self.update_session({ "mode": self.mode.value, "voice": self.voice, # To use the instruction control feature, uncomment the lines below and replace the model with qwen3-tts-instruct-flash-realtime in server_commit.py or 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: """Send an event to the server.""" event['event_id'] = "event_" + str(int(time.time() * 1000)) print(f"Sending event: type={event['type']}, event_id={event['event_id']}") await self.ws.send(json.dumps(event)) async def update_session(self, config: Dict[str, Any]) -> None: """Update session configuration.""" event = { "type": "session.update", "session": config } print("Updating session configuration: ", event) await self.send_event(event) async def append_text(self, text: str) -> None: """Send text data to the API.""" event = { "type": "input_text_buffer.append", "text": text } await self.send_event(event) async def commit_text_buffer(self) -> None: """Commit text buffer to trigger processing.""" event = { "type": "input_text_buffer.commit" } await self.send_event(event) async def clear_text_buffer(self) -> None: """Clear the text buffer.""" event = { "type": "input_text_buffer.clear" } await self.send_event(event) async def finish_session(self) -> None: """End the session.""" event = { "type": "session.finish" } await self.send_event(event) async def wait_for_response_done(self): """Wait for the response.done event""" if self._response_done_future: await self._response_done_future async def handle_messages(self) -> None: """Handle messages from the server.""" try: async for message in self.ws: event = json.loads(message) event_type = event.get("type") if event_type != "response.audio.delta": print(f"Received event: {event_type}") if event_type == "error": print("Error: ", event.get('error', {})) continue elif event_type == "session.created": print("Session created, ID: ", event.get('session', {}).get('id')) elif event_type == "session.updated": print("Session updated, ID: ", event.get('session', {}).get('id')) elif event_type == "input_text_buffer.committed": print("Text buffer committed, item ID: ", event.get('item_id')) elif event_type == "input_text_buffer.cleared": print("Text buffer cleared") elif event_type == "response.created": self._current_response_id = event.get("response", {}).get("id") self._is_responding = True # Create a new future to wait for response.done self._response_done_future = asyncio.Future() print("Response created, ID: ", self._current_response_id) elif event_type == "response.output_item.added": self._current_item_id = event.get("item", {}).get("id") print("Output item added, ID: ", self._current_item_id) # Handle audio delta elif event_type == "response.audio.delta" and self.audio_callback: audio_bytes = base64.b64decode(event.get("delta", "")) self.audio_callback(audio_bytes) elif event_type == "response.audio.done": print("Audio generation completed") elif event_type == "response.done": self._is_responding = False self._current_response_id = None self._current_item_id = None # Mark future as done if self._response_done_future and not self._response_done_future.done(): self._response_done_future.set_result(True) print("Response completed") elif event_type == "session.finished": print("Session finished") except websockets.exceptions.ConnectionClosed: print("Connection closed") except Exception as e: print("Error handling messages: ", str(e)) async def close(self) -> None: """Close the WebSocket connection.""" if self.ws: await self.ws.close()Java
Crie um arquivo Java chamado
TTSRealtimeClient.javae copie o código abaixo para o arquivo: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; /** * A client for interacting with the TTS Realtime API. * * This class provides methods for connecting to the TTS Realtime API, sending text data, retrieving audio output, and managing WebSocket connections. */ 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; } } /** * Audio callback interface */ 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"); } /** * Establish a WebSocket connection to the TTS Realtime API. */ public void connect() throws Exception { Map<String, String> headers = new HashMap<>(); headers.put("Authorization", "Bearer " + apiKey); responseDoneLatch = new CountDownLatch(0); sessionFinishedLatch = new CountDownLatch(1); ws = new WebSocketClient(new URI(baseUrl), headers) { @Override public void onOpen(ServerHandshake handshake) { System.out.println("WebSocket connection established"); // Send default session configuration JsonObject session = new JsonObject(); session.addProperty("mode", mode.getValue()); session.addProperty("voice", TTSRealtimeClient.this.voice); // To use the instruction control feature, uncomment the lines below and replace the model with 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("Received event: " + eventType); } switch (eventType) { case "error": System.err.println("Error: " + event.get("error")); break; case "session.created": System.out.println("Session created, ID: " + event.getAsJsonObject("session").get("id").getAsString()); break; case "session.updated": System.out.println("Session updated, ID: " + event.getAsJsonObject("session").get("id").getAsString()); break; case "input_text_buffer.committed": System.out.println("Text buffer committed, item ID: " + event.get("item_id")); break; case "input_text_buffer.cleared": System.out.println("Text buffer cleared"); break; case "response.created": System.out.println("Response created, ID: " + event.getAsJsonObject("response").get("id").getAsString()); responseDoneLatch = new CountDownLatch(1); break; case "response.output_item.added": System.out.println("Output item added, 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("Audio generation completed"); break; case "response.done": System.out.println("Response completed"); responseDoneLatch.countDown(); break; case "session.finished": System.out.println("Session finished"); sessionFinishedLatch.countDown(); break; } } @Override public void onClose(int code, String reason, boolean remote) { System.out.println("Connection closed: " + reason); } @Override public void onError(Exception ex) { System.err.println("WebSocket error: " + ex.getMessage()); } }; ws.connectBlocking(); } /** * Send an event to the server. */ public void sendEvent(JsonObject event) { String eventId = "event_" + System.currentTimeMillis(); event.addProperty("event_id", eventId); System.out.println("Sending event: type=" + event.get("type").getAsString() + ", event_id=" + eventId); ws.send(gson.toJson(event)); } /** * Update the session configuration. */ public void updateSession(JsonObject config) { JsonObject event = new JsonObject(); event.addProperty("type", "session.update"); event.add("session", config); System.out.println("Updating session configuration: " + event); sendEvent(event); } /** * Send text data to the API. */ public void appendText(String text) { JsonObject event = new JsonObject(); event.addProperty("type", "input_text_buffer.append"); event.addProperty("text", text); sendEvent(event); } /** * Commit the text buffer to trigger processing. */ public void commitTextBuffer() { JsonObject event = new JsonObject(); event.addProperty("type", "input_text_buffer.commit"); sendEvent(event); } /** * Clear the text buffer. */ public void clearTextBuffer() { JsonObject event = new JsonObject(); event.addProperty("type", "input_text_buffer.clear"); sendEvent(event); } /** * End the session. */ public void finishSession() { JsonObject event = new JsonObject(); event.addProperty("type", "session.finish"); sendEvent(event); } /** * Wait for the response.done event. */ public void waitForResponseDone() throws InterruptedException { responseDoneLatch.await(); } /** * Wait for the session.finished event. */ public void waitForSessionFinished() throws InterruptedException { sessionFinishedLatch.await(); } /** * Close the WebSocket connection. */ public void close() { if (ws != null) { ws.close(); } } } -
Selecione um modo de síntese de fala
A Realtime API oferece suporte a dois modos:
-
Modo server_commit
O servidor gerencia automaticamente a segmentação de texto e o tempo de síntese. O cliente apenas envia o texto. Ideal para cenários de baixa latência, como navegação GPS.
-
Modo commit
O cliente adiciona texto a um buffer e aciona explicitamente a síntese. Recomendado para casos que exigem controle preciso da segmentação de frases, como transmissões de notícias.
Modo server_commit
Python
No mesmo diretório do arquivo
tts_realtime_client.py, crie outro arquivo Python chamadoserver_commit.pye copie o código abaixo para ele:import os import asyncio import logging import wave from tts_realtime_client import TTSRealtimeClient, SessionMode import pyaudio # QwenTTS service configuration # To use the instruction control feature, replace the model with qwen3-tts-instruct-flash-realtime and uncomment instructions in tts_realtime_client.py # The following is the configuration for the Singapore region. URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime" # The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key # If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: API_KEY="sk-xxx" API_KEY = os.getenv("DASHSCOPE_API_KEY") if not API_KEY: raise ValueError("Please set DASHSCOPE_API_KEY environment variable") # Collect audio data _audio_chunks = [] # Real-time playback related _AUDIO_SAMPLE_RATE = 24000 _audio_pyaudio = pyaudio.PyAudio() _audio_stream = None # Will be opened at runtime def _audio_callback(audio_bytes: bytes): """TTSRealtimeClient audio callback: real-time playback and caching""" global _audio_stream if _audio_stream is not None: try: _audio_stream.write(audio_bytes) except Exception as exc: logging.error(f"PyAudio playback error: {exc}") _audio_chunks.append(audio_bytes) logging.info(f"Received audio chunk: {len(audio_bytes)} bytes") def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool: """Save collected audio data as a WAV file""" if not _audio_chunks: logging.warning("No audio data to save") return False try: audio_data = b"".join(_audio_chunks) with wave.open(filename, 'wb') as wav_file: wav_file.setnchannels(1) # Mono wav_file.setsampwidth(2) # 16-bit wav_file.setframerate(sample_rate) wav_file.writeframes(audio_data) logging.info(f"Audio saved to: {filename}") return True except Exception as exc: logging.error(f"Failed to save audio: {exc}") return False async def _produce_text(client: TTSRealtimeClient): """Send text fragments to the 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("Sending text fragments…") for text in text_fragments: logging.info(f"Sending fragment: {text}") await client.append_text(text) await asyncio.sleep(0.1) # Brief delay between fragments # Wait for the server to finish internal processing before ending the session await asyncio.sleep(1.0) await client.finish_session() async def _run_demo(): """Run the complete demo""" global _audio_stream # Open PyAudio output stream _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 ) # Establish connection await client.connect() # Run message handling and text sending in parallel consumer_task = asyncio.create_task(client.handle_messages()) producer_task = asyncio.create_task(_produce_text(client)) await producer_task # Wait for text sending to complete # Wait for response.done await client.wait_for_response_done() # Close connection and cancel consumer task await client.close() consumer_task.cancel() # Close audio stream if _audio_stream is not None: _audio_stream.stop_stream() _audio_stream.close() _audio_pyaudio.terminate() # Save audio data os.makedirs("outputs", exist_ok=True) _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav")) def main(): """Synchronous entry point""" logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logging.info("Starting QwenTTS Realtime Client demo…") asyncio.run(_run_demo()) if __name__ == "__main__": main()Execute o arquivo
server_commit.pypara ouvir em tempo real o áudio gerado pela Realtime API.Java
No mesmo diretório do arquivo
TTSRealtimeClient.java, crie outro arquivo Java chamadoServerCommit.javae copie o código abaixo para ele: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 { // The following is the configuration for the Singapore region. private static final String URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"; // The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key // If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: private static final String API_KEY = "sk-xxx"; private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY"); private static final int SAMPLE_RATE = 24000; // Audio data cache private static final List<byte[]> audioChunks = new ArrayList<>(); // Real-time playback queue 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("Please set the DASHSCOPE_API_KEY environment variable"); } // Initialize audio playback 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(); // Start the playback thread 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(); // Create the TTS client // To use the instruction control feature, replace the model with qwen3-tts-instruct-flash-realtime and uncomment the instructions in TTSRealtimeClient.java TTSRealtimeClient client = new TTSRealtimeClient( URL, API_KEY, "Cherry", TTSRealtimeClient.SessionMode.SERVER_COMMIT, audioData -> { playbackQueue.add(audioData); audioChunks.add(audioData); System.out.println("Received audio data: " + audioData.length + " bytes"); } ); client.connect(); // Send text fragments 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("Starting to send text..."); for (String text : textFragments) { System.out.println("Sending fragment: " + text); client.appendText(text); Thread.sleep(100); } Thread.sleep(1000); client.finishSession(); // Wait for the response to complete client.waitForResponseDone(); client.waitForSessionFinished(); client.close(); // Wait for playback to complete playing.set(false); playerThread.join(); audioLine.drain(); audioLine.close(); // Save the audio file saveWav("output.wav"); System.out.println("Done"); } private static void saveWav(String filename) throws IOException { if (audioChunks.isEmpty()) { System.out.println("No audio data to save"); 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 saved to: outputs/" + filename); } }Compile e execute o arquivo
ServerCommit.javapara ouvir em tempo real o áudio gerado pela Realtime API.Modo commit
Python
No mesmo diretório do arquivo
tts_realtime_client.py, crie outro arquivo Python chamadocommit.pye copie o código abaixo para ele:import os import asyncio import logging import wave from tts_realtime_client import TTSRealtimeClient, SessionMode import pyaudio # QwenTTS service configuration # To use the instruction control feature, replace the model with qwen3-tts-instruct-flash-realtime and uncomment instructions in tts_realtime_client.py # The following is the configuration for the Singapore region. URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime" # The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key # If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: API_KEY="sk-xxx" API_KEY = os.getenv("DASHSCOPE_API_KEY") if not API_KEY: raise ValueError("Please set DASHSCOPE_API_KEY environment variable") # Collect audio data _audio_chunks = [] _AUDIO_SAMPLE_RATE = 24000 _audio_pyaudio = pyaudio.PyAudio() _audio_stream = None def _audio_callback(audio_bytes: bytes): """TTSRealtimeClient audio callback: real-time playback and caching""" global _audio_stream if _audio_stream is not None: try: _audio_stream.write(audio_bytes) except Exception as exc: logging.error(f"PyAudio playback error: {exc}") _audio_chunks.append(audio_bytes) logging.info(f"Received audio chunk: {len(audio_bytes)} bytes") def _save_audio_to_file(filename: str = "output.wav", sample_rate: int = 24000) -> bool: """Save collected audio data as a WAV file""" if not _audio_chunks: logging.warning("No audio data to save") return False try: audio_data = b"".join(_audio_chunks) with wave.open(filename, 'wb') as wav_file: wav_file.setnchannels(1) # Mono wav_file.setsampwidth(2) # 16-bit wav_file.setframerate(sample_rate) wav_file.writeframes(audio_data) logging.info(f"Audio saved to: {filename}") return True except Exception as exc: logging.error(f"Failed to save audio: {exc}") return False async def _user_input_loop(client: TTSRealtimeClient): """Continuously get user input and send text. When user enters empty text, send a commit event and end the current session""" print("Enter text (press Enter directly to send a commit event and end the current session, press Ctrl+C or Ctrl+D to exit the program):") while True: try: user_text = input("> ") if not user_text: # User input is empty # Empty input is treated as end of a conversation: commit buffer -> end session -> break loop logging.info("Empty input, sending commit event and ending current session") await client.commit_text_buffer() # Wait briefly for the server to process the commit, preventing premature session end that could lose audio await asyncio.sleep(0.3) await client.finish_session() break # Exit user input loop directly, no need to press Enter again else: logging.info(f"Sending text: {user_text}") await client.append_text(user_text) except EOFError: # User pressed Ctrl+D break except KeyboardInterrupt: # User pressed Ctrl+C break # End session logging.info("Ending session...") async def _run_demo(): """Run the complete demo""" global _audio_stream # Open PyAudio output stream _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, # Changed to COMMIT mode audio_callback=_audio_callback ) # Establish connection await client.connect() # Run message handling and user input in parallel consumer_task = asyncio.create_task(client.handle_messages()) producer_task = asyncio.create_task(_user_input_loop(client)) await producer_task # Wait for user input to complete # Wait for response.done await client.wait_for_response_done() # Close connection and cancel consumer task await client.close() consumer_task.cancel() # Close audio stream if _audio_stream is not None: _audio_stream.stop_stream() _audio_stream.close() _audio_pyaudio.terminate() # Save audio data os.makedirs("outputs", exist_ok=True) _save_audio_to_file(os.path.join("outputs", "qwen_tts_output.wav")) def main(): logging.basicConfig( level=logging.INFO, format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logging.info("Starting QwenTTS Realtime Client demo…") asyncio.run(_run_demo()) if __name__ == "__main__": main()Execute o arquivo
commit.py. É possível inserir texto para síntese quantas vezes desejar. Pressione Enter sem digitar nenhum texto para ouvir, pelo alto-falante, o áudio retornado pela Realtime API.Java
No mesmo diretório do arquivo
TTSRealtimeClient.java, crie outro arquivo Java chamadoCommit.javae copie o código abaixo para ele: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 { // The following is the configuration for the Singapore region. private static final String URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-tts-flash-realtime"; // The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key // If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: private static final String API_KEY = "sk-xxx"; private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY"); private static final int SAMPLE_RATE = 24000; private static final List<byte[]> audioChunks = new ArrayList<>(); private static final ConcurrentLinkedQueue<byte[]> playbackQueue = new ConcurrentLinkedQueue<>(); private static final AtomicBoolean playing = new AtomicBoolean(true); public static void main(String[] args) throws Exception { if (API_KEY == null || API_KEY.isEmpty()) { throw new IllegalStateException("Please set the DASHSCOPE_API_KEY environment variable"); } // Initialize audio playback 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(); // Start the playback thread 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(); // Create a TTS client (commit mode) // To use instruction control, replace the model with qwen3-tts-instruct-flash-realtime and uncomment the instructions in TTSRealtimeClient.java TTSRealtimeClient client = new TTSRealtimeClient( URL, API_KEY, "Cherry", TTSRealtimeClient.SessionMode.COMMIT, audioData -> { playbackQueue.add(audioData); audioChunks.add(audioData); System.out.println("Received audio data: " + audioData.length + " bytes"); } ); client.connect(); // Interactive input System.out.println("Enter text (press Enter directly to send a commit event and end the session, press Ctrl+D to exit the 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()) { // Empty input: commit the buffer and end the session System.out.println("Empty input, sending commit event and ending the session"); client.commitTextBuffer(); Thread.sleep(300); client.finishSession(); break; } else { System.out.println("Sending text: " + userText); client.appendText(userText); } } scanner.close(); // Wait for the response to complete client.waitForResponseDone(); client.waitForSessionFinished(); client.close(); // Wait for playback to complete playing.set(false); playerThread.join(); audioLine.drain(); audioLine.close(); // Save the audio file saveWav("output.wav"); System.out.println("Done"); } private static void saveWav(String filename) throws IOException { if (audioChunks.isEmpty()) { System.out.println("No audio data to save"); 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 saved to: outputs/" + filename); } }Compile e execute o arquivo
Commit.java. É possível inserir texto para síntese quantas vezes desejar. Pressione Enter sem digitar nenhum texto para ouvir, pelo alto-falante, o áudio retornado pela Realtime API. -
Modo server_commit
Aplicação em produção
Reutilização de conexão (WebSocket)
As conexões WebSocket são reutilizáveis. Após a conclusão de uma tarefa de síntese, inicie a próxima tarefa na mesma conexão sem precisar restabelecê-la.
Processo de reutilização:
- Qwen-Audio-TTS / Qwen-Audio-TTS/CosyVoice: O cliente envia
finish-taske, após o servidor retornartask-finished, o cliente pode enviarrun-taskpara iniciar uma nova tarefa. - Qwen-TTS: O cliente envia
session.finishe, depois que o servidor retornasession.finished, o cliente pode criar uma nova sessão para começar a próxima tarefa.
Reutilização após cancelamento: Para Qwen-Audio-TTS / Qwen-Audio-TTS/CosyVoice, caso você cancele a tarefa atual com a diretiva cancel, também é possível enviar um novo run-task na mesma conexão assim que o servidor retornar task-finished. Para mais detalhes, consulte Cancel task.
Importante
- Aguarde o evento de conclusão do servidor (
task-finishedousession.finished) antes de iniciar uma nova tarefa. - Qwen-Audio-TTS, Qwen-Audio-TTS/CosyVoice exigem um
task_iddiferente para cada tarefa em uma conexão reutilizada. - Se uma tarefa falhar, o servidor retorna um evento de erro e encerra a conexão, impedindo sua reutilização.
- Caso nenhuma nova tarefa seja iniciada dentro de 60 segundos após o término da anterior, a conexão será fechada automaticamente.
Para obter detalhes sobre os eventos de cada modelo, consulte o API reference correspondente.
Limites de taxa
As chamadas aos modelos estão sujeitas a limites de taxa. Quando um limite é excedido, o servidor retorna o erro Requests rate limit exceeded, please try again later. Reduza a taxa de requisições ou a concorrência e tente novamente.
Para verificar os limites de taxa de cada modelo, consulte Rate limiting.
Melhores práticas para alta concorrência
O DashScope SDK possui pooling integrado que reutiliza conexões WebSocket e objetos sintetizadores, eliminando a sobrecarga de criá-los e destruí-los repetidamente.
Visualizar melhores práticas para alta concorrência
Qwen-Audio-TTS/CosyVoice
Qwen-Audio-TTS e Qwen-Audio-TTS/CosyVoice compartilham a mesma interface do SDK. Os exemplos abaixo também se aplicam aos modelos Qwen-Audio-TTS — basta substituir os parâmetros model e voice.
Pré-requisitos
-
DashScope SDK instalado e compatível com os requisitos de versão. Recomendamos installing the latest version:
- Python SDK: versão >= 1.25.2
- Java SDK: versão >= 2.16.6
Python SDK
O Python SDK utiliza SpeechSynthesizerObjectPool para gerenciar e reutilizar objetos SpeechSynthesizer.
Esse pool cria um número especificado de instâncias SpeechSynthesizer e estabelece conexões WebSocket durante a inicialização. Ao solicitar um objeto emprestado, ele já está pronto para enviar requisições imediatamente, reduzindo a latência do primeiro pacote. Após a devolução do objeto, a conexão permanece ativa para a próxima tarefa.
Etapas de implementação
-
Instale as dependências: Instale a dependência do DashScope (
pip install -U dashscope). -
Crie e configure o pool de objetos
Defina o tamanho do pool entre 1,5x e 2x a concorrência de pico, sem exceder o limite de QPS da sua conta.
Crie um pool singleton global (o estabelecimento da conexão durante a inicialização leva algum tempo):
from dashscope.audio.tts_v2 import SpeechSynthesizerObjectPool
synthesizer_object_pool = SpeechSynthesizerObjectPool(max_size=20)
import dashscope
# The following is the configuration for the China (Beijing) region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
dashscope.base_http_api_url = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1"
Importante
- No cenário de pool de objetos,
SpeechSynthesizerObjectPoolestabelece conexões WebSocket com o servidor usando adashscope.api_keyglobal atual durante a inicialização. A chave de API é gravada no cabeçalhoAuthorizationapenas durante o handshake WebSocket para autenticação. Mensagens subsequentes da tarefa (comorun-task) não carregam a chave de API. Modificardashscope.api_keyapós a criação do pool não afeta as conexões existentes — objetos obtidos viaborrow_synthesizer(incluindo aqueles devolvidos e solicitados novamente) ainda usam a chave de API do handshake original. O novo valor é ignorado silenciosamente, o que pode fazer com que a identidade, cota ou atribuição de faturamento difira do esperado. Nota:borrow_synthesizernão suporta a especificação de uma chave de API como parâmetro. - Para usar múltiplas chaves de API, mantenha uma instância separada de
SpeechSynthesizerObjectPoolpara cada chave.
-
Solicite um objeto
SpeechSynthesizerdo poolSe a quantidade de objetos não devolvidos exceder a capacidade do pool, o sistema criará objetos adicionais.
Esses objetos extras precisam estabelecer novas conexões e não se beneficiam do pooling.
speech_synthesizer = connectionPool.borrow_synthesizer(
model='cosyvoice-v3-flash',
voice='longanyang',
seed=12382,
callback=synthesizer_callback
)
-
Execute a síntese de fala
Chame o método call ou streaming_call do objeto
SpeechSynthesizerpara sintetizar a fala. -
Devolva o objeto
SpeechSynthesizerDevolva o objeto após a conclusão da tarefa para disponibilizá-lo para reutilização.
Não devolva objetos com tarefas incompletas ou falhas.
connectionPool.return_synthesizer(speech_synthesizer)
Código completo
ImportanteAntes de usar este código: SpeechSynthesizerObjectPool estabelece conexões WebSocket e realiza autenticação usando a dashscope.api_key global atual na inicialização. Modificar dashscope.api_key após a criação do pool não afeta as conexões existentes — o novo valor é ignorado silenciosamente. Para múltiplas chaves de API, mantenha uma instância de pool separada para cada chave. Para mais detalhes, consulte a nota importante acima.
# !/usr/bin/env python3
# Copyright (C) Alibaba Group. All Rights Reserved.
# MIT License (https://opensource.org/licenses/MIT)
import os
import time
import threading
import dashscope
from dashscope.audio.tts_v2 import *
USE_CONNECTION_POOL = True
text_to_synthesize = [
'Sentence 1: Welcome to Alibaba speech synthesis service.',
'Sentence 2: Welcome to Alibaba speech synthesis service.',
'Sentence 3: Welcome to Alibaba speech synthesis service.',
]
connectionPool = None
def init_dashscope_api_key():
'''
Set your DashScope API-key. More information:
https://github.com/aliyun/alibabacloud-bailian-speech-demo/blob/master/PREREQUISITES.md
'''
# The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
if 'DASHSCOPE_API_KEY' in os.environ:
dashscope.api_key = os.environ[
'DASHSCOPE_API_KEY'] # load API-key from environment variable DASHSCOPE_API_KEY
else:
dashscope.api_key = '<your-dashscope-api-key>' # set API-key manually
def synthesis_text_to_speech_and_play_by_streaming_mode(text, task_id):
global USE_CONNECTION_POOL, connectionPool
'''
Synthesize speech with given text by streaming mode, async call and play the synthesized audio in real-time.
for more information, please refer to https://www.alibabacloud.com/help/document_detail/2712523.html
'''
complete_event = threading.Event()
# Define a callback to handle the result
class Callback(ResultCallback):
def on_open(self):
# when using object pool, on_open will be called after task start
self.file = open(f'result_{task_id}.mp3', 'wb')
print(f'[task_{task_id}] start')
def on_complete(self):
print(f'[task_{task_id}] speech synthesis task complete successfully.')
complete_event.set()
def on_error(self, message: str):
print(f'[task_{task_id}] speech synthesis task failed, {message}')
def on_close(self):
# when using object pool, on_open will be called after task finished
print(f'[task_{task_id}] finished')
def on_event(self, message):
# print(f'recv speech synthsis message {message}')
pass
def on_data(self, data: bytes) -> None:
# send to player
# save audio to file
self.file.write(data)
# Call the speech synthesizer callback
synthesizer_callback = Callback()
# Initialize the speech synthesizer
# you can customize the synthesis parameters, like voice, format, sample_rate or other parameters
if USE_CONNECTION_POOL:
speech_synthesizer = connectionPool.borrow_synthesizer(
model='cosyvoice-v3-flash',
voice='longanyang',
seed=12382,
callback=synthesizer_callback
)
else:
speech_synthesizer = SpeechSynthesizer(model='cosyvoice-v3-flash',
voice='longanyang',
seed=12382,
callback=synthesizer_callback)
try:
speech_synthesizer.call(text)
except Exception as e:
print(f'[task_{task_id}] speech synthesis task failed, {e}')
if USE_CONNECTION_POOL:
# close the synthesizer connection manually if task failed when using connection pool.
speech_synthesizer.close()
return
print('[task_{}] Synthesized text: {}'.format(task_id, text))
complete_event.wait()
print('[task_{}][Metric] requestId: {}, first package delay ms: {}'.format(
task_id,
speech_synthesizer.get_last_request_id(),
speech_synthesizer.get_first_package_delay()))
if USE_CONNECTION_POOL:
connectionPool.return_synthesizer(speech_synthesizer)
# main function
if __name__ == '__main__':
# You must set dashscope.api_key and base_websocket_api_url before creating SpeechSynthesizerObjectPool.
# The pool establishes WebSocket connections using the current global dashscope.api_key at initialization time.
# Modifying dashscope.api_key after pool creation will not affect existing connections in the pool.
# The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
init_dashscope_api_key()
if USE_CONNECTION_POOL:
print('creating connection pool')
start_time = time.time() * 1000
connectionPool = SpeechSynthesizerObjectPool(max_size=3)
end_time = time.time() * 1000
print('connection pool created, cost: {} ms'.format(end_time - start_time))
task_thread_list = []
for task_id in range(3):
thread = threading.Thread(
target=synthesis_text_to_speech_and_play_by_streaming_mode,
args=(text_to_synthesize[task_id], task_id))
task_thread_list.append(thread)
for task_thread in task_thread_list:
task_thread.start()
for task_thread in task_thread_list:
task_thread.join()
if USE_CONNECTION_POOL:
connectionPool.shutdown()
Gerenciamento de recursos e tratamento de erros
-
Tarefa bem-sucedida: Após a conclusão normal de uma tarefa de síntese, chame
connectionPool.return_synthesizer(speech_synthesizer)para devolver o objetoSpeechSynthesizerao pool para reutilização.ImportanteNão devolva objetos
SpeechSynthesizercom tarefas incompletas ou falhas. -
Falha na tarefa: Se um erro interno do SDK ou uma exceção de lógica de negócio causar a interrupção da tarefa, feche a conexão WebSocket subjacente:
speech_synthesizer.close() -
Após a conclusão de todas as tarefas de síntese, encerre o pool:
connectionPool.shutdown() -
Quando ocorrer um erro TaskFailed no lado do servidor, nenhum tratamento adicional é necessário.
Java SDK
O Java SDK alcança desempenho ideal através da coordenação entre um pool de conexões integrado e um pool de objetos personalizado.
- Pool de conexões: O pool de conexões OkHttp3 integrado ao SDK gerencia e reutiliza conexões WebSocket subjacentes, reduzindo a sobrecarga de handshake de rede. Este recurso é ativado por padrão.
- Pool de objetos: Construído sobre
commons-pool2, este pool mantém um conjunto de objetosSpeechSynthesizerpré-conectados. Solicitar um objeto do pool elimina a latência de configuração de conexão, reduzindo significativamente o atraso do primeiro pacote.
Etapas de implementação
-
Adicione as dependências
Inclua dashscope-sdk-java e commons-pool2 na configuração de dependências do seu projeto, conforme a ferramenta de build utilizada.
Exemplos para Maven e Gradle:
Maven
- Abra o arquivo
pom.xmldo seu projeto Maven. - Adicione as seguintes dependências dentro da tag
<dependencies>.
<dependency> <groupId>com.alibaba</groupId> <artifactId>dashscope-sdk-java</artifactId> <!-- Replace 'the-latest-version' with version 2.16.9 or later. Check available versions at: 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> <!-- Replace 'the-latest-version' with the latest version. Check available versions at: https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 --> <version>the-latest-version</version> </dependency>- Salve o arquivo
pom.xml. - Execute um comando Maven como
mvn clean installoumvn compilepara atualizar as dependências do projeto.
Gradle
- Abra o arquivo
build.gradledo seu projeto Gradle. - Adicione as seguintes dependências dentro do bloco
dependencies.
dependencies { // Replace 'the-latest-version' with version 2.16.6 or later. Check available versions at: https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java implementation group: 'com.alibaba', name: 'dashscope-sdk-java', version: 'the-latest-version' // Replace 'the-latest-version' with the latest version. Check available versions at: https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 implementation group: 'org.apache.commons', name: 'commons-pool2', version: 'the-latest-version' }- Salve o arquivo
build.gradle. - No terminal, navegue até o diretório raiz do projeto e execute o seguinte comando Gradle para atualizar as dependências.
./gradlew build --refresh-dependenciesNo Windows, utilize o seguinte comando:
gradlew build --refresh-dependencies - Abra o arquivo
-
Configure o pool de conexões
Defina os principais parâmetros do pool de conexões por meio de variáveis de ambiente:
Variável de ambiente
Descrição
DASHSCOPE_CONNECTION_POOL_SIZE
Tamanho do pool de conexões.
Valor recomendado: pelo menos 2x a concorrência de pico.
Padrão: 32.
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS
Número máximo de requisições assíncronas.
Valor recomendado: igual a
DASHSCOPE_CONNECTION_POOL_SIZE.Padrão: 32.
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST
Número máximo de requisições assíncronas por host.
Valor recomendado: igual a
DASHSCOPE_CONNECTION_POOL_SIZE.Padrão: 32.
-
Configure o pool de objetos
Defina o tamanho do pool de objetos através de variáveis de ambiente:
Variável de ambiente
Descrição
COSYVOICE_OBJECTPOOL_SIZE
Tamanho do pool de objetos.
Valor recomendado: 1,5x a 2x a concorrência de pico.
Padrão: 500.
Importante
- O tamanho do pool de objetos (
COSYVOICE_OBJECTPOOL_SIZE) deve ser menor ou igual ao tamanho do pool de conexões (DASHSCOPE_CONNECTION_POOL_SIZE). Caso contrário, quando o pool de objetos solicitar objetos e o pool de conexões estiver cheio, as threads de chamada serão bloqueadas aguardando conexões disponíveis. - O tamanho do pool de objetos não deve exceder o limite de QPS (consultas por segundo) da sua conta.
Crie o pool de objetos com o seguinte código:
- O tamanho do pool de objetos (
class CosyvoiceObjectPool {
// ... Other code is omitted here. For the complete example, see the full code.
public static GenericObjectPool<SpeechSynthesizer> getInstance() {
lock.lock();
if (synthesizerPool == null) {
// You can set the object pool size here, or set it in the environment variable COSYVOICE_OBJECTPOOL_SIZE.
// It is recommended to set it to 1.5 to 2 times the maximum concurrent connections of the 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;
}
}
-
Solicite um objeto
SpeechSynthesizerdo poolSe o número de objetos não devolvidos exceder a capacidade máxima do pool, o sistema criará um objeto
SpeechSynthesizeradicional.Esses objetos recém-criados exigem reinicialização e uma nova conexão WebSocket, portanto não se beneficiam do pooling.
synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
-
Execute a síntese de fala
Após obter um objeto
SpeechSynthesizerdo pool, chameupdateParamAndCallback(param, callback)para vincular os parâmetros e o callback da tarefa atual e, em seguida, chamestreamingCalloucallpara sintetizar a fala.Importante
- No cenário de pool de objetos,
updateParamAndCallbacké chamado várias vezes (uma vez sempre que um objeto é solicitado, para definir o callback e parâmetros de nível de tarefa comovoiceeformat). AapiKeypassada em cada chamada deve permanecer a mesma.updateParamAndCallbackapenas atualiza os campos locais da instânciaSpeechSynthesizeratual e não reconstrói a conexão WebSocket subjacente. O SDK grava aapiKeyno cabeçalhoAuthorizationsomente durante o handshake WebSocket para autenticação. Mensagens subsequentes da tarefa (comorun-task) não carregam aapiKey. Desde que a conexão reutilizada não seja desconectada, passar uma novaapiKeynão a enviará ao servidor — a requisição ainda usará aapiKeydo handshake inicial, o que pode fazer com que a identidade, cota ou atribuição de faturamento difira do esperado. - Para usar múltiplas chaves de API, mantenha instâncias de pool de objetos separadas para cada chave.
- No cenário de pool de objetos,
-
Devolva o objeto
SpeechSynthesizerApós a conclusão da tarefa de síntese, devolva o objeto
SpeechSynthesizerpara que tarefas subsequentes possam reutilizá-lo.Não devolva objetos com tarefas incompletas ou falhas.
CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
Código completo
ImportanteAntes de usar este código: no cenário de pool de objetos, a apiKey passada para updateParamAndCallback em múltiplas chamadas deve permanecer a mesma — o SDK não atualiza a API key de conexões já estabelecidas, e passar uma API key diferente não surte efeito. Para utilizar múltiplas API keys, mantenha uma instância separada de pool de objetos para cada chave. Para mais detalhes, consulte a nota importante acima.
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;
/**
* You need to include the org.apache.commons.pool2 and DashScope packages in your project.
*
* DashScope SDK 2.16.6 and later versions are optimized for high-concurrency scenarios.
* Versions earlier than DashScope SDK 2.16.6 are not recommended for high-concurrency use.
*
*
* Before making high-concurrency calls to the TTS service,
* please configure the connection pool parameters through the following environment variables.
*
* DASHSCOPE_MAXIMUM_ASYNC_REQUESTS
* DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST
* DASHSCOPE_CONNECTION_POOL_SIZE
*
*/
class SpeechSynthesizerObjectFactory
extends BasePooledObjectFactory<SpeechSynthesizer> {
public SpeechSynthesizerObjectFactory() {
super();
}
@Override
public SpeechSynthesizer create() throws Exception {
return new SpeechSynthesizer();
}
@Override
public PooledObject<SpeechSynthesizer> wrap(SpeechSynthesizer obj) {
return new DefaultPooledObject<>(obj);
}
}
class CosyvoiceObjectPool {
public static GenericObjectPool<SpeechSynthesizer> synthesizerPool;
public static String COSYVOICE_OBJECTPOOL_SIZE_ENV = "COSYVOICE_OBJECTPOOL_SIZE";
public static int DEFAULT_OBJECT_POOL_SIZE = 500;
private static Lock lock = new java.util.concurrent.locks.ReentrantLock();
public static int getObjectivePoolSize() {
try {
Integer n = Integer.parseInt(System.getenv(COSYVOICE_OBJECTPOOL_SIZE_ENV));
System.out.println("Using Object Pool Size In Env: "+ n);
return n;
} catch (NumberFormatException e) {
System.out.println("Using Default Object Pool Size: "+ DEFAULT_OBJECT_POOL_SIZE);
return DEFAULT_OBJECT_POOL_SIZE;
}
}
public static GenericObjectPool<SpeechSynthesizer> getInstance() {
lock.lock();
if (synthesizerPool == null) {
// You can set the object pool size here or in the COSYVOICE_OBJECTPOOL_SIZE environment variable.
// It is recommended to set it to 1.5 to 2 times your server's maximum concurrent connections.
int objectPoolSize = getObjectivePoolSize();
SpeechSynthesizerObjectFactory speechSynthesizerObjectFactory =
new SpeechSynthesizerObjectFactory();
GenericObjectPoolConfig<SpeechSynthesizer> config =
new GenericObjectPoolConfig<>();
config.setMaxTotal(objectPoolSize);
config.setMaxIdle(objectPoolSize);
config.setMinIdle(objectPoolSize);
synthesizerPool =
new GenericObjectPool<>(speechSynthesizerObjectFactory, config);
}
lock.unlock();
return synthesizerPool;
}
}
class SynthesizeTaskWithCallback implements Runnable {
String[] textArray;
String requestId;
long timeCost;
public SynthesizeTaskWithCallback(String[] textArray) {
this.textArray = textArray;
}
@Override
public void run() {
SpeechSynthesizer synthesizer = null;
long startTime = System.currentTimeMillis();
// if recv onError
final boolean[] hasError = {false};
try {
class ReactCallback extends ResultCallback<SpeechSynthesisResult> {
ReactCallback() {}
@Override
public void onEvent(SpeechSynthesisResult message) {
if (message.getAudioFrame() != null) {
try {
byte[] bytesArray = message.getAudioFrame().array();
System.out.println("Received audio, audio stream length: " + bytesArray.length);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
@Override
public void onComplete() {}
@Override
public void onError(Exception e) {
System.out.println(e.getMessage());
e.printStackTrace();
hasError[0] = true;
}
}
SpeechSynthesisParam param =
SpeechSynthesisParam.builder()
.model("cosyvoice-v3-flash")
.voice("longanyang")
// The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: .apiKey("sk-xxx")
.apiKey(System.getenv("DASHSCOPE_API_KEY"))
.format(SpeechSynthesisAudioFormat
.MP3_22050HZ_MONO_256KBPS) // Use PCM or MP3 for streaming synthesis
.build();
try {
synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
// Note: In an object pool scenario, the apiKey passed in multiple calls to updateParamAndCallback must always be the same. The SDK does not update the apiKey for an established connection, so passing a different apiKey will not take effect. See the important notes in the "Perform speech synthesis" step above.
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) {
// If an exception occurs, close the connection and invalidate the object in the pool.
synthesizer.getDuplexApi().close(1000, "bye");
CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
} else {
// If the task completes normally, return the object to the 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() + "] Speech synthesis task completed. Time cost: " + timeCost + " ms, RequestId " + requestId);
}
}
}
@Slf4j
public class SynthesizeTextToSpeechWithCallbackConcurrently {
public static void checkoutEnv(String envName, int defaultSize) {
if (System.getenv(envName) != null) {
System.out.println("[ENV CHECK]: " + envName + " "
+ System.getenv(envName));
} else {
System.out.println("[ENV CHECK]: " + envName
+ " Using Default which is " + defaultSize);
}
}
public static void main(String[] args)
throws InterruptedException, NoApiKeyException {
// The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
// Check for connection pool env
checkoutEnv("DASHSCOPE_CONNECTION_POOL_SIZE", 32);
checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS", 32);
checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST", 32);
checkoutEnv(CosyvoiceObjectPool.COSYVOICE_OBJECTPOOL_SIZE_ENV, CosyvoiceObjectPool.DEFAULT_OBJECT_POOL_SIZE);
int runTimes = 3;
// Create the pool of SpeechSynthesis objects
ExecutorService executorService = Executors.newFixedThreadPool(runTimes);
for (int i = 0; i < runTimes; i++) {
// Record the task submission time
LocalDateTime submissionTime = LocalDateTime.now();
executorService.submit(new SynthesizeTaskWithCallback(new String[] {
"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."}));
}
// Shut down the ExecutorService and wait for all tasks to complete
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.MINUTES);
System.exit(0);
}
}
Configuração recomendada
A configuração abaixo baseia-se em testes executando exclusivamente o serviço de síntese de fala Qwen-Audio-TTS/CosyVoice em instâncias ECS do Alibaba Cloud com as especificações indicadas. Concorrência excessiva pode aumentar a latência no processamento das tarefas.
O termo "concorrência por máquina" refere-se à quantidade de tarefas de síntese Qwen-Audio-TTS/CosyVoice rodando simultaneamente, o que equivale ao número de threads de trabalho.
Especificações do ECS (Alibaba Cloud) | Concorrência máxima por máquina | Tamanho do pool de objetos | Tamanho do pool de conexões |
|---|---|---|---|
4 vCPUs, 8 GiB | 100 | 500 | 2000 |
8 vCPUs, 16 GiB | 150 | 500 | 2000 |
16 vCPUs, 32 GiB | 200 | 500 | 2000 |
Gerenciamento de recursos e tratamento de erros
-
Tarefa bem-sucedida: após a conclusão normal de uma tarefa de síntese, chame o método returnObject do GenericObjectPool para devolver o objeto
SpeechSynthesizerao pool e permitir sua reutilização.No código de exemplo, isso corresponde a
CosyvoiceObjectPool.getInstance().returnObject(synthesizer)ImportanteNão devolva objetos
SpeechSynthesizercujas tarefas estejam incompletas ou tenham falhado. -
Falha na tarefa: caso um erro interno do SDK ou uma exceção de lógica de negócio interrompa a tarefa, execute os passos a seguir:
- Feche a conexão WebSocket subjacente.
- Invalide o objeto no pool para impedir sua reutilização.
// In the current code, the corresponding content is as follows
// Close the connection
synthesizer.getDuplexApi().close(1000, "bye");
// Invalidate the synthesizer in the object pool when an exception occurs
CosyvoiceObjectPool.getInstance().invalidateObject(synthesizer);
- Quando ocorrer um erro TaskFailed no lado do servidor, nenhuma ação adicional é necessária.
Warmup e medição de latência
Ao avaliar o DashScope Java SDK quanto à latência de concorrência e desempenho, execute operações de warmup suficientes antes. Isso garante que as medições reflitam o desempenho em estado estacionário, e não a sobrecarga inicial de conexão.
Mecanismo de reutilização de conexões
O DashScope Java SDK utiliza um pool de conexões singleton global para gerenciar e reutilizar conexões WebSocket de forma eficiente, reduzindo a sobrecarga de criação e encerramento frequente de conexões em cargas de trabalho de alta concorrência.
Funcionamento desse mecanismo:
-
Criação sob demanda: o SDK não pré-cria conexões WebSocket na inicialização. As conexões são estabelecidas na primeira chamada.
-
Reutilização com limite de tempo: após a conclusão de uma requisição, a conexão permanece no pool por até 60 segundos para reutilização.
- Se uma nova requisição chegar dentro de 60 segundos, a conexão existente é reutilizada, evitando a sobrecarga de handshake.
- Caso a conexão fique ociosa por mais de 60 segundos, ela é fechada automaticamente para liberar recursos.
Importância do warmup
Nos cenários a seguir, o pool de conexões pode não ter conexões ativas reutilizáveis, forçando a criação de novas conexões:
- A aplicação acabou de iniciar e ainda não fez nenhuma chamada.
- O serviço ficou ocioso por mais de 60 segundos, e as conexões no pool expiraram e foram fechadas.
Nesses casos, a primeira requisição precisa concluir um handshake WebSocket completo (handshake TCP, negociação TLS, upgrade de protocolo), resultando em latência significativamente maior do que nas requisições subsequentes que reutilizam conexões. Sem warmup, os resultados dos testes de desempenho ficam distorcidos por essa sobrecarga inicial de conexão.
Latência reportada pelo SDK vs. latência real do primeiro pacote
A latência do primeiro pacote reportada pelo SDK (por exemplo, o valor de get_first_package_delay()) inclui o tempo de estabelecimento da conexão WebSocket e de transmissão pela rede, não correspondendo à latência real do primeiro pacote do serviço de modelo.
A latência real do primeiro pacote é o intervalo entre o momento em que o servidor recebe a instrução run-task e o momento em que retorna o primeiro evento result-generated. Esse valor pode ser encontrado nos logs do lado do servidor.
Em cenários de alta concorrência, devido à criação simultânea de conexões e ao agendamento de recursos, a latência reportada pelo SDK pode ser significativamente maior que a latência real do primeiro pacote no servidor. Se você observar uma latência alta do primeiro pacote reportada pelo SDK:
- Compare com a latência do primeiro pacote nos logs do servidor (de
run-taskaté o primeiroresult-generated) para verificar se o desempenho da inferência do modelo está normal. - Utilize o mecanismo de pool de objetos ou pool de conexões descrito acima para realizar o warmup. Isso elimina a sobrecarga do handshake WebSocket, aproximando a latência reportada pelo SDK da latência real do primeiro pacote.
Práticas recomendadas
Para obter dados de desempenho confiáveis, siga estas etapas de warmup antes de executar testes de carga ou coletar métricas de latência:
- Simule o nível de concorrência alvo e envie previamente uma quantidade de requisições de warmup (por exemplo, mantenha tráfego por 1 a 2 minutos) para preencher completamente o pool de conexões.
- Confirme que o pool de conexões estabeleceu e manteve conexões ativas suficientes antes de iniciar a coleta formal de dados de desempenho.
Um warmup adequado leva o pool de conexões do SDK a um estado estável de reutilização, produzindo métricas de latência que refletem com precisão o desempenho online em estado estacionário.
Exceções comuns do Java SDK
Exceção 1: Conexões TCP do servidor continuam aumentando apesar do tráfego estável
Cada objeto do SDK cria uma conexão ao ser inicializado. Sem um pool de objetos, o objeto é destruído após a conclusão de cada tarefa. A conexão entra então em um estado sem referência e permanece aberta até que o servidor a encerre por timeout após 61 segundos. Durante esse período, a conexão não pode ser reutilizada.
Sob alta concorrência, novas tarefas que não encontram conexões reutilizáveis criam novas conexões, levando a:
- Aumento contínuo na contagem de conexões.
- Esgotamento de recursos do servidor e degradação de desempenho devido ao excesso de conexões.
- Saturação do pool de conexões, fazendo com que novas tarefas bloqueiem enquanto aguardam conexões disponíveis.
O MaxIdle do pool de objetos está configurado com um valor inferior ao MaxTotal, causando a destruição de objetos ociosos além do limite de MaxIdle, o que vaza suas conexões. Essas conexões vazadas precisam aguardar 61 segundos até o timeout antes de serem desconectadas, assim como no Tipo 1.
Solução:
Para o Tipo 1: utilize um pool de objetos.
Para o Tipo 2: verifique a configuração do pool de objetos e defina MaxIdle igual a MaxTotal. Desative a política automática de evicção de objetos.
Exceção 2: Tarefa leva 60 segundos a mais do que o normal
Mesma causa raiz da Exceção 1: o pool de conexões atingiu seu limite máximo de conexões, e novas tarefas precisam esperar 61 segundos até que conexões sem referência expirem antes que uma conexão fique disponível.
Exceção 3: Tarefas são lentas na inicialização do serviço, mas recuperam gradualmente
Causa raiz:
Sob alta concorrência, um único objeto reutiliza a mesma conexão WebSocket. Conexões WebSocket só são criadas na inicialização do serviço. Se a alta concorrência começar imediatamente no lançamento, a criação simultânea de muitas conexões WebSocket causa bloqueio.
Solução:
Aumente a concorrência gradualmente após iniciar o serviço ou adicione tarefas de warmup.
Exceção 4: Servidor reporta "Invalid action('run-task')! Please follow the protocol!"
Causa raiz:
Ocorreu um erro no lado do cliente, mas o servidor não o detectou. A conexão permanece em um estado de tarefa em andamento. Quando essa conexão e esse objeto são reutilizados para a próxima tarefa, o fluxo do protocolo é quebrado e a tarefa seguinte falha.
Solução:
Após o lançamento de uma exceção, feche a conexão WebSocket antes de devolver o objeto ao pool.
Exceção 5: Picos de tráfego apesar da carga de negócios estável
Causa raiz:
A criação simultânea de muitas conexões WebSocket causa bloqueio. Enquanto bloqueado, o tráfego de negócios recebido se acumula. Assim que o bloqueio é resolvido, todas as tarefas acumuladas são disparadas de uma vez, criando um pico de tráfego que pode exceder o limite de concorrência da sua conta, causando falhas nas tarefas ou degradação do servidor.
Gatilhos comuns para a criação simultânea de muitas conexões WebSocket:
- Fase de inicialização do serviço
- Anomalias de rede causando a desconexão e reconexão simultânea de muitas conexões WebSocket
- Explosão de erros no lado do servidor causando reconexões em massa de WebSocket. Erros comuns incluem a ultrapassagem do limite de concorrência da conta ("Requests rate limit exceeded, please try again later.").
Solução:
- Verifique as condições da rede.
- Investigue se houve uma explosão de outros erros no lado do servidor antes do pico.
- Aumente o limite de concorrência da conta.
- Reduza os tamanhos do pool de objetos e do pool de conexões para limitar a concorrência máxima por meio dos limites do pool.
- Atualize as especificações do servidor ou adicione mais máquinas.
Exceção 6: Todas as tarefas ficam mais lentas conforme a concorrência aumenta
Solução:
- Verifique se a largura de banda da rede atingiu seu limite.
- Verifique se a concorrência real está alta demais.
Modelos e regiões suportados
Singapore
Para chamar os modelos a seguir, selecione uma API Key da região de Singapore:
-
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 (estável, atualmente equivalente a qwen3-tts-instruct-flash-realtime-2026-01-22), qwen3-tts-instruct-flash-realtime-2026-01-22 (snapshot mais recente)
- Qwen3-TTS-VD-Realtime: qwen3-tts-vd-realtime-2026-01-15 (snapshot mais recente), qwen3-tts-vd-realtime-2025-12-16 (snapshot)
- Qwen3-TTS-VC-Realtime: qwen3-tts-vc-realtime-2026-01-15 (snapshot mais recente), qwen3-tts-vc-realtime-2025-11-27 (snapshot)
- Qwen3-TTS-Flash-Realtime: qwen3-tts-flash-realtime (estável, atualmente equivalente a qwen3-tts-flash-realtime-2025-11-27), qwen3-tts-flash-realtime-2025-11-27 (snapshot mais recente), qwen3-tts-flash-realtime-2025-09-18 (snapshot)
China (Beijing)
Para chamar os modelos a seguir, selecione uma API Key da região de 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 (estável, atualmente equivalente a qwen3-tts-instruct-flash-realtime-2026-01-22), qwen3-tts-instruct-flash-realtime-2026-01-22 (snapshot mais recente)
- Qwen3-TTS-VD-Realtime: qwen3-tts-vd-realtime-2026-01-15 (snapshot mais recente), qwen3-tts-vd-realtime-2025-12-16 (snapshot)
- Qwen3-TTS-VC-Realtime: qwen3-tts-vc-realtime-2026-01-15 (snapshot mais recente), qwen3-tts-vc-realtime-2025-11-27 (snapshot)
- Qwen3-TTS-Flash-Realtime: qwen3-tts-flash-realtime (estável, atualmente equivalente a qwen3-tts-flash-realtime-2025-11-27), qwen3-tts-flash-realtime-2025-11-27 (snapshot mais recente), qwen3-tts-flash-realtime-2025-09-18 (snapshot)
- Qwen-TTS-Realtime: qwen-tts-realtime (estável, atualmente equivalente a qwen-tts-realtime-2025-07-15), qwen-tts-realtime-latest (mais recente, atualmente equivalente a qwen-tts-realtime-2025-07-15), qwen-tts-realtime-2025-07-15 (snapshot)
Vozes suportadas
Diferentes modelos suportam diferentes vozes. Defina o parâmetro de requisição voice com o valor indicado na coluna parâmetro voice da lista de vozes correspondente.
Referência da API
- Real-time speech synthesis - Qwen-Audio-TTS/CosyVoice API reference
- Real-time speech synthesis - Qwen-TTS API reference
- AOQ client SDK (para modelos CosyVoice)
FAQ
P: Como corrigir pronúncia incorreta na síntese de fala? Como controlar a pronúncia de caracteres polifônicos?
- Substitua o caractere polifônico por um homófono para corrigir rapidamente o problema de pronúncia.
- Use marcação SSML para controlar a pronúncia .
P: Como solucionar áudio mudo ao usar uma voz clonada?
-
Verifique o status da voz
Chame a interface Voice cloning/design API e confirme se o
statusda voz éOK. -
Verifique a consistência da versão do modelo
Certifique-se de que o parâmetro
target_modelusado durante a clonagem de voz corresponda ao parâmetromodelusado para a síntese de fala. Por exemplo:- A clonagem utilizou
cosyvoice-v3-plus - A síntese também deve utilizar
cosyvoice-v3-plus
- A clonagem utilizou
-
Verifique a qualidade do áudio de origem
Confira se o áudio de origem usado para clonagem de voz atende aos requisitos em Voice cloning/design API:
- Duração do áudio: 10-20 segundos
- Qualidade de áudio nítida
- Sem ruído de fundo
-
Verifique os parâmetros da requisição
Confirme se o parâmetro
voicena requisição de síntese de fala está definido com o ID da voz clonada.
P: O que fazer se o áudio sintetizado de uma voz clonada estiver instável ou incompleto?
Se o áudio sintetizado de uma voz clonada apresentar algum dos problemas a seguir:
- Reprodução de áudio incompleta, com apenas parte do texto falado
- Qualidade de síntese inconsistente
- Áudio contém pausas anormais ou segmentos silenciosos
Possível causa: O áudio de origem não atende aos requisitos de qualidade.
Solução: Verifique se o áudio de origem atende aos requisitos em Recording guide for voice cloning. Regrave o áudio seguindo as diretrizes de gravação.
P: Por que a duração real do áudio sintetizado difere da duração mostrada no arquivo WAV?
A síntese de fala utiliza um mecanismo de streaming que retorna dados à medida que são gerados. A duração no cabeçalho do arquivo WAV salvo é uma estimativa e pode ser imprecisa. Para obter a duração exata, defina o formato como pcm, aguarde o resultado completo da síntese e adicione o cabeçalho do arquivo WAV manualmente.
P: Por que o arquivo de áudio não reproduz?
Faça a solução de problemas com base no seu cenário:
-
Áudio salvo como arquivo completo (como xx.mp3)
- Consistência do formato de áudio: O formato de áudio nos parâmetros da requisição deve corresponder à extensão do arquivo (por exemplo, se o parâmetro for wav, o arquivo deve ser .wav).
- Compatibilidade do player: Confirme se o player suporta o formato de áudio e a taxa de amostragem.
-
Reprodução de áudio em streaming
- Salve o fluxo de áudio como um arquivo completo e tente reproduzi-lo com um media player. Se o arquivo não reproduzir, consulte o cenário 1 acima.
- Se o arquivo reproduzir corretamente, o problema está na implementação da reprodução em streaming. Confirme se o player suporta reprodução em streaming (como ffmpeg, pyaudio, AudioFormat ou MediaSource).
P: Por que a reprodução do áudio está travando?
Faça a solução de problemas seguindo estas etapas:
-
Verifique a taxa de envio de texto: Certifique-se de que o intervalo de envio seja razoável para evitar que o segmento de áudio anterior termine antes da chegada do próximo texto.
-
Verifique o desempenho da função de callback:
- Confirme que não existe lógica de bloqueio na função de callback.
- Os callbacks são executados na thread WebSocket. Operações de bloqueio afetam o recebimento de dados. Escreva os dados de áudio em um buffer separado e processe-os em outra thread.
-
Verifique a estabilidade da rede: Flutuações na rede podem causar interrupções ou atrasos na transmissão de áudio.
P: Por que a síntese de fala está demorando muito?
Faça a solução de problemas seguindo estas etapas:
-
Verifique os intervalos de entrada
Para síntese em streaming, verifique se o intervalo de envio de texto está muito longo. Intervalos longos aumentam o tempo total de síntese.
-
Analise as métricas de desempenho
- Latência do primeiro pacote: normalmente em torno de 500 ms.
- RTF (Fator de Tempo Real = tempo total de síntese / duração do áudio): deve ser menor que 1,0.
P: Como restringir uma API key apenas ao service de síntese de fala (isolamento de permissões)?
Crie um novo workspace e conceda acesso apenas a modelos específicos. Isso limita o escopo da API key. Para mais detalhes, consulte Manage workspaces.