Skip to main content
語音辨識

即時語音辨識

即時語音辨識服務接收音頻流並即時轉寫為帶標點的文本,適用於直播字幕、線上會議、語音交談、智能助手等情境。

概述

實現低延遲音頻到文本轉換。
  • 支援普通話及粵語、四川話等多種方言的高精度語音辨識
  • 具備應對複雜聲學環境的能力,支援自動語種檢測與智能非人聲過濾
  • 支援驚訝、平靜、愉快、悲傷、厭惡、憤怒、恐懼等多種情緒狀態識別
  • 支援熱詞定製,可提升特定詞彙的識別準確率
  • 支援上下文增強,通過配置上下文提高識別準確率
  • 支援時間戳記輸出,產生結構化識別結果
  • 靈活採樣率與多種音頻格式,適配不同錄音環境
批量情境(會議轉寫、通話分析、字幕產生等)可使用非即時語音辨識。各模型選型建議請參見語音辨識

前提條件

快速開始

以下樣本展示如何通過 DashScope SDK 快速調用即時語音辨識服務。
  • Qwen-Audio-3.0-ASR-Flash-Streaming/ Fun-ASR -Realtime
  • Qwen3-ASR-Flash-Realtime
  • Paraformer
該模型除 WebSocket 通訊協定外,還支援通過 AOQ 協議接入;如果是用戶端對接,且更看重穩定的延遲、弱網下的互動能力、即時雙工的降噪與回聲消除,可優先考慮 AOQ,協議對比與選型請參見模型/應用支援力度
  • 識別傳入麥克風的語音
  • 識別本地音頻檔案
識別麥克風傳入的語音並即時輸出文本,實現"邊說邊出字"的效果。
  • Java
  • Python
import com.alibaba.dashscope.audio.asr.recognition.Recognition;
import com.alibaba.dashscope.audio.asr.recognition.RecognitionParam;
import com.alibaba.dashscope.audio.asr.recognition.RecognitionResult;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.utils.Constants;

import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.TargetDataLine;

import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class Main {
    public static void main(String[] args) throws InterruptedException {
        // 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
        Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(new RealtimeRecognitionTask());
        executorService.shutdown();
        executorService.awaitTermination(1, TimeUnit.MINUTES);
        System.exit(0);
    }
}

class RealtimeRecognitionTask implements Runnable {
    @Override
    public void run() {
        RecognitionParam param = RecognitionParam.builder()
                .model("qwen-audio-3.0-asr-flash-streaming")
                // 新加坡地區和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                // 若沒有配置環境變數,請用百鍊API Key將下行替換為:.apiKey("sk-xxx")
                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                .format("pcm")
                .sampleRate(16000)
                .build();
        Recognition recognizer = new Recognition();

        ResultCallback<RecognitionResult> callback = new ResultCallback<RecognitionResult>() {
            @Override
            public void onEvent(RecognitionResult result) {
                if (result.isSentenceEnd()) {
                    System.out.println("Final Result: " + result.getSentence().getText());
                } else {
                    System.out.println("Intermediate Result: " + result.getSentence().getText());
                }
            }

            @Override
            public void onComplete() {
                System.out.println("Recognition complete");
            }

            @Override
            public void onError(Exception e) {
                System.out.println("RecognitionCallback error: " + e.getMessage());
            }
        };
        try {
            recognizer.call(param, callback);
            // 建立音頻格式
            AudioFormat audioFormat = new AudioFormat(16000, 16, 1, true, false);
            // 根據格式匹配預設錄音裝置
            TargetDataLine targetDataLine =
                    AudioSystem.getTargetDataLine(audioFormat);
            targetDataLine.open(audioFormat);
            // 開始錄音
            targetDataLine.start();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            long start = System.currentTimeMillis();
            // 錄音50s並進行即時轉寫
            while (System.currentTimeMillis() - start < 50000) {
                int read = targetDataLine.read(buffer.array(), 0, buffer.capacity());
                if (read > 0) {
                    buffer.limit(read);
                    // 將錄音音頻資料發送給流式識別服務
                    recognizer.sendAudioFrame(buffer);
                    buffer = ByteBuffer.allocate(1024);
                    // 錄音速率有限,防止cpu佔用過高,休眠一小會兒
                    Thread.sleep(20);
                }
            }
            recognizer.stop();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 任務結束後關閉 Websocket 串連
            recognizer.getDuplexApi().close(1000, "bye");
        }

        System.out.println(
                "[Metric] requestId: "
                        + recognizer.getLastRequestId()
                        + ", first package delay ms: "
                        + recognizer.getFirstPackageDelay()
                        + ", last package delay ms: "
                        + recognizer.getLastPackageDelay());
    }
}

識別配置

Qwen3-ASR-Flash-Realtime 互動模式

Qwen3-ASR-Flash-Realtime Realtime API 提供兩種互動模式:
  • VAD 模式(預設):服務端自動檢測語音的起點和終點(斷句),適用於即時對話、會議記錄等情境。啟用方式:配置 session.turn_detection 參數(預設啟用)。
  • Manual 模式:由用戶端通過發送 input_audio_buffer.commit 控制斷句,適用於需要明確控制發送時機的情境(如聊天軟體發送語音)。啟用方式:將 session.turn_detection 設為 null。
切換互動模式
  • WebSocket:通過 session.update 事件中的 turn_detection 欄位設定。
{
    "type": "session.update",
    "session": {
        "turn_detection": null
    }
}
  • Python SDK:在 update_session 方法中通過 enable_turn_detection 參數設定。
conversation.update_session(
    enable_turn_detection=False
)
  • Java SDK:通過 OmniRealtimeConfig.builder() 設定 enableTurnDetection 參數。
OmniRealtimeConfig config = OmniRealtimeConfig.builder()
        .enableTurnDetection(false)
        .build();
conversation.updateSession(config);
完整的 SDK 程式碼範例請參見Python SDKJava SDK。WebSocket 事件生命週期請參見事件互動流程

VAD 斷句配置

VAD(Voice Activity Detection,語音活動檢測)用於判定一段連續語音何時結束,從而觸發"最終識別結果"事件。三類模型均預設啟用服務端 VAD,但參數命名與可調粒度不同:
  • Qwen-Audio-3.0-ASR-Flash-Streaming / Fun-ASR-Realtime / Paraformer:通過 max_sentence_silence(VAD 斷句靜音閾值,毫秒)配置。當一段語音後的靜音時間長度超過該閾值時,系統判定該句子已結束。
  • Qwen3-ASR-Flash-Realtime:通過 session.turn_detection 配置,含 silence_duration_ms(靜音持續時間長度閾值,超過則判定 turn 結束,服務端預設 800,對話和聊天等需快速斷句的情境推薦設為 400)與 threshold(VAD 檢測靈敏度,服務端預設 0.2)。Qwen3-ASR-Flash-Realtime 還支援關閉 VAD 改用用戶端 commit 控制斷句的 Manual 模式,詳見上文 Qwen3-ASR-Flash-Realtime 互動模式
參數名因協議而異(同一含義在 Qwen-Audio-3.0-ASR-Flash-Streaming / Fun-ASR-Realtime / Paraformer 中稱 max_sentence_silence,在 Qwen3-ASR-Flash-Realtime 中稱 silence_duration_ms)。完整欄位定義請參見API參考

進階功能

使用熱詞提升準確率

支援通過熱詞提升特定詞彙(品牌名、人名、專有術語等)的識別準確率。 詳細的熱詞配置方法和使用說明,請參見提升識別準確率

使用上下文增強提升準確率

支援上下文增強功能,可將對話歷史或領域術語傳入 ASR 模型,顯著提升專有詞彙的轉寫準確率。詳細的使用方法和效果樣本,請參見上下文增強

擷取時間戳記

Qwen-Audio-3.0-ASR-Flash-Streaming、Fun-ASR-Realtime 和 Paraformer 系列模型預設輸出句級字級兩種粒度的時間戳記,便於字幕對齊、關鍵詞高亮、卡拉 OK 跟讀等情境。Qwen3-ASR-Flash-Realtime(qwen3-asr-flash-realtime)當前不返回時間戳記資訊,如需時間戳記請使用 Qwen-Audio-3.0-ASR-Flash-Streaming、Fun-ASR-Realtime 或 Paraformer。Qwen ASR 的錄音檔案轉寫模型 qwen3-asr-flash-filetrans 支援字級時間戳記,詳見非即時語音辨識 時間戳記單位均為毫秒,分兩個層級返回:
  • 句級payload.output.sentence.begin_timepayload.output.sentence.end_time,標識整句在音頻中的起止時刻。中間結果中 end_time 可能為 null,待句子結束(sentence_end = true)時填充最終值。
  • 字級payload.output.sentence.words 數組,每個元素包含 begin_timeend_timetext(該字/詞文本)以及 punctuation(該字後跟隨的標點,無則為空白串)。
返回結構樣本(節選):
{
  "payload": {
    "output": {
      "sentence": {
        "begin_time": 170,
        "end_time": 920,
        "text": "好,我知道了",
        "sentence_end": true,
        "words": [
          { "begin_time": 170, "end_time": 295, "text": "好", "punctuation": "," },
          { "begin_time": 295, "end_time": 503, "text": "我", "punctuation": "" },
          { "begin_time": 503, "end_time": 711, "text": "知道", "punctuation": "" },
          { "begin_time": 711, "end_time": 920, "text": "了", "punctuation": "" }
        ]
      }
    }
  }
}
以上欄位名以 WebSocket JSON 路徑為準。不同 SDK 暴露上述欄位的命名習慣不同(如字典 key、對象屬性、getter 方法等),完整欄位對照請參見各 SDK 的 API 參考。 完整欄位定義請參見API參考

情感識別

Qwen3-ASR-Flash-Realtime 與 Paraformer 部分模型可在轉寫結果中附帶說話人的情緒狀態,但兩者輸出粒度與開啟方式不同。 Qwen3-ASR-Flash-Realtime(qwen3-asr-flash-realtime):固定開啟,無需配置。在 conversation.item.input_audio_transcription.textconversation.item.input_audio_transcription.completed 事件中均通過頂層 emotion 欄位返回,取值為 7 類細粒度情緒:surprised(驚訝)、neutral(平靜)、happy(愉快)、sad(悲傷)、disgusted(厭惡)、angry(憤怒)、fearful(恐懼)。
{
  "type": "conversation.item.input_audio_transcription.text",
  "emotion": "neutral",
  "text": "今天天氣不錯",
  "stash": ""
}
Paraformer(paraformer-realtime-8k-v2):僅此一款 Paraformer 模型支援情感識別,結果通過 payload.output.sentence.emo_tagpayload.output.sentence.emo_confidence 返回,取值為 3 類極性:positive(正面,如開心、滿意)、negative(負面,如憤怒、沉悶)、neutral(無明顯情感),信賴度範圍 [0.0, 1.0]。 情感識別需同時滿足以下條件才會輸出:
  • 模型為 paraformer-realtime-8k-v2
  • 語義斷句關閉:semantic_punctuation_enabled = false(預設即為 false,無需特別設定)。
  • 僅在 sentence_end = true 的句子結束事件中返回。
如不希望返回情感識別欄位,可將 semantic_punctuation_enabled 設為 true,此時將啟用語義斷句、不再返回 emo_tagemo_confidence 欄位。 以上欄位名以 WebSocket JSON 路徑為準。不同 SDK 暴露上述欄位的命名習慣不同(如字典 key、對象屬性、getter 方法等),完整欄位對照請參見各 SDK 的 API 參考。 完整欄位定義、取值約束與樣本請參見API參考

敏感詞過濾

敏感詞過濾可對識別結果中的敏感詞執行替換或移除,適用於客服質檢、內容合規、字幕審核等情境。 支援範圍:僅Qwen-Audio-3.0-ASR-Flash-Streaming和Fun-ASR-Realtime。 使用限制:最多支援設定32個敏感詞。 預設行為:未傳入 special_word_filter 參數時,不會對敏感詞進行過濾。 如何配置:special_word_filter 是 JSON 對象,包含三個子欄位:
  • filter_with_signed.word_list:字串數組,列出需要被替換為等長 * 的敏感詞。例如 ["測試"],「幫我測試一下」會變成「幫我**一下」。
  • filter_with_empty.word_list:字串數組,列出需要從結果中完全移除的敏感詞。例如 ["開始"],「比賽這就要開始了嗎」會變成「比賽這就要了嗎」。
  • system_reserved_filter:布爾值,預設 false。是否啟用敏感詞過濾功能。
配置樣本:
{
  "special_word_filter": {
    "filter_with_signed": {
      "word_list": ["測試"]
    },
    "filter_with_empty": {
      "word_list": ["開始", "發生"]
    },
    "system_reserved_filter": true
  }
}
不同 SDK 暴露上述參數的命名習慣不同(如字典 key、對象屬性、方法等),完整欄位對照請參見 API參考。

WebSocket 原始協議調用

以下樣本展示如何通過 WebSocket 原始協議直連服務端,適用於不使用 DashScope SDK 的情境。此為最小可運行實現,WebSocket 通訊協定請參見各模型的 API參考
  • Qwen-Audio-3.0-ASR-Flash-Streaming/ Fun-ASR-Realtime
  • Qwen3-ASR-Flash-Realtime
  • Paraformer
  • Python
  • Java
  • Node.js
  • C#
  • PHP
  • Go
