Bangun percakapan suara push-to-talk dengan qwen3.5-omni-plus-realtime melalui AOQ
Gunakan AOQ untuk terhubung ke qwen3.5-omni-plus-realtime dan memungkinkan client mengontrol batas giliran (turn boundaries) untuk percakapan push-to-talk serta pertanyaan gambar opsional. Kode client menggunakan iOS Swift.
Qwen-Omni-Realtime mendukung VAD sisi server dan mode Manual yang dikontrol client. Tutorial ini mengatur session.turn_detection ke null. Client mengirim audio selama pengguna menahan tombol, lalu meng-commit audio tersebut dan secara eksplisit meminta respons saat pengguna melepas tombol.Mode Manual cocok untuk tombol interkom perangkat keras, kontrol tekan-dan-tahan, lingkungan bising di mana aplikasi menentukan batas giliran, serta giliran yang secara opsional menyertakan gambar. Audio ditransmisikan melalui AOQ Audio track. Jangan kirim input_audio_buffer.append.
Item
VAD mode
Manual mode
Turn boundary
Dideteksi oleh server_vad atau semantic_vad
Dikontrol oleh tombol atau status aplikasi
Session setting
turn_detection berisi pengaturan VAD
turn_detection bernilai null
Audio commit
Dilakukan secara otomatis oleh layanan
Client mengirim input_audio_buffer.commit
Response trigger
Dipicu secara otomatis oleh layanan
Client mengirim response.create
Image input
Continuous Video track atau gambar melalui Data track
Continuous Video track atau gambar melalui Data track
Aktifkan Model Studio dan ikuti Obtain and configure an API key. Simpan Kunci API hanya di server aplikasi Anda. Jangan sertakan dalam kode client atau commit ke repositori kode.
Unduh AOQ Client SDK versi terbaru sebagaimana dijelaskan dalam SDK download.
Bangun server aplikasi dan implementasikan autentikasi proxy sebagaimana dijelaskan dalam Token authentication. Sebelum setiap koneksi baru, client harus memperoleh kredensial koneksi baru dari server aplikasi.
Impor SDK untuk platform pengembangan Anda. Implementasi client menggunakan iOS Swift. Platform lain menyediakan antarmuka dan alur event yang sama. Tutorial ini menggunakan aliran audio PCM. Pengkodean Opus disediakan oleh plugin. Impor plugin Opus jika uplink menggunakan Opus.
Android
iOS
HarmonyOS
Linux (Python)
Letakkan AoqClientSdk-release.aar di app/libs, lalu konfigurasikan dependensi serta ABIs yang didukung SDK di app/build.gradle:
Minta izin RECORD_AUDIO dan CAMERA saat runtime sebelum perangkat terkait digunakan.
Tambahkan AoqClientSdk.framework ke proyek Xcode dan pilih Embed & Sign di Target > General > Frameworks, Libraries, and Embedded Content. SDK mendukung perangkat arm64 yang menjalankan iOS 13.0 atau lebih baru.
Tambahkan NSMicrophoneUsageDescription dan NSCameraUsageDescription ke Info.plist, lalu minta otorisasi sebelum perangkat terkait digunakan.
Gunakan import AoqClientSdk di Swift atau #import <AoqClientSdk/AoqClientSdk.h> di Objective-C.
Letakkan AoqClientSdk.har di entry/libs dan deklarasikan dependensi di entry/oh-package.json5. SDK kompatibel dengan API 12 dan mendukung arm64-v8a:
Sebelum perangkat terkait digunakan, panggil abilityAccessCtrl.createAtManager().requestPermissionsFromUser untuk meminta ohos.permission.MICROPHONE dan ohos.permission.CAMERA.
Ekstrak SDK dan simpan aoq_client_sdk.py, libAoqClientSdk.so, dan libonnxruntime.so.1.16.3 dalam direktori yang sama.
Tambahkan direktori SDK ke jalur pencarian Python dan pustaka dinamis:
Server aplikasi memperoleh parameter koneksi AOQ untuk qwen3.5-omni-plus-realtime dari URL token Realtime.
Client membuat engine dan mengonfigurasi codec audio serta track. Client juga mengonfigurasi Video track jika diperlukan pemahaman visual kontinu.
Client memulai capture dan pemutaran lokal, menonaktifkan pengiriman Audio-track secara default, terhubung ke AOQ, lalu mengirim session.update.
Setelah session.updated diterima, opsi continuous-video mengaktifkan Video track. Audio track tetap dinonaktifkan hingga pengguna menekan tombol bicara.
Saat pengguna menekan tombol, client mengaktifkan Audio track. Saat dilepas, client menonaktifkan Audio track, secara opsional mengirim gambar, lalu mengirim input_audio_buffer.commit dan response.create.
Setelah response.done diterima, giliran berikutnya dapat dimulai. Untuk mengakhiri, hentikan perangkat, putuskan koneksi, dan hapus engine.
Continuous Video track
Send an image over the Data track
Publikasikan Video track dan aktifkan pengiriman video setelah session.updated. Model terus-menerus melihat frame terbaru. Setiap giliran suara hanya perlu meng-commit audio dan meminta respons.
Jangan publikasikan Video track. Saat diperlukan gambar, kirim input_image_buffer.append setelah tombol dilepas, lalu commit audio dan minta respons.
Atur DASHSCOPE_API_KEY di server aplikasi dan kirim permintaan ke endpoint untuk wilayah yang dipilih. clientIp adalah Alamat IP publik aktual dari client. Field ini opsional, tetapi menentukannya membantu layanan mengalokasikan endpoint relay yang sesuai.
Jika server aplikasi tidak dapat memperoleh Alamat IP publik aktual dari client, abaikan clientIp daripada meneruskan string kosong.
Server aplikasi mengembalikan field respons berikut ke client. Jangan pernah mengembalikan Kunci API ke client di lingkungan produksi. Untuk semua field permintaan dan respons, lihat Token authentication.
Setelah client memperoleh AoqConnectConfig dari server aplikasi, ikuti langkah-langkah berikut untuk mengimplementasikan percakapan suara push-to-talk di iOS.
Buat engine AOQ singleton dan daftarkan objek aplikasi sebagai penerima callback. Tangani status koneksi, event server, error, dan peringatan dalam callback.
Copy
let createConfig = AoqCreateConfig()createConfig.workDir = workDirengine = AoqClientEngine.createEngine(createConfig, delegate: self)
Inisialisasi capture dan pemutaran audio. Mulai kamera hanya untuk opsi continuous-video. Peroleh izin mikrofon dan kamera sebelum metode ini dipanggil.
Konfigurasikan codec audio untuk model yang dipilih dan format audio aplikasi, serta pilih track untuk opsi input gambar. Nilai audio dan video berikut merupakan contoh. Sesuaikan dengan kebutuhan model dan skenario aplikasi. Nonaktifkan pengiriman Audio-track sebelum koneksi dibuat.
Continuous Video track
Send an image over the Data track
Konfigurasikan track publish Audio, Video, dan Data. Sesuaikan pengaturan pengkodean video untuk kualitas gambar dan bandwidth yang tersedia.
Konfigurasikan hanya track Audio dan Data. Jangan konfigurasi encoder video, yang menghindari capture, pengkodean, dan transmisi video kontinu. Nilai audio merupakan contoh.
Setelah koneksi dibuat, panggil sendDataMsg untuk mengirim event session.update. Atur turn_detection ke null dan pilih modalities voice, instruksi, dan output untuk aplikasi Anda. Pertahankan parameter audio contoh agar konsisten dengan pengaturan codec SDK. Untuk semua field, lihat Client events.
Tangani event session.updated dalam callback onDataMsg. Jangan kirim media hingga event ini diterima. Untuk opsi continuous-video, panggil enableSendMediaStream untuk mengaktifkan Video track pada titik ini, tetapi tetap nonaktifkan Audio track agar audio sebelum pengguna menekan tombol tidak masuk ke buffer input.
Copy
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.}
Saat tombol ditekan, panggil enableSendMediaStream untuk mengaktifkan Audio track. Saat dilepas, panggil enableSendMediaStream untuk menonaktifkan Audio track, pastikan giliran tersebut berisi audio, secara opsional kirim gambar, lalu panggil sendDataMsg untuk mengirim input_audio_buffer.commit diikuti response.create.
Copy
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}
Pemahaman visual kontinu dan pertanyaan gambar sesekali menggunakan konfigurasi track dan perilaku pengiriman yang berbeda. Pilih opsi berdasarkan bandwidth, konsumsi daya, dan desain interaksi.
Continuous Video track
Send a single image over the Data track
Gunakan opsi ini untuk panggilan video, adegan yang berubah cepat, atau konteks visual kontinu. Setelah Video track dipublikasikan, jangan kirim input_image_buffer.append.
Gunakan opsi ini untuk pertanyaan gambar atau gambar sesekali. Konversi gambar ke JPG atau JPEG, encode Base64, lalu kirim setelah tombol dilepas dan sebelum audio di-commit:
Gunakan resolusi 480p atau 720p untuk performa terbaik. Jangan melebihi 1080p.
Gambar yang diencode Base64 tidak boleh melebihi 256 KB. Pertahankan ukuran gambar sumber pada atau di bawah 190 KB dan sisakan ruang untuk wrapper JSON.
Audio track harus telah mengirim setidaknya satu frame audio dalam giliran sebelum gambar dikirim. Event input_audio_buffer.commit berikutnya akan meng-commit buffer audio dan gambar sekaligus.
Saat sesi berakhir, putuskan koneksi dan hapus engine. disconnect atau destroy secara otomatis menutup perangkat media, sehingga Anda tidak perlu memanggil metode stop secara terpisah. AoqClientEngine adalah singleton dan tidak dapat dibuat lagi hingga destroy dipanggil.
Kelas berikut menerima AoqConnectConfig yang dipetakan dari respons token server aplikasi. Tambahkan status UI, izin, pemulihan error, dan kompresi gambar di lingkungan produksi.
Copy
import Foundationimport AoqClientSdkfinal 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) {}}
Lengkapi satu giliran push-to-talk hanya audio dan satu giliran dengan gambar. Hasil yang diharapkan:
Audio track dinonaktifkan sebelum tombol ditekan dan mengirim audio secara kontinu selama tombol ditekan.
Setelah dilepas, input_audio_buffer.committed, response.created, dan response.done diterima secara berurutan, dan audio model diputar melalui Audio track yang berlangganan.
Dengan opsi single-image, model merespons menggunakan gambar dan audio dari giliran tersebut. Dengan video kontinu, model menggunakan frame video terbaru.
Untuk field event server dan skema respons lengkap, lihat Server events.