Utilisez AOQ pour vous connecter à qwen-audio-3,0-realtime-plus et exploitez la détection d'activité vocale (VAD) côté serveur afin de mettre en place des conversations vocales en temps réel à faible latence. Le code client est implémenté en Java pour Android.
Présentation de la solution
Qwen-Audio est un modèle d'interaction vocale en temps réel de bout en bout, conçu pour les scénarios nécessitant une faible latence, tels que les assistants vocaux, le service client et les compagnons IA. AOQ transporte l'audio et les événements sur des pistes distinctes. La piste Audio gère le flux montant PCM du microphone et le flux descendant PCM du modèle, tandis que la piste Data transporte les événements du protocole Realtime.
Ce tutoriel utilise server_vad. Le client envoie l'audio en continu et le service détecte automatiquement le début et la fin de la parole de l'utilisateur pour déclencher une réponse.
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 où 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.
- Mettez en place un serveur d'application et implémentez l'authentification par proxy comme indiqué dans la rubrique Authentification par token. Avant chaque nouvelle connexion, le client doit obtenir de nouvelles informations d'identification auprès du serveur d'application.
Importer le SDK
Importez le SDK adapté à votre plateforme de développement. L'implémentation client présentée ici utilise Java pour Android. Les autres plateformes proposent les mêmes interfaces et le même flux d'événements. Ce tutoriel utilise des flux audio PCM. L'encodage Opus est fourni par un plugin. Importez le plugin Opus si le flux montant utilise ce format.
Android
-
Placez le fichier AoqClientSdk-release.aar dans app/libs, puis configurez la dépendance et 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 autorisations suivantes dans AndroidManifest.xml :
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" /> -
Demandez les autorisations RECORD_AUDIO au moment de l'exécution avant d'utiliser les périphériques correspondants.
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 une version ultérieure.
- Ajoutez NSMicrophoneUsageDescription à Info.plist et demandez l'autorisation avant d'utiliser les périphériques correspondants.
- Utilisez import AoqClientSdk dans Swift ou #import <AoqClientSdk/AoqClientSdk.h> dans 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 autorisations suivantes dans entry/src/main/module.json5 :
"requestPermissions": [ { "name": "ohos.permission.INTERNET" }, { "name": "ohos.permission.MICROPHONE", "reason": "$string:perm_mic_reason", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } } ] -
Avant d'utiliser les périphériques correspondants, appelez abilityAccessCtrl.createAtManager().requestPermissionsFromUser pour demander ohos.permission.MICROPHONE.
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 dans 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 fournie par Alibaba Cloud Model Studio pour vérifier rapidement la connectivité AOQ. Téléchargez l'APK et configurez la clé API ainsi que workspaceId pour tester 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 informations d'identification pour la connexion AOQ actuelle à qwen-audio-3,0-realtime-plus depuis l'URL du token Realtime.
- Le client configure l'encodeur montant et le décodeur descendant du SDK pour le modèle sélectionné et le format audio de l'application.
- Le client initialise les périphériques d'enregistrement et de lecture, puis crée AoqConnectConfig. Il renseigne les champs d'identification pour la connexion actuelle et configure les pistes Audio et Data à publier et à s'abonner. Le client maintient l'envoi sur la piste Audio désactivé et appelle connect pour établir la connexion AOQ.
- Une fois la connexion établie, le client envoie session.update. Il active la piste Audio uniquement après réception de session.updated.
- La VAD côté serveur détermine automatiquement les limites des tours de parole. L'audio du modèle est lu via la piste Audio et les événements de conversation sont renvoyés via la piste Data.
- Pour terminer, déconnectez-vous et détruisez le moteur. Le SDK ferme automatiquement les périphériques audio.
Obtenir un token 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/realtime?model=qwen-audio-3.0-realtime-plus" \
-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. Un token AOQ ne peut être utilisé que pour une seule connexion. Avant chaque appel à connect, le client doit demander un nouveau token au lieu de mettre en cache ou de réutiliser un token existant. 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 token.
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
Avant chaque connexion, le client obtient de nouvelles informations d'identification auprès du serveur d'application et crée AoqConnectConfig. Mappez les champs de réponse du token et ajoutez les paramètres de connexion côté client, tels que les pistes à publier et à s'abonner. Suivez ces étapes pour implémenter des conversations vocales en temps réel sur Android.
1. Créer le moteur et enregistrer les rappels
Créez le moteur AOQ singleton et enregistrez les rappels d'événements. Configurez la session après la réussite de la connexion et distribuez les événements serveur à l'interface utilisateur et à la machine d'état de l'application.
AoqClientListener listener = new AoqClientListener() {
@Override
public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
if (status == AoqClientEngine.AoqConnectionStatus.AoqConnectionStatusConnected) {
configureSession();
}
}
@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. Configurer les codecs audio
Configurez l'encodeur montant et le décodeur descendant du SDK pour le modèle sélectionné et le format audio de l'application. Les valeurs suivantes sont des exemples PCM pour ce tutoriel et ne limitent pas le format audio de votre application.
AoqClientEngine.AoqAudioCodecConfig audioEncoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioEncoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioEncoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioEncoderConfig.sampleRate = 16000; // Example. Match the model and application format.
audioEncoderConfig.channel = 1;
engine.setAudioEncoderConfig(audioEncoderConfig);
AoqClientEngine.AoqAudioCodecConfig audioDecoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioDecoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioDecoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioDecoderConfig.sampleRate = 24000; // Example. Match the model output format.
audioDecoderConfig.channel = 1;
engine.setAudioDecoderConfig(audioDecoderConfig);
3. Configurer les pistes et se connecter
Utilisez les interfaces du SDK pour démarrer la capture et la lecture audio. Mappez la réponse actuelle du token du serveur d'application aux champs d'identification dans AoqConnectConfig, et configurez les pistes Audio et Data dans publishTracks et subscribeTracks. Maintenez l'envoi sur la piste Audio désactivé lors de l'appel à connect. Activez l'envoi uniquement après réception de session.updated.
AoqClientEngine.AoqAudioCaptureConfig captureConfig =
new AoqClientEngine.AoqAudioCaptureConfig();
captureConfig.channel = 1;
captureConfig.isVoipMode = true;
engine.startAudioCapture(captureConfig);
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isVoipMode = true;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
AoqClientEngine.AoqTrackParam publishAudioTrack = new AoqClientEngine.AoqTrackParam();
publishAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.publishTracks.add(publishAudioTrack);
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.enableSendMediaStream(
AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.connect(connectConfig);
4. Envoyer session.update
Après la réussite de la connexion, configurez les modalités de sortie, la voix, les formats audio, les instructions et la VAD. input_audio_format et output_audio_format utilisent tous deux pcm. La configuration du codec du SDK détermine les fréquences d'échantillonnage. Pour tous les paramètres, consultez la rubrique Événements client.
JSONObject vad = new JSONObject()
.put("type", "server_vad")
.put("threshold", 0.5)
.put("silence_duration_ms", 800);
JSONObject session = new JSONObject()
.put("modalities", new JSONArray().put("text").put("audio"))
.put("voice", "longanqian")
.put("input_audio_format", "pcm")
.put("output_audio_format", "pcm")
.put("instructions", "You are a helpful voice assistant.")
.put("turn_detection", vad);
JSONObject sessionUpdate = new JSONObject()
.put("type", "session.update")
.put("session", session);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = sessionUpdate.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
5. Activer le flux montant après session.updated
session.updated indique que la configuration de la session est active. Activez l'envoi sur la piste Audio uniquement à ce stade afin que l'audio capturé précédemment ne soit pas envoyé au modèle.
if ("session.updated".equals(type)) {
engine.enableSendMediaStream(
AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
}
6. Gérer les événements serveur
Dans onDataMsg, utilisez type pour afficher les transcriptions de l'utilisateur et du modèle, et gérer les erreurs. Pour tous les champs d'événement, consultez la rubrique Événements serveur.
if ("response.audio_transcript.delta".equals(type)) {
String delta = event.optString("delta");
// Append delta to the model transcript in the UI.
} else if ("conversation.item.input_audio_transcription.completed".equals(type)) {
String transcript = event.optString("transcript");
// Display the final user transcript in the UI.
} else if ("error".equals(type)) {
// Read the error fields and update the application state.
}
7. Se déconnecter et détruire le moteur
Lorsque la conversation est terminée, déconnectez-vous et détruisez le moteur singleton. disconnect ou destroy ferme automatiquement la capture et la lecture audio, il n'est donc pas nécessaire d'arrêter les périphériques séparément.
engine.disconnect();
AoqClientEngine.destroy();
Principaux événements serveur
Les événements de la piste Data sont identifiés par type. Le client doit gérer les événements clés suivants. Pour les schémas d'événements complets, consultez la rubrique Événements serveur.
Événement | Description |
session.created | La session est créée et les paramètres par défaut sont renvoyés |
session.updated | Les paramètres client sont actifs et le flux montant audio peut être activé |
input_audio_buffer.speech_started | Le service détecte que l'utilisateur a commencé à parler |
input_audio_buffer.speech_stopped | Le service détecte que l'utilisateur a arrêté de parler |
input_audio_buffer.committed | L'audio pour le tour de parole est validé |
response.created | Le modèle commence à générer une réponse |
response.audio_transcript.delta | Transcription incrémentielle du modèle |
conversation.item.input_audio_transcription.completed | La transcription finale de l'utilisateur est disponible |
response.done | La réponse est terminée |
error | Une erreur serveur se produit |
Exemple complet
La classe suivante accepte un AoqConnectConfig renseigné avec les informations d'identification pour la connexion actuelle et ajoute les paramètres des périphériques audio, ainsi que des pistes de publication et d'abonnement. Obtenez de nouvelles informations d'identification et créez une nouvelle configuration de connexion pour chaque reconnexion. Ajoutez les autorisations, 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.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
public final class RealtimeVoiceChatClient {
private AoqClientEngine engine;
public RealtimeVoiceChatClient(Context context, AoqClientEngine.AoqConnectConfig connectConfig) {
AoqClientListener listener = new AoqClientListener() {
@Override
public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
if (status == AoqClientEngine.AoqConnectionStatus.AoqConnectionStatusConnected) {
configureSession();
}
}
@Override
public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
try {
JSONObject event = new JSONObject(
new String(msg.data, StandardCharsets.UTF_8));
String type = event.optString("type");
if ("session.updated".equals(type)) {
engine.enableSendMediaStream(
AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
} else if ("response.audio_transcript.delta".equals(type)) {
String delta = event.optString("delta");
// Display delta in the UI.
} else if ("conversation.item.input_audio_transcription.completed".equals(type)) {
String transcript = event.optString("transcript");
// Display transcript in the UI.
} else if ("error".equals(type)) {
// Read error fields and update the application state.
}
} 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 model and application audio format.
AoqClientEngine.AoqAudioCodecConfig audioEncoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioEncoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioEncoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioEncoderConfig.sampleRate = 16000;
audioEncoderConfig.channel = 1;
engine.setAudioEncoderConfig(audioEncoderConfig);
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.AoqAudioCaptureConfig captureConfig =
new AoqClientEngine.AoqAudioCaptureConfig();
captureConfig.channel = 1;
captureConfig.isVoipMode = true;
engine.startAudioCapture(captureConfig);
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isVoipMode = true;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
AoqClientEngine.AoqTrackParam publishAudioTrack =
new AoqClientEngine.AoqTrackParam();
publishAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.publishTracks.add(publishAudioTrack);
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.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.connect(connectConfig);
}
private void configureSession() {
try {
JSONObject vad = new JSONObject()
.put("type", "server_vad")
.put("threshold", 0.5)
.put("silence_duration_ms", 800);
JSONObject session = new JSONObject()
.put("modalities", new JSONArray().put("text").put("audio"))
.put("voice", "longanqian")
.put("input_audio_format", "pcm")
.put("output_audio_format", "pcm")
.put("turn_detection", vad);
JSONObject sessionUpdate = new JSONObject()
.put("type", "session.update")
.put("session", session);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = sessionUpdate.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
} catch (JSONException e) {
throw new IllegalStateException("Failed to create session.update", e);
}
}
public void close() {
engine.disconnect();
AoqClientEngine.destroy();
}
}
Exécuter et vérifier
- Le flux audio du microphone démarre uniquement après réception de session.updated.
- Après que l'utilisateur a arrêté de parler, le service valide l'audio et commence à répondre. Les événements texte et l'audio de la piste Audio sont renvoyés en continu.
Scénarios courants
Modifier le mode d'interaction
Utilisez server_vad pour la détection de tour basée sur le silence, smart_turn pour la détection acoustique et sémantique, ou définissez turn_detection sur null pour le push-to-talk. turn_detection ne peut être modifié qu'avant la première entrée audio. Établissez une nouvelle session pour changer de mode.
Changer de voix
Définissez session.voice dans le premier session.update. Les voix système prises en charge varient selon le modèle. Pour les voix prises en charge et le clonage vocal, consultez la rubrique Conversation vocale en temps réel Qwen-Audio.
Haut-parleur ou écouteur
Définissez le périphérique de sortie par défaut en utilisant AoqAudioPlaybackConfig.isDefaultSpeaker, et appelez enableSpeakerphone pour basculer pendant que la session est active.
Appels en arrière-plan sur Android
Sur Android 10 ou version ultérieure, utilisez un service de premier plan avec foregroundServiceType="microphone|mediaPlayback" pour continuer la capture et la lecture en arrière-plan. Démarrez-le lorsque l'application est visible pour l'utilisateur.
Dépannage
Problème | Solution |
Échec de la connexion | Assurez-vous que le token est valide, que l'endpoint correspond à la région de déploiement et que les champs AoqConnectConfig sont correctement mappés. |
La session est établie mais aucune réponse n'est renvoyée | Vérifiez que la piste Audio est activée après session.updated et que l'encodeur montant du SDK correspond au modèle et au format audio de l'application. |
La réponse ne contient pas d'audio | Vérifiez que la piste Audio est abonnée et que le lecteur audio est en cours d'exécution, puis assurez-vous que le décodeur descendant du SDK correspond au format audio de sortie du modèle. |
Informations connexes
Pour tous les paramètres, champs d'événement et interfaces pour d'autres plateformes, consultez :