全部產品
Search
文件中心

Alibaba Cloud Model Studio:使用 AOQ 接入 fun-asr-realtime 實現即時語音辨識

更新時間:Aug 26, 2026

通過 AOQ 接入 fun-asr-realtime,發送麥克風音頻並即時接收語音辨識結果。用戶端代碼以 Android Java 為例,AOQ 支援的其他平台使用相同的介面。

方案概述

fun-asr-realtime 將音頻流即時轉寫為帶標點的文本。AOQ SDK 將媒體和事件分軌傳輸:用戶端通過 Audio 軌上行音頻,通過 Data 軌發送控制事件並接收識別事件。該模型使用 Inference 事件協議,而不是 Realtime 事件協議。

該方案適用於即時字幕、會議轉寫、語音輸入和智能助手。Audio 軌避免用戶端把音頻編碼成事件訊息,Data 軌則保留 run-task、result-generated 和 finish-task 等完整任務語義。

  1. 用戶端向業務 AppServer 請求臨時 AOQ 串連憑證。
  2. AppServer 使用 API Key 向百鍊申請 Token,並把串連欄位返回用戶端。
  3. 用戶端建立 AOQ 串連並發送 run-task;收到 task-started 後開始上行麥克風音頻。
  4. 服務端持續返回 result-generated;用戶端發送 finish-task 後等待最終結果和 task-finished。

準備工作

  1. 開通阿里雲百鍊,並按擷取與配置 API Key。API Key 只儲存在業務 AppServer,不要寫入用戶端代碼或提交到代碼倉庫。
  2. 根據業務部署地區確認 AOQ Endpoint。地區和接入地址的選擇方法請參見選擇地區、服務部署範圍和接入網域名稱
  3. SDK 下載擷取最新版 AOQ Client SDK。本文傳輸 PCM 音頻,不需要額外整合 Opus 外掛程式。
  4. 搭建業務 AppServer,並按Token 鑒權實現 AOQ Inference 協議的服務端代理鑒權。每次建立新串連前,用戶端都應從 AppServer 擷取新的串連憑證。

匯入 SDK

根據開發平台選擇相應的 SDK 匯入方式。後續用戶端實現以 Android Java 為例;其他平台使用相同的介面設計和事件流程。

Android

  1. 將 AoqClientSdk-release.aar 放入 app/libs 目錄,並在 app/build.gradle 中配置依賴和 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" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
  1. 在開始錄音前動態申請 RECORD_AUDIO 許可權。純語音辨識不需要 CAMERA 許可權。

iOS

  1. 將 AoqClientSdk.framework 拖入 Xcode 工程,在 Target > General > Frameworks, Libraries, and Embedded Content 中選擇 Embed & Sign。SDK 支援 iOS 13.0 及以上 arm64 裝置。
  2. 在 Info.plist 中添加 NSMicrophoneUsageDescription。純語音辨識不需要 NSCameraUsageDescription。
  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" },
  {
    "name": "ohos.permission.MICROPHONE",
    "reason": "$string:perm_mic_reason",
    "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" }
  }
]
  1. 在開始錄音前調用 abilityAccessCtrl.createAtManager().requestPermissionsFromUser 申請麥克風許可權。

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 的絕對路徑。

體驗 Demo

阿里雲百鍊提供適用於 Android 平台的 Demo,可用於快速驗證 AOQ 接入效果。下載 APK 並配置 API Key 和 workspaceId 後,即可體驗部分模型。

掃描以下二維碼下載 Demo:

Demo 下載二維碼

實現流程

  1. AppServer 使用 Inference Token 地址擷取 fun-asr-realtime 的 AOQ 串連參數。
  2. 用戶端把 Token 響應轉換為 AoqConnectConfig,發布 Audio 和 Data 軌,並訂閱 Data 軌。
  3. 用戶端按業務需求和模型要求配置音頻編碼參數,啟動麥克風採集但暫不發送音頻,然後建立 AOQ 串連。
  4. 串連成功後發送 run-task;收到 task-started 後開啟 Audio 軌發送。
  5. 用戶端在 onDataMsg 中處理 result-generated;結束錄音時先關閉 Audio 軌發送,再發送 finish-task。
  6. 收到 task-finished 後,可在同一串連上使用新的 task_id 發起下一輪識別,或中斷連線並銷毀引擎。
aoq-realtime-asr-sequence-zh

AppServer 擷取 Token

在 AppServer 設定 DASHSCOPE_API_KEY,並使用所選地區的 Endpoint 發送請求。clientIp 為終端的真實公網 IP;該欄位可選,但建議傳入,以便服務分配合適的 Relay 存取點。

curl -X POST \
  "https://{endpoint}/api/v1/webrtc/inference?model=fun-asr-realtime" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer ${DASHSCOPE_API_KEY}" \
  -H "x-dashscope-rtc-transport: moq" \
  -d "{\"clientIp\": \"${CLIENT_REAL_IP}\"}"

說明如果 AppServer 無法擷取終端真實公網 IP,請從請求體中刪除 clientIp 欄位,不要傳Null 字元串。

AppServer 將響應中的以下欄位返回用戶端。生產環境中不要把 API Key 返回用戶端。完整請求參數和響應欄位請參見Token 鑒權

響應欄位

SDK 欄位

aoqTokenForClient

AoqConnectConfig.token

sid

AoqConnectConfig.sid

clientRelayCertFingerprint

AoqConnectConfig.certFingerprint

clientRelayEndpoints

AoqConnectConfig.relayEndpoints

extraInfo.workspaceIdHash

AoqConnectConfig.workspaceIdHash

實現 Android 用戶端

以下步驟按串連和任務的實際執行順序拆分 Android Java 用戶端代碼。各片段來自後文的完整樣本。

1. 建立引擎並設定回調

建立 AOQ 用戶端引擎,並註冊串連狀態和 Data 軌事件回調。請根據商務邏輯實現回調處理;串連成功後再啟動識別任務。

AoqClientListener listener = new AoqClientListener() {
    @Override
    public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
        connected = status == AoqClientEngine.AoqConnectionStatus
                .AoqConnectionStatusConnected;
        if (connected) {
            beginRecognition();
        }
    }

    @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. 配置音頻編碼

配置發送給模型的音頻編碼。請根據業務需求和模型要求設定格式、採樣率和聲道數。以下代碼以 16 kHz 單聲道 PCM 為例;支援範圍請參見用戶端事件中的 run-task 參數。

AoqClientEngine.AoqAudioCodecConfig encoder =
        new AoqClientEngine.AoqAudioCodecConfig();
encoder.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
encoder.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
encoder.sampleRate = 16000;
encoder.channel = 1;
encoder.bitrate = 24000;
engine.setAudioEncoderConfig(encoder);

3. 配置串連和傳輸軌道

使用 AppServer 返回的憑證配置 AOQ 串連,並根據業務需要選擇發布和訂閱的軌道。以下代碼為即時語音辨識發布 Audio 和 Data 軌,並訂閱 Data 軌。

addTrack(connectConfig, true,
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio);
addTrack(connectConfig, true,
        AoqClientEngine.AoqTrackType.AoqTrackTypeData);
addTrack(connectConfig, false,
        AoqClientEngine.AoqTrackType.AoqTrackTypeData);

4. 啟動音頻採集並建立串連

配置音頻採集方式並建立 AOQ 串連。請根據業務選擇內建或外部採集、是否啟用 VoIP 模式以及聲道數。收到 task-started 前保持 Audio 軌發送關閉。

AoqClientEngine.AoqAudioCaptureConfig capture =
        new AoqClientEngine.AoqAudioCaptureConfig();
capture.isExternal = false;
capture.isVoipMode = true;
capture.channel = 1;
engine.startAudioCapture(capture);

// 收到 task-started 前不要發送音頻。
engine.enableSendMediaStream(
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.connect(connectConfig);

5. 啟動識別任務

串連成功後產生任務 ID,並發送 run-task 啟動識別。請根據實際使用的模型和音頻輸入配置 model、format、sample_rate 及其他任務參數,完整說明請參見用戶端事件

taskId = UUID.randomUUID().toString();
JSONObject header = createHeader("run-task");
JSONObject parameters = new JSONObject()
        .put("format", "pcm")
        .put("sample_rate", 16000);
JSONObject payload = new JSONObject()
        .put("task_group", "audio")
        .put("task", "asr")
        .put("function", "recognition")
        .put("model", "fun-asr-realtime")
        .put("parameters", parameters)
        .put("input", new JSONObject());
send(new JSONObject().put("header", header).put("payload", payload));

6. 處理服務端事件

處理任務狀態、識別結果和錯誤事件,並將結果傳遞給業務層。請根據應用的展示和狀態管理需求實現回調邏輯;收到 task-started 後再發送音頻,展示結果時過濾心跳事件。完整響應結構請參見服務端事件

String eventName = header.optString("event", "");
if ("task-started".equals(eventName)) {
    taskStarted = true;
    engine.enableSendMediaStream(
            AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
} else if ("result-generated".equals(eventName)) {
    JSONObject payload = event.optJSONObject("payload");
    JSONObject output = payload == null ? null : payload.optJSONObject("output");
    JSONObject sentence = output == null ? null : output.optJSONObject("sentence");
    if (sentence != null && !sentence.optBoolean("heartbeat", false)) {
        String text = sentence.optString("text", "");
        if (!text.isEmpty()) {
            resultListener.onResult(
                    text, sentence.optBoolean("sentence_end", false));
        }
    }
} else if ("task-finished".equals(eventName)) {
    resetTaskState();
    resultListener.onTaskFinished();
} else if ("task-failed".equals(eventName)) {
    String message = header.optString("error_message", "識別失敗");
    resetTaskState();
    resultListener.onError(message);
}

7. 結束識別任務

使用者結束本輪錄音時,停止音頻上行並發送 finish-task。保持串連直至收到最終識別結果和 task-finished;後續可按業務需要啟動新任務或釋放串連。事件格式請參見用戶端事件

engine.enableSendMediaStream(
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
JSONObject payload = new JSONObject().put("input", new JSONObject());
send(new JSONObject()
        .put("header", createHeader("finish-task"))
        .put("payload", payload));

8. 中斷連線並銷毀引擎

頁面銷毀或不再需要識別時,釋放音頻採集、AOQ 串連和引擎資源。請根據應用生命週期決定釋放時機,不要在剛發送 finish-task 時立即釋放。

engine.enableSendMediaStream(
        AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
engine.stopAudioCapture();
engine.disconnect();
AoqClientEngine.destroy();

完整樣本

該 Android Java 類將 AppServer 返回的 JSON 轉換為 AoqConnectConfig,並組合前述串連、採集、任務和資源釋放邏輯。

import android.content.Context;

import com.alibaba.aoq.clientsdk.AoqClientEngine;
import com.alibaba.aoq.clientsdk.AoqClientListener;

import org.json.JSONArray;
import org.json.JSONObject;

import java.nio.charset.StandardCharsets;
import java.util.UUID;

public final class AsrClient {
    public interface ResultListener {
        void onResult(String text, boolean sentenceEnd);

        void onTaskFinished();

        void onError(String message);
    }

    private final AoqClientEngine engine;
    private final ResultListener resultListener;
    private String taskId;
    private boolean connected;
    private boolean taskStarted;

    public AsrClient(Context context, AoqClientEngine.AoqConnectConfig connectConfig,
                     ResultListener resultListener) {
        this.resultListener = resultListener;
        AoqClientListener listener = new AoqClientListener() {
            @Override
            public void onConnectionStatusChange(AoqClientEngine.AoqConnectionStatus status) {
                connected = status == AoqClientEngine.AoqConnectionStatus
                        .AoqConnectionStatusConnected;
                if (connected) {
                    beginRecognition();
                }
            }

            @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);

        configureAudioEncoder();
        configureTracks(connectConfig);
        startAudioCapture();
        engine.connect(connectConfig);
    }

    private void configureAudioEncoder() {
        AoqClientEngine.AoqAudioCodecConfig encoder = new AoqClientEngine.AoqAudioCodecConfig();
        encoder.trackType = AoqClientEngine.AoqTrackType.AoqTrackTypeAudio;
        encoder.codecType = AoqClientEngine.AoqEncoderType.AoqEncoderTypeAudioPCM;
        encoder.sampleRate = 16000;
        encoder.channel = 1;
        encoder.bitrate = 24000;
        engine.setAudioEncoderConfig(encoder);
    }

    private static void configureTracks(AoqClientEngine.AoqConnectConfig connectConfig) {
        addTrack(connectConfig, true, AoqClientEngine.AoqTrackType.AoqTrackTypeAudio);
        addTrack(connectConfig, true, AoqClientEngine.AoqTrackType.AoqTrackTypeData);
        addTrack(connectConfig, false, AoqClientEngine.AoqTrackType.AoqTrackTypeData);
    }

    private void startAudioCapture() {
        AoqClientEngine.AoqAudioCaptureConfig capture =
                new AoqClientEngine.AoqAudioCaptureConfig();
        capture.isExternal = false;
        capture.isVoipMode = true;
        capture.channel = 1;
        engine.startAudioCapture(capture);
        engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
    }

    /** 在現有 AOQ 串連上啟動新一輪識別任務。 */
    public void beginRecognition() {
        if (!connected || taskStarted || taskId != null) {
            return;
        }
        taskId = UUID.randomUUID().toString();
        JSONObject header = createHeader("run-task");
        JSONObject parameters = new JSONObject()
                .put("format", "pcm")
                .put("sample_rate", 16000);
        JSONObject payload = new JSONObject()
                .put("task_group", "audio")
                .put("task", "asr")
                .put("function", "recognition")
                .put("model", "fun-asr-realtime")
                .put("parameters", parameters)
                .put("input", new JSONObject());
        send(new JSONObject().put("header", header).put("payload", payload));
    }

    /** 結束當前任務。收到 task-finished 後再中斷連線。 */
    public void finishRecognition() {
        if (taskId == null) {
            return;
        }
        engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
        JSONObject payload = new JSONObject().put("input", new JSONObject());
        send(new JSONObject()
                .put("header", createHeader("finish-task"))
                .put("payload", payload));
    }

    public void close() {
        engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
        engine.stopAudioCapture();
        engine.disconnect();
        AoqClientEngine.destroy();
    }

    private void handleServerEvent(AoqClientEngine.AoqDataMsg msg) {
        if (msg == null || msg.data == null) {
            return;
        }
        JSONObject event = new JSONObject(new String(msg.data, StandardCharsets.UTF_8));
        JSONObject header = event.optJSONObject("header");
        if (header == null) {
            return;
        }

        String eventName = header.optString("event", "");
        if ("task-started".equals(eventName)) {
            taskStarted = true;
            engine.enableSendMediaStream(
                    AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, true);
        } else if ("result-generated".equals(eventName)) {
            handleRecognitionResult(event);
        } else if ("task-finished".equals(eventName)) {
            resetTaskState();
            resultListener.onTaskFinished();
        } else if ("task-failed".equals(eventName)) {
            String message = header.optString("error_message", "識別失敗");
            resetTaskState();
            resultListener.onError(message);
        }
    }

    private void handleRecognitionResult(JSONObject event) {
        JSONObject payload = event.optJSONObject("payload");
        JSONObject output = payload == null ? null : payload.optJSONObject("output");
        JSONObject sentence = output == null ? null : output.optJSONObject("sentence");
        if (sentence == null || sentence.optBoolean("heartbeat", false)) {
            return;
        }
        String text = sentence.optString("text", "");
        if (!text.isEmpty()) {
            resultListener.onResult(text, sentence.optBoolean("sentence_end", false));
        }
    }

    private void resetTaskState() {
        engine.enableSendMediaStream(AoqClientEngine.AoqTrackType.AoqTrackTypeAudio, false);
        taskStarted = false;
        taskId = null;
    }

    private JSONObject createHeader(String action) {
        return new JSONObject()
                .put("action", action)
                .put("task_id", taskId)
                .put("streaming", "duplex");
    }

    private void send(JSONObject event) {
        AoqClientEngine.AoqDataMsg msg = new AoqClientEngine.AoqDataMsg();
        msg.data = event.toString().getBytes(StandardCharsets.UTF_8);
        engine.sendDataMsg(msg);
    }

    private static void addTrack(AoqClientEngine.AoqConnectConfig config, boolean publish,
                                 AoqClientEngine.AoqTrackType type) {
        AoqClientEngine.AoqTrackParam track = new AoqClientEngine.AoqTrackParam();
        track.trackType = type;
        if (publish) {
            config.publishTracks.add(track);
        } else {
            config.subscribeTracks.add(track);
        }
    }

    /** 將 AppServer 返回的 Token 響應轉換為 SDK 串連配置。 */
    public static AoqClientEngine.AoqConnectConfig parseConnectConfig(String responseText) {
        JSONObject response = new JSONObject(responseText);
        AoqClientEngine.AoqConnectConfig config = new AoqClientEngine.AoqConnectConfig();
        config.token = response.optString("aoqTokenForClient", "");
        config.sid = response.optString("sid", "");
        config.certFingerprint = response.optString("clientRelayCertFingerprint", "");

        JSONArray endpoints = response.optJSONArray("clientRelayEndpoints");
        if (endpoints != null) {
            for (int i = 0; i < endpoints.length(); i++) {
                JSONObject item = endpoints.optJSONObject(i);
                if (item == null) {
                    continue;
                }
                AoqClientEngine.AoqRelayEndpoint endpoint =
                        new AoqClientEngine.AoqRelayEndpoint();
                endpoint.routeIndex = item.has("route_index")
                        ? item.optInt("route_index", i) : i;
                endpoint.endpoint = item.optString("endpoint", "");
                endpoint.port = item.optInt("port", 0);
                config.relayEndpoints.add(endpoint);
            }
        }

        JSONObject extraInfo = response.optJSONObject("extraInfo");
        config.workspaceIdHash = extraInfo == null
                ? "" : extraInfo.optString("workspaceIdHash", "");
        return config;
    }
}

調用樣本

將 AppServer 的 Token 響應傳給 parseConnectConfig,然後建立用戶端。首次串連成功後自動開始識別。停止按鈕只結束當前任務;頁面銷毀時才釋放串連和本地資源。

private AsrClient client;

void startRecognition(Context context, String tokenResponseText) {
    AoqClientEngine.AoqConnectConfig config =
            AsrClient.parseConnectConfig(tokenResponseText);

    client = new AsrClient(context, config, new AsrClient.ResultListener() {
        @Override
        public void onResult(String text, boolean sentenceEnd) {
            // 使用中間句或最終句更新介面。
        }

        @Override
        public void onTaskFinished() {
            // 啟用開始按鈕,或調用 beginRecognition() 開始新任務。
        }

        @Override
        public void onError(String message) {
            // 展示或記錄錯誤。
        }
    });
}

void onStopButtonClick() {
    // 結束當前任務,並保持 AOQ 串連,直至收到 task-finished。
    client.finishRecognition();
}

void onPageDestroyed() {
    // 僅在頁面關閉時釋放本地資源。
    client.close();
}

運行並驗證

  1. 啟動 AppServer,確認 Token 請求返回 HTTP 200,並包含 sid、aoqTokenForClient、clientRelayEndpoints、clientRelayCertFingerprint 和 extraInfo.workspaceIdHash。
  2. 在 Android 裝置上安裝並運行應用,授予麥克風許可權,然後說一段話。
  3. 觀察回調。正常事件順序如下:
task-started
result-generated (sentence_end=false)
result-generated (sentence_end=true)
task-finished

說話過程中應持續收到中間識別文本。調用 finishRecognition 後,應收到當前句的最終文本和 task-finished。不要在 finish-task 發送後立即中斷連線。

典型情境

同一串連多次識別

收到 task-finished 後調用 beginRecognition,可在同一 AOQ 串連上啟動下一輪識別。每輪任務必須使用新的 task_id,不需要重新申請 Token 或重建串連;如果串連已經斷開,則需要重新擷取串連憑證。

Android 後台識別

Android 10 及以上版本中,如需在應用進入後台後繼續採集麥克風音頻,應使用 foregroundServiceType=microphone 的前台服務,並在應用仍對使用者可見時啟動該服務。

常見問題

問題

處理方法

串連失敗

確認 Token 尚未到期、Endpoint 與業務地區一致,並檢查 AppServer 是否傳入了終端真實公網 IP。串連斷開後不要複用舊 Token。

任務已啟動但沒有識別結果

確認收到 task-started 後才開啟 Audio 軌發送,並根據當前模型的用戶端事件檢查音頻格式、採樣率等輸入參數。

收不到最終結果

先關閉 Audio 軌發送,再發送 finish-task;等待最終 result-generated 和 task-finished,不要立即中斷連線。

Android 載入 SDK 失敗

確認 AAR 已加入依賴,並且應用只打包 SDK 支援的 armeabi-v7a 或 arm64-v8a ABI。

同一串連的下一輪任務被拒絕

確認上一輪已經收到 task-finished,並為新一輪 run-task 產生新的 task_id。

相關文檔

如需查詢完整參數、事件欄位或其他平台介面,請參見: