通過 AOQ 接入 qwen-audio-3.0-tts-flash,分段發送文本並即時播放合成語音。用戶端代碼以 Android Java 為例。
方案概述
qwen-audio-3.0-tts-flash 支援 AOQ Inference 事件協議。本教程選擇該模型示範通過 AOQ 進行流式語音合成:用戶端通過 Data 軌發送 run-task、continue-task 和 finish-task,服務端通過 Audio 軌流式返迴音頻,並通過 Data 軌返回任務事件。
同一任務可以多次發送 continue-task。完整語句會儘快合成,不完整語句會暫存在服務端,直到後續文本補全或用戶端發送 finish-task。該方式適合移動端播報、長文本分段輸入和低延遲語音輸出。
準備工作
- 開通阿里雲百鍊,並按擷取與配置 API Key。API Key 只儲存在業務 AppServer,不要寫入用戶端代碼或提交到代碼倉庫。
- 根據業務部署地區確認 AOQ Endpoint。地區和接入地址的選擇方法請參見選擇地區、服務部署範圍和接入網域名稱。
- 從SDK 下載擷取最新版 AOQ Client SDK。
- 搭建業務 AppServer,並按Token 鑒權實現服務端代理鑒權。每次建立新串連前,用戶端都應從 AppServer 擷取新的串連憑證。
匯入 SDK
根據開發平台匯入對應 SDK。後續用戶端代碼以 Android Java 為例,其他平台使用相同的介面設計和事件流程。本文以 PCM 音頻流為例;如果業務選擇 Opus,請按 SDK 下載文檔匯入對應外掛程式。
Android
- 將 AoqClientSdk-release.aar 放入 app/libs,並在 app/build.gradle 中配置依賴和 SDK 支援的 ABI:
android {
defaultConfig {
minSdk 21
ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' }
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.aar'])
}
- 在 AndroidManifest.xml 中聲明以下許可權:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
- 本情境不需要申請麥克風或網路攝影機許可權。
iOS
- 將 AoqClientSdk.framework 拖入 Xcode 工程,在 Target > General > Frameworks, Libraries, and Embedded Content 中選擇 Embed & Sign。SDK 支援 iOS 13.0 及以上 arm64 裝置。
- 本情境不使用麥克風或網路攝影機,無需聲明對應許可權。
- Swift 工程使用 import AoqClientSdk;Objective-C 工程使用 #import <AoqClientSdk/AoqClientSdk.h>。
HarmonyOS
- 將 AoqClientSdk.har 放入 entry/libs,並在 entry/oh-package.json5 中聲明依賴。該 SDK 相容 API 12,支援 arm64-v8a:
{
"dependencies": {
"@aoq/client-sdk": "file:./libs/AoqClientSdk.har"
}
}
- 在 entry/src/main/module.json5 中聲明以下許可權:
"requestPermissions": [
{ "name": "ohos.permission.INTERNET" }
]
- 本情境不需要申請麥克風或網路攝影機許可權。
Linux (Python)
- 解壓 SDK,並保持 aoq_client_sdk.py、libAoqClientSdk.so 和 libonnxruntime.so.1.16.3 位於同一目錄。
- 將 SDK 目錄加入 Python 和動態庫搜尋路徑:
export PYTHONPATH="$PWD/AoqClientSdk:$PYTHONPATH"
export LD_LIBRARY_PATH="$PWD/AoqClientSdk:$LD_LIBRARY_PATH"
- 在 Python 代碼中使用 import aoq_client_sdk。也可通過 AOQ_CLIENT_SDK_LIB 指定 libAoqClientSdk.so 的絕對路徑。
體驗 Demo
阿里雲百鍊提供適用於 Android 平台的 Demo,可用於快速驗證 AOQ 接入效果。下載 APK 並配置 API Key 和 workspaceId 後,即可體驗部分模型。
掃描以下二維碼下載 Demo:
實現流程
- AppServer 通過 Inference Token 地址擷取 qwen-audio-3.0-tts-flash 的 AOQ 串連參數。
- 用戶端發布 Data 軌,訂閱 Audio 和 Data 軌,並按 run-task 中選擇的輸出音頻格式配置 SDK 解碼參數。
- 用戶端啟動本地播放器並建立 AOQ 串連;串連成功後使用新的 task_id 發送 run-task。
- 收到 task-started 後,按業務節奏發送一個或多個 continue-task 文本片段。
- 所有文本發送完成後發送 finish-task。服務端繼續返回剩餘音頻,最終返回 task-finished。
- 收到 task-finished 後,可在同一 AOQ 串連上使用新的 task_id 開始下一輪合成,或中斷連線並銷毀引擎。
AppServer 擷取 Token
在 AppServer 設定 DASHSCOPE_API_KEY,並使用所選地區的 Endpoint 發送請求。clientIp 為用戶端的真實公網 IP;該欄位可選,但建議傳入,以便服務分配合適的 Relay 存取點。
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}\"}"
說明如果 AppServer 無法擷取用戶端真實公網 IP,請刪除 clientIp 欄位,不要傳Null 字元串。
AppServer 將響應中的以下欄位返回用戶端。生產環境中不要把 API Key 返回用戶端。完整請求和響應欄位請參見Token 鑒權。
響應欄位 | SDK 欄位 |
aoqTokenForClient | AoqConnectConfig.token |
sid | AoqConnectConfig.sid |
clientRelayCertFingerprint | AoqConnectConfig.certFingerprint |
clientRelayEndpoints | AoqConnectConfig.relayEndpoints |
extraInfo.workspaceIdHash | AoqConnectConfig.workspaceIdHash |
實現 Android 用戶端
用戶端從 AppServer 擷取 AoqConnectConfig 後,按以下步驟實現 Android 端流式語音合成。
1. 建立引擎並設定回調
建立 AOQ 單例引擎並註冊串連與 Data 軌事件回調。客戶需要在串連狀態回調中維護可用狀態,並把任務事件交給業務狀態機器。
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. 啟動音頻播放
TTS 情境不採集麥克風,只需初始化本地播放器。客戶可以選擇預設使用擴音器或耳機;服務端 Audio 軌音頻由 SDK 自動播放。
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
3. 配置解碼器、軌道並建立串連
按 run-task 中選擇的輸出音頻格式配置 SDK 解碼參數,然後發布 Data 軌、訂閱 Audio 和 Data 軌。以下數值僅為本教程的 PCM 樣本配置。AoqConnectConfig 的串連欄位由客戶根據 AppServer Token 響應填寫。
AoqClientEngine.AoqAudioCodecConfig audioDecoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioDecoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioDecoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioDecoderConfig.sampleRate = 24000; // 樣本值,應與 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. 調用 sendDataMsg 發送 run-task 事件
串連成功後為本輪產生新的 UUID task_id,並配置模型、音色、文本類型、音頻格式和採樣率。其他選擇性參數請參見用戶端事件。
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. 調用 sendDataMsg 發送 continue-task 事件
只能在收到 task-started 後發送 continue-task。同一任務可連續發送多個片段;單次最多 20,000 個字元,累計最多 200,000 個字元。客戶應及時發送後續片段或結束任務,不要依賴固定的連線逾時秒數。
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. 處理服務端事件
在 onDataMsg 中讀取 header.event,維護任務狀態並處理失敗。result-generated 只表示句子已合成,音頻仍通過 Audio 軌返回。完整欄位請參見服務端事件。
JSONObject header = event.optJSONObject("header");
if (header == null) return;
String name = header.optString("event");
if ("task-started".equals(name)) {
// 可以發送一個或多個 continue-task 事件。
} else if ("result-generated".equals(name)) {
// 一個語句已合成,音頻通過 Audio 軌返回。
} else if ("task-finished".equals(name)) {
taskActive = false;
} else if ("task-failed".equals(name)) {
taskActive = false;
String message = header.optString("error_message");
// 展示或記錄錯誤。
}
7. 調用 sendDataMsg 發送 finish-task 事件
發送完全部文本後立即發送 finish-task,以合成服務端緩衝的不完整語句,並等待 task-finished。詳細規則請參見用戶端事件。
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. 中斷連線並銷毀引擎
不要在發送 finish-task 後立即斷開。收到 task-finished 或 task-failed 後,如不再發起下一輪任務,再中斷連線並銷毀引擎。SDK 會自動關閉音頻播放器。
engine.disconnect();
AoqClientEngine.destroy();
主要服務端事件
事件 | 說明 |
task-started | 任務已啟動,可以發送 continue-task |
result-generated | 一個完整語句已合成,對應音頻通過 Audio 軌返回 |
task-finished | 所有緩衝文本已處理,任務結束 |
task-failed | 任務失敗,應讀取錯誤碼和錯誤訊息 |
完整樣本
以下類接收由 AppServer Token 響應轉換完成的 AoqConnectConfig。串連成功後調用 synthesize(text, voice);生產代碼還需補充許可權、UI 狀態和重連邏輯。
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);
// 樣本參數,應與 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("The connection is not ready or a task is active.");
}
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("Failed to create 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("Failed to create 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("Failed to create finish-task", e);
}
}
public void close() {
engine.disconnect();
AoqClientEngine.destroy();
}
}
運行並驗證
- 收到 task-started 後才提交文本。
- 完整語句的音頻通過 Audio 軌連續播放;不完整語句在 finish-task 後補充合成。
- 所有音頻完成後收到 task-finished;隨後可以使用新的 task_id 開始下一輪。
典型情境
同一串連多次合成
收到 task-finished 後,可在同一 AOQ 串連上使用新的 task_id 再次發送 run-task,無需重新申請 Token;如果串連已斷開,則必須擷取新的串連憑證。
切換音色
每個 run-task 都可以通過 parameters.voice 選擇系統音色或有效 voice_id,因此可在同一串連的不同任務間切換音色。
擴音器或耳機
通過 AoqAudioPlaybackConfig.isDefaultSpeaker 設定預設輸出裝置;運行中可調用 enableSpeakerphone 切換。
常見問題
問題 | 處理方法 |
串連成功但任務不啟動 | 確認通過 Inference Token 地址擷取憑證,並檢查 run-task 的模型名、task_id 和 Data 軌發布配置。 |
continue-task 被拒絕 | 等待 task-started 後再發送,並確保 run-task、continue-task、finish-task 使用同一個 task_id。 |
任務成功但沒有聲音 | 確認已訂閱 Audio 軌並啟動播放器,同時檢查 SDK 解碼配置是否與 run-task 中選擇的輸出音頻格式一致。 |
末尾文本沒有音頻 | 所有文本發送完畢後必鬚髮送 finish-task,並等待剩餘音頻和 task-finished 後再斷開。 |
相關文檔
如需查看完整參數、事件欄位或其他平台介面,請參見: