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
- 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.
- 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 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
1. Crie o motor e registre chamadas de retorno
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.
- O áudio do microfone começa a ser transmitido somente após a sessão.
- 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.
Mude a voz
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
- [Consiga e configure 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 Conversa de voz em tempo real./developer-guides/speech/qwen-audio-realtime)
- [Acontecimentos do cliente] (/api-reference/qwen-audio-realtime/client-events)
- [Eventos de servidor] (/api-reference/qwen-audio-realtime/server-events)