在運行樣本前,請確保已使用以下命令安裝依賴:
pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client
請不要將範例程式碼檔案命名為 websocket.py,這會與 websocket 庫產生命名衝突,導致如下錯誤:AttributeError: module 'websocket' has no attribute 'WebSocketApp'. Did you mean: 'WebSocket'?
# pip install websocket-client
import os
import json
import time
import uuid
import threading
import websocket

# 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:api_key = "sk-xxx"
api_key = os.environ.get('DASHSCOPE_API_KEY')
# 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'  # WebSocket伺服器位址
audio_file = '{YOUR_AUDIO_FILE}'  # 替換為您的音頻檔案路徑

# 產生32位隨機ID
TASK_ID = uuid.uuid4().hex[:32]

task_started = False  # 標記任務是否已啟動

# 發送run-task指令
def send_run_task(ws):
    run_task_message = {
        'header': {
            'action': 'run-task',
            'task_id': TASK_ID,
            'streaming': 'duplex'
        },
        'payload': {
            'task_group': 'audio',
            'task': 'asr',
            'function': 'recognition',
            'model': 'qwen-audio-3.0-asr-flash-streaming',
            'parameters': {
                'sample_rate': 16000,
                'format': 'wav'
            },
            'input': {}
        }
    }
    ws.send(json.dumps(run_task_message))

# 發送finish-task指令
def send_finish_task(ws):
    finish_task_message = {
        'header': {
            'action': 'finish-task',
            'task_id': TASK_ID,
            'streaming': 'duplex'
        },
        'payload': {
            'input': {}
        }
    }
    ws.send(json.dumps(finish_task_message))

# 發送音頻流(每100ms發送一個二進位chunk)
def send_audio_stream(ws):
    chunk_size = 3200  # 100ms @ 16kHz 16bit 單聲道
    try:
        with open(audio_file, 'rb') as f:
            while True:
                chunk = f.read(chunk_size)
                if not chunk:
                    break
                ws.send(chunk, opcode=websocket.ABNF.OPCODE_BINARY)
                time.sleep(0.1)
        print('音頻流結束')
        send_finish_task(ws)
    except Exception as e:
        print('讀取音頻檔案錯誤:', e)
        ws.close()

# 串連開啟時發送run-task指令
def on_open(ws):
    print('串連到伺服器')
    send_run_task(ws)

# 接收訊息處理
def on_message(ws, data):
    global task_started
    message = json.loads(data)
    event = message['header']['event']
    if event == 'task-started':
        print('任務開始')
        task_started = True
        threading.Thread(target=send_audio_stream, args=(ws,), daemon=True).start()
    elif event == 'result-generated':
        print('識別結果:', message['payload']['output']['sentence']['text'])
        if message['payload'].get('usage'):
            print('任務計費時間長度(秒):', message['payload']['usage']['duration'])
    elif event == 'task-finished':
        print('任務完成')
        ws.close()
    elif event == 'task-failed':
        print('任務失敗:', message['header'].get('error_message'))
        ws.close()
    else:
        print('未知事件:', event)

# 如果沒有收到task-started事件,關閉串連
def on_close(ws, close_status_code, close_msg):
    if not task_started:
        print('任務未啟動,關閉串連')

# 錯誤處理
def on_error(ws, error):
    print('WebSocket錯誤:', error)

if __name__ == '__main__':
    ws = websocket.WebSocketApp(
        url,
        header={'Authorization': f'bearer {api_key}'},
        on_open=on_open,
        on_message=on_message,
        on_error=on_error,
        on_close=on_close
    )
    ws.run_forever()

應用於生產環境

串連複用(WebSocket)

Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime 和 Paraformer 的 WebSocket 串連支援複用:一個識別任務結束後,無需重建立立串連即可開啟下一個任務。 複用流程:用戶端發送 finish-task,服務端返回 task-finished 後,可重新發送 run-task 開啟新任務。
  1. 必須等服務端返回 task-finished 事件後才可發起新任務。
  2. 複用串連中的不同任務需要使用不同的 task_id
  3. 任務失敗時服務端返回錯誤事件並關閉串連,該串連不可複用。
  4. 任務結束後 60 秒無新任務,串連自動斷開。
Qwen3-ASR-Flash-Realtime 採用會話模式,每次會話結束後需主動中斷連線,不支援串連複用。 各模型事件說明請參見對應的API參考

高並發最佳實務

DashScope SDK 內建池化機制,可複用 WebSocket 串連和識別對象,避免頻繁建立銷毀帶來的開銷。
目前僅 Paraformer Java SDK 支援此功能。

前提條件

Java SDK 通過內建的串連池和自訂的對象池協同工作,實現最佳效能:
  • 串連池:SDK 內部整合的 OkHttp3 串連池,負責管理和複用底層的 WebSocket 串連,減少網路握手開銷。此功能預設開啟。
  • 對象池:基於 commons-pool2 實現,用於維護一組已預先建立好串連的 Recognition 對象。從池中擷取對象可消除串連建立的延遲,顯著降低首包延遲。

實現步驟

  1. 添加依賴 根據專案構建工具,在依賴設定檔中添加 dashscope-sdk-java 和 commons-pool2。 以 Maven 和 Gradle 為例,配置如下:
    • Maven
    • Gradle
    1. 開啟 Maven 專案的 pom.xml 檔案。
    2. <dependencies> 標籤內添加以下依賴資訊。
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dashscope-sdk-java</artifactId>
        <!-- 請將 'the-latest-version' 替換為2.16.9及以上版本,可在如下連結查詢相關版本號碼:https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java -->
        <version>the-latest-version</version>
    </dependency>
    
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <!-- 請將 'the-latest-version' 替換為最新版本,可在如下連結查詢相關版本號碼:https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
        <version>the-latest-version</version>
    </dependency>
    
    1. 儲存 pom.xml 檔案。
    2. 使用 Maven 命令(如 mvn clean installmvn compile)來更新專案依賴。
  2. 配置串連池 通過環境變數配置串連池關鍵參數:

    環境變數

    描述

    DASHSCOPE_CONNECTION_POOL_SIZE

    串連池大小。

    推薦值:峰值並發數的 2 倍以上。

    預設值:32。

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS

    最大非同步請求數。

    推薦值:與 DASHSCOPE_CONNECTION_POOL_SIZE 保持一致。

    預設值:32。

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST

    單主機最大非同步請求數。

    推薦值:與 DASHSCOPE_CONNECTION_POOL_SIZE 保持一致。

    預設值:32。

  3. 設定物件池 通過環境變數設定物件池大小:

    環境變數

    描述

    RECOGNITION_OBJECTPOOL_SIZE

    對象池大小。

    推薦值:峰值並發數的 1.5 至 2 倍。

    預設值:500。

    • 對象池的大小(RECOGNITION_OBJECTPOOL_SIZE)必須小於或等於串連池的大小(DASHSCOPE_CONNECTION_POOL_SIZE)。否則,當對象池請求對象時,若串連池已滿,會導致調用線程阻塞。
    • 對象池大小不應超過賬戶的 QPS(每秒查詢率)限制。
    通過如下代碼建立對象池:
class RecognitionObjectPool {
    // ……完整樣本請參見完整代碼
    public static GenericObjectPool<Recognition> getInstance() {
        lock.lock();
        if (recognitionGenericObjectPool == null) {
            int objectPoolSize = getObjectivePoolSize();
            RecognitionObjectFactory recognitionObjectFactory =
                    new RecognitionObjectFactory();
            GenericObjectPoolConfig<Recognition> config =
                    new GenericObjectPoolConfig<>();
            config.setMaxTotal(objectPoolSize);
            config.setMaxIdle(objectPoolSize);
            config.setMinIdle(objectPoolSize);
            recognitionGenericObjectPool =
                    new GenericObjectPool<>(recognitionObjectFactory, config);
        }
        lock.unlock();
        return recognitionGenericObjectPool;
    }
}
  1. 從對象池中擷取 Recognition 對象 未歸還的對象數量超過對象池上限時,系統會額外建立新的 Recognition 對象。這類新對象需重建立立 WebSocket 串連,無法複用。
recognizer = RecognitionObjectPool.getInstance().borrowObject();
  1. 進行語音辨識 調用 Recognition 對象的 call 或 streamCall 方法進行語音辨識。
  2. 歸還 Recognition 對象 語音辨識任務結束後,歸還 Recognition 對象以供複用。不要歸還未完成任務或任務失敗的對象。
RecognitionObjectPool.getInstance().returnObject(recognizer);

完整代碼

package org.alibaba.bailian.example.examples;

