Utilisez AOQ pour vous connecter à qwen-audio-3,0-tts-flash, envoyez le texte par segments et lisez la parole synthétisée en temps réel. Le code client est écrit en Java pour Android.
Présentation de la solution
qwen-audio-3,0-tts-flash prend en charge le protocole d'événements d'inférence AOQ. Ce tutoriel utilise ce modèle pour illustrer la synthèse vocale en continu via AOQ. Le client envoie les événements run-task, continue-task et finish-task sur la piste Data. Le service diffuse l'audio sur la piste Audio et renvoie les événements de tâche sur la piste Data.
Une tâche peut contenir plusieurs événements continue-task. Les phrases complètes sont synthétisées rapidement. Les phrases incomplètes restent mises en mémoire tampon jusqu'à ce que du texte ultérieur les complète ou que le client envoie finish-task. Cette approche convient à la lecture sur mobile, à l'entrée de textes longs segmentés et à la sortie vocale à faible latence.
Prérequis
- Activez Model Studio et suivez les instructions de la rubrique Obtenir et configurer une clé API. Stockez la clé API uniquement sur votre serveur d'application. Ne l'incluez pas dans le code client et ne la commitez pas dans un dépôt de code.
- Confirmez l'endpoint AOQ pour la région dans laquelle votre application est déployée. Pour obtenir des conseils de sélection, consultez la rubrique Sélectionner une région, une étendue de déploiement et un endpoint.
- Téléchargez la dernière version du SDK Client AOQ comme décrit dans la rubrique Téléchargement du SDK.
- Créez un serveur d'application et implémentez l'authentification proxy comme décrit dans la rubrique Authentification par jeton. Avant chaque nouvelle connexion, le client doit obtenir de nouvelles informations d'identification de connexion auprès du serveur d'application.
Importer le SDK
Importez le SDK pour votre plateforme de développement. L'implémentation client utilise Java pour Android. D'autres plateformes fournissent les mêmes interfaces et le même flux d'événements. Ce tutoriel utilise des flux audio PCM. Si votre application sélectionne Opus, importez le plugin correspondant comme décrit dans la rubrique de téléchargement du SDK.
Android
-
Placez AoqClientSdk-release.aar dans app/libs, et configurez la dépendance ainsi que les ABI prises en charge par le SDK dans app/build.gradle :
android { defaultConfig { minSdk 21 ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.aar']) } -
Déclarez les permissions suivantes dans AndroidManifest.xml :
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> -
Ce scénario ne nécessite pas les permissions microphone ou caméra.
iOS
- Ajoutez AoqClientSdk.framework au projet Xcode et sélectionnez Embed & Sign sous Target > General > Frameworks, Libraries, and Embedded Content. Le SDK prend en charge les appareils arm64 exécutant iOS 13.0 ou version ultérieure.
- Ce scénario n'utilise pas le microphone ni la caméra et ne nécessite aucune permission associée.
- Utilisez import AoqClientSdk en Swift ou #import <AoqClientSdk/AoqClientSdk.h> en Objective-C.
HarmonyOS
-
Placez AoqClientSdk.har dans entry/libs et déclarez la dépendance dans entry/oh-package.json5. Le SDK est compatible avec l'API 12 et prend en charge arm64-v8a :
{ "dependencies": { "@aoq/client-sdk": "file:./libs/AoqClientSdk.har" } } -
Déclarez les permissions suivantes dans entry/src/main/module.json5 :
"requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] -
Ce scénario ne nécessite pas les permissions microphone ou caméra.
Linux (Python)
-
Extrayez le SDK et conservez aoq_client_sdk.py, libAoqClientSdk.so et libonnxruntime.so.1.16.3 dans le même répertoire.
-
Ajoutez le répertoire du SDK aux chemins de recherche Python et des bibliothèques dynamiques :
export PYTHONPATH="$PWD/AoqClientSdk:$PYTHONPATH" export LD_LIBRARY_PATH="$PWD/AoqClientSdk:$LD_LIBRARY_PATH" -
Utilisez import aoq_client_sdk en Python. Vous pouvez également spécifier le chemin absolu de libAoqClientSdk.so en utilisant AOQ_CLIENT_SDK_LIB.
Essayer la démo
Utilisez la démo Android d'Alibaba Cloud Model Studio pour vérifier rapidement la connectivité AOQ. Téléchargez l'APK et configurez la clé API et workspaceId pour essayer certains modèles.
Scannez le code QR suivant pour télécharger la démo :
Flux d'implémentation
- Le serveur d'application obtient les paramètres de connexion AOQ pour qwen-audio-3,0-tts-flash depuis l'URL du jeton d'inférence.
- Le client publie la piste Data, s'abonne aux pistes Audio et Data, et configure le décodeur du SDK pour le format audio de sortie sélectionné dans run-task.
- Le client démarre le lecteur local et se connecte à AOQ. Une fois la connexion établie, il envoie run-task avec un nouveau task_id.
- Après réception de task-started, le client envoie un ou plusieurs segments de texte continue-task au rythme requis par l'application.
- Une fois tout le texte envoyé, le client envoie finish-task. Le service renvoie l'audio restant, puis finalement task-finished.
- Après réception de task-finished, démarrez une autre tâche via la même connexion AOQ avec un nouveau task_id, ou déconnectez-vous et détruisez le moteur.
Obtenir un jeton depuis le serveur d'application
Définissez DASHSCOPE_API_KEY sur le serveur d'application et envoyez la requête à l'endpoint de la région sélectionnée. clientIp correspond à l'adresse IP publique réelle du client. Ce champ est facultatif, mais sa spécification aide le service à allouer un endpoint de relais approprié.
curl -X POST \
"https://{endpoint}/api/v1/webrtc/inference?model=qwen-audio-3.0-tts-flash" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \
-H "x-dashscope-rtc-transport: moq" \
-d "{\"clientIp\": \"${CLIENT_REAL_IP}\"}"
RemarqueSi le serveur d'application ne peut pas obtenir l'adresse IP publique réelle du client, omettez clientIp au lieu de transmettre une chaîne vide.
Le serveur d'application renvoie les champs de réponse suivants au client. Ne renvoyez jamais la clé API à un client en production. Pour tous les champs de requête et de réponse, consultez la rubrique Authentification par jeton.
Champ de réponse | Champ SDK |
aoqTokenForClient | AoqConnectConfig.token |
sid | AoqConnectConfig.sid |
clientRelayCertFingerprint | AoqConnectConfig.certFingerprint |
clientRelayEndpoints | AoqConnectConfig.relayEndpoints |
extraInfo.workspaceIdHash | AoqConnectConfig.workspaceIdHash |
Implémenter le client Android
Une fois que le client a obtenu AoqConnectConfig depuis le serveur d'application, suivez ces étapes pour implémenter la synthèse vocale en continu sur Android.
1. Créer le moteur et enregistrer les rappels
Créez le moteur AOQ singleton et enregistrez les rappels pour les événements de connexion et de piste Data. Maintenez l'état de préparation de la connexion dans le rappel de connexion et transmettez les événements de tâche à la machine d'états de l'application.
AoqClientListener listener = new AoqClientListener() {
@Override
public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
connected = status == AoqClientEngine.AoqConnectionStatus
.AoqConnectionStatusConnected;
}
@Override
public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
handleServerEvent(msg);
}
};
AoqClientEngine.AoqCreateConfig createConfig = new AoqClientEngine.AoqCreateConfig();
createConfig.workDir = context.getFilesDir().getAbsolutePath();
engine = AoqClientEngine.createEngine(context, createConfig, listener);
2. Démarrer la lecture audio
La synthèse vocale (TTS) ne capture pas l'audio du microphone. Initialisez uniquement le lecteur local. Sélectionnez le haut-parleur ou l'écouteur comme sortie par défaut. Le SDK lit automatiquement l'audio du serveur provenant de la piste Audio.
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
3. Configurer le décodeur et les pistes, puis se connecter
Configurez le décodeur du SDK pour le format audio de sortie sélectionné dans run-task. Publiez ensuite la piste Data et abonnez-vous aux pistes Audio et Data. Les valeurs suivantes sont des exemples PCM pour ce tutoriel. Remplissez les champs AoqConnectConfig à partir de la réponse du jeton du serveur d'application.
AoqClientEngine.AoqAudioCodecConfig audioDecoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioDecoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioDecoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioDecoderConfig.sampleRate = 24000; // Example. Match run-task.sample_rate.
audioDecoderConfig.channel = 1;
engine.setAudioDecoderConfig(audioDecoderConfig);
AoqClientEngine.AoqTrackParam publishDataTrack = new AoqClientEngine.AoqTrackParam();
publishDataTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeData;
connectConfig.publishTracks.add(publishDataTrack);
AoqClientEngine.AoqTrackParam subscribeAudioTrack = new AoqClientEngine.AoqTrackParam();
subscribeAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.subscribeTracks.add(subscribeAudioTrack);
AoqClientEngine.AoqTrackParam subscribeDataTrack = new AoqClientEngine.AoqTrackParam();
subscribeDataTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeData;
connectConfig.subscribeTracks.add(subscribeDataTrack);
engine.connect(connectConfig);
4. Appeler sendDataMsg pour envoyer un événement run-task
Une fois la connexion établie, générez un nouveau task_id UUID et configurez le modèle, la voix, le type de texte, le format audio et la fréquence d'échantillonnage. Pour les paramètres facultatifs, consultez la rubrique Événements client.
taskId = UUID.randomUUID().toString();
JSONObject header = new JSONObject()
.put("action", "run-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject parameters = new JSONObject()
.put("text_type", "PlainText")
.put("voice", voice)
.put("format", "pcm")
.put("sample_rate", 24000);
JSONObject payload = new JSONObject()
.put("task_group", "audio")
.put("task", "tts")
.put("function", "SpeechSynthesizer")
.put("model", "qwen-audio-3.0-tts-flash")
.put("input", new JSONObject())
.put("parameters", parameters);
JSONObject runTask = new JSONObject().put("header", header).put("payload", payload);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = runTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
5. Appeler sendDataMsg pour envoyer un événement continue-task
Envoyez continue-task uniquement après réception de task-started. Une tâche peut contenir plusieurs segments. Chaque événement prend en charge jusqu'à 20 000 caractères, et une tâche prend en charge jusqu'à 200 000 caractères au total. Envoyez les segments suivants ou terminez la tâche rapidement. Ne dépendez pas d'une valeur fixe de délai d'expiration de connexion.
JSONObject continueHeader = new JSONObject()
.put("action", "continue-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject payload = new JSONObject()
.put("input", new JSONObject().put("text", text));
JSONObject continueTask = new JSONObject()
.put("header", continueHeader)
.put("payload", payload);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = continueTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
6. Gérer les événements serveur
Dans onDataMsg, lisez header.event pour maintenir l'état de la tâche et gérer les échecs. result-generated indique qu'une phrase a été synthétisée, tandis que l'audio est toujours renvoyé via la piste Audio. Pour tous les champs, consultez la rubrique Événements serveur.
JSONObject header = event.optJSONObject("header");
if (header == null) return;
String name = header.optString("event");
if ("task-started".equals(name)) {
// The application can now send one or more continue-task events.
} else if ("result-generated".equals(name)) {
// A sentence was synthesized. Audio is delivered over the Audio track.
} else if ("task-finished".equals(name)) {
taskActive = false;
} else if ("task-failed".equals(name)) {
taskActive = false;
String message = header.optString("error_message");
// Display or log the error.
}
7. Appeler sendDataMsg pour envoyer un événement finish-task
Immédiatement après l'envoi de tout le texte, envoyez finish-task pour synthétiser le texte incomplet mis en mémoire tampon par le service, et attendez task-finished. Pour plus de détails, consultez la rubrique Événements client.
JSONObject finishHeader = new JSONObject()
.put("action", "finish-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject finishTask = new JSONObject()
.put("header", finishHeader)
.put("payload", new JSONObject().put("input", new JSONObject()));
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = finishTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
8. Se déconnecter et détruire le moteur
Ne vous déconnectez pas immédiatement après l'envoi de finish-task. Après réception de task-finished ou task-failed, déconnectez-vous et détruisez le moteur si aucune tâche suivante ne doit être démarrée. Le SDK ferme automatiquement le lecteur audio.
engine.disconnect();
AoqClientEngine.destroy();
Principaux événements serveur
Événement | Description |
task-started | La tâche a démarré et continue-task peut être envoyé |
result-generated | Une phrase complète a été synthétisée et son audio est renvoyé via la piste Audio |
task-finished | Tout le texte mis en mémoire tampon a été traité et la tâche est terminée |
task-failed | La tâche a échoué. Lisez le code d'erreur et le message |
Exemple complet
La classe suivante accepte un AoqConnectConfig mappé à partir de la réponse du jeton du serveur d'application. Une fois la connexion établie, appelez synthesize(text, voice). Ajoutez les permissions, l'état de l'interface utilisateur et la logique de reconnexion en production.
import android.content.Context;
import com.alibaba.aoq.clientsdk.AoqClientEngine;
import com.alibaba.aoq.clientsdk.AoqClientListener;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
public final class TtsClient {
private AoqClientEngine engine;
private String taskId;
private String pendingText;
private String pendingVoice;
private boolean connected;
private boolean taskActive;
public TtsClient(Context context, AoqClientEngine.AoqConnectConfig connectConfig) {
AoqClientListener listener = new AoqClientListener() {
@Override
public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
if (status == AoqClientEngine.AoqConnectionStatus.AoqConnectionStatusConnected) {
connected = true;
} else if (status == AoqClientEngine.AoqConnectionStatus
.AoqConnectionStatusDisconnected) {
connected = false;
}
}
@Override
public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
try {
JSONObject event = new JSONObject(
new String(msg.data, StandardCharsets.UTF_8));
String eventName = event.optJSONObject("header") == null
? "" : event.optJSONObject("header").optString("event");
if ("task-started".equals(eventName)) {
sendContinueTask();
sendFinishTask();
} else if ("task-finished".equals(eventName)
|| "task-failed".equals(eventName)) {
taskActive = false;
}
} catch (JSONException e) {
throw new IllegalArgumentException("Invalid server event", e);
}
}
};
AoqClientEngine.AoqCreateConfig createConfig = new AoqClientEngine.AoqCreateConfig();
createConfig.workDir = context.getFilesDir().getAbsolutePath();
engine = AoqClientEngine.createEngine(context, createConfig, listener);
// Example values. Match these settings to the output audio format in run-task.
AoqClientEngine.AoqAudioCodecConfig audioDecoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioDecoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioDecoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioDecoderConfig.sampleRate = 24000;
audioDecoderConfig.channel = 1;
engine.setAudioDecoderConfig(audioDecoderConfig);
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
AoqClientEngine.AoqTrackParam publishDataTrack =
new AoqClientEngine.AoqTrackParam();
publishDataTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeData;
connectConfig.publishTracks.add(publishDataTrack);
AoqClientEngine.AoqTrackParam subscribeAudioTrack =
new AoqClientEngine.AoqTrackParam();
subscribeAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.subscribeTracks.add(subscribeAudioTrack);
AoqClientEngine.AoqTrackParam subscribeDataTrack =
new AoqClientEngine.AoqTrackParam();
subscribeDataTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeData;
connectConfig.subscribeTracks.add(subscribeDataTrack);
engine.connect(connectConfig);
}
public void synthesize(String text, String voice) {
if (!connected || taskActive) {
throw new IllegalStateException("The connection is not ready or a task is active.");
}
taskId = UUID.randomUUID().toString();
pendingText = text;
pendingVoice = voice;
taskActive = true;
sendRunTask();
}
private void sendRunTask() {
try {
JSONObject header = new JSONObject()
.put("action", "run-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject parameters = new JSONObject()
.put("text_type", "PlainText")
.put("voice", pendingVoice)
.put("format", "pcm")
.put("sample_rate", 24000);
JSONObject payload = new JSONObject()
.put("task_group", "audio")
.put("task", "tts")
.put("function", "SpeechSynthesizer")
.put("model", "qwen-audio-3.0-tts-flash")
.put("input", new JSONObject())
.put("parameters", parameters);
JSONObject runTask = new JSONObject().put("header", header).put("payload", payload);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = runTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
} catch (JSONException e) {
throw new IllegalStateException("Failed to create run-task", e);
}
}
private void sendContinueTask() {
try {
JSONObject header = new JSONObject()
.put("action", "continue-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject payload = new JSONObject()
.put("input", new JSONObject().put("text", pendingText));
JSONObject continueTask = new JSONObject()
.put("header", header)
.put("payload", payload);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = continueTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
} catch (JSONException e) {
throw new IllegalStateException("Failed to create continue-task", e);
}
}
private void sendFinishTask() {
try {
JSONObject header = new JSONObject()
.put("action", "finish-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject finishTask = new JSONObject()
.put("header", header)
.put("payload", new JSONObject().put("input", new JSONObject()));
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = finishTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
} catch (JSONException e) {
throw new IllegalStateException("Failed to create finish-task", e);
}
}
public void close() {
engine.disconnect();
AoqClientEngine.destroy();
}
}
Exécuter et vérifier
- Le texte est soumis uniquement après réception de task-started.
- L'audio des phrases complètes est lu en continu via la piste Audio. Les phrases incomplètes sont synthétisées après finish-task.
- task-finished est reçu une fois tout l'audio terminé. Une autre tâche peut alors démarrer avec un nouveau task_id.
Scénarios courants
Plusieurs tâches via une seule connexion
Après réception de task-finished, envoyez un autre run-task avec un nouveau task_id via la même connexion AOQ. Aucun nouveau jeton n'est nécessaire tant que la connexion reste active. Si la connexion est fermée, obtenez de nouvelles informations d'identification de connexion.
Changer de voix
Chaque run-task peut sélectionner une voix système ou un voice_id valide dans parameters.voice. Vous pouvez donc changer de voix entre les tâches via la même connexion.
Haut-parleur ou écouteur
Définissez la sortie par défaut en utilisant AoqAudioPlaybackConfig.isDefaultSpeaker, et appelez enableSpeakerphone pour basculer pendant que la connexion est active.
Dépannage
Problème | Solution |
La connexion réussit mais la tâche ne démarre pas | Assurez-vous que les informations d'identification ont été obtenues depuis l'URL du jeton d'inférence, et vérifiez le nom du modèle, le task_id et la publication de la piste Data dans run-task. |
continue-task est rejeté | Attendez task-started, et utilisez le même task_id dans run-task, continue-task et finish-task. |
La tâche réussit mais aucun audio n'est lu | Assurez-vous que la piste Audio est abonnée et que le lecteur est en cours d'exécution, et vérifiez que le décodeur du SDK correspond au format audio de sortie sélectionné dans run-task. |
Le texte final n'a pas d'audio | Envoyez finish-task après l'envoi de tout le texte, et attendez l'audio restant et task-finished avant de vous déconnecter. |
Informations connexes
Pour tous les paramètres, champs d'événement et interfaces pour d'autres plateformes, consultez :