AOQ を使用して qwen-audio-3.0-realtime-plus に接続し、サーバーサイド VAD を使用して低遅延のリアルタイム音声会話を構築します。クライアントコードは Android Java を使用します。
ソリューションの概要
Qwen-Audio は、音声アシスタント、カスタマーサービス、AI コンパニオンなどの低遅延シナリオ向けのエンドツーエンドのリアルタイム音声対話モデルです。AOQ は、音声とイベントを別々のトラックで転送します。音声トラックはマイクからの PCM アップリンクとモデルからの PCM ダウンリンクを伝送し、データトラックはリアルタイムのプロトコルイベントを伝送します。
このチュートリアルでは、server_vad を使用します。クライアントは継続的に音声を送信し、サービスはユーザーが話し始めたときと話し終えたときを検出し、応答をトリガーします。
前提条件
- Model Studio をアクティブ化し、「API キーの取得と設定」を参照してください。API キーはアプリケーションサーバーにのみ保存してください。クライアントコードに含めたり、コードリポジトリにコミットしたりしないでください。
- アプリケーションがデプロイされているリージョンの AOQ エンドポイントを確認してください。選択ガイダンスについては、「リージョン、デプロイスコープ、エンドポイントの選択」をご参照ください。
- 「SDK のダウンロード」で説明されているように、最新の AOQ クライアント SDK をダウンロードしてください。
- 「トークン認証」で説明されているように、アプリケーションサーバーを構築し、プロキシ認証を実装してください。新しい接続ごとに、クライアントはアプリケーションサーバーから新しい接続認証情報を取得する必要があります。
SDK のインポート
開発プラットフォーム用の SDK をインポートしてください。クライアントの実装には Android Java を使用します。他のプラットフォームも同じインターフェースとイベントフローを提供します。このチュートリアルでは PCM 音声ストリームを使用します。Opus エンコーディングはプラグインによって提供されます。アップリンクで Opus を使用する場合は、Opus プラグインをインポートしてください。
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" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
- 対応するデバイスを使用する前に、ランタイムで RECORD_AUDIO 権限を要求してください。
iOS
- AoqClientSdk.framework を Xcode プロジェクトに追加し、[Target] > [General] > [Frameworks, Libraries, and Embedded Content] で [Embed & Sign] を選択してください。SDK は、iOS 13.0 以降を実行している arm64 デバイスをサポートします。
- Info.plist に NSMicrophoneUsageDescription を追加し、対応するデバイスを使用する前に認可を要求してください。
- 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" },
{ "name": "ohos.permission.MICROPHONE",
"reason": "$string:perm_mic_reason",
"usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }
]
- 対応するデバイスを使用する前に、
abilityAccessCtrl.createAtManager().requestPermissionsFromUserを呼び出してohos.permission.MICROPHONEを要求してください。
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-realtime-plus への現在の AOQ 接続用の認証情報を取得します。
- クライアントは、選択したモデルとアプリケーションの音声フォーマットに合わせて、SDK のアップリンクエンコーダーとダウンリンクデコーダーを設定します。
- クライアントは、録音および再生デバイスを初期化し、AoqConnectConfig を作成します。現在の接続の認証情報フィールドを入力し、パブリッシュおよびサブスクライブする音声トラックとデータトラックを設定します。クライアントは、音声トラックの送信を無効にしたまま
connectを呼び出して AOQ 接続を確立します。 - 接続が確立された後、クライアントは
session.updateを送信します。session.updatedを受信した後にのみ、音声トラックを有効にします。 - サーバーサイド VAD は、ターンの境界を自動的に判断します。モデルの音声は音声トラックで再生され、会話イベントはデータトラックで返されます。
- 終了するには、エンジンを切断して破棄してください。SDK は自動的にオーディオデバイスを閉じます。
アプリケーションサーバーからのトークンの取得
アプリケーションサーバーで DASHSCOPE_API_KEY を設定し、選択したリージョンのエンドポイントにリクエストを送信してください。clientIp はクライアントの実際のパブリック IP アドレスです。このフィールドはオプションですが、指定するとサービスが適切なリレーエンドポイントを割り当てるのに役立ちます。
curl -X POST \
"https://{endpoint}/api/v1/webrtc/realtime?model=qwen-audio-3.0-realtime-plus" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \
-H "x-dashscope-rtc-transport: moq" \
-d "{\"clientIp\": \"${CLIENT_REAL_IP}\"}"
注記アプリケーションサーバーがクライアントの実際のパブリック IP アドレスを取得できない場合は、空の文字列を渡すのではなく clientIp を省略してください。
アプリケーションサーバーは、次のレスポンスフィールドをクライアントに返します。AOQ トークンは 1 つの接続にのみ使用できます。各 connect 呼び出しの前に、クライアントはトークンをキャッシュまたは再利用するのではなく、新しいトークンを要求する必要があります。本番環境では、API キーをクライアントに返さないでください。すべてのリクエストおよびレスポンスフィールドについては、「トークン認証」をご参照ください。
レスポンスフィールド | SDK フィールド |
aoqTokenForClient | AoqConnectConfig.token |
sid | AoqConnectConfig.sid |
clientRelayCertFingerprint | AoqConnectConfig.certFingerprint |
clientRelayEndpoints | AoqConnectConfig.relayEndpoints |
extraInfo.workspaceIdHash | AoqConnectConfig.workspaceIdHash |
Android クライアントの実装
各接続の前に、クライアントはアプリケーションサーバーから新しい接続認証情報を取得し、AoqConnectConfig を作成してください。トークンのレスポンスフィールドをマッピングし、パブリッシュおよびサブスクライブトラックなどのクライアント側の接続設定を追加してください。Android でリアルタイム音声会話を実装するには、次の手順に従ってください。
1. エンジンの作成とコールバックの登録
シングルトンの AOQ エンジンを作成し、イベントコールバックを登録してください。接続が成功した後でセッションを設定し、サーバーイベントを UI とアプリケーションのステートマシンにディスパッチしてください。
AoqClientListener listener = new AoqClientListener() {
@Override
public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
if (status == AoqClientEngine.AoqConnectionStatus.AoqConnectionStatusConnected) {
configureSession();
}
}
@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. オーディオコーデックの設定
選択したモデルとアプリケーションの音声フォーマットに合わせて、SDK のアップリンクエンコーダーとダウンリンクデコーダーを設定してください。以下の値はこのチュートリアルの PCM の例であり、アプリケーションの音声フォーマットを制限するものではありません。
AoqClientEngine.AoqAudioCodecConfig audioEncoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioEncoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioEncoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioEncoderConfig.sampleRate = 16000; // 例:モデルとアプリケーションのフォーマットに合わせてください。
audioEncoderConfig.channel = 1;
engine.setAudioEncoderConfig(audioEncoderConfig);
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);
3. トラックの設定と接続
SDK インターフェースを使用して、オーディオキャプチャと再生を開始してください。現在のアプリケーションサーバートークンのレスポンスを AoqConnectConfig の認証情報フィールドにマッピングし、publishTracks と subscribeTracks で音声トラックとデータトラックを設定してください。connect を呼び出すときは、音声トラックの送信を無効にしたままにしてください。session.updated を受信した後にのみ送信を有効にしてください。
AoqClientEngine.AoqAudioCaptureConfig captureConfig =
new AoqClientEngine.AoqAudioCaptureConfig();
captureConfig.channel = 1;
captureConfig.isVoipMode = true;
engine.startAudioCapture(captureConfig);
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isVoipMode = true;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
AoqClientEngine.AoqTrackParam publishAudioTrack = new AoqClientEngine.AoqTrackParam();
publishAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.publishTracks.add(publishAudioTrack);
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.enableSendMediaStream(
AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.connect(connectConfig);
4. session.update の送信
接続が成功した後、出力モダリティ、音声、音声フォーマット、指示、および VAD を設定してください。input_audio_format と output_audio_format の両方に pcm を使用してください。サンプルレートは SDK のコーデック設定によって決まります。すべてのパラメーターについては、「クライアントイベント」をご参照ください。
JSONObject vad = new JSONObject()
.put("type", "server_vad")
.put("threshold", 0.5)
.put("silence_duration_ms", 800);
JSONObject session = new JSONObject()
.put("modalities", new JSONArray().put("text").put("audio"))
.put("voice", "longanqian")
.put("input_audio_format", "pcm")
.put("output_audio_format", "pcm")
.put("instructions", "You are a helpful voice assistant.")
.put("turn_detection", vad);
JSONObject sessionUpdate = new JSONObject()
.put("type", "session.update")
.put("session", session);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = sessionUpdate.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
5. session.updated 後のアップリンクの有効化
session.updated は、セッション設定がアクティブであることを示します。この時点で初めて音声トラックの送信を有効にし、それ以前にキャプチャされた音声がモデルに送信されないようにします。
if ("session.updated".equals(type)) {
engine.enableSendMediaStream(
AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
}
6. サーバーイベントの処理
onDataMsg で、type を使用してユーザーとモデルのトランスクリプトを表示し、エラーを処理してください。すべてのイベントフィールドについては、「サーバーイベント」をご参照ください。
if ("response.audio_transcript.delta".equals(type)) {
String delta = event.optString("delta");
// UI のモデルトランスクリプトに差分を追加します。
} else if ("conversation.item.input_audio_transcription.completed".equals(type)) {
String transcript = event.optString("transcript");
// UI に最終的なユーザートランスクリプトを表示します。
} else if ("error".equals(type)) {
// エラーフィールドを読み取り、アプリケーションの状態を更新します。
}
7. エンジンの切断と破棄
会話が終了したら、シングルトンエンジンを切断して破棄してください。disconnect または destroy は自動的にオーディオキャプチャと再生を閉じるため、デバイスを個別に停止する必要はありません。
engine.disconnect();
AoqClientEngine.destroy();
主なサーバーイベント
データトラックのイベントは type で識別されます。クライアントは、以下の主要なイベントを処理する必要があります。完全なイベントスキーマについては、「サーバーイベント」をご参照ください。
イベント | 説明 |
session.created | セッションが作成され、デフォルト設定が返されます |
session.updated | クライアント設定がアクティブになり、音声アップリンクを有効にできます |
input_audio_buffer.speech_started | サービスがユーザーの発話開始を検出します |
input_audio_buffer.speech_stopped | サービスがユーザーの発話停止を検出します |
input_audio_buffer.committed | ターンの音声がコミットされます |
response.created | モデルがレスポンスの生成を開始します |
response.audio_transcript.delta | インクリメンタルなモデルトランスクリプト |
conversation.item.input_audio_transcription.completed | 最終的なユーザートランスクリプトが利用可能になります |
response.done | レスポンスが完了しました |
error | サーバーエラーが発生しました |
完全な例
以下のクラスは、現在の接続の認証情報が入力された AoqConnectConfig を受け取り、オーディオデバイス、パブリッシュトラック、およびサブスクライブトラックの設定を追加します。再接続のたびに新しい認証情報を取得し、新しい接続設定を作成してください。本番環境では、権限、UI の状態、および再接続ロジックを追加してください。
import android.content.Context;
import com.alibaba.aoq.clientsdk.AoqClientEngine;
import com.alibaba.aoq.clientsdk.AoqClientListener;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
public final class RealtimeVoiceChatClient {
private AoqClientEngine engine;
public RealtimeVoiceChatClient(Context context, AoqClientEngine.AoqConnectConfig connectConfig) {
AoqClientListener listener = new AoqClientListener() {
@Override
public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
if (status == AoqClientEngine.AoqConnectionStatus.AoqConnectionStatusConnected) {
configureSession();
}
}
@Override
public void onDataMsg(AoqClientEngine.AoqDataMsg msg) {
try {
JSONObject event = new JSONObject(
new String(msg.data, StandardCharsets.UTF_8));
String type = event.optString("type");
if ("session.updated".equals(type)) {
engine.enableSendMediaStream(
AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
} else if ("response.audio_transcript.delta".equals(type)) {
String delta = event.optString("delta");
// UI に差分を表示します。
} else if ("conversation.item.input_audio_transcription.completed".equals(type)) {
String transcript = event.optString("transcript");
// UI にトランスクリプトを表示します。
} else if ("error".equals(type)) {
// エラーフィールドを読み取り、アプリケーションの状態を更新します。
}
} 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);
// 値は一例です。これらの設定をモデルとアプリケーションの音声フォーマットに合わせてください。
AoqClientEngine.AoqAudioCodecConfig audioEncoderConfig =
new AoqClientEngine.AoqAudioCodecConfig();
audioEncoderConfig.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
audioEncoderConfig.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
audioEncoderConfig.sampleRate = 16000;
audioEncoderConfig.channel = 1;
engine.setAudioEncoderConfig(audioEncoderConfig);
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.AoqAudioCaptureConfig captureConfig =
new AoqClientEngine.AoqAudioCaptureConfig();
captureConfig.channel = 1;
captureConfig.isVoipMode = true;
engine.startAudioCapture(captureConfig);
AoqClientEngine.AoqAudioPlaybackConfig playbackConfig =
new AoqClientEngine.AoqAudioPlaybackConfig();
playbackConfig.channel = 1;
playbackConfig.isVoipMode = true;
playbackConfig.isDefaultSpeaker = true;
engine.startAudioPlayer(playbackConfig);
AoqClientEngine.AoqTrackParam publishAudioTrack =
new AoqClientEngine.AoqTrackParam();
publishAudioTrack.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
connectConfig.publishTracks.add(publishAudioTrack);
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.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.connect(connectConfig);
}
private void configureSession() {
try {
JSONObject vad = new JSONObject()
.put("type", "server_vad")
.put("threshold", 0.5)
.put("silence_duration_ms", 800);
JSONObject session = new JSONObject()
.put("modalities", new JSONArray().put("text").put("audio"))
.put("voice", "longanqian")
.put("input_audio_format", "pcm")
.put("output_audio_format", "pcm")
.put("instructions", "You are a helpful voice assistant.")
.put("turn_detection", vad);
JSONObject sessionUpdate = new JSONObject()
.put("type", "session.update")
.put("session", session);
AoqClientEngine.AoqDataMsg dataMessage = new AoqClientEngine.AoqDataMsg();
dataMessage.data = sessionUpdate.toString().getBytes(StandardCharsets.UTF_8);
engine.sendDataMsg(dataMessage);
} catch (JSONException e) {
throw new IllegalStateException("Failed to create session.update", e);
}
}
public void close() {
engine.disconnect();
AoqClientEngine.destroy();
}
}
実行と検証
- マイク音声のストリーミングは、
session.updatedを受信した後にのみ開始されます。 - ユーザーが話し終えると、サービスは音声をコミットし、応答を開始します。テキストイベントと音声トラックの音声は継続的に返されます。
一般的なシナリオ
インタラクションモードの変更
無音ベースのターン検出には server_vad を、音響的および意味的なターン検出には smart_turn を使用するか、プッシュツートークの場合は turn_detection を null に設定してください。turn_detection は、最初の音声入力の前にのみ変更できます。モードを変更するには、新しいセッションを確立してください。
音声の変更
最初の session.update で session.voice を設定してください。サポートされているシステム音声はモデルによって異なります。サポートされている音声と音声クローニングについては、「Qwen-Audio リアルタイム音声会話ユーザーガイド」をご参照ください。
スピーカーまたはイヤピース
AoqAudioPlaybackConfig.isDefaultSpeaker を使用してデフォルトの出力デバイスを設定し、セッションがアクティブな間に enableSpeakerphone を呼び出して切り替えてください。
Android でのバックグラウンド通話
Android 10 以降では、foregroundServiceType="microphone|mediaPlayback" を持つフォアグラウンドサービスを使用して、バックグラウンドでのキャプチャと再生を継続してください。アプリがユーザーに表示されている間に開始してください。
トラブルシューティング
問題 | 解決策 |
接続に失敗する | トークンが有効であること、エンドポイントがデプロイリージョンと一致していること、および AoqConnectConfig フィールドが正しくマッピングされていることを確認してください。 |
セッションは確立されたが、応答が返されない | 音声トラックが |
レスポンスに音声がない | 音声トラックがサブスクライブされており、オーディオプレーヤーが実行中であることを確認してから、SDK のダウンリンクデコーダーがモデルの出力音声フォーマットと一致していることを確認してください。 |
関連情報
他のプラットフォームのすべてのパラメーター、イベントフィールド、およびインターフェースについては、以下をご参照ください。