Utilize o AOQ para se conectar ao qwen-audio-3.0-realtime-plus e use o VAD no lado do servidor para criar conversas de voz em tempo real com baixa latência. O código do cliente utiliza Android Java.
Visão geral da solução
O Qwen-Audio é um modelo de interação de voz em tempo real ponta a ponta, ideal 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 tracks separadas. A track de Audio transporta o uplink PCM do microfone e o downlink PCM do modelo, enquanto a track de Data transporta eventos do protocolo Realtime.
Este tutorial utiliza server_vad. O cliente envia áudio continuamente, e o service detecta quando o usuário começa e para de falar, acionando uma resposta.
Pré-requisitos
- Ative o Model Studio e siga Obtain and configure an API key. Armazene a API key apenas no seu servidor de aplicação. Não a inclua no código do cliente nem a envie para um repositório de código.
- Confirme o endpoint do AOQ para a região onde sua aplicação está implantada. Para orientações sobre a seleção, consulte Select a region, deployment scope, and endpoint.
- Baixe a versão mais recente do AOQ Client SDK conforme descrito em SDK download.
- 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 do servidor de aplicação.
Importe o SDK
Importe o SDK para sua plataforma de desenvolvimento. A implementação do cliente usa Android Java. Outras plataformas fornecem as mesmas interfaces e fluxo de eventos. Este tutorial utiliza streams de áudio PCM. A codificação Opus é fornecida por um plugin. Importe o plugin Opus caso o uplink utilize Opus.
Android
- Coloque AoqClientSdk-release.aar em app/libs e configure a dependência e as ABIs suportadas pelo SDK em app/build.gradle:
android {
defaultConfig {
minSdk 21
ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}
- Declare as seguintes permissões no AndroidManifest.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" />
- Solicite as permissões RECORD_AUDIO em tempo de execução antes de utilizar os dispositivos correspondentes.
iOS
- Adicione AoqClientSdk.framework ao projeto Xcode e selecione Embed & Sign em Target > General > Frameworks, Libraries, and Embedded Content. O SDK suporta dispositivos arm64 com iOS 13.0 ou posterior.
- Adicione NSMicrophoneUsageDescription ao Info.plist e solicite autorização antes de utilizar os dispositivos correspondentes.
- Use import AoqClientSdk em Swift ou #import <AoqClientSdk/AoqClientSdk.h> em Objective-C.
HarmonyOS
- Coloque AoqClientSdk.har em entry/libs e declare a dependência em entry/oh-package.json5. O SDK é compatível com a API 12 e suporta arm64-v8a:
{
"dependencies": {
"@aoq/client-sdk": "file:./libs/AoqClientSdk.har"
}
}
- Declare as seguintes permissões em entry/src/main/module.json5:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" },
{ "name": "ohos.permission.MICROPHONE",
"reason": "$string:perm_mic_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }
]
- Antes de utilizar os dispositivos correspondentes, chame abilityAccessCtrl.createAtManager().requestPermissionsFromUser para solicitar ohos.permission.MICROPHONE.
Linux (Python)
- Extraia o SDK e mantenha aoq_client_sdk.py, libAoqClientSdk.so e libonnxruntime.so.1.16.3 no mesmo diretório.
- Adicione o diretório do SDK aos caminhos de busca do Python e de bibliotecas dinâmicas:
export PYTHONPATH="$PWD/AoqClientSdk:$PYTHONPATH"
export LD_LIBRARY_PATH="$PWD/AoqClientSdk:$LD_LIBRARY_PATH"
- Use import aoq_client_sdk em Python. Você também pode especificar o caminho absoluto de libAoqClientSdk.so utilizando AOQ_CLIENT_SDK_LIB.
Experimente a demonstração
Utilize a demonstração para Android do Alibaba Cloud Model Studio para verificar rapidamente a conectividade do AOQ. Baixe o APK e configure a API key e o workspaceId para testar modelos selecionados.
Escaneie o QR code a seguir para baixar a demonstração:
Fluxo de implementação
- O servidor de aplicação obtém credenciais para a conexão AOQ atual com o qwen-audio-3.0-realtime-plus a partir da URL de token Realtime.
- O cliente configura o codificador de uplink e o decodificador de downlink do SDK 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 o AoqConnectConfig. Ele preenche os campos de credencial para a conexão atual e configura as tracks de Audio e Data para publicação e assinatura. O cliente mantém o envio da track de Audio desativado e chama connect para estabelecer a conexão AOQ.
- Após o estabelecimento da conexão, o cliente envia session.update. Ele ativa a track de Audio somente após receber session.updated.
- O VAD no lado do servidor determina automaticamente os limites dos turnos. O áudio do modelo é reproduzido pela track de Audio e os eventos da conversa são retornados pela track de Data.
- Para finalizar, desconecte e destrua o engine. O SDK fecha automaticamente os dispositivos de áudio.
Obtenha um token do servidor de aplicação
Defina DASHSCOPE_API_KEY no servidor de aplicação e envie a solicitação para o endpoint da região selecionada. clientIp é o endereço IP público real do cliente. Este campo é opcional, mas especificá-lo ajuda o service a alocar um endpoint de retransmissão apropriado.
curl -X POST \
"https://{endpoint}/api/v1/webrtc/realtime?model=qwen-audio-3.0-realtime-plus" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \
-H "x-dashscope-rtc-transport: moq" \
-d "{\"clientIp\": \"${CLIENT_REAL_IP}\"}"
ObservaçãoSe o servidor de aplicação não conseguir obter o endereço IP público real do cliente, omita clientIp em vez de passar uma string vazia.
O servidor de aplicação retorna os seguintes campos de resposta ao cliente. Um token AOQ pode ser usado para apenas uma conexão. Antes de cada chamada connect, o cliente deve solicitar um novo token em vez de armazenar em cache ou reutilizar um existente. Nunca retorne a API key para um cliente em produção. Para todos os campos de solicitaçã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 |
Implemente o cliente Android
Antes de cada conexão, o cliente obtém novas credenciais de conexão do servidor de aplicação e cria o AoqConnectConfig. Mapeie os campos de resposta do token e adicione configurações de conexão do lado do cliente, como as tracks de publicação e assinatura. Siga estas etapas para implementar conversas de voz em tempo real no Android.
1. Crie o engine e registre callbacks
Crie o engine AOQ singleton e registre callbacks de eventos. Configure a sessão após o sucesso da conexão e despache eventos do servidor para a UI e a máquina de estados da 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);
2. Configure codecs de áudio
Configure o codificador de uplink e o decodificador de downlink do SDK para o modelo selecionado e o formato de áudio da aplicação. Os valores a seguir são exemplos PCM para este tutorial e não restringem o formato de áudio da sua aplicação.
AoqClientEngine.AoqAudioCodecConfig audioEncoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioEncoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioEncoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioEncoderConfig.sampleRate = 16000; // Example. Match the model and application format.
audioEncoderConfig.channel = 1;
engine.setAudioEncoderConfig(audioEncoderConfig);
AoqClientEngine.AoqAudioCodecConfig audioDecoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioDecoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioDecoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioDecoderConfig.sampleRate = 24000; // Example. Match the model output format.
audioDecoderConfig.channel = 1;
engine.setAudioDecoderConfig(audioDecoderConfig);
3. Configure tracks e conecte
Use as interfaces do SDK para iniciar a captura e a reprodução de áudio. Mapeie a resposta atual do token do servidor de aplicação para os campos de credencial em AoqConnectConfig e configure as tracks de Audio e Data em publishTracks e subscribeTracks. Mantenha o envio da track de Audio desativado ao chamar connect. Ative o envio somente após receber session.updated.
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);
4. Envie session.update
Após o sucesso da conexão, configure modalidades de saída, voz, formatos de áudio, instruções e VAD. Tanto input_audio_format quanto output_audio_format usam pcm. A configuração de codec do SDK determina as taxas de amostragem. Para todos os parâmetros, consulte 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")
.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);
5. Ative o uplink após session.updated
session.updated indica que a configuração da sessão está ativa. Ative o envio da track de Audio apenas neste momento para que o áudio capturado anteriormente não seja enviado ao modelo.
if ("session.updated".equals(type)) {
engine.enableSendMediaStream(
AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
}
6. Trate eventos do servidor
Em onDataMsg, use type para exibir transcrições do usuário e do modelo e tratar erros. Para todos os campos de eventos, consulte 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. Desconecte e destrua o engine
Quando a conversa terminar, desconecte e destrua o engine singleton. disconnect ou destroy fecham automaticamente a captura e a reprodução de áudio, portanto, não é necessário parar os dispositivos separadamente.
engine.disconnect();
AoqClientEngine.destroy();
Principais eventos do servidor
Os eventos da track de Data são identificados por type. O cliente deve tratar os seguintes eventos principais. Para esquemas completos de eventos, consulte Server events.
Evento | Descrição |
session.created | A sessão é criada e as configurações padrão são retornadas |
session.updated | As configurações do cliente estão ativas e o uplink de áudio pode ser ativado |
input_audio_buffer.speech_started | O service detecta que o usuário começou a falar |
input_audio_buffer.speech_stopped | O service detecta que o usuário parou de falar |
input_audio_buffer.committed | O áudio do turno foi confirmado |
response.created | O modelo começa a gerar uma resposta |
response.audio_transcript.delta | Transcrição incremental do modelo |
conversation.item.input_audio_transcription.completed | A transcrição final do usuário está disponível |
response.done | A resposta está completa |
error | Ocorreu um erro no servidor |
Exemplo completo
A classe a seguir aceita um AoqConnectConfig preenchido com credenciais para a conexão atual e adiciona as configurações de dispositivo de áudio, track de publicação e track de assinatura. Obtenha novas credenciais e crie uma nova configuração de conexão para cada reconexão. Adicione permissões, estado da UI e lógica de reconexão em 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);
// Example values. Match these settings to the model and application audio format.
AoqClientEngine.AoqAudioCodecConfig audioEncoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioEncoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioEncoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioEncoderConfig.sampleRate = 16000;
audioEncoderConfig.channel = 1;
engine.setAudioEncoderConfig(audioEncoderConfig);
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.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")
.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();
}
}
Execute e verifique
- O áudio do microfone começa a ser transmitido apenas após o recebimento de session.updated.
- Depois que o usuário para de falar, o service confirma o áudio e começa a responder. Eventos de texto e áudio da track de Audio são retornados continuamente.
Cenários comuns
Altere o modo de interação
Use server_vad para detecção de turnos baseada em silêncio, smart_turn para detecção de turnos acústica e semântica, ou defina turn_detection como null para push-to-talk. turn_detection só pode ser alterado antes da primeira entrada de áudio. Estabeleça uma nova sessão para mudar de modo.
Altere a voz
Defina session.voice no primeiro session.update. As vozes do sistema suportadas variam conforme o modelo. Para vozes suportadas e clonagem de voz, consulte Qwen-Audio real-time voice conversation.
Alto-falante ou auricular
Defina o dispositivo de saída padrão usando AoqAudioPlaybackConfig.isDefaultSpeaker e chame enableSpeakerphone para alternar enquanto a sessão estiver ativa.
Chamadas em segundo plano no Android
No Android 10 ou posterior, use um service em primeiro plano com foregroundServiceType="microphone|mediaPlayback" para continuar a captura e a reprodução em segundo plano. Inicie-o enquanto o aplicativo estiver visível para o usuário.
Solução de problemas
Problema | Solução |
A conexão falha | Certifique-se de que o token é válido, o endpoint corresponde à região de implantação e os campos do AoqConnectConfig estão mapeados corretamente. |
A sessão é estabelecida, mas nenhuma resposta é retornada | Verifique se a track de Audio está ativada após session.updated e se o codificador de uplink do SDK corresponde ao formato de áudio do modelo e da aplicação. |
A resposta não tem áudio | Confirme se a track de Audio está assinada e o player de áudio está em execução e, em seguida, verifique se o decodificador de downlink do SDK corresponde ao formato de áudio de saída do modelo. |
Informações relacionadas
Para todos os parâmetros, campos de eventos e interfaces para outras plataformas, consulte: