QwenCloudBRASIL
Tradução de apoio para consulta. Código e identificadores técnicos preservados. Consultar referência original ↗

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

  1. 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.
  2. Baixe o mais recente cliente do AOQ SDK como descrito em [SDK download] (/api-reference/realtime-api/sdk-download).
  3. 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

  1. 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.
  2. O cliente publica a faixa de dados, assina as faixas de áudio e dados, e configura o decodificador de áudio SDK.
  3. 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.
  4. 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.
  5. Depois que toda mensagem é enviada, o cliente manda a tarefa final. O serviço devolve o áudio restante e finalmente finalizado.
  6. 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.

  1. O texto só é enviado após a tarefa ser recebida.
  2. Áudio para frases completas é reproduzido continuamente sobre a faixa de áudio. Frases incompletas são sintetizadas após a tarefa final.
  3. A tarefa final é recebida depois que todo o áudio está completo. Outra tarefa pode começar com uma nova tarefa.

Cenários comuns

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: