Skip to main content

リアルタイム音声認識 (Qwen-ASR-Realtime) のインタラクションフロー

Qwen-ASR-Realtime は、WebSocket 経由でオーディオストリームを受信し、リアルタイムで音声を文字起こしします。このサービスは、 VAD モード と 手動モード の 2 つのインタラクションモードをサポートしています。

ユーザーガイド: モデルの概要と選択ガイダンスについては、「音声テキスト変換」をご参照ください。サンプルコードについては、「リアルタイム音声認識」をご参照ください。 レート制限: モデルの呼び出しにはレート制限が適用されます。制限を超えると、サーバーはエラー Requests rate limit exceeded, please try again later. を返します。リクエストレートまたは同時実行数を減らして、再試行してください。各モデルのレート制限については、「レート制限」をご参照ください。

サービスエンドポイント

以下の WebSocket URL を使用します。model クエリパラメーターでモデルを指定します。<model_name> をモデル名に置き換えてください:
  • シンガポール
  • 中国 (北京)
wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime?model=<model_name>{WorkspaceId} を実際のワークスペース ID に置き換えてください。
Alibaba Cloud Model Studio は、中国 (北京) およびシンガポールの各リージョン向けにワークスペース固有ドメインをリリースしました。これらの新しい専用ドメインにより、推論リクエストのパフォーマンスと安定性が向上します。以下の新しいドメインへの移行を推奨します:
  • 中国 (北京):dashscope.aliyuncs.com から {WorkspaceId}.cn-beijing.maas.aliyuncs.com へ
  • シンガポール:dashscope-intl.aliyuncs.com から {WorkspaceId}.ap-southeast-1.maas.aliyuncs.com へ
{WorkspaceId} を実際の ワークスペース ID に置き換えてください。既存のドメインは引き続き問題なく利用できます。
wss:// スキームを使用してください。リクエストヘッダーで Authorization を設定してください (「リクエストヘッダー」をご参照ください)。model クエリパラメーターでモデルを指定してください。

リクエストヘッダー

リクエストヘッダーに次のフィールドを含めてください:

パラメーター

タイプ

必須

説明

Authorization

string

はい

Bearer <your_api_key> 形式の認証トークンです。<your_api_key> をご自身の API キーに置き換えてください。

user-agent

string

いいえ

サーバーがリクエストソースを追跡するためのクライアント識別子です。

X-DashScope-WorkSpace

string

いいえ

Alibaba Cloud Model Studio のワークスペース ID です。

X-DashScope-DataInspection

string

いいえ

データ検査を有効にするかどうか。データ検査が必要な場合を除き、このヘッダーは省略してください。必要な場合は、値を enable に設定してください。

Authorization は WebSocket ハンドシェイク中に検証されます。API キーが無効または欠落している場合、ハンドシェイクは HTTP 401 または 403 エラーで失敗します。

インタラクションフロー

クライアントイベントとサーバーイベントの詳細については、「Qwen-ASR-Realtime のクライアントイベント」および「サーバーイベント」をご参照ください。 Qwen-ASR-Realtime は 2 つのインタラクションモードをサポートしています:
  • VAD モード (デフォルト): サーバーは音声アクティビティ検出 (VAD) を使用して、各発話の開始と終了を自動的に検出します。このモードは、リアルタイムの会話や会議の文字起こしなどのシナリオで使用します。
  • 手動モード: クライアントが発話の境界を制御します。このモードは、メッセージングアプリで音声メッセージを送信する場合など、クライアントがこれらの境界を明示的に判断できる場合に使用します。
このモデルは文字起こしの文単位・単語単位のタイムスタンプを返しません。VAD イベントの発話開始・終了時間は音声活動の境界を示します。フィールド定義は各イベントをご参照ください。

VAD モード (デフォルト)

silence_duration_ms のサーバーデフォルトは 800 です。素早い区切りが必要な対話では 400 を参考にできます。threshold のサーバーデフォルトは 0.2 です。詳細はクライアントイベントをご参照ください。 サーバーは各発話の開始と終了を自動的に検出します。オーディオストリームを継続的に送信すると、サーバーは発話の終了を検出するたびに、その発話の最終的な文字起こし結果を返します。このモードは、リアルタイムの会話や会議の文字起こしなどのシナリオで使用します。 有効化の方法: このモードはデフォルトで有効です。VAD の動作をカスタマイズするには、クライアントの session.update イベントで session.turn_detection パラメーターに設定オブジェクトを渡します。
image
  • クライアントは input_audio_buffer.append イベントを送信して、オーディオをバッファーに追加します。
  • サーバーは、音声が検出されると input_audio_buffer.speech_started イベントを返します。 注: このイベントが到着する前にクライアントが session.finish を送信してセッションを終了した場合、サーバーは直ちに session.finished イベントを返します。その後、クライアントは接続を閉じる必要があります。
  • クライアントは引き続き input_audio_buffer.append イベントを送信してオーディオを送信します。
  • すべてのオーディオを送信した後、クライアントは session.finish イベントを送信してセッションを終了します。
    VAD モードでは、クライアントは接続を閉じる前に session.finish イベントを送信する必要があります。クライアントが session.finish を送信せずに WebSocket 接続を閉じると、サーバーは進行中のアイテムを破棄し、conversation.item.input_audio_transcription.completed などのイベントは配信されません。{"type":"session.finish"} を送信し、session.finished イベントを待機してから ws.Close() を呼び出して接続を閉じてください。
  • サーバーは、音声の終了を検出すると input_audio_buffer.speech_stopped イベントを返します。
  • サーバーは input_audio_buffer.committed イベントを返します。
  • サーバーは conversation.item.created イベントを返します。
  • サーバーは、部分的な文字起こし結果を含む conversation.item.input_audio_transcription.text イベントを返します。
  • サーバーは、最終的な文字起こし結果を含む conversation.item.input_audio_transcription.completed イベントを返します。
  • サーバーは session.finished イベントを返して、認識が完了したことをシグナルします。その後、クライアントは接続を閉じる必要があります。

手動モード

クライアントが発話の境界を制御します。発話全体のオーディオを送信した後、クライアントは input_audio_buffer.commit イベントを送信してサーバーに通知します。このモードは、メッセージングアプリで音声メッセージを送信する場合など、クライアントがこれらの境界を明示的に判断できる場合に使用します。 有効化の方法: クライアントの session.update イベントで session.turn_detection を null に設定してください。
image

呼び出し例

以下の例では qwen3-asr-flash-realtime を使用します。コンソールで API キーと Base URL を取得し、環境変数 DASHSCOPE_API_KEY と DASHSCOPE_BASE_URL に設定します。 サンプル音声を asr_example.wav として保存してください。16 kHz、モノラル、16-bit PCM エンコードの WAV ファイルです。
サンプルコードは asr_example.pcm (PCM16、16 kHz、モノラル) を読み込みます。MP3 や WAV などのフォーマットしかない場合は、ffmpeg で変換してください:
ffmpeg -i asr_example.wav -ar 16000 -ac 1 -f s16le asr_example.pcm
  • Java
  • Python
  • Go
  • Node.js
  • C#
  • PHP
例を実行する前に、Java-WebSocket 依存関係をインストールしてください:
Maven
<dependency>
    <groupId>org.java-websocket</groupId>
    <artifactId>Java-WebSocket</artifactId>
    <version>1.5.6</version>
</dependency>
<dependency>
    <groupId>org.json</groupId>
    <artifactId>json</artifactId>
    <version>20240303</version>
</dependency>
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONObject;

import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.logging.*;

public class QwenASRRealtimeClient {

    private static final Logger logger = Logger.getLogger(QwenASRRealtimeClient.class.getName());
    private static final String API_KEY = System.getenv("DASHSCOPE_API_KEY");
    private static final String MODEL = "qwen3-asr-flash-realtime";

    // Controls whether to use VAD mode
    private static final boolean enableServerVad = true;

    private static final AtomicBoolean isRunning = new AtomicBoolean(true);
    private static WebSocketClient client;

    public static void main(String[] args) throws Exception {
        initLogger();

        String baseUrl = "wss://" + URI.create(System.getenv("DASHSCOPE_BASE_URL")).getHost() + "/api-ws/v1/realtime";
        String url = baseUrl + "?model=" + MODEL;
        logger.info("Connecting to server: " + url);

        client = new WebSocketClient(new URI(url)) {
            @Override
            public void onOpen(ServerHandshake handshake) {
                logger.info("Connected to server.");
                sendSessionUpdate();
            }

            @Override
            public void onMessage(String message) {
                try {
                    JSONObject data = new JSONObject(message);
                    String eventType = data.optString("type");

                    logger.info("Received event: " + data.toString(2));

                    // The final recognition result is in the transcription.completed event
                    if ("conversation.item.input_audio_transcription.completed".equals(eventType)) {
                        logger.info("Final transcript: " + data.optString("transcript"));
                    }

                    // On receiving the finished event -> stop the sending thread and close the connection
                    if ("session.finished".equals(eventType)) {
                        logger.info("Closing WebSocket connection after session finished...");

                        isRunning.set(false); // Stop the audio sending thread
                        if (this.isOpen()) {
                            this.close(1000, "ASR finished");
                        }
                    }
                } catch (Exception e) {
                    logger.severe("Failed to parse message: " + message);
                }
            }

            @Override
            public void onClose(int code, String reason, boolean remote) {
                logger.info("Connection closed: " + code + " - " + reason);
            }

            @Override
            public void onError(Exception ex) {
                logger.severe("Error: " + ex.getMessage());
            }
        };

        // Add request headers
        client.addHeader("Authorization", "Bearer " + API_KEY);
        client.addHeader("OpenAI-Beta", "realtime=v1");

        client.connectBlocking(); // Block until the connection is established

        // Replace with the path to the audio file to be recognized
        String localAudioPath = "asr_example.pcm";
        Thread audioThread = new Thread(() -> {
            try {
                sendAudio(localAudioPath);
            } catch (Exception e) {
                logger.severe("Audio sending thread error: " + e.getMessage());
            }
        });
        audioThread.start();
    }

    /** Session update event (enable/disable VAD) */
    private static void sendSessionUpdate() {
        JSONObject eventNoVad = new JSONObject()
                .put("event_id", "event_123")
                .put("type", "session.update")
                .put("session", new JSONObject()
                        .put("modalities", new String[]{"text"})
                        .put("input_audio_format", "pcm")
                        .put("sample_rate", 16000)
                        // .put("input_audio_transcription", new JSONObject()
                        //         .put("language", "zh"))
                        .put("turn_detection", JSONObject.NULL) // Manual mode
                );

        JSONObject eventVad = new JSONObject()
                .put("event_id", "event_123")
                .put("type", "session.update")
                .put("session", new JSONObject()
                        .put("modalities", new String[]{"text"})
                        .put("input_audio_format", "pcm")
                        .put("sample_rate", 16000)
                        // .put("input_audio_transcription", new JSONObject()
                        //         .put("language", "zh"))
                        .put("turn_detection", new JSONObject()
                                .put("type", "server_vad")
                                .put("threshold", 0.2)
                                .put("silence_duration_ms", 400))
                );

        if (enableServerVad) {
            logger.info("Sending event (VAD):\n" + eventVad.toString(2));
            client.send(eventVad.toString());
        } else {
            logger.info("Sending event (Manual):\n" + eventNoVad.toString(2));
            client.send(eventNoVad.toString());
        }
    }

    /** Send the audio file stream */
    private static void sendAudio(String localAudioPath) throws Exception {
        Thread.sleep(3000); // Wait for the session to be ready
        byte[] allBytes = Files.readAllBytes(Paths.get(localAudioPath));
        logger.info("File reading started");

        int offset = 0;
        while (isRunning.get() && offset < allBytes.length) {
            int chunkSize = Math.min(3200, allBytes.length - offset);
            byte[] chunk = new byte[chunkSize];
            System.arraycopy(allBytes, offset, chunk, 0, chunkSize);
            offset += chunkSize;

            if (client != null && client.isOpen()) {
                String encoded = Base64.getEncoder().encodeToString(chunk);
                JSONObject eventd = new JSONObject()
                        .put("event_id", "event_" + System.currentTimeMillis())
                        .put("type", "input_audio_buffer.append")
                        .put("audio", encoded);

                client.send(eventd.toString());
                logger.info("Sending audio event: " + eventd.getString("event_id"));
            } else {
                break; // Avoid continuing to send after disconnection
            }

            Thread.sleep(100); // Simulate real-time sending
        }

        logger.info("File reading finished");

        if (client != null && client.isOpen()) {
            // Commit is required in non-VAD mode
            if (!enableServerVad) {
                JSONObject commitEvent = new JSONObject()
                        .put("event_id", "event_789")
                        .put("type", "input_audio_buffer.commit");
                client.send(commitEvent.toString());
                logger.info("Sent commit event for manual mode.");
            }

            JSONObject finishEvent = new JSONObject()
                    .put("event_id", "event_987")
                    .put("type", "session.finish");
            client.send(finishEvent.toString());
            logger.info("Sent finish event.");
        }
    }

    /** Initialize logging */
    private static void initLogger() {
        logger.setLevel(Level.ALL);
        Logger rootLogger = Logger.getLogger("");
        for (Handler h : rootLogger.getHandlers()) {
            rootLogger.removeHandler(h);
        }

        Handler consoleHandler = new ConsoleHandler();
        consoleHandler.setLevel(Level.ALL);
        consoleHandler.setFormatter(new SimpleFormatter());
        logger.addHandler(consoleHandler);
    }
}