Utilize o AOQ para se conectar ao qwen-audio-3.0-tts-flash, enviar texto em segmentos e reproduzir a fala sintetizada em tempo real. O código do cliente utiliza Android Java.
Visão geral da solução
O modelo qwen-audio-3.0-tts-flash suporta o protocolo de eventos de inferência AOQ. Este tutorial demonstra como realizar a síntese de fala em streaming via AOQ utilizando esse modelo. O cliente envia os comandos run-task, continue-task e finish-task pela faixa Data. O service transmite o áudio pela faixa Audio e retorna eventos de tarefa pela faixa Data.
Uma tarefa pode conter múltiplos eventos continue-task. Frases completas são sintetizadas imediatamente. Frases incompletas permanecem em buffer até que textos subsequentes as completem ou até que o cliente envie finish-task. Essa abordagem é ideal para reprodução em dispositivos móveis, entrada de textos longos segmentados e saída de fala com baixa latência.
Pré-requisitos
- Ative o Model Studio e siga as instruções em Obtain and configure an API key. Armazene a chave de API exclusivamente no servidor da sua aplicação. Não a inclua no código do cliente nem a envie para repositórios de código.
- Confirme o endpoint AOQ da região onde sua aplicação está implantada. Para orientações sobre a escolha, consulte Select a region, deployment scope, and endpoint.
- Baixe a versão mais recente do SDK do cliente AOQ conforme descrito em SDK download.
- Construa um servidor de aplicação e implemente a autenticação por proxy conforme descrito em Token authentication. Antes de cada nova conexão, o cliente deve obter novas credenciais de conexão no servidor da aplicação.
Importar o SDK
Importe o SDK correspondente à sua plataforma de desenvolvimento. A implementação do cliente neste tutorial usa Android Java, mas outras plataformas oferecem as mesmas interfaces e fluxo de eventos. Este tutorial utiliza streams de áudio PCM. Caso sua aplicação utilize Opus, importe o plugin correspondente conforme descrito no tópico de download do SDK.
Android
- Coloque o arquivo AoqClientSdk-release.aar em app/libs e configure a dependência e as ABIs suportadas pelo SDK no arquivo app/build.gradle:
android {
defaultConfig {
minSdk 21
ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}
- Declare as seguintes permissões no arquivo AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- Este cenário não requer permissões de microfone ou câmera.
iOS
- Adicione o AoqClientSdk.framework ao projeto Xcode e selecione Embed & Sign em Target > General > Frameworks, Libraries, and Embedded Content. O SDK suporta dispositivos arm64 com iOS 13.0 ou superior.
- Este cenário não utiliza microfone ou câmera, portanto não exige permissões relacionadas.
- Utilize import AoqClientSdk em Swift ou #import <AoqClientSdk/AoqClientSdk.h> em Objective-C.
HarmonyOS
- Coloque o arquivo AoqClientSdk.har em entry/libs e declare a dependência em entry/oh-package.json5. O SDK é compatível com a API 12 e suporta arm64-v8a:
{
"dependencies": {
"@aoq/client-sdk": "file:./libs/AoqClientSdk.har"
}
}
- Declare as seguintes permissões em entry/src/main/module.json5:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
- Este cenário não requer permissões de microfone ou câmera.
Linux (Python)
- Extraia o SDK e mantenha os arquivos aoq_client_sdk.py, libAoqClientSdk.so e libonnxruntime.so.1.16.3 no mesmo diretório.
- Adicione o diretório do SDK aos caminhos de busca do Python e das bibliotecas dinâmicas:
export PYTHONPATH="$PWD/AoqClientSdk:$PYTHONPATH"
export LD_LIBRARY_PATH="$PWD/AoqClientSdk:$LD_LIBRARY_PATH"
- Utilize import aoq_client_sdk em Python. Também é possível especificar o caminho absoluto do arquivo libAoqClientSdk.so por meio da variável AOQ_CLIENT_SDK_LIB.
Testar a demonstração
Utilize a demonstração para Android do Alibaba Cloud Model Studio para verificar rapidamente a conectividade AOQ. Baixe o APK e configure a chave de API e o workspaceId para testar modelos selecionados.
Escaneie o QR code abaixo para baixar a demonstração:
Fluxo de implementação
- O servidor da aplicação obtém os parâmetros de conexão AOQ para o qwen-audio-3.0-tts-flash na URL de token de inferência.
- O cliente publica a faixa Data, inscreve-se nas faixas Audio e Data, e configura o decodificador do SDK para o formato de áudio de saída selecionado em run-task.
- O cliente inicia o player local e conecta-se ao AOQ. Após o sucesso da conexão, ele envia run-task com um novo task_id.
- Após receber task-started, o cliente envia um ou mais segmentos de texto continue-task no ritmo necessário para a aplicação.
- Depois de enviar todo o texto, o cliente envia finish-task. O service retorna o áudio restante e, por fim, task-finished.
- Ao receber task-finished, inicie outra tarefa na mesma conexão AOQ com um novo task_id ou desconecte e destrua o engine.
Obter um token do servidor da aplicação
Defina DASHSCOPE_API_KEY no servidor da aplicação e envie a requisição para o endpoint da região selecionada. O campo clientIp corresponde ao endereço IP público real do cliente. Esse campo é opcional, mas especificá-lo ajuda o service a alocar um endpoint de retransmissão adequado.
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}\"}"
ObservaçãoCaso o servidor da aplicação não consiga obter o endereço IP público real do cliente, omita o campo clientIp em vez de passar uma string vazia.
O servidor da aplicação retorna os seguintes campos de resposta ao cliente. Nunca retorne a chave de API para um cliente em ambiente de produção. Para todos os campos de requisição e resposta, consulte Token authentication.
Campo de resposta | Campo do SDK |
aoqTokenForClient | AoqConnectConfig.token |
sid | AoqConnectConfig.sid |
clientRelayCertFingerprint | AoqConnectConfig.certFingerprint |
clientRelayEndpoints | AoqConnectConfig.relayEndpoints |
extraInfo.workspaceIdHash | AoqConnectConfig.workspaceIdHash |
Implementar o cliente Android
Após o cliente obter o AoqConnectConfig do servidor da aplicação, siga estas etapas para implementar a síntese de fala em streaming no Android.
1. Criar o engine e registrar callbacks
Crie o engine AOQ singleton e registre callbacks para eventos de conexão e da faixa Data. Mantenha o status de prontidão da conexão no callback de conexão e encaminhe os eventos de tarefa para a máquina de estados da aplicação.
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 a reprodução de áudio
O TTS não captura áudio do microfone. Inicialize apenas o player local. Selecione o alto-falante ou o auricular como saída padrão. O SDK reproduz automaticamente o áudio do servidor proveniente da faixa Audio.
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
3. Configurar o decodificador, as faixas e conectar
Configure o decodificador do SDK para o formato de áudio de saída selecionado em run-task. Em seguida, publique a faixa Data e inscreva-se nas faixas Audio e Data. Os valores abaixo são exemplos PCM para este tutorial. Preencha os campos do AoqConnectConfig com a resposta de token do servidor da aplicação.
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. Chamar sendDataMsg para enviar um evento run-task
Após o sucesso da conexão, gere um novo task_id UUID e configure o modelo, a voz, o tipo de texto, o formato de áudio e a taxa de amostragem. Para parâmetros opcionais, consulte Client events.
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. Chamar sendDataMsg para enviar um evento continue-task
Envie continue-task somente após receber task-started. Uma tarefa pode conter múltiplos segmentos. Cada evento suporta até 20.000 caracteres, e uma tarefa suporta até 200.000 caracteres no total. Envie os segmentos subsequentes ou finalize a tarefa prontamente. Não dependa de um valor fixo de timeout de conexão.
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. Tratar eventos do servidor
Em onDataMsg, leia header.event para manter o estado da tarefa e tratar falhas. O evento result-generated indica que uma frase foi sintetizada, enquanto o áudio ainda é retornado pela faixa Audio. Para todos os campos, consulte Server events.
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. Chamar sendDataMsg para enviar um evento finish-task
Imediatamente após enviar todo o texto, envie finish-task para sintetizar qualquer texto incompleto armazenado em buffer pelo service e aguarde task-finished. Para detalhes, consulte Client events.
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 e destruir o engine
Não desconecte imediatamente após o envio de finish-task. Somente após receber task-finished ou task-failed, desconecte e destrua o engine caso nenhuma tarefa subsequente seja iniciada. O SDK fecha automaticamente o player de áudio.
engine.disconnect();
AoqClientEngine.destroy();
Principais eventos do servidor
Evento | Descrição |
task-started | A tarefa foi iniciada e continue-task pode ser enviado |
result-generated | Uma frase completa foi sintetizada e seu áudio é retornado pela faixa Audio |
task-finished | Todo o texto em buffer foi processado e a tarefa está concluída |
task-failed | A tarefa falhou. Leia o código de erro e a mensagem |
Exemplo completo
A classe a seguir aceita um AoqConnectConfig mapeado a partir da resposta de token do servidor da aplicação. Após o sucesso da conexão, chame synthesize(text, voice). Adicione permissões, estado de UI e lógica de reconexão em ambientes de produção.
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();
}
}
Executar e verificar
- O texto é enviado somente após o recebimento de task-started.
- O áudio de frases completas é reproduzido continuamente pela faixa Audio. Frases incompletas são sintetizadas após finish-task.
- O evento task-finished é recebido após a conclusão de todo o áudio. Outra tarefa pode então ser iniciada com um novo task_id.
Cenários comuns
Múltiplas tarefas em uma única conexão
Após receber task-finished, envie outro run-task com um novo task_id na mesma conexão AOQ. Nenhum novo token é necessário enquanto a conexão permanecer ativa. Se a conexão for fechada, obtenha novas credenciais de conexão.
Alterar a voz
Cada run-task pode selecionar uma voz do sistema ou um voice_id válido em parameters.voice. Portanto, é possível alterar a voz entre tarefas na mesma conexão.
Alto-falante ou auricular
Defina a saída padrão utilizando AoqAudioPlaybackConfig.isDefaultSpeaker e chame enableSpeakerphone para alternar durante uma conexão ativa.
Solução de problemas
Problema | Solução |
A conexão é bem-sucedida, mas a tarefa não inicia | Verifique se as credenciais foram obtidas na URL de token de inferência e confira o nome do modelo, o task_id e a publicação da faixa Data em run-task. |
continue-task é rejeitado | Aguarde task-started e utilize o mesmo task_id em run-task, continue-task e finish-task. |
A tarefa é bem-sucedida, mas nenhum áudio é reproduzido | Certifique-se de que a faixa Audio está inscrita e o player está em execução. Verifique também se o decodificador do SDK corresponde ao formato de áudio de saída selecionado em run-task. |
O texto final não gera áudio | Envie finish-task após transmitir todo o texto e aguarde o áudio restante e task-finished antes de desconectar. |
Informações relacionadas
Para todos os parâmetros, campos de eventos e interfaces de outras plataformas, consulte: