Gunakan AOQ untuk terhubung ke qwen-audio-3.0-tts-flash, kirim teks dalam segmen, dan putar suara hasil sintesis secara real time. Kode klien menggunakan Android Java.
Ikhtisar solusi
qwen-audio-3.0-tts-flash mendukung protokol event Inferensi AOQ. Tutorial ini menggunakan model tersebut untuk menunjukkan sintesis suara streaming melalui AOQ. Klien mengirim run-task, continue-task, dan finish-task melalui track Data. Layanan melakukan streaming audio melalui track Audio dan mengembalikan event tugas melalui track Data.
Satu tugas dapat berisi beberapa event continue-task. Kalimat lengkap langsung disintesis, sedangkan kalimat yang belum lengkap tetap di-buffer hingga teks berikutnya melengkapinya atau klien mengirim finish-task. Pendekatan ini cocok untuk pemutaran di perangkat mobile, input teks panjang yang tersegmentasi, dan output suara berlatensi rendah.
Prasyarat
- Aktifkan Model Studio dan ikuti Mendapatkan dan mengonfigurasi Kunci API. Simpan Kunci API hanya di server aplikasi Anda. Jangan sertakan dalam kode klien atau commit ke repositori kode.
- Konfirmasi titik akhir AOQ untuk wilayah tempat aplikasi Anda diterapkan. Untuk panduan pemilihan, lihat Memilih wilayah, cakupan penerapan, dan titik akhir.
- Unduh AOQ Client SDK versi terbaru seperti dijelaskan dalam Unduhan SDK.
- Buat server aplikasi dan terapkan otentikasi proxy seperti dijelaskan dalam Otentikasi token. Sebelum setiap koneksi baru, klien harus memperoleh kredensial koneksi baru dari server aplikasi.
Impor SDK
Impor SDK untuk platform pengembangan Anda. Implementasi klien menggunakan Android Java. Platform lain menyediakan antarmuka dan alur event yang sama. Tutorial ini menggunakan aliran audio PCM. Jika aplikasi Anda memilih Opus, impor plugin yang sesuai seperti dijelaskan dalam topik unduhan SDK.
Android
- Letakkan AoqClientSdk-release.aar di app/libs, lalu konfigurasikan dependensi dan ABI yang didukung SDK di app/build.gradle:
android {
defaultConfig {
minSdk 21
ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}
- Deklarasikan izin berikut di AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- Skenario ini tidak memerlukan izin mikrofon atau kamera.
iOS
- Tambahkan AoqClientSdk.framework ke proyek Xcode dan pilih Embed & Sign di bawah Target > General > Frameworks, Libraries, and Embedded Content. SDK mendukung perangkat arm64 yang menjalankan iOS 13.0 atau lebih baru.
- Skenario ini tidak menggunakan mikrofon atau kamera dan tidak memerlukan izin terkait.
- Gunakan import AoqClientSdk di Swift atau #import <AoqClientSdk/AoqClientSdk.h> di Objective-C.
HarmonyOS
- Letakkan AoqClientSdk.har di entry/libs dan deklarasikan dependensi di entry/oh-package.json5. SDK kompatibel dengan API 12 dan mendukung arm64-v8a:
{
"dependencies": {
"@aoq/client-sdk": "file:./libs/AoqClientSdk.har"
}
}
- Deklarasikan izin berikut di entry/src/main/module.json5:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
- Skenario ini tidak memerlukan izin mikrofon atau kamera.
Linux (Python)
- 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:
export PYTHONPATH="$PWD/AoqClientSdk:$PYTHONPATH"
export LD_LIBRARY_PATH="$PWD/AoqClientSdk:$LD_LIBRARY_PATH"
- Gunakan import aoq_client_sdk di Python. Anda juga dapat menentukan jalur mutlak libAoqClientSdk.so dengan menggunakan AOQ_CLIENT_SDK_LIB.
Coba demo
Gunakan demo Android dari Alibaba Cloud Model Studio untuk memverifikasi konektivitas AOQ dengan cepat. Unduh APK dan konfigurasikan Kunci API serta workspaceId untuk mencoba model yang dipilih.
Pindai kode QR berikut untuk mengunduh demo:
Alur implementasi
- Server aplikasi memperoleh parameter koneksi AOQ untuk qwen-audio-3.0-tts-flash dari URL token Inferensi.
- Klien menerbitkan track Data, berlangganan track Audio dan Data, serta mengonfigurasi decoder SDK sesuai format audio output yang dipilih di run-task.
- Klien memulai pemutar lokal dan terhubung ke AOQ. Setelah koneksi berhasil, klien mengirim run-task dengan task_id baru.
- Setelah task-started diterima, klien mengirim satu atau beberapa segmen teks continue-task sesuai kecepatan yang dibutuhkan aplikasi.
- Setelah semua teks dikirim, klien mengirim finish-task. Layanan mengembalikan sisa audio dan akhirnya mengirim task-finished.
- Setelah task-finished diterima, mulai tugas lain melalui koneksi AOQ yang sama dengan task_id baru, atau putuskan koneksi dan hapus engine.
Memperoleh token dari server aplikasi
Atur DASHSCOPE_API_KEY di server aplikasi dan kirim permintaan ke titik akhir untuk wilayah yang dipilih. clientIp adalah Alamat IP publik aktual klien. Field ini opsional, tetapi menentukannya membantu layanan mengalokasikan titik akhir relay yang sesuai.
curl -X POST \
"https://{endpoint}/api/v1/webrtc/inference?model=qwen-audio-3.0-tts-flash" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \
-H "x-dashscope-rtc-transport: moq" \
-d "{\"clientIp\": \"${CLIENT_REAL_IP}\"}"
CatatanJika server aplikasi tidak dapat memperoleh Alamat IP publik aktual klien, abaikan clientIp daripada mengirim string kosong.
Server aplikasi mengembalikan field respons berikut ke klien. Jangan pernah mengembalikan Kunci API ke klien di lingkungan produksi. Untuk semua field permintaan dan respons, lihat Otentikasi token.
Field respons | Bidang SDK |
aoqTokenForClient | AoqConnectConfig.token |
sid | AoqConnectConfig.sid |
clientRelayCertFingerprint | AoqConnectConfig.certFingerprint |
clientRelayEndpoints | AoqConnectConfig.relayEndpoints |
extraInfo.workspaceIdHash | AoqConnectConfig.workspaceIdHash |
Implementasi client Android
Setelah klien memperoleh AoqConnectConfig dari server aplikasi, ikuti langkah-langkah berikut untuk mengimplementasikan sintesis suara streaming di Android.
1. Buat engine dan daftarkan callback
Buat engine AOQ singleton dan daftarkan callback untuk status koneksi dan event track Data. Pertahankan kesiapan koneksi di callback koneksi dan teruskan event tugas ke mesin keadaan aplikasi.
AoqClientListener listener = new AoqClientListener() {
@Override
public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
connected = status == AoqClientEngine.AoqConnectionStatus
.AoqConnectionStatusConnected;
}
@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. Mulai pemutaran audio
TTS tidak merekam audio mikrofon. Inisialisasi hanya pemutar lokal. Pilih speaker atau earpiece sebagai output default. SDK secara otomatis memutar audio server dari track Audio.
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
3. Konfigurasi decoder dan track lalu hubungkan
Konfigurasi decoder SDK sesuai format audio output yang dipilih di run-task. Lalu terbitkan track Data dan berlangganan track Audio dan Data. Nilai berikut adalah contoh PCM untuk tutorial ini. Isi field AoqConnectConfig dari respons token server aplikasi.
AoqClientEngine.AoqAudioCodecConfig audioDecoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioDecoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioDecoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioDecoderConfig.sampleRate = 24000; // Contoh. Sesuaikan dengan run-task.sample_rate.
audioDecoderConfig.channel = 1;
engine.setAudioDecoderConfig(audioDecoderConfig);
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.connect(connectConfig);
4. Panggil sendDataMsg untuk mengirim event run-task
Setelah koneksi berhasil, buat UUID task_id baru dan konfigurasikan model, voice, jenis teks, format audio, dan laju sampel. Untuk parameter opsional, lihat Event klien.
taskId = UUID.randomUUID().toString();
JSONObject header = new JSONObject()
.put("action", "run-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject parameters = new JSONObject()
.put("text_type", "PlainText")
.put("voice", voice)
.put("format", "pcm")
.put("sample_rate", 24000);
JSONObject payload = new JSONObject()
.put("task_group", "audio")
.put("task", "tts")
.put("function", "SpeechSynthesizer")
.put("model", "qwen-audio-3.0-tts-flash")
.put("input", new JSONObject())
.put("parameters", parameters);
JSONObject runTask = new JSONObject().put("header", header).put("payload", payload);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = runTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
5. Panggil sendDataMsg untuk mengirim event continue-task
Kirim continue-task hanya setelah task-started diterima. Satu tugas dapat berisi beberapa segmen. Setiap event mendukung hingga 20.000 karakter, dan satu tugas mendukung hingga total 200.000 karakter. Kirim segmen berikutnya atau selesaikan tugas segera. Jangan bergantung pada nilai timeout koneksi tetap.
JSONObject continueHeader = new JSONObject()
.put("action", "continue-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject payload = new JSONObject()
.put("input", new JSONObject().put("text", text));
JSONObject continueTask = new JSONObject()
.put("header", continueHeader)
.put("payload", payload);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = continueTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
6. Tangani event server
Di onDataMsg, baca header.event untuk mempertahankan status tugas dan menangani kegagalan. result-generated menunjukkan bahwa satu kalimat telah disintesis, sedangkan audio tetap dikembalikan melalui track Audio. Untuk semua field, lihat Event server.
JSONObject header = event.optJSONObject("header");
if (header == null) return;
String name = header.optString("event");
if ("task-started".equals(name)) {
// Aplikasi kini dapat mengirim satu atau beberapa event continue-task.
} else if ("result-generated".equals(name)) {
// Satu kalimat telah disintesis. Audio dikirimkan melalui track Audio.
} else if ("task-finished".equals(name)) {
taskActive = false;
} else if ("task-failed".equals(name)) {
taskActive = false;
String message = header.optString("error_message");
// Tampilkan atau catat error tersebut.
}
7. Panggil sendDataMsg untuk mengirim event finish-task
Segera setelah semua teks dikirim, kirim finish-task untuk mensintesis teks yang belum lengkap yang di-buffer oleh layanan, lalu tunggu task-finished. Untuk detailnya, lihat Event klien.
JSONObject finishHeader = new JSONObject()
.put("action", "finish-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject finishTask = new JSONObject()
.put("header", finishHeader)
.put("payload", new JSONObject().put("input", new JSONObject()));
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = finishTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
8. Putuskan koneksi dan hapus engine
Jangan putuskan koneksi segera setelah finish-task dikirim. Setelah task-finished atau task-failed diterima, putuskan koneksi dan hapus engine jika tidak ada tugas lanjutan yang akan dimulai. SDK secara otomatis menutup pemutar audio.
engine.disconnect();
AoqClientEngine.destroy();
Event server utama
Event | Deskripsi |
task-started | Tugas telah dimulai dan continue-task dapat dikirim |
result-generated | Satu kalimat lengkap telah disintesis dan audionya dikembalikan melalui track Audio |
task-finished | Semua teks yang di-buffer telah diproses dan tugas selesai |
task-failed | Tugas gagal. Baca kode kesalahan dan pesannya |
Contoh lengkap
Kelas berikut menerima AoqConnectConfig yang dipetakan dari respons token server aplikasi. Setelah koneksi berhasil, panggil synthesize(text, voice). Tambahkan izin, status UI, dan logika rekoneksi di lingkungan produksi.
import android.content.Context;
import com.alibaba.aoq.clientsdk.AoqClientEngine;
import com.alibaba.aoq.clientsdk.AoqClientListener;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
public final class TtsClient {
private AoqClientEngine engine;
private String taskId;
private String pendingText;
private String pendingVoice;
private boolean connected;
private boolean taskActive;
public TtsClient(Context context, AoqClientEngine.AoqConnectConfig connectConfig) {
AoqClientListener listener = new AoqClientListener() {
@Override
public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
if (status == AoqClientEngine.AoqConnectionStatus.AoqConnectionStatusConnected) {
connected = true;
} else if (status == AoqClientEngine.AoqConnectionStatus
.AoqConnectionStatusDisconnected) {
connected = false;
}
}
@Override
public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
try {
JSONObject event = new JSONObject(
new String(msg.data, StandardCharsets.UTF_8));
String eventName = event.optJSONObject("header") == null
? "" : event.optJSONObject("header").optString("event");
if ("task-started".equals(eventName)) {
sendContinueTask();
sendFinishTask();
} else if ("task-finished".equals(eventName)
|| "task-failed".equals(eventName)) {
taskActive = false;
}
} 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);
// Nilai contoh. Sesuaikan pengaturan ini dengan format audio output di run-task.
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.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
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.connect(connectConfig);
}
public void synthesize(String text, String voice) {
if (!connected || taskActive) {
throw new IllegalStateException("Koneksi belum siap atau tugas sedang aktif.");
}
taskId = UUID.randomUUID().toString();
pendingText = text;
pendingVoice = voice;
taskActive = true;
sendRunTask();
}
private void sendRunTask() {
try {
JSONObject header = new JSONObject()
.put("action", "run-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject parameters = new JSONObject()
.put("text_type", "PlainText")
.put("voice", pendingVoice)
.put("format", "pcm")
.put("sample_rate", 24000);
JSONObject payload = new JSONObject()
.put("task_group", "audio")
.put("task", "tts")
.put("function", "SpeechSynthesizer")
.put("model", "qwen-audio-3.0-tts-flash")
.put("input", new JSONObject())
.put("parameters", parameters);
JSONObject runTask = new JSONObject().put("header", header).put("payload", payload);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = runTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
} catch (JSONException e) {
throw new IllegalStateException("Gagal membuat run-task", e);
}
}
private void sendContinueTask() {
try {
JSONObject header = new JSONObject()
.put("action", "continue-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject payload = new JSONObject()
.put("input", new JSONObject().put("text", pendingText));
JSONObject continueTask = new JSONObject()
.put("header", header)
.put("payload", payload);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = continueTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
} catch (JSONException e) {
throw new IllegalStateException("Gagal membuat continue-task", e);
}
}
private void sendFinishTask() {
try {
JSONObject header = new JSONObject()
.put("action", "finish-task")
.put("task_id", taskId)
.put("streaming", "duplex");
JSONObject finishTask = new JSONObject()
.put("header", header)
.put("payload", new JSONObject().put("input", new JSONObject()));
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = finishTask.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
} catch (JSONException e) {
throw new IllegalStateException("Gagal membuat finish-task", e);
}
}
public void close() {
engine.disconnect();
AoqClientEngine.destroy();
}
}
Jalankan dan verifikasi
- Teks hanya dikirim setelah task-started diterima.
- Audio untuk kalimat lengkap diputar terus-menerus melalui track Audio. Kalimat yang belum lengkap disintesis setelah finish-task.
- task-finished diterima setelah semua audio selesai. Tugas lain kemudian dapat dimulai dengan task_id baru.
Skenario umum
Beberapa tugas melalui satu koneksi
Setelah task-finished diterima, kirim run-task lain dengan task_id baru melalui koneksi AOQ yang sama. Tidak diperlukan token baru selama koneksi tetap aktif. Jika koneksi ditutup, peroleh kredensial koneksi baru.
Ubah suara
Setiap run-task dapat memilih voice sistem atau voice_id yang valid di parameters.voice. Oleh karena itu, Anda dapat mengubah voice antar tugas melalui koneksi yang sama.
Speaker atau earpiece
Atur output default menggunakan AoqAudioPlaybackConfig.isDefaultSpeaker, dan panggil enableSpeakerphone untuk beralih saat koneksi aktif.
Pemecahan masalah
Masalah | Solusi |
Koneksi berhasil tetapi tugas tidak dimulai | Pastikan kredensial diperoleh dari URL token Inferensi, dan periksa nama model, task_id, serta publikasi track Data di run-task. |
continue-task ditolak | Tunggu task-started, dan gunakan task_id yang sama di run-task, continue-task, dan finish-task. |
Tugas berhasil tetapi tidak ada audio yang diputar | Pastikan track Audio telah berlangganan dan pemutar sedang berjalan, serta verifikasi bahwa decoder SDK sesuai dengan format audio output yang dipilih di run-task. |
Teks terakhir tidak memiliki audio | Kirim finish-task setelah semua teks dikirim, dan tunggu sisa audio serta task-finished sebelum memutus koneksi. |
Informasi terkait
Untuk semua parameter, field event, dan antarmuka untuk platform lain, lihat: