Skip to main content

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

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. Qwen-Audio-TTS の AOQ 認証手順に従って AppServer を構築します。新しい接続のたびに、クライアントは AppServer から新しい接続認証情報を取得する必要があります。

SDKのインポート

開発プラットフォーム用の SDK をインポートしてください。クライアントの実装には Android Java を使用します。他のプラットフォームでも同じインターフェースとイベントフローが提供されます。このチュートリアルでは PCM オーディオストリームを使用します。アプリケーションが Opus を選択した場合は、「SDKのダウンロード」のトピックで説明されているように、対応するプラグインをインポートしてください。
  • Android
  • iOS
  • HarmonyOS
  • Linux (Python)
  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. このシナリオでは、マイクやカメラの権限は必要ありません。

デモの試用

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 を省略してください。
AppServer は、レスポンスの次のフィールドをクライアントへ返します。API キーは AppServer にのみ保存してください。

応答フィールド

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 を待機してください。

関連情報

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