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 を送信するまでバッファリングされたままになります。このアプローチは、モバイルでの再生、分割された長文テキストの入力、および低レイテンシーの音声出力に適しています。
前提条件
- Alibaba Cloud Model Studio を有効化し、「APIキーの取得と設定」に従ってください。API キーはアプリケーションサーバーにのみ保存してください。クライアントコードに含めたり、コードリポジトリにコミットしたりしないでください。
- アプリケーションがデプロイされているリージョンの AOQ エンドポイントを確認してください。選択ガイダンスについては、「リージョン、デプロイ範囲、およびエンドポイントの選択」をご参照ください。
- 「SDKのダウンロード」で説明されているように、最新の AOQ クライアント SDK をダウンロードしてください。
- 「トークン認証」で説明されているように、アプリケーションサーバーを構築し、プロキシ認証を実装してください。新しい接続ごとに、クライアントはアプリケーションサーバーから新しい接続認証情報を取得する必要があります。
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 の絶対パスを指定することもできます。
デモの試用
Alibaba Cloud Model Studio の Android デモを使用すると、AOQ 接続を迅速に検証できます。APK をダウンロードし、API キーと workspaceId を設定して、選択したモデルを試してください。
次の QR コードをスキャンしてデモをダウンロードしてください:
実装フロー
- アプリケーションサーバーは、推論トークン URL から qwen-audio-3.0-tts-flash の AOQ 接続パラメーターを取得します。
- クライアントはデータトラックをパブリッシュし、オーディオトラックとデータトラックをサブスクライブし、run-task で選択した出力オーディオフォーマットに合わせて SDK デコーダーを設定します。
- クライアントはローカルプレーヤーを起動し、AOQ に接続します。接続が成功した後、新しい task_id を使用して run-task を送信します。
- task-started を受信した後、クライアントはアプリケーションが必要とするペースで 1 つ以上の continue-task テキストセグメントを送信します。
- すべてのテキストを送信した後、クライアントは finish-task を送信します。サービスは残りのオーディオを返し、最後に task-finished を返します。
- task-finished を受信した後、同じ AOQ 接続で新しい task_id を使用して別のタスクを開始するか、エンジンを切断して破棄します。
アプリケーションサーバーからのトークン取得
アプリケーションサーバーで 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();
}
}
実行と検証
- テキストは task-started を受信した後にのみ送信されます。
- 完全な文のオーディオは、オーディオトラックを介して継続的に再生されます。不完全な文は finish-task の後に合成されます。
- すべてのオーディオが完了した後に 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 を待機してください。 |
関連情報
すべてのパラメーター、イベントフィールド、および他のプラットフォームのインターフェースについては、以下をご参照ください: