全部產品
Search
文件中心

Alibaba Cloud Model Studio:使用 AOQ 接入 qwen-audio-3.0-tts-flash 實現語音合成

更新時間:Aug 26, 2026

通過 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。該方式適合移動端播報、長文本分段輸入和低延遲語音輸出。

準備工作

  1. 開通阿里雲百鍊,並按擷取與配置 API Key。API Key 只儲存在業務 AppServer,不要寫入用戶端代碼或提交到代碼倉庫。
  2. 根據業務部署地區確認 AOQ Endpoint。地區和接入地址的選擇方法請參見選擇地區、服務部署範圍和接入網域名稱
  3. SDK 下載擷取最新版 AOQ Client SDK。
  4. 搭建業務 AppServer,並按Token 鑒權實現服務端代理鑒權。每次建立新串連前,用戶端都應從 AppServer 擷取新的串連憑證。

匯入 SDK

根據開發平台匯入對應 SDK。後續用戶端代碼以 Android Java 為例,其他平台使用相同的介面設計和事件流程。本文以 PCM 音頻流為例;如果業務選擇 Opus,請按 SDK 下載文檔匯入對應外掛程式。

Android

  1. 將 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'])
}
  1. 在 AndroidManifest.xml 中聲明以下許可權:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  1. 本情境不需要申請麥克風或網路攝影機許可權。

iOS

  1. 將 AoqClientSdk.framework 拖入 Xcode 工程,在 Target > General > Frameworks, Libraries, and Embedded Content 中選擇 Embed & Sign。SDK 支援 iOS 13.0 及以上 arm64 裝置。
  2. 本情境不使用麥克風或網路攝影機,無需聲明對應許可權。
  3. Swift 工程使用 import AoqClientSdk;Objective-C 工程使用 #import <AoqClientSdk/AoqClientSdk.h>。

HarmonyOS

  1. 將 AoqClientSdk.har 放入 entry/libs,並在 entry/oh-package.json5 中聲明依賴。該 SDK 相容 API 12,支援 arm64-v8a:
{
  "dependencies": {
    "@aoq/client-sdk": "file:./libs/AoqClientSdk.har"
  }
}
  1. 在 entry/src/main/module.json5 中聲明以下許可權:
"requestPermissions": [
  { "name": "ohos.permission.INTERNET" }
]
  1. 本情境不需要申請麥克風或網路攝影機許可權。

Linux (Python)

  1. 解壓 SDK,並保持 aoq_client_sdk.py、libAoqClientSdk.so 和 libonnxruntime.so.1.16.3 位於同一目錄。
  2. 將 SDK 目錄加入 Python 和動態庫搜尋路徑:
export PYTHONPATH="$PWD/AoqClientSdk:$PYTHONPATH"
export LD_LIBRARY_PATH="$PWD/AoqClientSdk:$LD_LIBRARY_PATH"
  1. 在 Python 代碼中使用 import aoq_client_sdk。也可通過 AOQ_CLIENT_SDK_LIB 指定 libAoqClientSdk.so 的絕對路徑。

體驗 Demo

阿里雲百鍊提供適用於 Android 平台的 Demo,可用於快速驗證 AOQ 接入效果。下載 APK 並配置 API Key 和 workspaceId 後,即可體驗部分模型。

掃描以下二維碼下載 Demo:

Demo 下載二維碼

實現流程

  1. AppServer 通過 Inference Token 地址擷取 qwen-audio-3.0-tts-flash 的 AOQ 串連參數。
  2. 用戶端發布 Data 軌,訂閱 Audio 和 Data 軌,並按 run-task 中選擇的輸出音頻格式配置 SDK 解碼參數。
  3. 用戶端啟動本地播放器並建立 AOQ 串連;串連成功後使用新的 task_id 發送 run-task。
  4. 收到 task-started 後,按業務節奏發送一個或多個 continue-task 文本片段。
  5. 所有文本發送完成後發送 finish-task。服務端繼續返回剩餘音頻,最終返回 task-finished。
  6. 收到 task-finished 後,可在同一 AOQ 串連上使用新的 task_id 開始下一輪合成,或中斷連線並銷毀引擎。
AOQ 流式語音合成時序圖

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();
    }
}

運行並驗證

  1. 收到 task-started 後才提交文本。
  2. 完整語句的音頻通過 Audio 軌連續播放;不完整語句在 finish-task 後補充合成。
  3. 所有音頻完成後收到 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 後再斷開。

相關文檔

如需查看完整參數、事件欄位或其他平台介面,請參見: