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

Conversa de voz em tempo real com o AOQ + Qwen-Audio

Use o QOA para se conectar qwen-audio-3.1-realtime-plus e usar VAD do lado do servidor para construir conversas de voz em tempo real. Exemplos de código de cliente usam Android Java.

Visão geral da solução

Qwen-Audio é um modelo de interação de voz em tempo real para cenários de baixa latência, como assistentes de voz, atendimento ao cliente e companheiros de IA. O AOQ transporta áudio e eventos em faixas separadas. A faixa de áudio carrega microfones de áudio e modelo de áudio de Downlink, e a faixa de dados carrega eventos de protocolo em tempo real.

Este tutorial usa servidor_vad. O cliente envia áudio continuamente, e o serviço detecta quando o usuário começa e pára de falar e desencadeia uma resposta.

Pré-requisitos

  1. Ativar QwenCloud e obter uma chave API seguindo [Obter e configurar uma chave API] (/api-reference/preparation/api-key). Guarde a chave 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 Opus 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" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
```

3.  Peça permissão de registro antes que os dispositivos correspondentes sejam usados.

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.  Adicione NSMicrophoneUsageDescription para Info.plist e solicite autorização antes que os dispositivos correspondentes sejam usados.
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" },
  { "name": "ohos.permission.MICROPHONE",
    "reason": "$string:perm_mic_reason",
    "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }
]
```

3.  Antes que os dispositivos correspondentes sejam usados, ligue para `abilityAccessCtrl.createAtManager().requestPermissionsFromUser` para pedir `ohos.permission.MICROPHONE`.

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 credenciais para a conexão atual do AOQ. qwen-audio-3.1-realtime-plus da URL do símbolo em tempo real.
  2. O cliente configura o codificador de link SDK e o decodificador de link para o modelo selecionado e o formato de áudio da aplicação.
  3. O cliente inicializa os dispositivos de gravação e reprodução e cria AoqConnectConfig. Ele povoa os campos de credencial para a conexão atual e configura as faixas de áudio e dados para publicar e assinar. O cliente mantém a faixa de áudio enviando desativadas e as chamadas se conectam para estabelecer a conexão do QOA.
  4. Depois que a conexão for estabelecida, o cliente envia session.update. Ele permite que a faixa de áudio só após a sessão.updated é recebida.
  5. O VAD do lado do servidor determina automaticamente os limites de giro. O áudio do modelo é reproduzido na faixa de áudio e os eventos de conversa são retornados na faixa de dados.
  6. Para terminar, desligue e destrua o motor. O SDK fecha automaticamente os dispositivos de áudio.

[Diagrama de sequência para conversas de voz em tempo real sobre o AOQ] (http://icms-x-dita-hz.oss-cn-hangzhou.aliyuncs.com/en-US/images/p1094861.png)

Obter um token do servidor de aplicativos

Defina DASHSCOPE_API_KEY no servidor de aplicativos e envie o pedido para o ponto final. 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/realtime?model=qwen-audio-3.1-realtime-plus" \
  -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. clientIp em vez de passar uma corda vazia.

O servidor de aplicativos devolve os seguintes campos de resposta ao cliente. Um símbolo de QAO pode ser usado para uma única conexão. Antes de cada ligação, o cliente deve pedir um novo símbolo em vez de caching ou reutilizar um. 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

Antes de cada conexão, o cliente obtém novas credenciais de conexão do servidor de aplicativos e cria AoqConnectConfig. Mapeie os campos de resposta e adicione configurações de conexão do lado do cliente, como as faixas de publicação e assinatura. Siga estes passos para implementar conversas de voz em tempo real no Android.

Crie o motor de singleton AOQ e registre as chamadas de eventos. Configure a sessão após a conexão ter sucesso, e envie eventos de servidor para a UI e a máquina de estado de aplicação.

AoqClientListener listener = new AoqClientListener() {
  @Override
  public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
    if (status == AoqClientEngine.AoqConnectionStatus.AoqConnectionStatusConnected) {
      configureSession();
    }
  }
  @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. Configure os codecs de áudio.

Configure o codificador de uplink SDK e o decodificador de downlink para o modelo selecionado e o formato de áudio da aplicação. Os seguintes valores são exemplos de Opus para este tutorial e não restrinjam o formato de áudio de sua aplicação.

AoqClientEngine.AoqAudioCodecConfig audioEncoderConfig =
    new AoqClientEngine.AoqAudioCodecConfig();
audioEncoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioEncoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioOpus;
audioEncoderConfig.sampleRate = 16000;
audioEncoderConfig.channel = 1;
engine.setAudioEncoderConfig(audioEncoderConfig);
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);

3. Configure faixas e conecte-se.

Use interfaces SDK para iniciar a captura e reprodução de áudio. Mapeie a resposta atual do token do servidor de aplicativos aos campos de credencial no AoqConnectConfig, e configure as faixas de áudio e dados na publicação de faixas e subscreva as faixas. Mantenha o áudio enviando desativado quando ligar para a conexão. Ativar envio somente após sessão.

AoqClientEngine.AoqAudioCaptureConfig captureConfig =
    new AoqClientEngine.AoqAudioCaptureConfig();
captureConfig.channel = 1;
captureConfig.isVoipMode = true;
engine.startAudioCapture(captureConfig);
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
    new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isVoipMode = true;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
AoqClientEngine.AoqTrackParam publishAudioTrack = new AoqClientEngine.AoqTrackParam();
publishAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.publishTracks.add(publishAudioTrack);
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.enableSendMediaStream(
    AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.connect(connectConfig);

- Quatro. Envie sessão. Atualização.

Depois que a conexão tiver sucesso, configure modalidades de saída, voz, formatos de áudio, instruções e VAD. Ambos input_audio_format e output_audio_format use pcm. A configuração do codec SDK determina as taxas de amostragem. Para todos os parâmetros, veja [Eventos do cliente] (/api-reference/qwen-audio-realtime/client-events).

JSONObject vad = new JSONObject()
    .put("type", "server_vad")
    .put("threshold", 0.5)
    .put("silence_duration_ms", 800);
JSONObject session = new JSONObject()
    .put("modalities", new JSONArray().put("text").put("audio"))
    .put("voice", "longanqian_v3.1")
    .put("input_audio_format", "pcm")
    .put("output_audio_format", "pcm")
    .put("instructions", "You are a helpful voice assistant.")
    .put("turn_detection", vad);
JSONObject sessionUpdate = new JSONObject()
    .put("type", "session.update")
    .put("session", session);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = sessionUpdate.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);

- Cinco. Activar ligação após sessão. Actualizada.

Session.updated indica que a configuração da sessão está ativa. Ative o envio de áudio apenas neste ponto para que o áudio capturado mais cedo não seja enviado para o modelo.

