Utilice AOQ para conectarse a qwen-audio-3.0-tts-flash, enviar texto en segmentos y reproducir la voz sintetizada en tiempo real. El código del cliente utiliza Android Java.
Descripción general de la solución
qwen-audio-3.0-tts-flash admite el protocolo de eventos de inferencia AOQ. Este tutorial utiliza el modelo para demostrar la síntesis de voz en streaming a través de AOQ. El cliente envía run-task, continue-task y finish-task a través de la pista Data. El servicio transmite audio por la pista Audio y devuelve eventos de tarea por la pista Data.
Una tarea puede contener múltiples eventos continue-task. Las oraciones completas se sintetizan de inmediato. Las oraciones incompletas permanecen en búfer hasta que el texto subsiguiente las complete o el cliente envíe finish-task. Este enfoque es adecuado para reproducción en dispositivos móviles, entrada de texto largo segmentada y salida de voz de baja latencia.
Requisitos previos
- Active Model Studio y siga los pasos en Obtener y configurar una clave API. Almacene la clave API únicamente en su servidor de aplicaciones. No la incluya en el código del cliente ni la confirme en un repositorio de código.
- Confirm el endpoint de AOQ correspondiente a la región donde está implementada su aplicación. Para obtener orientación sobre la selección, consulte Seleccionar una región, un alcance de implementación y un endpoint.
- Descargue la versión más reciente del SDK de cliente AOQ como se describe en Descarga del SDK.
- Construya un servidor de aplicaciones e implemente la autenticación proxy tal como se describe en Autenticación por token. Antes de cada nueva conexión, el cliente debe obtener nuevas credenciales de conexión del servidor de aplicaciones.
Importar el SDK
Importe el SDK correspondiente a su plataforma de desarrollo. La implementación del cliente utiliza Android Java. Otras plataformas proporcionan las mismas interfaces y flujo de eventos. Este tutorial utiliza flujos de audio Opus. Importe el complemento correspondiente como se describe en el tema de descarga del SDK antes de continuar.
Android
- Coloque libPluginOpus.so del complemento Opus en app/src/main/jniLibs/armeabi-v7a/ y app/src/main/jniLibs/arm64-v8a/ para las ABI correspondientes. Coloque AoqClientSdk-release.aar en app/libs, y configure la dependencia y las ABI compatibles con el SDK en app/build.gradle:
android {
defaultConfig {
minSdk 21
ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}
- Declare los siguientes permisos en AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- Este escenario no requiere permisos de micrófono ni cámara.
iOS
- Agregue AoqClientSdk.framework y PluginOpus.framework al proyecto de Xcode y seleccione Embed & Sign en Target > General > Frameworks, Libraries, and Embedded Content. El SDK es compatible con dispositivos arm64 que ejecutan iOS 13.0 o posterior.
- Este escenario no utiliza el micrófono ni la cámara y no requiere permisos relacionados.
- Utilice import AoqClientSdk en Swift o #import <AoqClientSdk/AoqClientSdk.h> en Objective-C.
HarmonyOS
- Coloque libPluginOpus.so del complemento Opus en entry/libs/arm64-v8a/. Coloque AoqClientSdk.har en entry/libs y declare la dependencia en entry/oh-package.json5. El SDK es compatible con API 12 y admite arm64-v8a:
{
"dependencies": {
"@aoq/client-sdk": "file:./libs/AoqClientSdk.har"
}
}
- Declare los siguientes permisos en entry/src/main/module.json5:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
- Este escenario no requiere permisos de micrófono ni cámara.
Linux (Python)
- Extraiga el SDK y conserve aoq_client_sdk.py, libAoqClientSdk.so y libonnxruntime.so.1.16.3 en el mismo directorio.
- Agregue el directorio del SDK a las rutas de búsqueda de Python y de bibliotecas dinámicas:
export PYTHONPATH="$PWD/AoqClientSdk:$PYTHONPATH"
export LD_LIBRARY_PATH="$PWD/AoqClientSdk:$LD_LIBRARY_PATH"
- Utilice import aoq_client_sdk en Python. También puede especificar la ruta absoluta de libAoqClientSdk.so mediante AOQ_CLIENT_SDK_LIB.
Probar la demostración
Utilice la demostración de Android de Alibaba Cloud Model Studio para verificar rápidamente la conectividad AOQ. Descargue el APK y configure la clave de API y workspaceId para probar modelos seleccionados.
Escanee el siguiente código QR para descargar la demostración:
Flujo de implementación
- El servidor de aplicaciones obtiene los parámetros de conexión AOQ para qwen-audio-3.0-tts-flash desde la URL del token de inferencia.
- El cliente publica la pista Data, se suscribe a las pistas Audio y Data, y configura el decodificador de audio del SDK.
- El cliente inicia el reproductor local y se conecta a AOQ. Después de que la conexión se realiza correctamente, envía run-task con un nuevo task_id.
- Después de recibir task-started, el cliente envía uno o más segmentos de texto continue-task al ritmo requerido por la aplicación.
- Después de enviar todo el texto, el cliente envía finish-task. El servicio devuelve el audio restante y finalmente task-finished.
- Después de recibir task-finished, inicie otra tarea sobre la misma conexión AOQ con un nuevo task_id, o desconéctese y destruya el motor.
Obtener un token del servidor de aplicaciones
Establezca DASHSCOPE_API_KEY en el servidor de aplicaciones y envíe la solicitud al endpoint de la región seleccionada. clientIp es la dirección IP pública real del cliente. Este campo es opcional, pero especificarlo ayuda al servicio a asignar un endpoint de retransmisión adecuado.
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}\"}"
NotaSi el servidor de aplicaciones no puede obtener la dirección IP pública real del cliente, omita clientIp en lugar de pasar una cadena vacía.
El servidor de aplicaciones devuelve los siguientes campos de respuesta al cliente. Nunca devuelva la clave API a un cliente en producción. Para todos los campos de solicitud y respuesta, consulte Autenticación por token.
Campo de respuesta | Campo del SDK |
aoqTokenForClient | AoqConnectConfig.token |
sid | AoqConnectConfig.sid |
clientRelayCertFingerprint | AoqConnectConfig.certFingerprint |
clientRelayEndpoints | AoqConnectConfig.relayEndpoints |
extraInfo.workspaceIdHash | AoqConnectConfig.workspaceIdHash |
Implementar el cliente de Android
Después de que el cliente obtenga AoqConnectConfig del servidor de aplicaciones, siga estos pasos para implementar la síntesis de voz en streaming en Android.
1. Crear el motor y registrar devoluciones de llamada
Cree el motor AOQ singleton y registre callbacks para eventos de conexión y de la pista Data. Mantenga la disponibilidad de conexión en el callback de conexión y pase los eventos de tarea a la máquina de estados de la aplicación.
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. Iniciar reproducción de audio
TTS no captura audio del micrófono. Inicialice solo el reproductor local. Seleccione el altavoz o el auricular como salida predeterminada. El SDK reproduce automáticamente el audio del servidor desde la pista Audio.
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
3. Configurar el decodificador y las pistas y conectar
Configure el decodificador de audio del SDK. Luego, publique la pista Data y suscríbase a las pistas Audio y Data. Los siguientes valores son ejemplos de Opus para este tutorial. Complete los campos de AoqConnectConfig con la respuesta de token del servidor de aplicaciones.
AoqClientEngine.AoqAudioCodecConfig audioDecoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioDecoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioDecoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioOpus;
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. Llamar a sendDataMsg para enviar un evento run-task
Después de que la conexión se realice correctamente, genere un nuevo UUID task_id y configure el modelo, la voz, el tipo de texto, el formato de audio y la frecuencia de muestreo. Para parámetros opcionales, consulte Eventos del cliente.
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. Llamar a sendDataMsg para enviar un evento continue-task
Envíe continue-task solo después de recibir task-started. Una tarea puede contener múltiples segmentos. Cada evento admite hasta 20,000 caracteres, y una tarea admite hasta 200,000 caracteres en total. Envíe los segmentos subsiguientes o finalice la tarea de inmediato. No dependa de un valor fijo de tiempo de espera de conexión.
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. Manejar eventos del servidor
En onDataMsg, lea header.event para mantener el estado de la tarea y gestionar fallos. result-generated indica que se sintetizó una oración, mientras que el audio se sigue devolviendo a través de la pista Audio. Para todos los campos, consulte Eventos del servidor.
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. Llamar a sendDataMsg para enviar un evento finish-task
Inmediatamente después de enviar todo el texto, envíe finish-task para sintetizar el texto incompleto almacenado en búfer por el servicio y espere a task-finished. Para más detalles, consulte Eventos del cliente.
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. Desconectar y destruir el motor
No se desconecte inmediatamente después de enviar finish-task. Después de recibir task-finished o task-failed, desconéctese y destruya el motor si no se va a iniciar ninguna tarea posterior. El SDK cierra automáticamente el reproductor de audio.
engine.disconnect();
AoqClientEngine.destroy();
Principales eventos del servidor
Evento | Descripción |
task-started | La tarea ha comenzado y se puede enviar continue-task |
result-generated | Se sintetizó una oración completa y su audio se devuelve a través de la pista de Audio |
task-finished | Todo el texto almacenado en búfer fue procesado y la tarea está completa |
task-failed | La tarea falló. Lea el código de error y el mensaje |
Ejemplo completo
La siguiente clase acepta un AoqConnectConfig mapeado desde la respuesta de token del servidor de aplicaciones. Una vez que la conexión se establezca correctamente, llame a synthesize(text, voice). Agregue permisos, estado de la interfaz de usuario y lógica de reconexión en producción.
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.AoqEncoderTypeAudioOpus;
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();
}
}
Ejecutar y verificar
- El texto se envía solo después de recibir task-started.
- El audio de las oraciones completas se reproduce continuamente a través de la pista Audio. Las oraciones incompletas se sintetizan después de finish-task.
- task-finished se recibe después de que todo el audio esté completo. Luego se puede iniciar otra tarea con un nuevo task_id.
Escenarios comunes
Múltiples tareas en una sola conexión
Después de recibir task-finished, envíe otro run-task con un nuevo task_id a través de la misma conexión AOQ. No se requiere un nuevo token mientras la conexión permanezca activa. Si la conexión se cierra, obtenga nuevas credenciales de conexión.
Cambiar la voz
Cada run-task puede seleccionar una voz del sistema o un voice_id válido en parameters.voice. Por lo tanto, puede cambiar la voz entre tareas en la misma conexión.
Altavoz o auricular
Establezca la salida predeterminada utilizando AoqAudioPlaybackConfig.isDefaultSpeaker y llame a enableSpeakerphone para cambiar mientras la conexión está activa.
Solución de problemas
Problema | Solución |
La conexión se realiza correctamente pero la tarea no comienza | Asegúrese de que las credenciales se obtuvieron de la URL del token de inferencia y verifique el nombre del modelo, task_id y la publicación de la pista Data en run-task. |
continue-task es rechazado | Espere a task-started y utilice el mismo task_id en run-task, continue-task y finish-task. |
La tarea se completa correctamente pero no se reproduce audio | Asegúrese de que la pista Audio esté suscrita y el reproductor esté en funcionamiento, y verifique que el decodificador del SDK coincida con el formato de audio de salida seleccionado en run-task. |
El texto final no tiene audio | Envíe finish-task después de enviar todo el texto y espere al audio restante y a task-finished antes de desconectarse. |
Información relacionada
Para todos los parámetros, campos de eventos e interfaces para otras plataformas, consulte: