Skip to main content
Melhores Práticas

Crie conversas de voz push-to-talk com qwen3.5-omni-plus-realtime via AOQ

Use o AOQ para conectar-se ao qwen3.5-omni-plus-realtime e permita que o cliente controle os limites de turno em conversas push-to-talk, com suporte opcional a perguntas por imagem. O código do cliente usa iOS Swift.

Visão geral da solução

O Qwen-Omni-Realtime oferece suporte a VAD no lado do servidor e ao modo Manual controlado pelo cliente. Este tutorial define session.turn_detection como null. O cliente envia áudio enquanto o usuário mantém um botão pressionado, confirma o envio e solicita explicitamente uma resposta ao soltar o botão. O modo Manual é ideal para botões de interfone físico, controles de pressionar e segurar, ambientes ruidosos em que a aplicação determina os limites de turno e turnos que incluem opcionalmente uma imagem. O transporte de áudio ocorre pela faixa de áudio do AOQ. Não envie input_audio_buffer.append.

Item

Modo VAD

Modo Manual

Limite de turno

Detectado por server_vad ou semantic_vad

Controlado por botão ou estado da aplicação

Configuração da sessão

turn_detection contém definições de VAD

turn_detection é null

Confirmação de áudio

O service realiza automaticamente

O cliente envia input_audio_buffer.commit

Gatilho de resposta

O service aciona automaticamente

O cliente envia response.create

Entrada de imagem

Faixa de vídeo contínua ou imagem via faixa de dados

Faixa de vídeo contínua ou imagem via faixa de dados

Pré-requisitos

  1. Ative o Model Studio e siga 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 confirme em repositórios de código.
  2. Confirme o endpoint do AOQ correspondente à região onde sua aplicação está implantada. Para orientações sobre a seleção, 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 proxy conforme detalhado em Token authentication. Antes de cada nova conexão, o cliente deve obter novas credenciais de conexão no servidor da aplicação.

Importe o SDK

Importe o SDK adequado à sua plataforma de desenvolvimento. A implementação do cliente usa iOS Swift, mas outras plataformas oferecem as mesmas interfaces e fluxo de eventos. Este tutorial emprega streams de áudio PCM. Um plugin fornece a codificação Opus. Caso o uplink utilize Opus, importe também o plugin correspondente.
  • 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 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" />
<uses-permission android:name="android.permission.CAMERA" />
  1. Solicite as permissões RECORD_AUDIO e CAMERA em tempo de execução antes de usar os dispositivos correspondentes.

Fluxo de implementação

  1. O servidor da aplicação obtém os parâmetros de conexão AOQ para o qwen3.5-omni-plus-realtime pela URL de token do Realtime.
  2. O cliente cria o engine e configura os codecs de áudio e as faixas. Se for necessária compreensão visual contínua, configure também a faixa de vídeo.
  3. O cliente inicia a captura e a reprodução locais, desativa o envio da faixa de áudio por padrão, conecta-se ao AOQ e envia session.update.
  4. Após receber session.updated, a opção de vídeo contínuo ativa a faixa de vídeo. A faixa de áudio permanece desativada até que o usuário pressione o botão de fala.
  5. Ao pressionar o botão, o cliente ativa a faixa de áudio. Ao soltá-lo, desativa a faixa, envia opcionalmente uma imagem e, em seguida, transmite input_audio_buffer.commit e response.create.
  6. Depois de receber response.done, outro turno pode ser iniciado. Para encerrar, pare os dispositivos, desconecte-se e destrua o engine.
  • Faixa de vídeo contínua
  • Envio de imagem pela faixa de dados
Publique a faixa de vídeo e ative o envio de vídeo após session.updated. O modelo visualiza continuamente os frames mais recentes. Cada turno de voz requer apenas a confirmação do áudio e a solicitação de resposta.
Sequence diagram for AOQ Manual mode with continuous video streaming

Obtenha 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 representa o endereço IP público real do cliente. Embora opcional, especificá-lo auxilia o service na alocação de um endpoint de retransmissão adequado.
curl -X POST \
  "https://{endpoint}/api/v1/webrtc/realtime?model=qwen3.5-omni-plus-realtime" \
  -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 clientIp em vez de passar uma string vazia.
O servidor da aplicação retorna os seguintes campos de resposta ao cliente. Jamais 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

Implemente o cliente iOS

Após o cliente obter AoqConnectConfig do servidor da aplicação, siga estas etapas para implementar conversas de voz push-to-talk no iOS.

1. Crie o engine e registre callbacks

Crie a instância singleton do engine AOQ e registre o objeto da aplicação como receptor dos callbacks. Trate estados de conexão, eventos do servidor, erros e avisos nesses callbacks.
let createConfig = AoqCreateConfig()
createConfig.workDir = workDir
engine = AoqClientEngine.createEngine(createConfig, delegate: self)

2. Inicie os dispositivos de áudio e vídeo

Inicialize a captura e a reprodução de áudio. Inicie a câmera apenas se a opção de vídeo contínuo for utilizada. Obtenha as permissões de microfone e câmera antes de chamar estes métodos.
let captureConfig = AoqAudioCaptureConfig()
captureConfig.channel = 1
captureConfig.isExternal = false
engine.startAudioCapture(captureConfig)

let playbackConfig = AoqAudioPlaybackConfig()
playbackConfig.channel = 1
playbackConfig.isExternal = false
playbackConfig.isDefaultSpeaker = true
engine.startAudioPlayer(playbackConfig)

3. Configure codecs e faixas

Configure os codecs de áudio compatíveis com o modelo selecionado e o formato de áudio da aplicação, além de escolher as faixas adequadas à opção de entrada de imagem. Os valores de áudio e vídeo abaixo são exemplos; ajuste-os conforme os requisitos do modelo e o cenário da aplicação. Desative o envio da faixa de áudio antes de estabelecer a conexão.
  • Faixa de vídeo contínua
  • Envio de imagem pela faixa de dados
Configure as faixas de publicação de áudio, vídeo e dados. Ajuste as configurações de codificação de vídeo para atender à qualidade de imagem desejada e à largura de banda disponível.
let audioEncoderConfig = AoqAudioCodecConfig()
audioEncoderConfig.trackType = .audio
audioEncoderConfig.codecType = .audioPCM
audioEncoderConfig.sampleRate = 16_000
audioEncoderConfig.channel = 1
engine.setAudioEncoderConfig(audioEncoderConfig)

let audioDecoderConfig = AoqAudioCodecConfig()
audioDecoderConfig.trackType = .audio
audioDecoderConfig.codecType = .audioPCM
audioDecoderConfig.sampleRate = 24_000
audioDecoderConfig.channel = 1
engine.setAudioDecoderConfig(audioDecoderConfig)

let videoEncoderConfig = AoqVideoCodecConfig()
videoEncoderConfig.trackType = .video
videoEncoderConfig.codecType = .videoJpeg
videoEncoderConfig.width = 960
videoEncoderConfig.height = 540
videoEncoderConfig.fps = 2
videoEncoderConfig.bitrate = 500_000
engine.setVideoEncoderConfig(videoEncoderConfig)

let publishAudioTrack = AoqTrackParam()
publishAudioTrack.trackType = .audio
let publishVideoTrack = AoqTrackParam()
publishVideoTrack.trackType = .video
let publishDataTrack = AoqTrackParam()
publishDataTrack.trackType = .data
let subscribeAudioTrack = AoqTrackParam()
subscribeAudioTrack.trackType = .audio
let subscribeDataTrack = AoqTrackParam()
subscribeDataTrack.trackType = .data
connectConfig.publishTracks = [publishAudioTrack, publishVideoTrack, publishDataTrack]
connectConfig.subscribeTracks = [subscribeAudioTrack, subscribeDataTrack]

4. Configure uma sessão Manual

Após estabelecer a conexão, chame sendDataMsg para enviar um evento session.update. Defina turn_detection como null e selecione a voz, as instruções e as modalidades de saída adequadas à sua aplicação. Mantenha os parâmetros de áudio do exemplo consistentes com as configurações de codec do SDK. Para todos os campos, consulte Client events.
private func sendSessionUpdate() {
    let event: [String: Any] = [
        "type": "session.update",
        "session": [
            "modalities": ["text", "audio"],
            "voice": "Ethan",
            "audio": [
                "input": ["format": ["type": "pcm", "sample_rate": 16_000]],
                "output": ["format": ["type": "pcm", "sample_rate": 24_000]]
            ],
            "turn_detection": NSNull()
        ]
    ]
    guard let data = try? JSONSerialization.data(withJSONObject: event) else { return }
    let dataMessage = AoqDataMsg()
    dataMessage.data = data
    engine.sendDataMsg(dataMessage)
}

5. Aguarde a configuração da sessão

Trate o evento session.updated no callback onDataMsg. Não envie mídia até receber esse evento. Na opção de vídeo contínuo, chame enableSendMediaStream para ativar a faixa de vídeo neste momento, mas mantenha a faixa de áudio desativada para evitar que sons anteriores ao pressionamento do botão entrem no buffer de entrada.
func onDataMsg(_ msg: AoqDataMsg) {
    guard let event = try? JSONSerialization.jsonObject(with: msg.data) as? [String: Any],
          let type = event["type"] as? String else { return }
    if type == "session.updated", imageMode == .continuousVideo {
        engine.enableSendMediaStream(.video, enable: true)
    }
    // Keep Audio-track sending disabled until the talk button is pressed.
}

6. Implemente a interação push-to-talk

Ao pressionar o botão, chame enableSendMediaStream para ativar a faixa de áudio. Ao soltá-lo, chame enableSendMediaStream para desativar a faixa, garanta que o turno contenha áudio, envie opcionalmente uma imagem e chame sendDataMsg para transmitir input_audio_buffer.commit seguido de response.create.
func onPushToTalkPressed() {
    hasAudioInCurrentTurn = true
    engine.enableSendMediaStream(.audio, enable: true)
}

func onPushToTalkReleased(base64Jpeg: String? = nil) {
    engine.enableSendMediaStream(.audio, enable: false)
    guard hasAudioInCurrentTurn else { return }
    if imageMode == .singleImage, let base64Jpeg {
        let imageEvent: [String: Any] = [
            "type": "input_image_buffer.append",
            "image": base64Jpeg
        ]
        if let data = try? JSONSerialization.data(withJSONObject: imageEvent) {
            let dataMessage = AoqDataMsg()
            dataMessage.data = data
            engine.sendDataMsg(dataMessage)
        }
    }
    for event in [
        ["type": "input_audio_buffer.commit"],
        ["type": "response.create"]
    ] {
        guard let data = try? JSONSerialization.data(withJSONObject: event) else { continue }
        let dataMessage = AoqDataMsg()
        dataMessage.data = data
        engine.sendDataMsg(dataMessage)
    }
    hasAudioInCurrentTurn = false
}

7. Escolha uma opção de entrada de imagem

A compreensão visual contínua e as perguntas pontuais por imagem usam configurações de faixa e comportamentos de envio distintos. Selecione uma opção considerando largura de banda, consumo de energia e design de interação.
  • Faixa de vídeo contínua
  • Envio de imagem única pela faixa de dados
Indicado para chamadas de vídeo, cenários com mudanças rápidas ou contexto visual contínuo. Após publicar a faixa de vídeo, não envie input_image_buffer.append.

8. Desconecte e destrua o engine

Ao finalizar a sessão, desconecte-se e destrua o engine. As operações disconnect ou destroy fecham automaticamente os dispositivos de mídia, eliminando a necessidade de chamar métodos stop separadamente. O AoqClientEngine é um singleton e só pode ser criado novamente após a chamada de destroy.
engine.disconnect()
AoqClientEngine.destroy()

Exemplo completo

A classe a seguir aceita um AoqConnectConfig mapeado a partir da resposta de token do servidor da aplicação. Em produção, adicione gerenciamento de estado da UI, tratamento de permissões, recuperação de erros e compressão de imagens.
import Foundation
import AoqClientSdk

final class ManualPushToTalkClient: NSObject, AoqEngineDelegate {
    enum ImageMode: Equatable {
        case none
        case continuousVideo
        case singleImage
    }

    private var engine: AoqClientEngine!
    private let imageMode: ImageMode
    private var hasAudioInCurrentTurn = false

    init(workDir: String, connectConfig: AoqConnectConfig, imageMode: ImageMode) {
        self.imageMode = imageMode
        super.init()
        let createConfig = AoqCreateConfig()
        createConfig.workDir = workDir
        self.engine = AoqClientEngine.createEngine(createConfig, delegate: self)

        // Example values. Match these settings to the selected model and application format.
        let audioEncoderConfig = AoqAudioCodecConfig()
        audioEncoderConfig.trackType = .audio
        audioEncoderConfig.codecType = .audioPCM
        audioEncoderConfig.sampleRate = 16_000
        audioEncoderConfig.channel = 1
        engine.setAudioEncoderConfig(audioEncoderConfig)

        let audioDecoderConfig = AoqAudioCodecConfig()
        audioDecoderConfig.trackType = .audio
        audioDecoderConfig.codecType = .audioPCM
        audioDecoderConfig.sampleRate = 24_000
        audioDecoderConfig.channel = 1
        engine.setAudioDecoderConfig(audioDecoderConfig)

        let publishAudioTrack = AoqTrackParam()
        publishAudioTrack.trackType = .audio
        let publishDataTrack = AoqTrackParam()
        publishDataTrack.trackType = .data
        let subscribeAudioTrack = AoqTrackParam()
        subscribeAudioTrack.trackType = .audio
        let subscribeDataTrack = AoqTrackParam()
        subscribeDataTrack.trackType = .data

        connectConfig.publishTracks = [publishAudioTrack, publishDataTrack]
        connectConfig.subscribeTracks = [subscribeAudioTrack, subscribeDataTrack]

        if imageMode == .continuousVideo {
            let videoEncoderConfig = AoqVideoCodecConfig()
            videoEncoderConfig.trackType = .video
            videoEncoderConfig.codecType = .videoJpeg
            videoEncoderConfig.width = 960
            videoEncoderConfig.height = 540
            videoEncoderConfig.fps = 2
            videoEncoderConfig.bitrate = 500_000
            engine.setVideoEncoderConfig(videoEncoderConfig)

            let publishVideoTrack = AoqTrackParam()
            publishVideoTrack.trackType = .video
            connectConfig.publishTracks = [publishAudioTrack, publishVideoTrack, publishDataTrack]
        }

        let captureConfig = AoqAudioCaptureConfig()
        captureConfig.channel = 1
        captureConfig.isExternal = false
        engine.startAudioCapture(captureConfig)

        let playbackConfig = AoqAudioPlaybackConfig()
        playbackConfig.channel = 1
        playbackConfig.isExternal = false
        playbackConfig.isDefaultSpeaker = true
        engine.startAudioPlayer(playbackConfig)

        if imageMode == .continuousVideo {
            let videoCaptureConfig = AoqVideoCaptureConfig()
            videoCaptureConfig.width = 1280
            videoCaptureConfig.height = 720
            videoCaptureConfig.fps = 15
            engine.startVideoCapture(videoCaptureConfig)
        }

        engine.enableSendMediaStream(.audio, enable: false)
        if imageMode == .continuousVideo {
            engine.enableSendMediaStream(.video, enable: false)
        }
        engine.connect(connectConfig)
    }

    func onPushToTalkPressed() {
        hasAudioInCurrentTurn = true
        engine.enableSendMediaStream(.audio, enable: true)
    }

    func onPushToTalkReleased(base64Jpeg: String? = nil) {
        engine.enableSendMediaStream(.audio, enable: false)
        guard hasAudioInCurrentTurn else { return }
        if imageMode == .singleImage, let base64Jpeg {
            let imageEvent: [String: Any] = [
                "type": "input_image_buffer.append",
                "image": base64Jpeg
            ]
            if let data = try? JSONSerialization.data(withJSONObject: imageEvent) {
                let dataMessage = AoqDataMsg()
                dataMessage.data = data
                engine.sendDataMsg(dataMessage)
            }
        }
        for event in [
            ["type": "input_audio_buffer.commit"],
            ["type": "response.create"]
        ] {
            guard let data = try? JSONSerialization.data(withJSONObject: event) else { continue }
            let dataMessage = AoqDataMsg()
            dataMessage.data = data
            engine.sendDataMsg(dataMessage)
        }
        hasAudioInCurrentTurn = false
    }

    private func sendSessionUpdate() {
        let event: [String: Any] = [
            "type": "session.update",
            "session": [
                "modalities": ["text", "audio"],
                "voice": "Ethan",
                "audio": [
                    "input": ["format": ["type": "pcm", "sample_rate": 16_000]],
                    "output": ["format": ["type": "pcm", "sample_rate": 24_000]]
                ],
                "turn_detection": NSNull()
            ]
        ]
        guard let data = try? JSONSerialization.data(withJSONObject: event) else { return }
        let dataMessage = AoqDataMsg()
        dataMessage.data = data
        engine.sendDataMsg(dataMessage)
    }

    func close() {
        engine.disconnect()
        AoqClientEngine.destroy()
    }

    func onConnectionStatusChange(_ status: AoqConnectionStatus) {
        if status == .connected { sendSessionUpdate() }
    }

    func onDataMsg(_ msg: AoqDataMsg) {
        guard let event = try? JSONSerialization.jsonObject(with: msg.data) as? [String: Any],
              let type = event["type"] as? String else { return }
        if type == "session.updated", imageMode == .continuousVideo {
            engine.enableSendMediaStream(.video, enable: true)
        }
    }

    func onError(_ code: Int, message: String) {}
    func onWarning(_ code: Int, message: String) {}
    func onStats(_ stats: AoqStats) {}
    func onAudioDeviceStateChanged(_ state: AoqAudioDeviceState) {}
    func onAudioDeviceRouteChanged(_ routeType: Int) {}
    func onAudioDeviceInterrupted(_ interrupt: Bool) {}
    func onAudioFileState(_ state: AoqAudioFileState) {}
    func onVideoDeviceStateChanged(_ state: AoqVideoDeviceState) {}
}

Execute e verifique

Realize um turno push-to-talk apenas com áudio e outro com imagem. Resultados esperados:
  1. A faixa de áudio permanece desativada antes do pressionamento do botão e transmite áudio continuamente enquanto ele estiver pressionado.
  2. Após soltar o botão, os eventos input_audio_buffer.committed, response.created e response.done são recebidos em sequência, e o áudio do modelo é reproduzido na faixa de áudio inscrita.
  3. Na opção de imagem única, o modelo responde usando a imagem e o áudio do turno. No modo de vídeo contínuo, ele usa os frames de vídeo mais recentes.
Para detalhes sobre campos de eventos do servidor e esquemas completos de resposta, consulte Server events.

Considerações importantes

  1. O AOQ transporta áudio pela faixa de áudio. Não envie adicionalmente input_audio_buffer.append.
  2. O evento input_audio_buffer.commit apenas confirma o turno, sem acionar uma resposta do modelo. Envie response.create logo em seguida.
  3. Evite confirmar um buffer de áudio vazio, pois o service retornará um erro.
  4. Não ative o envio de mídia antes de receber session.updated. No modo Manual, não ative a faixa de áudio antes que o usuário pressione o botão.

Informações relacionadas

Para consultar todos os parâmetros, campos de eventos e interfaces para outras plataformas, veja:
Plano de Tokens
Playground de Modelos
  • Music generation
Inferência do Modelo
Avaliação
Compressão de Modelos
Estatísticas e Monitoramento
Suporte