if ("session.updated".equals(type)) {
  engine.enableSendMediaStream(
      AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
}

Seis. Lidar com eventos de servidores

Em DataMsg, use type para exibir transcrições de usuários e modelos e lidar com erros. Para todos os campos de eventos, veja [Eventos de servidores] (/api-reference/qwen-audio-realtime/server-events).

if ("response.audio_transcript.delta".equals(type)) {
  String delta = event.optString("delta");
  // Append delta to the model transcript in the UI.
} else if ("conversation.item.input_audio_transcription.completed".equals(type)) {
  String transcript = event.optString("transcript");
  // Display the final user transcript in the UI.
} else if ("error".equals(type)) {
  // Read the error fields and update the application state.
}

- 7? - 7? Desligue e destrua o motor.

Quando a conversa terminar, desligue e destrua o motor singleton. disconnect Ou destroy Fecha automaticamente a captura de áudio e reprodução, então você não precisa parar os dispositivos separadamente.

engine.disconnect();
AoqClientEngine.destroy();

Principais eventos do servidor

Os eventos da trilha de dados são identificados por type. O cliente deve lidar com os seguintes eventos chave. Para esquemas completos de eventos, veja [Eventos de servidores] (/api-reference/qwen-audio-realtime/server-events).

Evento Descrição
sessão.criado A sessão é criada e as configurações padrão são devolvidas.
sessão. atualizada. As configurações do cliente estão ativas e o link de áudio pode ser ativado.
Input_audio_buffer.speech_iniciada O serviço detecta que o usuário começou a falar.
Input_audio_buffer.speech_stoped O serviço detecta que o usuário parou de falar.
Input_audio_buffer.commit Áudio para a volta está comprometido
Resposta. Criada. O modelo começa a gerar uma resposta.
Resposta. Audio_transcript.delta. Tradução:
Conversa. Item. Entrada. Audio. Transcrição. Completa. A transcrição final do usuário está disponível.
Resposta. Feito. A resposta está completa.
Erro Um erro de servidor ocorre.

Exemplo completo.

A seguinte classe aceita um AoqConnectConfig povoado com credenciais para a conexão atual e adiciona o áudio-dispositivo, publicar-pista, e assinar-pista configurações. Obter novas credenciais e criar uma nova configuração de conexão para cada reconexão. 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.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
public final class RealtimeVoiceChatClient {
  private AoqClientEngine engine;
  public RealtimeVoiceChatClient(Context context, AoqClientEngine.AoqConnectConfig connectConfig) {
    AoqClientListener listener = new AoqClientListener() {
      @Override
      public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
        if (status == AoqClientEngine.AoqConnectionStatus.AoqConnectionStatusConnected) {
          configureSession();
        }
      }
      @Override
      public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
        try {
          JSONObject event = new JSONObject(
              new String(msg.data, StandardCharsets.UTF_8));
          String type = event.optString("type");
          if ("session.updated".equals(type)) {
            engine.enableSendMediaStream(
                AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
          } else if ("response.audio_transcript.delta".equals(type)) {
            String delta = event.optString("delta");
            // Display delta in the UI.
          } else if ("conversation.item.input_audio_transcription.completed".equals(type)) {
            String transcript = event.optString("transcript");
            // Display transcript in the UI.
          } else if ("error".equals(type)) {
            // Read error fields and update the application state.
          }
        } 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);
    AoqClientEngine.AoqAudioCodecConfig audioEncoderConfig =
        new AoqClientEngine.AoqAudioCodecConfig();
    audioEncoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
    audioEncoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioOpus;
    audioEncoderConfig.sampleRate = 16000;
    audioEncoderConfig.channel = 1;
    engine.setAudioEncoderConfig(audioEncoderConfig);
    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.AoqAudioCaptureConfig captureConfig =
        new AoqClientEngine.AoqAudioCaptureConfig();
    captureConfig.channel = 1;
    captureConfig.isVoipMode = true;
    engine.startAudioCapture(captureConfig);
    AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
        new AoqClientEngine.AoqAudioPlaybackConfig();
    playbackConfig.channel = 1;
    playbackConfig.isVoipMode = true;
    playbackConfig.isDefaultSpeaker = true;
    engine.startAudioPlayer(playbackConfig);
    AoqClientEngine.AoqTrackParam publishAudioTrack =
        new AoqClientEngine.AoqTrackParam();
    publishAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
    connectConfig.publishTracks.add(publishAudioTrack);
    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.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
    engine.connect(connectConfig);
  }
  private void configureSession() {
    try {
      JSONObject vad = new JSONObject()
          .put("type", "server_vad")
          .put("threshold", 0.5)
          .put("silence_duration_ms", 800);
      JSONObject session = new JSONObject()
          .put("modalities", new JSONArray().put("text").put("audio"))
          .put("voice", "longanqian_v3.1")
          .put("input_audio_format", "pcm")
          .put("output_audio_format", "pcm")
          .put("turn_detection", vad);
      JSONObject sessionUpdate = new JSONObject()
          .put("type", "session.update")
          .put("session", session);
      AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
      dataMessage.data = sessionUpdate.toString().getBytes(StandardCharsets.UTF_8);
      engine.sendDataMsg(dataMessage);
    } catch (JSONException e) {
      throw new IllegalStateException("Failed to create session.update", e);
    }
  }
  public void close() {
    engine.disconnect();
    AoqClientEngine.destroy();
  }
}

Corra e verifique.

  1. O áudio do microfone começa a ser transmitido somente após a sessão.
  2. Depois que o usuário parar de falar, o serviço comprime o áudio e começa a responder. Eventos de texto e áudio áudio são retornados continuamente.

Cenários comuns

Mude o modo de interação.

Use servidor_vad para detecção de turnos baseados em silêncio, semântico_vad para detecção de turnos acústicos e semânticos, ou ajuste turno_detecção para nulo para push-to-talk. turno_detecção pode ser alterada apenas antes da primeira entrada de áudio. Estabelecer uma nova sessão para mudar de modo.

Preparar. session.voice Na primeira sessão. Atualização. Vozes suportadas variam de acordo com o modelo. Para vozes apoiadas e clonagem de voz, veja [Qwen-Audio Conversa de voz em tempo real./developer-guides/speech/qwen-audio-realtime).

Alto-falante ou fone de ouvido

Defina o dispositivo de saída padrão usando AoqAudioPlaybackConfig.isDefaultSpeaker, e chamar enableSpeakerphone Para trocar enquanto a sessão está ativa.

Chamadas de fundo no Android

No Android 10 ou posterior, use um serviço de primeiro plano com foregroundServiceType="microphone|mediaPlayback" para continuar captura e reprodução no fundo. Comece enquanto o aplicativo é visível para o usuário.

Solução de problemas

Questão Solução
A conexão falhou. Certifique-se de que o token é válido, o ponto final corresponde à região de implantação, e os campos AoqConnectConfig são mapeados corretamente.
A sessão está estabelecida, mas nenhuma resposta é devolvida. Certifique-se de que a faixa de áudio esteja ativada após a sessão.atualizada e que o codificador de uplink SDK combine com o formato de áudio do modelo e aplicativo.
A resposta não tem áudio. Certifique-se de que a faixa de áudio esteja assinada e o reprodutor de áudio esteja rodando, e então verifique se o decodificador SDK downlink combina com o formato de saída de áudio do modelo.

Informação relacionada