import com.alibaba.dashscope.audio.asr.recognition.Recognition;
import com.alibaba.dashscope.audio.asr.recognition.RecognitionParam;
import com.alibaba.dashscope.audio.asr.recognition.RecognitionResult;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.alibaba.dashscope.utils.ApiKey;
import org.apache.commons.pool2.BasePooledObjectFactory;
import org.apache.commons.pool2.PooledObject;
import org.apache.commons.pool2.impl.DefaultPooledObject;
import org.apache.commons.pool2.impl.GenericObjectPool;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;

import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import com.alibaba.dashscope.utils.Constants;

public class Main {
    public static void checkoutEnv(String envName, int defaultSize) {
        if (System.getenv(envName) != null) {
            System.out.println("[ENV CHECK]: " + envName + " "
                    + System.getenv(envName));
        } else {
            System.out.println("[ENV CHECK]: " + envName
                    + " Using Default which is " + defaultSize);
        }
    }

    public static void main(String[] args)
            throws NoApiKeyException, InterruptedException {
        // 以下為華北2(北京)地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
        Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1";
        checkoutEnv("DASHSCOPE_CONNECTION_POOL_SIZE", 32);
        checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS", 32);
        checkoutEnv("DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST", 32);
        checkoutEnv(RecognitionObjectPool.RECOGNITION_OBJECTPOOL_SIZE_ENV,
                RecognitionObjectPool.DEFAULT_OBJECT_POOL_SIZE);

        int threadNums = 3;
        String currentDir = System.getProperty("user.dir");
        Path[] filePaths = {
                Paths.get(currentDir, "{YOUR_AUDIO_FILE}"),
                Paths.get(currentDir, "{YOUR_AUDIO_FILE}"),
                Paths.get(currentDir, "{YOUR_AUDIO_FILE}"),
        };
        ExecutorService executorService = Executors.newFixedThreadPool(threadNums);
        for (int i = 0; i < threadNums; i++) {
            executorService.submit(new RealtimeRecognizeTask(filePaths));
        }
        executorService.shutdown();
        executorService.awaitTermination(10, TimeUnit.MINUTES);
        System.exit(0);
    }
}

class RecognitionObjectFactory extends BasePooledObjectFactory<Recognition> {
    public RecognitionObjectFactory() {
        super();
    }

    @Override
    public Recognition create() throws Exception {
        return new Recognition();
    }

    @Override
    public PooledObject<Recognition> wrap(Recognition obj) {
        return new DefaultPooledObject<>(obj);
    }
}

class RecognitionObjectPool {
    public static GenericObjectPool<Recognition> recognitionGenericObjectPool;
    public static String RECOGNITION_OBJECTPOOL_SIZE_ENV =
            "RECOGNITION_OBJECTPOOL_SIZE";
    public static int DEFAULT_OBJECT_POOL_SIZE = 500;
    private static Lock lock = new java.util.concurrent.locks.ReentrantLock();

    public static int getObjectivePoolSize() {
        try {
            Integer n = Integer.parseInt(
                    System.getenv(RECOGNITION_OBJECTPOOL_SIZE_ENV));
            return n;
        } catch (NumberFormatException e) {
            return DEFAULT_OBJECT_POOL_SIZE;
        }
    }

    public static GenericObjectPool<Recognition> getInstance() {
        lock.lock();
        if (recognitionGenericObjectPool == null) {
            int objectPoolSize = getObjectivePoolSize();
            System.out.println("RECOGNITION_OBJECTPOOL_SIZE: "
                    + objectPoolSize);
            RecognitionObjectFactory recognitionObjectFactory =
                    new RecognitionObjectFactory();
            GenericObjectPoolConfig<Recognition> config =
                    new GenericObjectPoolConfig<>();
            config.setMaxTotal(objectPoolSize);
            config.setMaxIdle(objectPoolSize);
            config.setMinIdle(objectPoolSize);
            recognitionGenericObjectPool =
                    new GenericObjectPool<>(recognitionObjectFactory, config);
        }
        lock.unlock();
        return recognitionGenericObjectPool;
    }
}

class RealtimeRecognizeTask implements Runnable {
    private static final Object lock = new Object();
    private Path[] filePaths;

    public RealtimeRecognizeTask(Path[] filePaths) {
        this.filePaths = filePaths;
    }

    private static String getDashScopeApiKey() throws NoApiKeyException {
        String dashScopeApiKey = null;
        try {
            ApiKey apiKey = new ApiKey();
            dashScopeApiKey = ApiKey.getApiKey(null);
        } catch (NoApiKeyException e) {
            System.out.println("No API key found in environment.");
        }
        if (dashScopeApiKey == null) {
            dashScopeApiKey = "your-dashscope-apikey";
        }
        return dashScopeApiKey;
    }

