すべてのプロダクト
Search
ドキュメントセンター

:AOQ を介した qwen-audio-3.0-tts-flash での音声合成

最終更新日:Sep 02, 2026

AOQ を使用して qwen-audio-3.0-tts-flash に接続し、テキストをセグメントに分割して送信し、合成された音声をリアルタイムで再生します。クライアントコードには Android Java を使用します。

ソリューションの概要

qwen-audio-3.0-tts-flash は、AOQ 推論イベントプロトコルをサポートしています。このチュートリアルでは、このモデルを使用して、AOQ を介したストリーミング音声合成をデモンストレーションします。クライアントは、データトラックを介して run-task、continue-task、finish-task を送信します。サービスは、オーディオトラックを介してオーディオをストリーミングし、データトラックを介してタスクイベントを返します。

1 つのタスクには、複数の continue-task イベントを含めることができます。完全な文は迅速に合成されます。不完全な文は、後続のテキストによって完全な文になるか、クライアントが finish-task を送信するまでバッファリングされたままになります。このアプローチは、モバイルでの再生、分割された長文テキストの入力、および低レイテンシーの音声出力に適しています。

前提条件

  1. Alibaba Cloud Model Studio を有効化し、「APIキーの取得と設定」に従ってください。API キーはアプリケーションサーバーにのみ保存してください。クライアントコードに含めたり、コードリポジトリにコミットしたりしないでください。
  2. アプリケーションがデプロイされているリージョンの AOQ エンドポイントを確認してください。選択ガイダンスについては、「リージョン、デプロイ範囲、およびエンドポイントの選択」をご参照ください。
  3. 「SDKのダウンロード」で説明されているように、最新の AOQ クライアント SDK をダウンロードしてください。
  4. 「トークン認証」で説明されているように、アプリケーションサーバーを構築し、プロキシ認証を実装してください。新しい接続ごとに、クライアントはアプリケーションサーバーから新しい接続認証情報を取得する必要があります。

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 の絶対パスを指定することもできます。

デモの試用

Alibaba Cloud Model Studio の Android デモを使用すると、AOQ 接続を迅速に検証できます。APK をダウンロードし、API キーと workspaceId を設定して、選択したモデルを試してください。

次の QR コードをスキャンしてデモをダウンロードしてください:

QR code for downloading the demo

実装フロー

  1. アプリケーションサーバーは、推論トークン URL から qwen-audio-3.0-tts-flash の AOQ 接続パラメーターを取得します。
  2. クライアントはデータトラックをパブリッシュし、オーディオトラックとデータトラックをサブスクライブし、run-task で選択した出力オーディオフォーマットに合わせて SDK デコーダーを設定します。
  3. クライアントはローカルプレーヤーを起動し、AOQ に接続します。接続が成功した後、新しい task_id を使用して run-task を送信します。
  4. task-started を受信した後、クライアントはアプリケーションが必要とするペースで 1 つ以上の continue-task テキストセグメントを送信します。
  5. すべてのテキストを送信した後、クライアントは finish-task を送信します。サービスは残りのオーディオを返し、最後に task-finished を返します。
  6. task-finished を受信した後、同じ AOQ 接続で新しい task_id を使用して別のタスクを開始するか、エンジンを切断して破棄します。
Sequence diagram for streaming speech synthesis over AOQ

アプリケーションサーバーからのトークン取得

アプリケーションサーバーで DASHSCOPE_API_KEY を設定し、選択したリージョンのエンドポイントにリクエストを送信してください。clientIp は、クライアントの実際のパブリック IP アドレスです。このフィールドはオプションですが、指定すると、サービスが適切なリレーエンドポイントを割り当てるのに役立ちます。

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}\"}"

注記アプリケーションサーバーがクライアントの実際のパブリック IP アドレスを取得できない場合は、空の文字列を渡すのではなく、clientIp を省略してください。

アプリケーションサーバーは、次の応答フィールドをクライアントに返します。本番環境では、API キーをクライアントに返さないでください。すべてのリクエストフィールドと応答フィールドについては、「トークン認証」をご参照ください。

応答フィールド

SDKフィールド

aoqTokenForClient

AoqConnectConfig.token

sid

AoqConnectConfig.sid

clientRelayCertFingerprint

AoqConnectConfig.certFingerprint

clientRelayEndpoints

AoqConnectConfig.relayEndpoints

extraInfo.workspaceIdHash

AoqConnectConfig.workspaceIdHash

Androidクライアントの実装

クライアントがアプリケーションサーバーから AoqConnectConfig を取得した後、次の手順に従って Android でストリーミング音声合成を実装してください。

1. エンジンの作成とコールバックの登録

シングルトン AOQ エンジンを作成し、接続イベントとデータトラックイベントのコールバックを登録します。接続コールバックで接続準備状態を維持し、タスクイベントをアプリケーションのステートマシンに渡します。

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 はマイクのオーディオをキャプチャしません。ローカルプレーヤーのみを初期化します。スピーカーまたはイヤーピースをデフォルトの出力として選択します。SDK は、オーディオトラックからサーバーのオーディオを自動的に再生します。

AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
        new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);

3. デコーダーとトラックの設定、および接続

run-task で選択した出力オーディオフォーマットに合わせて SDK デコーダーを設定します。次に、データトラックをパブリッシュし、オーディオトラックとデータトラックをサブスクライブします。次の値は、このチュートリアルの PCM の例です。アプリケーションサーバーのトークン応答から AoqConnectConfig フィールドに値を入力します。

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 を送信してください。1 つのタスクには複数のセグメントを含めることができます。各イベントは最大 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 は文が合成されたことを示しますが、オーディオは引き続きオーディオトラックを介して返されます。すべてのフィールドについては、「サーバーイベント」をご参照ください。

JSONObject header = event.optJSONObject("header");
if (header == null) return;
String name = header.optString("event");
if ("task-started".equals(name)) {
    // これで、アプリケーションは 1 つ以上の continue-task イベントを送信できます。
} else if ("result-generated".equals(name)) {
    // 文が合成されました。オーディオはオーディオトラックを介して配信されます。
} 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

完全な文が合成され、そのオーディオがオーディオトラックを介して返されます

task-finished

バッファリングされたすべてのテキストが処理され、タスクは完了です

task-failed

タスクが失敗しました。エラーコードとメッセージを読み取ってください

完全な例

次のクラスは、アプリケーションサーバーのトークン応答からマッピングされた 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. 完全な文のオーディオは、オーディオトラックを介して継続的に再生されます。不完全な文は finish-task の後に合成されます。
  3. すべてのオーディオが完了した後に task-finished が受信されます。その後、新しい task_id を使用して別のタスクを開始できます。

適用シナリオ

1つの接続での複数タスク

task-finished を受信した後、同じ AOQ 接続で新しい task_id を使用して別の run-task を送信できます。接続がアクティブな間は、新しいトークンは必要ありません。接続が閉じられた場合は、新しい接続認証情報を取得してください。

ボイスの変更

各 run-task では、parameters.voice でシステムボイスまたは有効な voice_id を選択できます。そのため、同じ接続でタスク間でボイスを変更できます。

スピーカーまたはイヤーピース

AoqAudioPlaybackConfig.isDefaultSpeaker を使用してデフォルトの出力を設定し、接続がアクティブな間に enableSpeakerphone を呼び出して切り替えることができます。

トラブルシューティング

問題

解決策

接続は成功するが、タスクが開始されない

認証情報が推論トークン URL から取得されていることを確認し、run-task のモデル名、task_id、およびデータトラックがパブリッシュされていることを確認してください。

continue-taskが拒否される

task-started を待機し、run-task、continue-task、および finish-task で同じ task_id を使用してください。

タスクは成功するが、オーディオが再生されない

オーディオトラックがサブスクライブされていること、およびプレーヤーが実行中であることを確認し、SDK デコーダーが run-task で選択した出力オーディオフォーマットと一致することを確認してください。

最後のテキストにオーディオがない

すべてのテキストを送信した後に finish-task を送信し、切断する前に残りのオーディオと task-finished を待機してください。

関連情報

すべてのパラメーター、イベントフィールド、および他のプラットフォームのインターフェースについては、以下をご参照ください: