Sintetize o discurso com qwen-audio-3.0-tts-flash sobre o AOQ
Use o QOA para se conectar qwen-audio-3.0-tts-flash, enviar texto em segmentos, e tocar sintetizar fala em tempo real. O código do cliente usa o Android Java.
Visão geral da solução
qwen-audio-3.0-tts-flash suporta o protocolo de eventos de inferência do AOQ. Este tutorial usa o modelo para demonstrar a síntese de fala em streaming sobre o AOQ. O cliente manda executar, continuar e terminar a tarefa sobre a trilha de dados. O serviço transmite áudio sobre a faixa de áudio e retorna eventos de tarefas sobre a faixa de dados.
Uma tarefa pode conter múltiplos eventos de tarefas contínuas. As frases completas são sintetizadas rapidamente. Frases incompletas permanecem em forma de tampão até que o texto subsequente as complete ou o cliente envie a tarefa final. Esta abordagem é adequada para reprodução móvel, entrada de texto segmentado e saída de fala de baixa latência.
Pré-requisitos
- Ativar QwenCloud e siga [Obter e configurar uma chave API] (/api-reference/preparation/api-key). Guarde a chave da API apenas no servidor de aplicativos. Não inclua em código de cliente ou o entregue em um repositório de códigos.
- Baixe o mais recente cliente do AOQ SDK como descrito em [SDK download] (/api-reference/realtime-api/sdk-download).
- Construa um servidor de aplicativos e implemente a autenticação proxy como descrito em Autenticação do Token. Antes de cada nova conexão, o cliente deve obter novas credenciais de conexão do servidor de aplicativos.
Importar o SDK
Importar o SDK para sua plataforma de desenvolvimento. A implementação do cliente usa o Android Java. Outras plataformas fornecem as mesmas interfaces e fluxo de eventos. Este tutorial usa fluxos de áudio Opus. Importar o plugin correspondente como descrito no tópico de download SDK antes de prosseguir.
Android
1. Coloque a libPluginOpus.so do plugin Opus em app/src/main/jniLibs/armeabi-v7a/ e app/src/main/jniLibs/arm64-v8a/ para os ABIs correspondentes. Coloque AoqClientSdk-release.aar em app/libs, e configure a dependência e ABIs suportados por SDK em app/build.gradle:
```groovy
android {
defaultConfig {
minSdk 21
ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}
```
2. Declare as seguintes permissões no AndroidManifest.xml:
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
```
3. Este cenário não requer permissão de microfone ou câmera.
iOS
1. Adicione AoqClientSdk.framework e PluginOpus.framework ao projeto Xcode e selecione Incorporar e assinar em Target > General > Frameworks, Bibliotecas e Conteúdo Incorporado. O SDK suporta dispositivos arm64 rodando iOS 13.0 ou posterior.
2. Este cenário não usa o microfone ou a câmera e não requer permissões relacionadas.
3. Use `import AoqClientSdk` em Swift ou `#import <AoqClientSdk/AoqClientSdk.h>` no Objetivo-C.
HarmonyOS
1. Coloque libPluginOpus.so do plugin Opus em entrada/libs/arm64-v8a/. Coloque AoqClientSdk.har em entrada/libs e declare a dependência em entrada/oh-pacote.json5. O SDK é compatível com API 12 e suporta arm64-v8a:
```json
{
"dependencies": {
"@aoq/client-sdk": "file:./libs/AoqClientSdk.har"
}
}
```
2. Declare as seguintes permissões em entrada/src/main/module.json5:
```json
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
```
3. Este cenário não requer permissão de microfone ou câmera.
Linux (Python)
1. Extrair o SDK e manter aoq\_client\_sdk.py, libAoqClientSdk.so, e libonnxruntime.so.1.16.3 no mesmo diretório.
2. Adicione o diretório SDK aos caminhos de pesquisa de bibliotecas dinâmicas e Python:
```bash
export PYTHONPATH="$PWD/AoqClientSdk:$PYTHONPATH"
export LD_LIBRARY_PATH="$PWD/AoqClientSdk:$LD_LIBRARY_PATH"
```
3. Use `import aoq_client_sdk` em Python. Você também pode especificar o caminho absoluto da libAoqClientSdk.so usando AOQ\_CLIENT\_SDK\_LIB.
Tente a demo.
QwenCloud fornece um aplicativo de demonstração Android para verificar rapidamente conectividade AOQ. Baixe o APK e configure a chave da API e workspaceId para tentar modelos selecionados.
Analise o seguinte código QR para baixar a demonstração:
[Código QR para baixar a demo] (https://g-adoc.alcasset.com/media/maas_docs/sfm/common/images/6a4b3c2d1e0f92cc.png)
Fluxo de implementação
- O servidor de aplicativos obtém parâmetros de conexão AOQ para qwen-audio-3.0-tts-flash da URL do símbolo de inferência.
- O cliente publica a faixa de dados, assina as faixas de áudio e dados, e configura o decodificador de áudio SDK.
- O cliente começa o jogador local e se conecta ao AOQ. Depois que a conexão tiver sucesso, ela envia uma tarefa com uma nova tarefa.
- Após a tarefa ser recebida, o cliente envia um ou mais segmentos de texto de tarefas contínuas no ritmo exigido pela aplicação.
- Depois que toda mensagem é enviada, o cliente manda a tarefa final. O serviço devolve o áudio restante e finalmente finalizado.
- Após a tarefa ser recebida, comece outra tarefa sobre a mesma conexão com uma nova tarefa ou desligue e destrua o motor.
[Diagrama de sequência para transmissão de síntese de fala sobre AOQ] (http://icms-x-dita-hz.oss-cn-hangzhou.aliyuncs.com/en-US/images/p1094860.png)
Obter um token do servidor de aplicativos
Coloque o DASHSCOPE_API_KEY no servidor de aplicativos e envie o pedido. clientIp é o endereço IP público do cliente. Este campo é opcional, mas especificando que ajuda o serviço alocar um ponto final adequado.
curl -X POST \
"https://maas.qwencloudapi.com/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}\"}"
Se o servidor de aplicativos não conseguir o endereço IP público do cliente, omita o clienteIp em vez de passar uma string vazia.
O servidor de aplicativos devolve os seguintes campos de resposta ao cliente. Nunca devolva a chave da API para um cliente em produção. Para todos os campos de solicitação e resposta, veja [Autenticação do Token] (/api-reference/realtime-api/token-auth).
| Campo de resposta. | Campo SDK |
|---|---|
| AoqTokenForClient | AoqConnectConfig.token |
| Sid. | AoqConnectConfig.sid |
| CLIENTE RELAYCertFingerprint | AoqConnectConfig.certFingerprint |
| CLIENTE RELAYEndpoints | AoqConnectConfig.relayEndpoints |
| ExtraInfo. Espaço de trabalhoIdHash | AoqConnectConfig. Espaço de trabalhoIdHash |
Implemente o cliente Android
Depois que o cliente obter AoqConnectConfig do servidor de aplicativos, siga estes passos para implementar a síntese de voz em streaming no Android.
1. Crie o motor e registre chamadas de retorno
Crie o motor de som único e registre chamadas para conexão e eventos de dados. Mantenha a conexão pronta na ligação de retorno e passe eventos de tarefas para a máquina de estado de 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);
- Dois. Comece a reprodução de áudio.
TTS não capta áudio de microfone. Inicialize apenas o jogador local. Selecione o alto-falante ou fone de ouvido como a saída padrão. O SDK automaticamente toca áudio do servidor da faixa de áudio.
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
3. Configure o decodificador e as faixas e conecte-se.
Configure o decodificador de áudio SDK. Então publique a faixa de dados e assine as faixas de áudio e dados. Os seguintes valores são exemplos de Opus para este tutorial. Popular os campos AoqConnectConfig da resposta do servidor de aplicativos.
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);
- Quatro. Ligue para enviar DataMsg para enviar um evento de execução.
Depois que a conexão tiver sucesso, gere uma nova tarefa UUID_id e configure o modelo, voz, tipo de texto, formato de áudio e taxa de amostra. Para parâmetros opcionais, veja [Eventos do cliente] (/api-reference/speech-synthesis/qwen-tts-realtime/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);
- Cinco. Ligue para enviar DataMsg para enviar um evento de tarefas contínuas
Enviem tarefas contínuas só depois que a tarefa começar. Uma tarefa pode conter vários segmentos. Cada evento suporta até 20.000 personagens, e uma tarefa suporta até 200.000 personagens no total. Envie segmentos subsequentes ou termine a tarefa imediatamente. Não depende de um valor fixo de tempo limite 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);
Seis. Lidar com eventos de servidores
No DataMsg, leia header.event para manter o estado de tarefa e lidar com falhas. O resultado gerado indica que uma frase foi sintetizada, enquanto o áudio ainda é retornado sobre a faixa de áudio. Para todos os campos, veja [Eventos de servidores] (/api-reference/speech-synthesis/qwen-tts-realtime/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? - 7? Ligue para enviar DataMsg para enviar um evento de final de tarefa
Imediatamente após todo texto ser enviado, envie uma tarefa final para sintetizar texto incompleto, tamponado pelo serviço, e espere por tarefas concluídas. Para detalhes, veja [Eventos do cliente] (/api-reference/speech-synthesis/qwen-tts-realtime/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);
Oito. Desligue e destrua o motor.
Não desligue imediatamente após a tarefa final ser enviada. Após a tarefa terminada ou falhada, é recebida, desconectar e destruir o motor se nenhuma tarefa subsequente será iniciada. O SDK fecha automaticamente o reprodutor de áudio.
engine.disconnect();
AoqClientEngine.destroy();
Principais eventos do servidor
| Evento | Descrição |
|---|---|
| task-started | A tarefa começou e pode ser enviada. |
| result-generated | Uma frase completa foi sintetizada e seu áudio é retornado sobre a faixa de áudio |
| task-finished | Todo texto foi processado e a tarefa está completa. |
| task-failed | A tarefa falhou. Leia o código de erro e mensagem. |
Exemplo completo.
A classe seguinte aceita um AoqConnectConfig mapeado da resposta do servidor de aplicativos. Depois que a conexão tiver sucesso, chamem sintetizar (texto, voz). Adicione permissões, UI estado, e lógica de reconexão na 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.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();
}
}
Corra e verifique.
- O texto só é enviado após a tarefa ser recebida.
- Áudio para frases completas é reproduzido continuamente sobre a faixa de áudio. Frases incompletas são sintetizadas após a tarefa final.
- A tarefa final é recebida depois que todo o áudio está completo. Outra tarefa pode começar com uma nova tarefa.
Cenários comuns
Múltiplas tarefas sobre uma conexão.
Depois que a tarefa for finalizada, envie outra tarefa com uma nova tarefa sobre a mesma conexão do QAO. Não é necessário novo símbolo enquanto a conexão permanece ativa. Se a conexão estiver fechada, obtenha novas credenciais de conexão.
Mude a voz
Cada tarefa pode selecionar uma voz do sistema ou uma voz válida em parâmetros.voz. Você pode mudar a voz entre tarefas sobre a mesma conexão.
Alto-falante ou fone de ouvido
Definir a saída padrão usando AoqAudioPlaybackConfig.isDefaultSpeaker, e chamar enableSpeakerphone Para trocar enquanto a conexão está ativa.
Solução de problemas
| Questão | Solução |
|---|---|
| A conexão é bem sucedida mas a tarefa não começa. | Certifique-se de que as credenciais foram obtidas da URL do símbolo de inferência, e verifique o nome do modelo, task_id, e a publicação de dados em execução. |
| A tarefa continua é rejeitada. | Espere por tarefas iniciadas, e use a mesma tarefa em tarefas de corrida, continuar tarefas e terminar tarefas. |
| A tarefa é bem sucedida, mas nenhum áudio é reproduzido. | Certifique-se de que a faixa de áudio esteja assinada e o jogador esteja rodando, e verifique se o decodificador SDK corresponde ao formato de áudio de saída selecionado em execução. |
| O texto final não tem áudio. | Enviem a tarefa final depois que o texto for enviado, e aguardem o áudio restante e a tarefa terminada antes de desconectar. |
Informação relacionada
Para todos os parâmetros, campos de eventos e interfaces para outras plataformas, veja:
- [Obter e configurar uma chave API] (/api-reference/preparation/api-key)
- [AOQ Cliente SDK visão geral] (/api-reference/realtime-api/aoq-sdk-intro)
- [SDK download] (/api-reference/realtime-api/sdk-download)
- [Autenticação do token] (/api-reference/realtime-api/token-auth)
- [Qwen-Audio-TTS Guia do usuário./developer-guides/speech/tts-models)
- [Acontecimentos do cliente] (/api-reference/speech-synthesis/qwen-tts-realtime/client-events)
- [Eventos de servidor] (/api-reference/speech-synthesis/qwen-tts-realtime/server-events)