    public void runCallback() {
        for (Path filePath : filePaths) {
            RecognitionParam param = null;
            try {
                param = RecognitionParam.builder()
                        .model("paraformer-realtime-v2")
                        .format("pcm")
                        .sampleRate(16000)
                        .apiKey(getDashScopeApiKey())
                        .build();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            Recognition recognizer = null;
            final boolean[] hasError = {false};
            try {
                recognizer = RecognitionObjectPool.getInstance().borrowObject();
                String threadName = Thread.currentThread().getName();

                ResultCallback<RecognitionResult> callback =
                        new ResultCallback<RecognitionResult>() {
                            @Override
                            public void onEvent(RecognitionResult message) {
                                synchronized (lock) {
                                    if (message.isSentenceEnd()) {
                                        System.out.println("[process " + threadName
                                                + "] Fix:" + message.getSentence().getText());
                                    } else {
                                        System.out.println("[process " + threadName
                                                + "] Result: " + message.getSentence().getText());
                                    }
                                }
                            }

                            @Override
                            public void onComplete() {
                                System.out.println("[" + threadName
                                        + "] Recognition complete");
                            }

                            @Override
                            public void onError(Exception e) {
                                System.out.println("[" + threadName
                                        + "] RecognitionCallback error: " + e.getMessage());
                                hasError[0] = true;
                            }
                        };
                System.out.println("[" + threadName
                        + "] Input file_path is: " + filePath);
                FileInputStream fis = null;
                try {
                    fis = new FileInputStream(filePath.toFile());
                } catch (Exception e) {
                    System.out.println("Error when loading file: " + filePath);
                    e.printStackTrace();
                }
                recognizer.call(param, callback);

                // chunk size set to 100 ms for 16KHz sample rate
                byte[] buffer = new byte[3200];
                int bytesRead;
                while ((bytesRead = fis.read(buffer)) != -1) {
                    ByteBuffer byteBuffer;
                    if (bytesRead < buffer.length) {
                        byteBuffer = ByteBuffer.wrap(buffer, 0, bytesRead);
                    } else {
                        byteBuffer = ByteBuffer.wrap(buffer);
                    }
                    recognizer.sendAudioFrame(byteBuffer);
                    Thread.sleep(100);
                    buffer = new byte[3200];
                }
                System.out.println("[" + threadName + "] send audio done");
                recognizer.stop();
                System.out.println("[" + threadName + "] asr task finished");
            } catch (Exception e) {
                e.printStackTrace();
                hasError[0] = true;
            }
            if (recognizer != null) {
                try {
                    if (hasError[0] == true) {
                        recognizer.getDuplexApi().close(1000, "bye");
                        RecognitionObjectPool.getInstance()
                                .invalidateObject(recognizer);
                    } else {
                        RecognitionObjectPool.getInstance()
                                .returnObject(recognizer);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public void run() {
        runCallback();
    }
}

推薦配置

以下配置基於在指定規格的阿里雲伺服器上僅運行 Paraformer 即時語音辨識服務的測試結果。其中單機並發數指的是同一時刻正在啟動並執行 Paraformer 即時語音辨識任務數(即背景工作執行緒數)。

機器配置(阿里雲)

單機最大並發數

對象池大小

串連池大小

4核8GiB

100

500

2000

8核16GiB

200

500

2000

16核32GiB

400

500

2000

資源管理與異常處理

  • 任務成功:必須調用 GenericObjectPool.returnObject() 將 Recognition 對象歸還到池中以便複用。
    不要歸還未完成任務或任務失敗的 Recognition 對象。
  • 任務失敗:當 SDK 內部或商務邏輯拋出異常導致任務中斷時,必須執行以下兩個操作:
    1. 主動關閉底層的 WebSocket 串連
    2. 從對象池中廢棄該對象,防止被再次使用
// 關閉串連
recognizer.getDuplexApi().close(1000, "bye");
// 在對象池中廢棄出現異常的 recognizer
RecognitionObjectPool.getInstance().invalidateObject(recognizer);
  • 在服務出現 TaskFailed 報錯時,不需要額外處理。

調用預熱與耗時統計

在對 DashScope Java SDK 進行並發調用延遲等效能評估時,建議在正式測試前執行充分的預熱操作。
串連複用機制
DashScope Java SDK 通過全域單例的串連池管理和複用 WebSocket 串連。該機制的工作特點如下:
  • 按需建立:SDK 不會在服務啟動時預建立 WebSocket 串連,而是在首次調用時按需建立。
  • 限時複用:請求完成後,串連將在池中保留最多 60 秒以備複用。
    • 若 60 秒內有新請求,將複用現有串連,避免重複握手開銷。
    • 若串連空閑超過 60 秒,將被自動關閉以釋放資源。
預熱的重要性
在以下情境中,串連池中可能沒有可複用的活躍串連,導致請求需要建立串連:
  • 應用剛啟動,尚未發起任何調用。
  • 服務空閑時間超過 60 秒,池中串連已因逾時而關閉。
在這些情境下,首次或初期請求會觸發完整的 WebSocket 建連過程(包括 TCP 握手、TLS 加密協商和協議升級),其端到端延遲會顯著高於後續複用串連的請求。
推薦做法
在正式進行效能壓測或延遲統計前,請遵循以下預熱步驟:
  1. 類比正式測試的並發層級,提前發起一定數量的調用(例如,持續 1~2 分鐘),以充分填充串連池。
  2. 確認串連池已建立並維持足夠的活躍串連後,再開始正式的效能資料採集。

提升識別效果

  • 選擇匹配採樣率的模型:8kHz 電話音頻直接使用 8kHz 模型,避免升採樣到 16kHz 造成的資訊失真。
  • 最佳化輸入音頻品質:使用高品質麥克風,確保錄音環境信噪比高、無回聲。可在應用程式層整合降噪(如 RNNoise)、回聲消除(AEC)等演算法做預先處理。

設定容錯策略

  • 用戶端重連:用戶端應實現斷線自動重連機制,以應對網路抖動。Python SDK 參考實現如下:
    1. 捕獲異常:在Callback類中實現on_error方法。當dashscope SDK遇到網路錯誤或其他問題時,會調用該方法。
    2. 狀態通知:當on_error被觸發時,設定重連訊號。在Python中可以使用threading.Event,它是一種安全執行緒的訊號標誌。
    3. 重連迴圈:將主邏輯包裹在一個for迴圈中(例如重試3次)。當檢測到重連訊號後,當前輪次的識別會中斷,清理資源,然後等待幾秒鐘,再次進入迴圈,建立一個全新的串連。
  • 設定心跳防止串連斷開:當需要與服務端保持長串連時,可將參數heartbeat設定為true,即使音頻中長時間沒有聲音,與服務端的串連也不會中斷。
  • 模型限流:在調用模型介面時請注意模型的限流規則。

支援的模型與地區

  • 新加坡
  • 華北2(北京)
調用以下模型時,請選擇新加坡地區的API Key
  • Qwen-Audio-3.0-ASR-Flash-Streaming:qwen-audio-3.0-asr-flash-streaming
  • Fun-ASR-Realtime:fun-asr-realtime(穩定版,當前等同fun-asr-realtime-2025-11-07)、fun-asr-realtime-2025-11-07(快照版)
  • Qwen3-ASR-Flash-Realtime:qwen3-asr-flash-realtime(穩定版,當前等同qwen3-asr-flash-realtime-2025-10-27)、qwen3-asr-flash-realtime-2026-02-10(最新快照版)、qwen3-asr-flash-realtime-2025-10-27(快照版)

API參考

常見問題

即時語音辨識支援哪些音頻格式?

Qwen-Audio-3.0-ASR-Flash-Streaming、Fun-ASR-Realtime 和 Paraformer 模型支援 pcm、wav、mp3、opus、speex、aac、amr 格式。Qwen3-ASR-Flash-Realtime 模型推薦使用 pcm 或 opus 格式;其他格式(如 wav、aac、amr)雖然在 session.update 校正層會被接受,但服務端實際解碼可能失敗,請務必確認音頻流為推薦格式後再發送。

SDK 和 WebSocket API 有什麼區別?該如何選擇?

DashScope SDK 封裝了 WebSocket 串連管理、鑒權、重連等細節,適合快速整合。WebSocket API 直連提供更細粒度的控制能力,適用於 SDK 未覆蓋的程式設計語言或需要自訂串連管理的情境。推薦優先使用 SDK。

如何提升專有名詞的識別準確率?

使用熱詞或上下文增強。詳細的配置方法和使用說明,請參見提升識別準確率

串連經常斷開怎麼辦?

建議實現用戶端重連機制,並開啟心跳參數(heartbeat=true)防止長時間無音頻導致串連斷開。詳細的容錯策略請參見應用於生產環境
Token Plan
模型體驗
用量統計與效能監控
資產中心
服務支援