Skip to main content
Melhores Práticas

Sintetizar fala com qwen-audio-3.0-tts-flash via AOQ

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

  1. 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.
  2. 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.
  3. Baixe a versão mais recente do SDK do cliente AOQ conforme descrito em SDK download.
  4. 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
  • iOS
  • HarmonyOS
  • Linux (Python)
  1. 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'])
}
  1. 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" />
  1. Este cenário não requer permissões de microfone ou câmera.

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:
QR code for downloading the demo

Fluxo de implementação

  1. 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.
  2. 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.
  3. 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.
  4. Após receber task-started, o cliente envia um ou mais segmentos de texto continue-task no ritmo necessário para a aplicação.
  5. Depois de enviar todo o texto, o cliente envia finish-task. O service retorna o áudio restante e, por fim, task-finished.
  6. Ao receber task-finished, inicie outra tarefa na mesma conexão AOQ com um novo task_id ou desconecte e destrua o engine.
Sequence diagram for streaming speech synthesis over AOQ

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}\"}"
Caso 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

  1. O texto é enviado somente após o recebimento de task-started.
  2. O áudio de frases completas é reproduzido continuamente pela faixa Audio. Frases incompletas são sintetizadas após finish-task.
  3. 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:
Plano de Tokens
Playground de Modelos
  • Music generation
Inferência do Modelo
Avaliação
Compressão de Modelos
Estatísticas e Monitoramento
Suporte
Sintetizar fala com qwen-audio-3.0-tts-flash via AOQ - Alibaba Cloud Model Studio