Convertissez du texte en parole avec une faible latence du premier paquet. La synthèse vocale en temps réel prend en charge les flux d'entrée et de sortie, le clonage de voix, la conception de voix ainsi que des contrôles audio précis pour les assistants vocaux, les livres audio et les services clients intelligents.
Présentation
Convertissez du texte en parole en temps réel avec une latence réduite.
- Entrée et sortie en streaming avec une faible latence du premier paquet
- Réglage du débit de parole, de la hauteur, du volume et du débit binaire pour un contrôle audio précis
- Compatibilité avec les principaux formats audio (PCM, WAV, MP3, Opus), avec une fréquence d'échantillonnage de sortie allant jusqu'à 48 kHz
- Prise en charge du contrôle par instructions pour piloter l'expressivité de la voix en langage naturel
- Prise en charge du clonage vocal et de la création de voix pour personnaliser les voix
- Prise en charge des balises d'émotion et d'expression pour intégrer des émotions et des effets sonores au texte
Pour les traitements par lots tels que les livres audio ou le doublage de supports pédagogiques, utilisez la synthèse vocale non temps réel. Pour obtenir des conseils sur le choix du modèle, consultez la rubrique Synthèse vocale.
Prérequis
- Configurez une clé API et définissez-la comme variable d'environnement.
- Pour utiliser le SDK DashScope, installez sa dernière version.
- Pour utiliser le protocole AOQ avec les modèles CosyVoice, téléchargez et intégrez le SDK client AOQ. Consultez Présentation du SDK.
Démarrage rapide
Les exemples suivants illustrent la synthèse vocale pour chaque modèle. Pour consulter d'autres exemples et obtenir des détails sur les paramètres, reportez-vous à la référence de l'API.
Qwen-Audio-TTS
L'exemple ci-dessous synthétise la parole à l'aide d'une voix système.
Pour utiliser la fonctionnalité de contrôle par instructions, définissez vos instructions via le paramètre 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/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/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
Outre WebSocket, ce modèle prend en charge le protocole AOQ. AOQ est recommandé pour les intégrations côté client privilégiant une latence stable, la résistance aux réseaux dégradés ainsi que la suppression du bruit et de l'écho en duplex intégral. Pour comparer les protocoles, consultez Présentation de l'API Realtime.
ImportantLes modèles cosyvoice-v3.5-plus et cosyvoice-v3.5-flash sont disponibles uniquement dans la région de Pékin et ne prennent en charge que les scénarios de conception de voix et de clonage de voix (aucune voix système n'est fournie). Avant toute utilisation, créez une voix personnalisée via le clonage de voix ou la conception de voix, puis définissez le paramètre voice sur l'ID de la voix et le paramètre model sur le nom du modèle correspondant dans votre code.
L'exemple suivant synthétise la parole à l'aide d'une voix système (consultez la liste des voix CosyVoice).
Pour utiliser la fonctionnalité de contrôle par instructions, définissez vos instructions via le paramètre 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/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/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
L'exemple suivant synthétise la parole à l'aide d'une voix système (voir Voix prises en charge).
Pour utiliser la fonctionnalité de Contrôle par instructions, remplacez model par qwen3-tts-instruct-flash-realtime et définissez les instructions via le paramètre 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/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/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
Mode server_commit
appendText()
import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.AudioSystem;
import java.io.*;
import java.util.Base64;
import java.util.Queue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class Main {
static String[] textToSynthesize = {
"Right? I really love this kind of supermarket.",
"Especially during the Chinese New Year.",
"Going to the supermarket.",
"It just makes me feel.",
"Super, super happy!",
"I want to buy so many things!"
};
public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;
// 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/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);
}
}
Mode commit
commit()
import com.alibaba.dashscope.audio.qwen_tts_realtime.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.AudioSystem;
import java.io.*;
import java.util.Base64;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
public class Main {
public static QwenTtsRealtimeAudioFormat ttsFormat = QwenTtsRealtimeAudioFormat.PCM_24000HZ_MONO_16BIT;
// 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/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);
}
}
Configuration de la session
Modes d'interaction de Qwen-TTS
L'API Realtime de Qwen-TTS propose deux modes d'interaction :
- Mode server_commit : le serveur gère automatiquement la segmentation du texte et le déclenchement de la synthèse. Ce mode convient à la synthèse continue de grands volumes de texte. Le client ajoute du texte sans gérer sa segmentation ni sa soumission.
- Mode commit : le client soumet explicitement le tampon de texte pour déclencher la synthèse. Ce mode convient aux scénarios nécessitant un contrôle précis du déclenchement, comme la synthèse à chaque tour d'une conversation avec une IA.
Changer de mode d'interaction :
- WebSocket : définissez le champ
modede l'événementsession.update.
{
"type": "session.update",
"session": {
"mode": "server_commit"
}
}
- SDK Python : définissez le paramètre
modedans la méthodeupdate_session.
qwen_tts_realtime.update_session(
voice='Cherry',
response_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
mode='server_commit'
)
- SDK Java : définissez
modeviaQwenTtsRealtimeConfig.builder().
QwenTtsRealtimeConfig config = QwenTtsRealtimeConfig.builder()
.voice("Cherry")
.responseFormat(ttsFormat)
.mode("server_commit")
.build();
qwenTtsRealtime.updateSession(config);
Pour consulter des exemples de code SDK complets, reportez-vous au SDK Python et au SDK Java. Pour plus d'informations sur le cycle de vie des événements WebSocket et la réutilisation des connexions, consultez la référence de l'API WebSocket.
Fonctionnalités avancées
Contrôle par instructions
Le contrôle par instructions utilise des descriptions en langage naturel pour ajuster le ton, la vitesse, l'émotion et les caractéristiques du timbre vocal, sans nécessiter la configuration de paramètres audio complexes.
Spécifications des instructions par modèle :
Qwen-Audio-TTS
Modèles pris en charge : qwen-audio-3.0-tts-plus, qwen-audio-3.0-tts-flash
Voix système et voix clonées : acceptent toute instruction.
CosyVoice
Modèles pris en charge : cosyvoice-v3.5-plus, cosyvoice-v3.5-flash, cosyvoice-v3-plus, cosyvoice-v3-flash
Les exigences de format des instructions varient selon le modèle :
-
cosyvoice-v3.5-plus, cosyvoice-v3.5-flash:
- Voix clonées ou créées : acceptent toute instruction.
- Voix système : non prises en charge par v3.5.
-
cosyvoice-v3-plus:
- Voix clonées ou créées : ne prennent pas en charge le contrôle par instructions.
- Voix système : les instructions doivent respecter un format et un contenu fixes. Consultez la liste des voix CosyVoice.
-
cosyvoice-v3-flash:
- Voix clonées ou créées : acceptent toute instruction.
- Voix système : les instructions doivent respecter un format et un contenu fixes. Consultez la liste des voix CosyVoice.
Utilisation : Spécifiez le contenu de l'instruction via le paramètre instruction.
Langues prises en charge pour le texte des instructions :
-
cosyvoice-v3.5-plus, cosyvoice-v3.5-flash:
- Voix clonées ou créées : chinois, anglais, français, allemand, japonais, coréen, russe, portugais, thaï, indonésien et vietnamien.
- Voix système : non prises en charge par v3.5.
-
cosyvoice-v3-plus:
- Voix clonées ou créées : chinois, anglais, français, allemand, japonais, coréen et russe.
- Voix système : les instructions doivent respecter un format et un contenu fixes. Consultez la liste des voix CosyVoice.
-
cosyvoice-v3-flash:
- Voix clonées ou créées : chinois, anglais, français, allemand, japonais, coréen et russe.
- Voix système : chinois uniquement.
Limite de longueur du texte d'instruction : 100 caractères maximum. Les caractères chinois (y compris le chinois simplifié/traditionnel, les kanjis japonais et les hanjas coréens) comptent pour 2 caractères chacun. Tous les autres caractères (ponctuation, lettres, chiffres, kanas japonais et hanguls coréens) comptent pour 1 caractère chacun.
Qwen-TTS
Modèles pris en charge : Uniquement les modèles de la série Qwen3-TTS-Instruct-Flash-Realtime.
Utilisation : Spécifiez le contenu de l'instruction via le paramètre instructions.
Langues prises en charge pour le texte des instructions : Chinois et anglais uniquement.
Longueur maximale des instructions : 1,600 tokens.
Cas d'utilisation :
- Narration de livres audio et de fictions radiophoniques
- Voix off de publicités et de vidéos promotionnelles
- Doublage de personnages de jeux vidéo et d'animation
- Assistants vocaux expressifs
- Narration de documentaires et de journaux d'information
Rédiger des descriptions vocales efficaces :
-
Principes essentiels :
- Soyez précis : décrivez les qualités vocales, par exemple « grave », « claire » ou « débit légèrement rapide ». Évitez les termes subjectifs ou vagues comme « agréable » ou « normale ».
- Décrivez plusieurs dimensions : une bonne description combine généralement le genre, l'âge, l'émotion, etc. La seule indication « voix féminine » est trop générale pour produire un timbre distinctif.
- Soyez objectif : décrivez les caractéristiques physiques et perceptibles de la voix. Préférez « hauteur élevée et ton énergique » à « ma voix préférée ».
- Décrivez une voix originale : précisez ses qualités plutôt que de demander l'imitation d'une personne, telle qu'une célébrité ou un acteur. L'imitation n'est pas prise en charge et peut poser des problèmes de droits d'auteur.
- Soyez concis : chaque mot doit être utile. Évitez les synonymes répétitifs et les qualificatifs sans valeur ajoutée.
-
Dimensions de description :
Combinez les dimensions suivantes pour décrire une voix. Plus vous incluez de dimensions, plus le résultat sera précis.
Dimension
Exemples de descriptions
Genre
Masculin, féminin, androgyne
Âge
Enfant (5-12), adolescent (13-18), jeune adulte (19-35), adulte d'âge moyen (36-55), personne âgée (55+)
Hauteur
Aiguë, moyenne, grave, légèrement aiguë, légèrement grave
Vitesse
Rapide, modérée, lente, légèrement rapide, légèrement lente
Émotion
Joyeuse, calme, douce, sérieuse, vive, posée, apaisante
Caractéristiques
Envoûtante, claire, rauque, ronde, suave, riche, puissante
Utilisation
Journal d'information, publicité, livre audio, personnage animé, assistant vocal, documentaire
-
Exemples :
- Style radiophonique standard : articulation claire et précise, prononciation parfaite
- Voix féminine jeune et vive, au débit rapide et à l'intonation nettement montante, adaptée à la présentation de produits de mode
- Homme d'âge moyen, calme, au débit lent et à la voix grave et envoûtante, adapté à la lecture d'actualités ou à la narration documentaire
- Femme douce et cultivée d'environ 30 ans, au ton régulier, adaptée à la narration de livres audio
- Voix enfantine mignonne, évoquant une fille d'environ 8 ans, légèrement puérile et adaptée au doublage de personnages animés
Dialectes
Cette section explique comment produire une synthèse vocale en dialectes chinois (tels que le dialecte du Henan, le dialecte du Sichuan et le cantonais). Les méthodes de configuration varient selon le modèle et le type de voix.
Configuration des dialectes par modèle :
Qwen-Audio-TTS
-
Voix système : choisissez l'un des types suivants :
- Une voix système prenant nativement en charge un dialecte, qui le produit sans configuration supplémentaire.
- Une voix prenant en charge le contrôle par instructions, configurable pour produire un dialecte précis au moyen d'instructions.
-
Voix clonées : utilisez le contrôle par instructions, par exemple avec l'instruction
请用河南话表达.
Dialectes pris en charge : Consultez la colonne « Langues prises en charge » pour chaque modèle dans Qwen-Audio-TTS.
CosyVoice
-
Voix système : choisissez l'un des types suivants dans la liste des voix CosyVoice :
- Une voix avec dialecte intégré, telle que
longshange_v3, qui produit le dialecte sans configuration supplémentaire. - Une voix prenant en charge le contrôle par instructions, configurable pour produire un dialecte précis, telle que
longanhuan_v3.
- Une voix avec dialecte intégré, telle que
-
Voix clonées : utilisez le contrôle par instructions, par exemple avec l'instruction
请用河南话表达. -
Voix créées : les dialectes ne sont pas pris en charge.
Dialectes pris en charge : Consultez la colonne « Langues prises en charge » pour chaque modèle dans CosyVoice.
Exemple : Utilisez cosyvoice-v3-flash avec la voix longanhuan_v3, et définissez le texte d'instruction sur "请用河南话表达。" pour produire une synthèse vocale en dialecte du 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/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
- Voix système : utilisez une voix avec dialecte intégré. Consultez la liste Qwen-TTS dans Voix prises en charge.
- Voix clonées : les dialectes ne sont pas pris en charge.
- Voix créées : les dialectes ne sont pas pris en charge.
Dialectes pris en charge : Consultez la colonne « Langues prises en charge » pour chaque modèle dans Qwen3-TTS.
Balises d'émotion et d'expression
Les modèles de la série Qwen-Audio-TTS permettent d'intégrer des tags d'émotion et de langage riche directement dans le texte à synthétiser (paramètre text). Ces tags contrôlent l'expression émotionnelle ou insèrent des effets vocaux (comme des rires ou des soupirs) à des positions précises. Vous obtenez ainsi une parole plus expressive sans avoir à configurer de paramètres audio complexes.
ImportantModèles pris en charge : qwen-audio-3.0-tts-plus et qwen-audio-3.0-tts-flash uniquement.
Limitation : Seul le mode streaming unidirectionnel est pris en charge.
Les tags de contrôle définissent l'émotion ou le style de la parole. Insérez un tag dans le texte pour modifier tout le texte suivant, jusqu'à l'apparition du prochain tag de contrôle ou jusqu'à ce que la phrase soit automatiquement segmentée en raison de sa longueur.
Balise | Description |
|---|---|
| Triste |
| Étonné |
| Cri grave et puissant |
| Tremblant |
| En colère |
| Enthousiaste |
| Sarcastique |
| Curieux |
| Style Dracula (grave et inquiétant) |
| Ennuyé |
| Fatigué |
| Méprisant |
| Cri |
| Chuchotement doux ASMR |
| Paniqué |
| Espiègle |
| Empathique |
| Chuchotement |
| Réticent |
| Pleurs |
| Sérieux |
| Parole très lente |
| Parole très rapide |
Les tags de langage riche insèrent un effet vocal à la position actuelle dans le texte, sans modifier le style émotionnel du texte environnant.
Balise | Description |
|---|---|
| Halètement |
| Soupir |
| Raclement de gorge |
| Petit rire |
| Rire |
| Toux |
| Reniflement |
L'exemple suivant montre comment combiner des tags de contrôle et des tags de langage riche dans le paramètre text :
[excited]What a beautiful day today![laughing]Let's go out and have fun together!
Dans ce texte, [excited] est un tag de contrôle qui applique une émotion d'excitation à tout le texte suivant. [laughing] est un tag de langage riche qui insère un rire à cette position avant de poursuivre la synthèse du reste du texte.
Il est également possible de basculer entre différentes émotions au sein d'un même texte :
[serious]Please pay attention to the safety precautions.[excited]Alright, let's get started now!
Ici, [serious] donne un ton sérieux à la première phrase, tandis que [excited] passe à un ton excité à partir de la deuxième phrase.
Annuler une tâche
Pour interrompre le tour de synthèse en cours lors d'une synthèse vocale en temps réel, envoyez une commande d'annulation. Après l'annulation, le serveur met immédiatement fin à la tâche actuelle et renvoie un événement de complétion. Vous pouvez alors démarrer une nouvelle tâche de synthèse sur la même connexion WebSocket sans avoir à vous reconnecter.
Utilisation :
- SDK Python : à partir de la version 1.26.4, appelez
SpeechSynthesizer.streaming_cancel(). - SDK Java : à partir de la version 2.22.26, appelez
SpeechSynthesizer.streamingCancel(). - Protocole WebSocket natif : envoyez un événement
finish-tasket définissezdirective=canceldansinput.
ImportantLimitations liées aux modèles :
- China (Beijing) : tous les modèles Qwen-Audio-TTS prennent en charge cette fonctionnalité. CosyVoice nécessite la version v2 ou ultérieure.
- Singapore : tous les modèles Qwen-Audio-TTS prennent en charge cette fonctionnalité. CosyVoice ne la prend pas en charge.
Appels avec le protocole WebSocket natif
Les exemples suivants illustrent la connexion directe au serveur via le protocole WebSocket natif. Cette approche convient aux scénarios où le SDK DashScope n'est pas utilisé. Il s'agit d'implémentations minimales et exécutables. Pour plus de détails sur le protocole WebSocket, consultez la référence API correspondant à chaque modèle.
Consultez les exemples d'appels directs au protocole WebSocket
Qwen-Audio-TTS/CosyVoice
Qwen-Audio-TTS et Qwen-Audio-TTS/CosyVoice partagent le même protocole WebSocket. Les exemples suivants utilisent qwen-audio-3.0-tts-flash. Pour employer Qwen-Audio-TTS/CosyVoice, remplacez le paramètre model par un modèle Qwen-Audio-TTS/CosyVoice (tel que cosyvoice-v3-flash) et le paramètre voice par la voix souhaitée.
Go
package main
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
const (
// The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
wsURL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"
outputFile = "output.mp3"
)
func main() {
// The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/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 := os.Getenv("DASHSCOPE_API_KEY")
// Clear the output file
os.Remove(outputFile)
os.Create(outputFile)
// Connect to WebSocket
header := make(http.Header)
header.Add("X-DashScope-DataInspection", "enable")
header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, header)
if err != nil {
if resp != nil {
fmt.Printf("Connection failed, HTTP status code: %d\n", resp.StatusCode)
}
fmt.Println("Connection failed:", err)
return
}
defer conn.Close()
// Generate task ID
taskID := uuid.New().String()
fmt.Printf("Generated task ID: %s\n", taskID)
// Sending run-task event
runTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "run-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"task_group": "audio",
"task": "tts",
"function": "SpeechSynthesizer",
"model": "qwen-audio-3.0-tts-flash",
"parameters": map[string]interface{}{
"text_type": "PlainText",
"voice": "longanhuan_v3.6",
"format": "mp3",
"sample_rate": 22050,
"volume": 50,
"rate": 1,
"pitch": 1,
// If enable_ssml is set to true, only one continue-task event can be sent; otherwise the error "Text request limit violated, expected 1." will be returned
"enable_ssml": false,
},
"input": map[string]interface{}{},
},
}
runTaskJSON, _ := json.Marshal(runTaskCmd)
fmt.Printf("Sending run-task event: %s\n", string(runTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, runTaskJSON)
if err != nil {
fmt.Println("Failed to send run-task:", err)
return
}
textSent := false
// Process messages
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
fmt.Println("Failed to read message:", err)
break
}
// Process binary message
if messageType == websocket.BinaryMessage {
fmt.Printf("Received binary message, length: %d\n", len(message))
file, _ := os.OpenFile(outputFile, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
file.Write(message)
file.Close()
continue
}
// Process text message
messageStr := string(message)
fmt.Printf("Received text message: %s\n", strings.ReplaceAll(messageStr, "\n", ""))
// Parse JSON to get event type
var msgMap map[string]interface{}
if json.Unmarshal(message, &msgMap) == nil {
if header, ok := msgMap["header"].(map[string]interface{}); ok {
if event, ok := header["event"].(string); ok {
fmt.Printf("Event type: %s\n", event)
switch event {
case "task-started":
fmt.Println("=== Received task-started event ===")
if !textSent {
// Sending continue-task event
texts := []string{"Before my bed, moonlight shines bright, I suspect it's frost upon the ground.", "I raise my eyes to gaze at the bright moon, then bow my head, thinking of home."}
for _, text := range texts {
continueTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "continue-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"input": map[string]interface{}{
"text": text,
},
},
}
continueTaskJSON, _ := json.Marshal(continueTaskCmd)
fmt.Printf("Sending continue-task event: %s\n", string(continueTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, continueTaskJSON)
if err != nil {
fmt.Println("Failed to send continue-task:", err)
return
}
}
textSent = true
// Delay before sending finish-task
time.Sleep(500 * time.Millisecond)
// Sending finish-task event
finishTaskCmd := map[string]interface{}{
"header": map[string]interface{}{
"action": "finish-task",
"task_id": taskID,
"streaming": "duplex",
},
"payload": map[string]interface{}{
"input": map[string]interface{}{},
},
}
finishTaskJSON, _ := json.Marshal(finishTaskCmd)
fmt.Printf("Sending finish-task event: %s\n", string(finishTaskJSON))
err = conn.WriteMessage(websocket.TextMessage, finishTaskJSON)
if err != nil {
fmt.Println("Failed to send finish-task:", err)
return
}
}
case "task-finished":
fmt.Println("=== Task completed ===")
return
case "task-failed":
fmt.Println("=== Task failed ===")
if header["error_message"] != nil {
fmt.Printf("Error message: %s\n", header["error_message"])
}
return
case "result-generated":
fmt.Println("Received result-generated event")
}
}
}
}
}
}
C#
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
class Program {
// The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/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 readonly string ApiKey = "sk-xxx"
private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");
// The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
private const string WebSocketUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
// Output file path
private const string OutputFilePath = "output.mp3";
// WebSocket client
private static ClientWebSocket _webSocket = new ClientWebSocket();
// Cancellation token source
private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
// Task ID
private static string? _taskId;
// Whether the task has started
private static TaskCompletionSource<bool> _taskStartedTcs = new TaskCompletionSource<bool>();
static async Task Main(string[] args) {
try {
// Clear the output file
ClearOutputFile(OutputFilePath);
// Connect to the WebSocket service
await ConnectToWebSocketAsync(WebSocketUrl);
// Start the message receiving task
Task receiveTask = ReceiveMessagesAsync();
// Send the run-task event
_taskId = GenerateTaskId();
await SendRunTaskCommandAsync(_taskId);
// Wait for the task-started event
await _taskStartedTcs.Task;
// Send continue-task events
string[] texts = {
"Before my bed, moonlight shines bright,",
"I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,",
"then bow my head, thinking of home."
};
foreach (string text in texts) {
await SendContinueTaskCommandAsync(text);
}
// Send the finish-task event
await SendFinishTaskCommandAsync(_taskId);
// Wait for the receiving task to complete
await receiveTask;
Console.WriteLine("Task completed, connection closed.");
} catch (OperationCanceledException) {
Console.WriteLine("Task cancelled.");
} catch (Exception ex) {
Console.WriteLine($"Error occurred: {ex.Message}");
} finally {
_cancellationTokenSource.Cancel();
_webSocket.Dispose();
}
}
private static void ClearOutputFile(string filePath) {
if (File.Exists(filePath)) {
File.WriteAllText(filePath, string.Empty);
Console.WriteLine("Output file cleared.");
} else {
Console.WriteLine("Output file does not exist, no need to clear.");
}
}
private static async Task ConnectToWebSocketAsync(string url) {
var uri = new Uri(url);
if (_webSocket.State == WebSocketState.Connecting || _webSocket.State == WebSocketState.Open) {
return;
}
// Set the WebSocket connection headers
_webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
_webSocket.Options.SetRequestHeader("X-DashScope-DataInspection", "enable");
try {
await _webSocket.ConnectAsync(uri, _cancellationTokenSource.Token);
Console.WriteLine("Successfully connected to the WebSocket service.");
} catch (OperationCanceledException) {
Console.WriteLine("WebSocket connection cancelled.");
} catch (Exception ex) {
Console.WriteLine($"WebSocket connection failed: {ex.Message}");
throw;
}
}
private static async Task SendRunTaskCommandAsync(string taskId) {
var command = CreateCommand("run-task", taskId, "duplex", new {
task_group = "audio",
task = "tts",
function = "SpeechSynthesizer",
model = "qwen-audio-3.0-tts-flash",
parameters = new
{
text_type = "PlainText",
voice = "longanhuan_v3.6",
format = "mp3",
sample_rate = 22050,
volume = 50,
rate = 1,
pitch = 1,
// If enable_ssml is set to true, only one continue-task event can be sent; otherwise the error "Text request limit violated, expected 1." will be returned
enable_ssml = false
},
input = new { }
});
await SendJsonMessageAsync(command);
Console.WriteLine("run-task event sent.");
}
private static async Task SendContinueTaskCommandAsync(string text) {
if (_taskId == null) {
throw new InvalidOperationException("Task ID not initialized.");
}
var command = CreateCommand("continue-task", _taskId, "duplex", new {
input = new {
text
}
});
await SendJsonMessageAsync(command);
Console.WriteLine("continue-task event sent.");
}
private static async Task SendFinishTaskCommandAsync(string taskId) {
var command = CreateCommand("finish-task", taskId, "duplex", new {
input = new { }
});
await SendJsonMessageAsync(command);
Console.WriteLine("finish-task event sent.");
}
private static async Task SendJsonMessageAsync(string message) {
var buffer = Encoding.UTF8.GetBytes(message);
try {
await _webSocket.SendAsync(new ArraySegment<byte>(buffer), WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
} catch (OperationCanceledException) {
Console.WriteLine("Message sending cancelled.");
}
}
private static async Task ReceiveMessagesAsync() {
while (_webSocket.State == WebSocketState.Open) {
var response = await ReceiveMessageAsync();
if (response != null) {
var eventStr = response.RootElement.GetProperty("header").GetProperty("event").GetString();
switch (eventStr) {
case "task-started":
Console.WriteLine("Task started.");
_taskStartedTcs.TrySetResult(true);
break;
case "task-finished":
Console.WriteLine("Task completed.");
_cancellationTokenSource.Cancel();
break;
case "task-failed":
Console.WriteLine("Task failed: " + response.RootElement.GetProperty("header").GetProperty("error_message").GetString());
_cancellationTokenSource.Cancel();
break;
default:
// result-generated can be handled here
break;
}
}
}
}
private static async Task<JsonDocument?> ReceiveMessageAsync() {
var buffer = new byte[1024 * 4];
var segment = new ArraySegment<byte>(buffer);
try {
WebSocketReceiveResult result = await _webSocket.ReceiveAsync(segment, _cancellationTokenSource.Token);
if (result.MessageType == WebSocketMessageType.Close) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cancellationTokenSource.Token);
return null;
}
if (result.MessageType == WebSocketMessageType.Binary) {
// Process binary data
Console.WriteLine("Received binary data...");
// Save the binary data to file
using (var fileStream = new FileStream(OutputFilePath, FileMode.Append)) {
fileStream.Write(buffer, 0, result.Count);
}
return null;
}
string message = Encoding.UTF8.GetString(buffer, 0, result.Count);
return JsonDocument.Parse(message);
} catch (OperationCanceledException) {
Console.WriteLine("Message receiving cancelled.");
return null;
}
}
private static string GenerateTaskId() {
return Guid.NewGuid().ToString("N").Substring(0, 32);
}
private static string CreateCommand(string action, string taskId, string streaming, object payload) {
var command = new {
header = new {
action,
task_id = taskId,
streaming
},
payload
};
return JsonSerializer.Serialize(command);
}
}
PHP
Structure du répertoire de code exemple :
my-php-project/
├── composer.json
├── vendor/
└── index.php
Contenu du fichier composer.json (adaptez les versions des dépendances selon vos besoins) :
{
"require": {
"react/event-loop": "^1.3",
"react/socket": "^1.11",
"react/stream": "^1.2",
"react/http": "^1.1",
"ratchet/pawl": "^0.4"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
Contenu du fichier index.php :
<?php
require __DIR__ . '/vendor/autoload.php';
use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;
// The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/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 = getenv("DASHSCOPE_API_KEY");
// The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'; // WebSocket server URL
$output_file = 'output.mp3'; // Output file path
$loop = Loop::get();
if (file_exists($output_file)) {
// Clear file content
file_put_contents($output_file, '');
}
// Create a custom connector
$socketConnector = new SocketConnector($loop, [
'tcp' => [
'bindto' => '0.0.0.0:0',
],
'tls' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$connector = new Connector($loop, $socketConnector);
$headers = [
'Authorization' => 'bearer ' . $api_key,
'X-DashScope-DataInspection' => 'enable'
];
$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $output_file) {
echo "Connected to the WebSocket server\n";
// Generate task ID
$taskId = generateTaskId();
// Send the run-task event
sendRunTaskMessage($conn, $taskId);
// Define the function to send continue-task events
$sendContinueTask = function() use ($conn, $loop, $taskId) {
// Text to be sent
$texts = ["Before my bed, moonlight shines bright,", "I suspect it\'s frost upon the ground.", "I raise my eyes to gaze at the bright moon,", "then bow my head, thinking of home."];
$continueTaskCount = 0;
foreach ($texts as $text) {
$continueTaskMessage = json_encode([
"header" => [
"action" => "continue-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => [
"text" => $text
]
]
]);
echo "Sending continue-task event: " . $continueTaskMessage . "\n";
$conn->send($continueTaskMessage);
$continueTaskCount++;
}
echo "Number of continue-task events sent: " . $continueTaskCount . "\n";
// Send the finish-task event
sendFinishTaskMessage($conn, $taskId);
};
// Flag for whether the task-started event was received
$taskStarted = false;
// Listen for messages
$conn->on('message', function($msg) use ($conn, $sendContinueTask, $loop, &$taskStarted, $taskId, $output_file) {
if ($msg->isBinary()) {
// Write binary data to local file
file_put_contents($output_file, $msg->getPayload(), FILE_APPEND);
} else {
// Process non-binary message
$response = json_decode($msg, true);
if (isset($response['header']['event'])) {
handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, $taskStarted);
} else {
echo "Unknown message format\n";
}
}
});
// Listen for connection close
$conn->on('close', function($code = null, $reason = null) {
echo "Connection closed\n";
if ($code !== null) {
echo "Close code: " . $code . "\n";
}
if ($reason !== null) {
echo "Close reason: " . $reason . "\n";
}
});
}, function ($e) {
echo "Unable to connect: {$e->getMessage()}\n";
});
$loop->run();
/**
* Generate task ID
* @return string
*/
function generateTaskId(): string {
return bin2hex(random_bytes(16));
}
/**
* Send the run-task event
* @param $conn
* @param $taskId
*/
function sendRunTaskMessage($conn, $taskId) {
$runTaskMessage = json_encode([
"header" => [
"action" => "run-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"task_group" => "audio",
"task" => "tts",
"function" => "SpeechSynthesizer",
"model" => "qwen-audio-3.0-tts-flash",
"parameters" => [
"text_type" => "PlainText",
"voice" => "longanhuan_v3.6",
"format" => "mp3",
"sample_rate" => 22050,
"volume" => 50,
"rate" => 1,
"pitch" => 1,
// If enable_ssml is set to true, only one continue-task event can be sent; otherwise the error "Text request limit violated, expected 1." will be returned
"enable_ssml" => false
],
"input" => (object) []
]
]);
echo "Sending run-task event: " . $runTaskMessage . "\n";
$conn->send($runTaskMessage);
echo "run-task event sent\n";
}
/**
* Read audio file
* @param string $filePath
* @return bool|string
*/
function readAudioFile(string $filePath) {
$voiceData = file_get_contents($filePath);
if ($voiceData === false) {
echo "Unable to read audio file\n";
}
return $voiceData;
}
/**
* Split audio data
* @param string $data
* @param int $chunkSize
* @return array
*/
function splitAudioData(string $data, int $chunkSize): array {
return str_split($data, $chunkSize);
}
/**
* Send the finish-task event
* @param $conn
* @param $taskId
*/
function sendFinishTaskMessage($conn, $taskId) {
$finishTaskMessage = json_encode([
"header" => [
"action" => "finish-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => (object) []
]
]);
echo "Sending finish-task event: " . $finishTaskMessage . "\n";
$conn->send($finishTaskMessage);
echo "finish-task event sent\n";
}
/**
* Handle events
* @param $conn
* @param $response
* @param $sendContinueTask
* @param $loop
* @param $taskId
* @param $taskStarted
*/
function handleEvent($conn, $response, $sendContinueTask, $loop, $taskId, &$taskStarted) {
switch ($response['header']['event']) {
case 'task-started':
echo "Task started, sending continue-task events...\n";
$taskStarted = true;
// Send continue-task events
$sendContinueTask();
break;
case 'result-generated':
// Received result-generated event
break;
case 'task-finished':
echo "Task completed\n";
$conn->close();
break;
case 'task-failed':
echo "Task failed\n";
echo "Error code: " . $response['header']['error_code'] . "\n";
echo "Error message: " . $response['header']['error_message'] . "\n";
$conn->close();
break;
case 'error':
echo "Error: " . $response['payload']['message'] . "\n";
break;
default:
echo "Unknown event: " . $response['header']['event'] . "\n";
break;
}
// If the task is completed, close the connection
if ($response['header']['event'] == 'task-finished') {
// Wait 1 second to ensure all data has been transmitted
$loop->addTimer(1, function() use ($conn) {
$conn->close();
echo "Client closing connection\n";
});
}
// If the task-started event has not been received, close the connection
if (!$taskStarted && in_array($response['header']['event'], ['task-failed', 'error'])) {
$conn->close();
}
}
Node.js
Installez les dépendances requises :
npm install ws
npm install uuid
Code exemple :
const WebSocket = require('ws');
const fs = require('fs');
const uuid = require('uuid').v4;
// The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/model-studio/get-api-key
// If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
const url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference';
// Output file path
const outputFilePath = 'output.mp3';
// Clear output file
fs.writeFileSync(outputFilePath, '');
// Create WebSocket client
const ws = new WebSocket(url, {
headers: {
Authorization: `bearer ${apiKey}`,
'X-DashScope-DataInspection': 'enable'
}
});
let taskStarted = false;
let taskId = uuid();
ws.on('open', () => {
console.log('Connected to the WebSocket server');
// Send the run-task event
const runTaskMessage = JSON.stringify({
header: {
action: 'run-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
task_group: 'audio',
task: 'tts',
function: 'SpeechSynthesizer',
model: 'qwen-audio-3.0-tts-flash',
parameters: {
text_type: 'PlainText',
voice: 'longanhuan_v3.6', // Voice
format: 'mp3', // Audio format
sample_rate: 22050, // Sample rate
volume: 50, // Volume
rate: 1, // Speech rate
pitch: 1, // Pitch
enable_ssml: false // Whether to enable SSML. If enable_ssml is set to true, only one continue-task event can be sent; otherwise the error "Text request limit violated, expected 1." will be returned
},
input: {}
}
});
ws.send(runTaskMessage);
console.log('run-task message sent');
});
const fileStream = fs.createWriteStream(outputFilePath, { flags: 'a' });
ws.on('message', (data, isBinary) => {
if (isBinary) {
// Write binary data to file
fileStream.write(data);
} else {
const message = JSON.parse(data);
switch (message.header.event) {
case 'task-started':
taskStarted = true;
console.log('Task started');
// Send continue-task events
sendContinueTasks(ws);
break;
case 'task-finished':
console.log('Task completed');
ws.close();
fileStream.end(() => {
console.log('File stream closed');
});
break;
case 'task-failed':
console.error('Task failed: ', message.header.error_message);
ws.close();
fileStream.end(() => {
console.log('File stream closed');
});
break;
default:
// result-generated can be handled here
break;
}
}
});
function sendContinueTasks(ws) {
const texts = [
'Before my bed, moonlight shines bright,',
'I suspect it is frost upon the ground.',
'I raise my eyes to gaze at the bright moon,',
'then bow my head, thinking of home.'
];
texts.forEach((text, index) => {
setTimeout(() => {
if (taskStarted) {
const continueTaskMessage = JSON.stringify({
header: {
action: 'continue-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
input: {
text: text
}
}
});
ws.send(continueTaskMessage);
console.log(`continue-task sent, text: ${text}`);
}
}, index * 1000); // Send one every second
});
// Send the finish-task event
setTimeout(() => {
if (taskStarted) {
const finishTaskMessage = JSON.stringify({
header: {
action: 'finish-task',
task_id: taskId,
streaming: 'duplex'
},
payload: {
input: {}
}
});
ws.send(finishTaskMessage);
console.log('finish-task sent');
}
}, texts.length * 1000 + 1000); // Send 1 second after all continue-task events are sent
}
ws.on('close', () => {
console.log('Disconnected from the WebSocket server');
});
Java
Nous vous recommandons d'utiliser le SDK DashScope Java pour vos développements. Consultez la rubrique SDK Java.
L'exemple ci-dessous illustre une connexion WebSocket directe en Java. Importez les dépendances suivantes avant l'exécution :
Java-WebSocketjackson-databind
Gérez les dépendances avec Maven ou Gradle :
<dependencies>
<!-- WebSocket Client -->
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.0</version>
</dependency>
</dependencies>
// Other code omitted
dependencies {
// WebSocket Client
implementation 'org.java-websocket:Java-WebSocket:1.5.3'
// JSON Processing
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.0'
}
// Other code omitted
Code Java :
import com.fasterxml.jackson.databind.ObjectMapper;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.ByteBuffer;
import java.util.*;
public class TTSWebSocketClient extends WebSocketClient {
private final String taskId = UUID.randomUUID().toString();
private final String outputFile = "output_" + System.currentTimeMillis() + ".mp3";
private boolean taskFinished = false;
public TTSWebSocketClient(URI serverUri, Map<String, String> headers) {
super(serverUri, headers);
}
@Override
public void onOpen(ServerHandshake serverHandshake) {
System.out.println("Connection established");
// Send the run-task event
// If enable_ssml is set to true, only one continue-task event can be sent; otherwise the error "Text request limit violated, expected 1." will be returned
String runTaskCommand = "{ \"header\": { \"action\": \"run-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"task_group\": \"audio\", \"task\": \"tts\", \"function\": \"SpeechSynthesizer\", \"model\": \"qwen-audio-3.0-tts-flash\", \"parameters\": { \"text_type\": \"PlainText\", \"voice\": \"longanhuan_v3.6\", \"format\": \"mp3\", \"sample_rate\": 22050, \"volume\": 50, \"rate\": 1, \"pitch\": 1, \"enable_ssml\": false }, \"input\": {} }}";
send(runTaskCommand);
}
@Override
public void onMessage(String message) {
System.out.println("Received message from server: " + message);
try {
// Parse JSON message
Map<String, Object> messageMap = new ObjectMapper().readValue(message, Map.class);
if (messageMap.containsKey("header")) {
Map<String, Object> header = (Map<String, Object>) messageMap.get("header");
if (header.containsKey("event")) {
String event = (String) header.get("event");
if ("task-started".equals(event)) {
System.out.println("Received task-started event from server");
List<String> texts = Arrays.asList(
"Before my bed, moonlight shines bright,I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,then bow my head, thinking of home."
);
for (String text : texts) {
// Send the continue-task event
sendContinueTask(text);
}
// Send the finish-task event
sendFinishTask();
} else if ("task-finished".equals(event)) {
System.out.println("Received task-finished event from server");
taskFinished = true;
closeConnection();
} else if ("task-failed".equals(event)) {
System.out.println("Task failed: " + message);
closeConnection();
}
}
}
} catch (Exception e) {
System.err.println("Exception occurred: " + e.getMessage());
}
}
@Override
public void onMessage(ByteBuffer message) {
System.out.println("Received binary audio data of size: " + message.remaining());
try (FileOutputStream fos = new FileOutputStream(outputFile, true)) {
byte[] buffer = new byte[message.remaining()];
message.get(buffer);
fos.write(buffer);
System.out.println("Audio data written to local file " + outputFile);
} catch (IOException e) {
System.err.println("Failed to write audio data to local file: " + e.getMessage());
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("Connection closed: " + reason + " (" + code + ")");
}
@Override
public void onError(Exception ex) {
System.err.println("Error: " + ex.getMessage());
ex.printStackTrace();
}
private void sendContinueTask(String text) {
String command = "{ \"header\": { \"action\": \"continue-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": { \"text\": \"" + text + "\" } }}";
send(command);
}
private void sendFinishTask() {
String command = "{ \"header\": { \"action\": \"finish-task\", \"task_id\": \"" + taskId + "\", \"streaming\": \"duplex\" }, \"payload\": { \"input\": {} }}";
send(command);
}
private void closeConnection() {
if (!isClosed()) {
close();
}
}
public static void main(String[] args) {
try {
// The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/model-studio/get-api-key
// If you have not configured the environment variable, replace the next line with your Chinese Model Studio API Key: String apiKey = "sk-xxx"
String apiKey = System.getenv("DASHSCOPE_API_KEY");
if (apiKey == null || apiKey.isEmpty()) {
System.err.println("Please set the DASHSCOPE_API_KEY environment variable");
return;
}
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "bearer " + apiKey);
// The following is the configuration for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
TTSWebSocketClient client = new TTSWebSocketClient(new URI("wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference"), headers);
client.connect();
while (!client.isClosed() && !client.taskFinished) {
Thread.sleep(1000);
}
} catch (Exception e) {
System.err.println("Failed to connect to WebSocket service: " + e.getMessage());
e.printStackTrace();
}
}
}
Python
Nous recommandons d'utiliser le SDK DashScope pour Python lors du développement. Consultez SDK Python.
L'exemple ci-dessous illustre une connexion directe via WebSocket en Python. Installez les dépendances suivantes avant l'exécution :
pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client
ImportantNe nommez pas votre fichier Python « websocket.py », sous peine de provoquer une erreur (AttributeError: module 'websocket' has no attribute 'WebSocketApp'. Did you mean: 'WebSocket'?).
import websocket
import json
import uuid
import os
import time
class TTSClient:
def __init__(self, api_key, uri):
"""
Initialize TTSClient instance
Parameters:
api_key (str): API Key for authentication
uri (str): WebSocket service URL
"""
self.api_key = api_key # Replace with your API Key
self.uri = uri # Replace with your WebSocket URL
self.task_id = str(uuid.uuid4()) # Generate unique task ID
self.output_file = f"output_{int(time.time())}.mp3" # Output audio file path
self.ws = None # WebSocketApp instance
self.task_started = False # Whether task-started has been received
self.task_finished = False # Whether task-finished / task-failed has been received
def on_open(self, ws):
"""
Callback when WebSocket connection is established
Sends run-task event to start speech synthesis task
"""
print("WebSocket connected")
# Construct run-task event
run_task_cmd = {
"header": {
"action": "run-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"task_group": "audio",
"task": "tts",
"function": "SpeechSynthesizer",
"model": "qwen-audio-3.0-tts-flash",
"parameters": {
"text_type": "PlainText",
"voice": "longanhuan_v3.6",
"format": "mp3",
"sample_rate": 22050,
"volume": 50,
"rate": 1,
"pitch": 1,
# If enable_ssml is set to true, only one continue-task event can be sent; otherwise an error will be returned
"enable_ssml": False
},
"input": {}
}
}
# Send run-task event
ws.send(json.dumps(run_task_cmd))
print("Sent run-task event")
def on_message(self, ws, message):
"""
Callback when a message is received
Handles text and binary messages differently
"""
if isinstance(message, str):
# Handle JSON text messages
try:
msg_json = json.loads(message)
print(f"Received JSON message: {msg_json}")
if "header" in msg_json:
header = msg_json["header"]
if "event" in header:
event = header["event"]
if event == "task-started":
print("Task started")
self.task_started = True
# Send continue-task events
texts = [
"Before my bed, moonlight shines bright,I suspect it\'s frost upon the ground.",
"I raise my eyes to gaze at the bright moon,then bow my head, thinking of home."
]
for text in texts:
self.send_continue_task(text)
# Send finish-task after all continue-task events are sent
self.send_finish_task()
elif event == "task-finished":
print("Task completed")
self.task_finished = True
self.close(ws)
elif event == "task-failed":
error_msg = msg_json.get("error_message", "Unknown error")
print(f"Task failed: {error_msg}")
self.task_finished = True
self.close(ws)
except json.JSONDecodeError as e:
print(f"JSON parsing failed: {e}")
else:
# Handle binary messages (audio data)
print(f"Received binary message, size: {len(message)} bytes")
with open(self.output_file, "ab") as f:
f.write(message)
print(f"Audio data written to local file {self.output_file}")
def on_error(self, ws, error):
"""Callback when an error occurs"""
print(f"WebSocket error: {error}")
def on_close(self, ws, close_status_code, close_msg):
"""Callback when connection is closed"""
print(f"WebSocket closed: {close_msg} ({close_status_code})")
def send_continue_task(self, text):
"""Send continue-task event with text content to be synthesized"""
cmd = {
"header": {
"action": "continue-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"input": {
"text": text
}
}
}
self.ws.send(json.dumps(cmd))
print(f"Sent continue-task event, text content: {text}")
def send_finish_task(self):
"""Send finish-task event to end the speech synthesis task"""
cmd = {
"header": {
"action": "finish-task",
"task_id": self.task_id,
"streaming": "duplex"
},
"payload": {
"input": {}
}
}
self.ws.send(json.dumps(cmd))
print("Sent finish-task event")
def close(self, ws):
"""Actively close the connection"""
if ws and ws.sock and ws.sock.connected:
ws.close()
print("Connection closed actively")
def run(self):
"""Start the WebSocket client"""
# Set request headers (authentication)
header = {
"Authorization": f"bearer {self.api_key}",
"X-DashScope-DataInspection": "enable"
}
# Create WebSocketApp instance
self.ws = websocket.WebSocketApp(
self.uri,
header=header,
on_open=self.on_open,
on_message=self.on_message,
on_error=self.on_error,
on_close=self.on_close
)
print("Listening for WebSocket messages...")
self.ws.run_forever() # Start persistent connection listener
# Example usage
if __name__ == "__main__":
# The API Key differs between the Singapore and Beijing regions. Get your API Key: https://www.alibabacloud.com/help/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.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.
SERVER_URI = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference" # Replace with your WebSocket URL
client = TTSClient(API_KEY, SERVER_URI)
client.run()
Qwen-TTS
-
Créer le client
Python
Créez un fichier Python nommé
tts_realtime_client.pyet copiez-y le code suivant :# -- 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
Créez un fichier Java nommé
TTSRealtimeClient.javaet copiez-y le code suivant :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(); } } } -
Choisir un mode de synthèse vocale
L'API Realtime prend en charge deux modes :
-
Mode server_commit
Le serveur gère automatiquement la segmentation du texte et le timing de la synthèse. Le client se contente d'envoyer le texte. Ce mode convient aux scénarios à faible latence (comme la navigation GPS).
-
Mode commit
Le client ajoute le texte dans un tampon et déclenche explicitement la synthèse. Cette approche est recommandée lorsque vous devez contrôler précisément la segmentation des phrases (par exemple pour un journal télévisé).
Mode server_commit
Python
Dans le même répertoire que
tts_realtime_client.py, créez un autre fichier Python nomméserver_commit.pyet copiez-y le code suivant :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/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()Exécutez
server_commit.pypour écouter en temps réel l'audio généré par l'API Realtime.Java
Dans le même répertoire que
TTSRealtimeClient.java, créez un autre fichier Java nomméServerCommit.javaet copiez-y le code suivant :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/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); } }Compilez et exécutez
ServerCommit.javapour écouter en temps réel l'audio généré par l'API Realtime.Mode commit
Python
Dans le même répertoire que
tts_realtime_client.py, créez un autre fichier Python nommécommit.pyet copiez-y le code suivant :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/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()Exécutez
commit.py. Vous avez la possibilité de saisir du texte à synthétiser à plusieurs reprises. Appuyez sur Entrée sans saisir de texte pour écouter via le haut-parleur l'audio renvoyé par l'API Realtime.Java
Dans le même répertoire que
TTSRealtimeClient.java, créez un autre fichier Java nomméCommit.javaet copiez-y le code suivant :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/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); } }Compilez et exécutez
Commit.java. Il est possible de saisir du texte à synthétiser plusieurs fois de suite. Appuyez simplement sur Entrée sans texte pour diffuser l'audio généré par l'API Realtime sur votre haut-parleur. -
Mode server_commit
Utilisation en production
Réutilisation des connexions WebSocket
Les connexions WebSocket sont réutilisables : une fois une tâche de synthèse terminée, vous pouvez lancer la tâche suivante sur la même connexion sans avoir à la rétablir.
Processus de réutilisation :
- Qwen-Audio-TTS / Qwen-Audio-TTS/CosyVoice : le client envoie
finish-task, puis peut envoyerrun-taskpour démarrer une nouvelle tâche après réception detask-finisheddu serveur. - Qwen-TTS : le client envoie
session.finish, puis peut créer une nouvelle session pour la tâche suivante après réception desession.finisheddu serveur.
Réutilisation après annulation : pour Qwen-Audio-TTS et Qwen-Audio-TTS/CosyVoice, si vous annulez la tâche en cours via la directive cancel, vous pouvez également envoyer un nouveau run-task sur la même connexion une fois que le serveur a retourné task-finished. Pour plus de détails, consultez Annuler une tâche.
Important
- Attendez l'événement de fin du serveur (
task-finishedousession.finished) avant de démarrer une nouvelle tâche. - Qwen-Audio-TTS et Qwen-Audio-TTS/CosyVoice nécessitent un
task_iddifférent pour chaque tâche sur une connexion réutilisée. - En cas d'échec d'une tâche, le serveur renvoie un événement d'erreur et ferme la connexion. Celle-ci n'est alors plus réutilisable.
- Si aucune nouvelle tâche ne démarre dans les 60 secondes suivant la fin de la précédente, la connexion se ferme automatiquement.
Pour consulter le détail des événements propres à chaque modèle, reportez-vous à la référence API correspondante.
Limites de débit
Les appels aux modèles sont soumis à des limites de taux. Lorsqu'une limite est dépassée, le serveur retourne l'erreur Requests rate limit exceeded, please try again later. Réduisez votre fréquence de requêtes ou votre niveau de concurrence, puis réessayez.
Pour connaître les limites de taux applicables à chaque modèle, consultez Limitation du taux.
Bonnes pratiques de forte concurrence
Le SDK DashScope intègre un mécanisme de pool qui réutilise les connexions WebSocket et les objets synthesizer, ce qui évite la surcharge liée à leur création et destruction répétées.
Afficher les bonnes pratiques pour une concurrence élevée
Qwen-Audio-TTS/CosyVoice
Qwen-Audio-TTS et Qwen-Audio-TTS/CosyVoice partagent la même interface SDK. Les exemples ci-dessous s'appliquent également aux modèles Qwen-Audio-TTS ; il suffit de remplacer les paramètres model et voice.
Prérequis
-
Le SDK DashScope doit être installé et respecter les versions minimales. Il est recommandé d'installer la dernière version :
- SDK Python : version >= 1.25.2
- SDK Java : version >= 2.16.6
SDK Python
Le SDK Python utilise SpeechSynthesizerObjectPool pour gérer et réutiliser les objets SpeechSynthesizer.
Ce pool crée un nombre défini d'instances SpeechSynthesizer et établit les connexions WebSocket lors de son initialisation. Lorsque vous empruntez un objet, celui-ci est prêt à envoyer immédiatement des requêtes, ce qui réduit la latence du premier paquet. Une fois l'objet restitué, la connexion reste active pour la tâche suivante.
Étapes de mise en œuvre
-
Installez les dépendances : installez DashScope (
pip install -U dashscope). -
Créez et configurez le pool d'objets
Fixez la taille du pool à 1.5x-2x le pic de concurrence, sans dépasser la limite QPS du compte.
Créez un pool singleton global (l'établissement des connexions lors de l'initialisation prend un certain temps) :
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"
Important
- Dans un pool d'objets,
SpeechSynthesizerObjectPoolouvre les connexions WebSocket avec la valeur globaledashscope.api_keycourante lors de l'initialisation. La clé API est écrite dans l'en-têteAuthorizationuniquement pendant la négociation WebSocket pour l'authentification. Les messages ultérieurs, tels querun-task, ne la contiennent pas. Modifierdashscope.api_keyaprès la création du pool n'affecte pas les connexions existantes : les objets empruntés viaborrow_synthesizer, même après restitution et nouvel emprunt, utilisent toujours la clé de la négociation initiale. La nouvelle valeur est ignorée sans avertissement, ce qui peut modifier l'identité, le quota ou l'imputation des frais par rapport aux attentes. Remarque :borrow_synthesizern'accepte pas de clé API en paramètre. - Pour utiliser plusieurs clés API, créez pour chacune une instance distincte de
SpeechSynthesizerObjectPool.
-
Empruntez un objet
SpeechSynthesizerau poolSi le nombre d'objets non restitués dépasse la capacité du pool, le système crée des objets supplémentaires.
Ces objets additionnels doivent établir de nouvelles connexions et ne bénéficient pas des avantages du pool.
speech_synthesizer = connectionPool.borrow_synthesizer(
model='cosyvoice-v3-flash',
voice='longanyang',
seed=12382,
callback=synthesizer_callback
)
-
Effectuez la synthèse vocale
Appelez la méthode call ou streaming_call de l'objet
SpeechSynthesizerpour synthétiser la voix. -
Restituez l'objet
SpeechSynthesizerRenvoyez l'objet une fois la tâche terminée afin qu'il puisse être réutilisé.
Ne restituez pas les objets dont la tâche a échoué ou n'est pas terminée.
connectionPool.return_synthesizer(speech_synthesizer)
Code complet
ImportantAvant d'exécuter ce code : notez que SpeechSynthesizerObjectPool établit les connexions WebSocket et s'authentifie en utilisant la variable globale actuelle dashscope.api_key lors de l'initialisation. Modifier dashscope.api_key après la création du pool n'affecte pas les connexions existantes : la nouvelle valeur est ignorée silencieusement. Pour utiliser plusieurs clés API, maintenez une instance de pool distincte pour chacune d'elles. Pour plus de détails, consultez la remarque importante ci-dessus.
# !/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/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()
Gestion des ressources et des erreurs
-
Réussite : après la fin normale de la synthèse, appelez
connectionPool.return_synthesizer(speech_synthesizer)pour restituer l'objetSpeechSynthesizerau pool et le réutiliser.ImportantNe restituez pas les objets
SpeechSynthesizerdont la tâche a échoué ou n'est pas terminée. -
Échec : si une erreur interne du SDK ou une exception métier interrompt la tâche, fermez la connexion WebSocket sous-jacente :
speech_synthesizer.close() -
Lorsque toutes les synthèses sont terminées, arrêtez le pool :
connectionPool.shutdown() -
Une erreur TaskFailed côté serveur ne nécessite aucun traitement supplémentaire.
SDK Java
Le SDK Java offre des performances optimales grâce à la coordination d'un pool de connexions intégré et d'un pool d'objets personnalisé.
- Pool de connexions : le pool OkHttp3 intégré au SDK gère et réutilise les connexions WebSocket, réduisant le coût des négociations réseau. Il est activé par défaut.
- Pool d'objets : fondé sur
commons-pool2, il maintient des objetsSpeechSynthesizerpréconnectés. Leur emprunt élimine le délai d'établissement de connexion et réduit nettement la latence du premier paquet.
Étapes de mise en œuvre
-
Ajouter des dépendances
Ajoutez dashscope-sdk-java et commons-pool2 à la configuration des dépendances de votre projet selon l'outil de build utilisé.
Exemples pour Maven et Gradle :
Maven
- Ouvrez le fichier
pom.xmldu projet Maven. - Ajoutez les dépendances suivantes dans la balise
<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>- Enregistrez le fichier
pom.xml. - Exécutez une commande Maven telle que
mvn clean installoumvn compilepour actualiser les dépendances.
Gradle
- Ouvrez le fichier
build.gradledu projet Gradle. - Ajoutez les dépendances suivantes dans le bloc
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' }- Enregistrez le fichier
build.gradle. - Dans le terminal, placez-vous à la racine du projet et exécutez la commande Gradle suivante pour actualiser les dépendances.
./gradlew build --refresh-dependenciesSous Windows, utilisez plutôt la commande suivante :
gradlew build --refresh-dependencies - Ouvrez le fichier
-
Configurer le pool de connexions
Définissez les paramètres clés du pool de connexions via des variables d'environnement :
Variable d'environnement
Description
DASHSCOPE_CONNECTION_POOL_SIZE
Taille du pool de connexions.
Valeur recommandée: au moins 2x le pic de concurrence.
Valeur par défaut: 32.
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS
Nombre maximal de requêtes asynchrones.
Valeur recommandée: identique à
DASHSCOPE_CONNECTION_POOL_SIZE.Valeur par défaut: 32.
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST
Nombre maximal de requêtes asynchrones par hôte.
Valeur recommandée: identique à
DASHSCOPE_CONNECTION_POOL_SIZE.Valeur par défaut: 32.
-
Configurer le pool d'objets
Définissez la taille du pool d'objets via des variables d'environnement :
Variable d'environnement
Description
COSYVOICE_OBJECTPOOL_SIZE
Taille du pool d'objets.
Valeur recommandée: 1.5x-2x le pic de concurrence.
Valeur par défaut: 500.
Important
- La taille du pool d'objets (
COSYVOICE_OBJECTPOOL_SIZE) doit être inférieure ou égale à celle du pool de connexions (DASHSCOPE_CONNECTION_POOL_SIZE). Sinon, lorsque ce dernier est saturé, les threads appelants se bloquent dans l'attente d'une connexion disponible. - La taille du pool d'objets ne doit pas dépasser la limite QPS (requêtes par seconde) du compte.
Créez le pool d'objets à l'aide du code suivant :
- La taille du pool d'objets (
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;
}
}
-
Empruntez un objet
SpeechSynthesizerau poolSi le nombre d'objets non restitués dépasse la capacité maximale du pool, le système crée un objet
SpeechSynthesizersupplémentaire.Ces nouveaux objets nécessitent une réinitialisation ainsi qu'une nouvelle connexion WebSocket ; ils ne bénéficient donc pas des avantages du pooling.
synthesizer = CosyvoiceObjectPool.getInstance().borrowObject();
-
Effectuez la synthèse vocale
Après avoir emprunté un objet
SpeechSynthesizerdepuis le pool, appelezupdateParamAndCallback(param, callback)pour lier les paramètres et le callback de la tâche en cours, puis appelezstreamingCalloucallpour synthétiser la voix.Important
- Dans un pool d'objets,
updateParamAndCallbackest appelé plusieurs fois : à chaque emprunt, pour définir le callback et les paramètres de tâche tels quevoiceetformat. La valeur deapiKeydoit rester identique à chaque appel.updateParamAndCallbackne met à jour que les champs locaux de l'instanceSpeechSynthesizersans recréer la connexion WebSocket. Le SDK écritapiKeydans l'en-têteAuthorizationuniquement pendant la négociation WebSocket. Les messages ultérieurs, tels querun-task, ne contiennent pasapiKey. Tant que la connexion réutilisée reste ouverte, une nouvelle valeur deapiKeyn'est pas transmise au serveur : la requête utilise toujoursapiKeyde la négociation initiale, ce qui peut affecter l'identité, les quotas ou l'imputation des frais. - Pour utiliser plusieurs clés API, maintenez des pools d'objets distincts pour chaque clé.
- Dans un pool d'objets,
-
Restituez l'objet
SpeechSynthesizerUne fois la tâche de synthèse terminée, restituez l'objet
SpeechSynthesizerafin que les tâches suivantes puissent le réutiliser.Ne restituez pas les objets dont la tâche a échoué ou n'est pas terminée.
CosyvoiceObjectPool.getInstance().returnObject(synthesizer);
Code complet
ImportantAvant d'utiliser ce code : dans le cadre d'un pool d'objets, l'apiKey transmis à updateParamAndCallback lors d'appels multiples doit rester identique. En effet, le SDK ne met pas à jour la clé API des connexions déjà établies ; transmettre une clé différente n'a donc aucun effet. Si vous devez utiliser plusieurs clés API, créez une instance de pool d'objets distincte pour chacune d'elles. Pour plus de détails, reportez-vous à la note importante ci-dessus.
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/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);
}
}
Configuration recommandée
La configuration suivante repose sur des tests effectués en exécutant uniquement le service de synthèse vocale Qwen-Audio-TTS/CosyVoice sur des instances ECS Alibaba Cloud aux spécifications indiquées. Une concurrence excessive risque d'accroître la latence de traitement des tâches.
La « concurrence par machine » correspond au nombre de tâches de synthèse Qwen-Audio-TTS/CosyVoice exécutées simultanément, soit l'équivalent du nombre de threads de travail.
Caractéristiques ECS (Alibaba Cloud) | Concurrence maximale par machine | Taille du pool d'objets | Taille du pool de connexions |
|---|---|---|---|
4 vCPUs, 8 GiB | 100 | 500 | 2000 |
8 vCPUs, 16 GiB | 150 | 500 | 2000 |
16 vCPUs, 32 GiB | 200 | 500 | 2000 |
Gestion des ressources et des erreurs
-
Réussite : après la fin normale de la synthèse, appelez la méthode returnObject de GenericObjectPool pour restituer l'objet
SpeechSynthesizerau pool et le réutiliser.Dans l'exemple de code, cela correspond à
CosyvoiceObjectPool.getInstance().returnObject(synthesizer).ImportantNe restituez pas les objets
SpeechSynthesizerdont la tâche a échoué ou n'est pas terminée. -
Échec : si une erreur interne du SDK ou une exception métier interrompt la tâche :
- Fermez la connexion WebSocket sous-jacente.
- Invalidez l'objet dans le pool pour empêcher sa réutilisation.
// 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);
- Une erreur TaskFailed côté serveur ne nécessite aucun traitement supplémentaire.
Préchauffage et mesure de la latence
Pour évaluer correctement la latence et les performances du SDK Java DashScope en contexte concurrentiel, exécutez d'abord un nombre suffisant d'opérations de préchauffage. Ainsi, vos mesures refléteront les performances en régime permanent plutôt que la surcharge initiale liée à l'établissement des connexions.
Mécanisme de réutilisation des connexions
Le SDK Java DashScope utilise un pool de connexions global singleton pour gérer et réutiliser efficacement les connexions WebSocket. Cette approche réduit la surcharge associée à la création et à la fermeture fréquentes de connexions lors de charges de travail fortement concurrentes.
Fonctionnement de ce mécanisme :
-
Création à la demande : le SDK ne précrée pas les connexions au démarrage. Elles sont établies au premier appel.
-
Réutilisation limitée dans le temps : après une requête, la connexion reste disponible dans le pool pendant au plus 60 secondes.
- Si une nouvelle requête arrive dans les 60 secondes, elle réutilise la connexion sans nouvelle négociation.
- Après plus de 60 secondes d'inactivité, la connexion se ferme automatiquement pour libérer les ressources.
Importance du préchauffage
Dans les situations suivantes, le pool peut ne contenir aucune connexion active réutilisable, obligeant ainsi le système à créer de nouvelles connexions :
- L'application vient de démarrer et n'a encore effectué aucun appel.
- Le service est resté inactif plus de 60 secondes ; les connexions du pool ont expiré et ont été fermées.
Dans ces cas de figure, la première requête doit effectuer un handshake WebSocket complet (handshake TCP, négociation TLS, mise à niveau du protocole), ce qui engendre une latence nettement supérieure à celle des requêtes suivantes qui réutilisent les connexions. Sans préchauffage préalable, les résultats des tests de performance seront faussés par cette surcharge initiale de connexion.
Latence signalée par le SDK et latence réelle du premier paquet
La latence du premier paquet indiquée par le SDK (par exemple, la valeur retournée par get_first_package_delay()) inclut le temps d'établissement de la connexion WebSocket et de transmission réseau. Elle ne correspond donc pas à la latence réelle du premier paquet traitée par le service de modèle.
La latence réelle du premier paquet représente l'intervalle entre la réception de l'instruction run-task par le serveur et le renvoi du premier événement result-generated. Vous trouverez cette valeur dans les journaux côté serveur.
En contexte de forte concurrence, la création simultanée de connexions et la planification des ressources peuvent entraîner une latence rapportée par le SDK bien supérieure à la latence réelle du premier paquet côté serveur. Si vous constatez une latence élevée du premier paquet dans les rapports du SDK :
- Comparez la latence du premier paquet dans les journaux du serveur, de
run-taskau premierresult-generated, pour vérifier les performances d'inférence. - Préchauffez le service avec le pool d'objets ou de connexions décrit ci-dessus. Cela élimine le coût de négociation WebSocket et rapproche la latence du SDK de celle du premier paquet.
Pratiques recommandées
Pour obtenir des données de performance fiables, suivez ces étapes de préchauffage avant de lancer des tests de charge ou de collecter des métriques de latence :
- Simulez le niveau de concurrence visé et envoyez des requêtes de préchauffage, par exemple pendant 1-2 minutes, pour remplir le pool de connexions.
- Vérifiez que le pool a établi et maintient suffisamment de connexions actives avant de mesurer les performances.
Un préchauffage adéquat amène le pool de connexions du SDK à un état stable de réutilisation, produisant ainsi des métriques de latence qui reflètent fidèlement les performances en régime permanent de production.
Exceptions courantes du SDK Java
Exception 1 : Augmentation continue des connexions TCP serveur malgré un trafic stable
Chaque objet SDK établit une connexion lors de son initialisation. En l'absence de pool d'objets, l'objet est détruit à la fin de chaque tâche. La connexion passe alors dans un état non référencé et reste ouverte jusqu'à expiration du délai d'attente serveur, fixé à 61 secondes. Pendant cette période, la connexion ne peut pas être réutilisée.
En cas de forte concurrence, les nouvelles tâches qui ne trouvent aucune connexion réutilisable en créent de nouvelles, ce qui entraîne :
- Augmentation continue du nombre de connexions.
- Épuisement des ressources serveur et baisse des performances dus à un nombre excessif de connexions.
- Saturation du pool de connexions, bloquant les nouvelles tâches en attente d'une connexion.
Le paramètre MaxIdle du pool d'objets est configuré à une valeur inférieure à MaxTotal. Les objets inactifs dépassant le seuil MaxIdle sont alors détruits, ce qui libère leurs connexions de manière incontrôlée. Ces connexions orphelines doivent attendre 61 secondes avant d'expirer, comme pour le Type 1.
Solution :
Pour le Type 1 : Utilisez un pool d'objets.
Pour le Type 2 : Vérifiez la configuration du pool d'objets et définissez MaxIdle à une valeur égale à MaxTotal. Désactivez la politique d'éviction automatique des objets.
Exception 2 : Allongement anormal de 60 secondes par tâche
La cause racine est identique à celle de l'Exception 1 : le pool de connexions a atteint sa limite maximale. Les nouvelles tâches doivent patienter 61 secondes, le temps que les connexions non référencées expirent, avant qu'une connexion ne se libère.
Exception 3 : Lenteurs au démarrage du service avec récupération progressive
Cause racine :
Sous forte concurrence, un même objet réutilise la même connexion WebSocket. Or, les connexions WebSocket ne sont établies qu'au démarrage du service. Si une charge concurrentielle élevée survient immédiatement après le lancement, la création simultanée d'un trop grand nombre de connexions WebSocket provoque un blocage.
Solution :
Augmentez progressivement la concurrence après le démarrage du service ou ajoutez des tâches de préchauffage.
Exception 4 : le serveur renvoie "Invalid action('run-task')! Please follow the protocol!"
Cause racine :
Une erreur côté client s'est produite sans être détectée par le serveur. La connexion reste alors dans un état « en cours de tâche ». Lors de la réutilisation de cette connexion et de cet objet pour la tâche suivante, le flux protocolaire est rompu et la tâche échoue.
Solution :
Après la levée d'une exception, fermez la connexion WebSocket avant de retourner l'objet au pool.
Exception 5 : Pics de trafic malgré une charge métier stable
Cause racine :
La création simultanée d'un trop grand nombre de connexions WebSocket entraîne un blocage. Pendant ce blocage, le trafic métier entrant s'accumule. Une fois le blocage résolu, toutes les tâches accumulées s'exécutent en même temps, créant un pic de trafic susceptible de dépasser la limite de concurrence de votre compte. Cela peut provoquer l'échec des tâches ou la dégradation du serveur.
Les déclencheurs fréquents de créations massives et simultanées de connexions WebSocket incluent :
- Démarrage du service
- Anomalie réseau déconnectant puis reconnectant simultanément de nombreuses connexions WebSocket
- Pic d'erreurs serveur provoquant des reconnexions WebSocket massives, notamment un dépassement de la concurrence autorisée (« Requests rate limit exceeded, please try again later. »).
Solution :
- Vérifiez l'état du réseau.
- Recherchez un pic d'autres erreurs serveur précédant cette hausse.
- Augmentez la limite de concurrence du compte.
- Réduisez les pools d'objets et de connexions pour plafonner la concurrence.
- Augmentez les capacités du serveur ou ajoutez des machines.
Exception 6 : Ralentissement généralisé des tâches lors de l'augmentation de la concurrence
Solution :
- Vérifiez si la bande passante réseau est saturée.
- Vérifiez si la concurrence réelle est trop élevée.
Modèles et régions pris en charge
Singapore
Pour appeler les modèles suivants, sélectionnez une API Key dans la région de Singapour :
-
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 (stable, actuellement équivalent à qwen3-tts-instruct-flash-realtime-2026-01-22), qwen3-tts-instruct-flash-realtime-2026-01-22 (dernier snapshot)
- Qwen3-TTS-VD-Realtime : qwen3-tts-vd-realtime-2026-01-15 (dernier snapshot), qwen3-tts-vd-realtime-2025-12-16 (snapshot)
- Qwen3-TTS-VC-Realtime : qwen3-tts-vc-realtime-2026-01-15 (dernier snapshot), qwen3-tts-vc-realtime-2025-11-27 (snapshot)
- Qwen3-TTS-Flash-Realtime : qwen3-tts-flash-realtime (stable, actuellement équivalent à qwen3-tts-flash-realtime-2025-11-27), qwen3-tts-flash-realtime-2025-11-27 (dernier snapshot), qwen3-tts-flash-realtime-2025-09-18 (snapshot)
China (Beijing)
Pour appeler les modèles suivants, sélectionnez une API Key dans la région de Pékin :
-
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 (stable, actuellement équivalent à qwen3-tts-instruct-flash-realtime-2026-01-22), qwen3-tts-instruct-flash-realtime-2026-01-22 (dernier snapshot)
- Qwen3-TTS-VD-Realtime : qwen3-tts-vd-realtime-2026-01-15 (dernier snapshot), qwen3-tts-vd-realtime-2025-12-16 (snapshot)
- Qwen3-TTS-VC-Realtime : qwen3-tts-vc-realtime-2026-01-15 (dernier snapshot), qwen3-tts-vc-realtime-2025-11-27 (snapshot)
- Qwen3-TTS-Flash-Realtime : qwen3-tts-flash-realtime (stable, actuellement équivalent à qwen3-tts-flash-realtime-2025-11-27), qwen3-tts-flash-realtime-2025-11-27 (dernier snapshot), qwen3-tts-flash-realtime-2025-09-18 (snapshot)
- Qwen-TTS-Realtime : qwen-tts-realtime (stable, actuellement équivalent à qwen-tts-realtime-2025-07-15), qwen-tts-realtime-latest (latest, actuellement équivalent à qwen-tts-realtime-2025-07-15), qwen-tts-realtime-2025-07-15 (snapshot)
Voix prises en charge
Les voix disponibles varient selon les modèles. Définissez le paramètre de requête voice sur la valeur indiquée dans la colonne Paramètre voice de la liste de voix correspondante.
Référence API
- Synthèse vocale en temps réel : référence API Qwen-Audio-TTS/CosyVoice
- Synthèse vocale en temps réel : référence API Qwen-TTS
- SDK client AOQ (pour les modèles CosyVoice)
FAQ
Q : Comment corriger une prononciation incorrecte lors de la synthèse vocale ? Comment contrôler la prononciation des caractères polyphoniques ?
- Remplacez le caractère polyphonique par un homophone pour corriger rapidement sa prononciation.
- Utilisez les balises SSML pour contrôler la prononciation.
Q : Comment diagnostiquer un audio silencieux avec une voix clonée ?
-
Vérifiez l'état de la voix
Appelez l'interface API de clonage/conception de voix et confirmez que le
statusde la voix estOK. -
Vérifiez la cohérence des versions du modèle
Assurez-vous que le paramètre
target_modelutilisé lors du clonage de la voix correspond au paramètremodelutilisé pour la synthèse vocale. Par exemple :- Clonage effectué avec
cosyvoice-v3-plus - La synthèse doit également utiliser
cosyvoice-v3-plus
- Clonage effectué avec
-
Vérifiez la qualité de l'audio source
Contrôlez si l'audio source utilisé pour le clonage vocal respecte les exigences décrites dans l'API de clonage/conception de voix :
- Durée audio : 10-20 secondes
- Audio clair
- Absence de bruit de fond
-
Vérifiez les paramètres de la requête
Confirmez que le paramètre
voicedans la requête de synthèse vocale correspond bien à l'ID de la voix clonée.
Q : Que faire si l'audio synthétisé avec une voix clonée est instable ou incomplet ?
Si l'audio synthétisé à partir d'une voix clonée présente l'un des problèmes suivants :
- Lecture incomplète : seule une partie du texte est prononcée
- Qualité de synthèse irrégulière
- Pauses anormales ou segments silencieux dans l'audio
Cause possible : L'audio source ne répond pas aux exigences de qualité.
Solution : Vérifiez si l'audio source respecte les critères indiqués dans le Guide d'enregistrement pour le clonage vocal. Réenregistrez l'audio en suivant ces directives.
Q : Pourquoi la durée réelle de l'audio synthétisé diffère-t-elle de celle indiquée dans le fichier WAV ?
La synthèse vocale utilise un mécanisme de streaming qui retourne les données au fur et à mesure de leur génération. La durée inscrite dans l'en-tête du fichier WAV sauvegardé est une estimation et peut être inexacte. Pour obtenir une durée précise, définissez le format sur pcm, attendez le résultat complet de la synthèse, puis ajoutez manuellement l'en-tête du fichier WAV.
Q : Pourquoi le fichier audio ne peut-il pas être lu ?
Effectuez le dépannage selon votre scénario :
-
Audio enregistré dans un fichier complet, tel que xx.mp3
- Cohérence du format : le format demandé doit correspondre à l'extension du fichier ; par exemple, avec le paramètre wav, utilisez l'extension .wav.
- Compatibilité du lecteur : vérifiez la prise en charge du format audio et de la fréquence d'échantillonnage.
-
Lecture audio en streaming
- Enregistrez le flux dans un fichier complet et essayez de le lire avec un lecteur multimédia. En cas d'échec, consultez le scénario 1 ci-dessus.
- Si le fichier est lisible, le problème se situe dans la lecture en streaming. Vérifiez que le lecteur la prend en charge, par exemple ffmpeg, pyaudio, AudioFormat ou MediaSource.
Q : Pourquoi la lecture audio est-elle saccadée ?
Procédez au dépannage en suivant les étapes ci-dessous :
-
Vérifiez le rythme d'envoi du texte : adaptez l'intervalle pour que le segment audio précédent ne se termine pas avant l'arrivée du texte suivant.
-
Vérifiez les performances du callback :
- Vérifiez que le callback ne contient aucune opération bloquante.
- Les callbacks s'exécutent sur le thread WebSocket. Une opération bloquante perturbe la réception. Écrivez les données audio dans un tampon distinct et traitez-les dans un autre thread.
-
Vérifiez la stabilité du réseau : ses fluctuations peuvent interrompre ou retarder la transmission audio.
Q : Pourquoi la synthèse vocale prend-elle beaucoup de temps ?
Procédez au dépannage en suivant les étapes ci-dessous :
-
Vérifiez les intervalles d'entrée
Pour la synthèse en streaming, vérifiez si l'intervalle d'envoi du texte est trop long. Des intervalles prolongés augmentent le temps total de synthèse.
-
Analysez les indicateurs de performance
- Latence du premier paquet : généralement environ 500 ms.
- RTF (facteur temps réel = durée totale de synthèse / durée audio) : doit être inférieur à 1.0.
Q : Comment limiter une clé API au seul service de synthèse vocale pour isoler ses permissions ?
Créez un nouvel espace de travail et accordez l'accès uniquement à des modèles spécifiques. Cela limite la portée de la clé API. Pour plus de détails, consultez Gérer les espaces de travail.