跳转到主要内容
Realtime API

使用 AOQ 接入 qwen-audio-3.0-tts-flash 实现语音合成

通过 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. 开通千问AI平台,并按获取与配置 API Key获取 API Key。API Key 只保存在业务 AppServer,不要写入客户端代码或提交到代码仓库。
  2. AOQ 接入地址为 maas.qianwenaiapi.com,协议与模型支持情况参见 Realtime API 概述。
  3. 从 SDK 下载获取最新版 AOQ Client SDK。
  4. 搭建业务 AppServer,并按 Token 鉴权实现服务端代理鉴权。每次建立新连接前,客户端都应从 AppServer 获取新的连接凭证。

导入 SDK

根据开发平台导入对应 SDK。后续客户端代码以 Android Java 为例,其他平台使用相同的接口设计和事件流程。本文以 Opus 音频流为例,请先按 SDK 下载文档导入对应插件。
  • Android
  • iOS
  • HarmonyOS
  • Linux (Python)
  1. 将 Opus 插件中的 libPluginOpus.so 按 ABI 放入 app/src/main/jniLibs/armeabi-v7a/ 和 app/src/main/jniLibs/arm64-v8a/,将 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. 本场景不需要申请麦克风或摄像头权限。

体验 Demo

千问AI平台提供适用于 Android 平台的 Demo,可用于快速验证 AOQ 接入效果。下载 APK 并配置 API Key 和 workspaceId 后,即可体验部分模型。 扫描以下二维码下载 Demo:
Demo 下载二维码

实现流程

  1. AppServer 通过 Inference Token 地址获取 qwen-audio-3.0-tts-flash 的 AOQ 连接参数。
  2. 客户端发布 Data 轨,订阅 Audio 和 Data 轨,并配置 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,并向接入地址 maas.qianwenaiapi.com 发送请求。clientIp 为客户端的真实公网 IP;该字段可选,但建议传入,以便服务分配合适的 Relay 接入点。
curl -X POST \
  "https://maas.qianwenaiapi.com/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 字段,不要传空字符串。
AppServer 将响应中的以下字段返回客户端。生产环境中不要把 API Key 返回客户端。完整请求和响应字段请参见 Token 鉴权。
响应字段SDK 字段
aoqTokenForClientAoqConnectConfig.token
sidAoqConnectConfig.sid
clientRelayCertFingerprintAoqConnectConfig.certFingerprint
clientRelayEndpointsAoqConnectConfig.relayEndpoints
extraInfo.workspaceIdHashAoqConnectConfig.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. 配置解码器、轨道并建立连接

配置 SDK 的音频解码参数,然后发布 Data 轨、订阅 Audio 和 Data 轨。以下数值仅为本教程的 Opus 示例配置。AoqConnectConfig 的连接字段由客户根据 AppServer Token 响应填写。
AoqClientEngine.AoqAudioCodecConfig audioDecoderConfig =
        new AoqClientEngine.AoqAudioCodecConfig();
audioDecoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioDecoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioOpus;
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.AoqEncoderTypeAudioOpus;
        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 后再断开。

相关文档

如需查看完整参数、事件字段或其他平台接口,请参见:
使用 AOQ 接入 qwen-audio-3.0-tts-flash 实现语音合成 - 千问AI平台