即時語音辨識服務接收音頻流並即時轉寫為帶標點的文本,適用於直播字幕、線上會議、語音交談、智能助手等情境。
概述
實現低延遲音頻到文本轉換。
- 支援普通話及粵語、四川話等多種方言的高精度語音辨識
- 具備應對複雜聲學環境的能力,支援自動語種檢測與智能非人聲過濾
- 支援驚訝、平靜、愉快、悲傷、厭惡、憤怒、恐懼等多種情緒狀態識別
- 支援熱詞定製,可提升特定詞彙的識別準確率
- 支援上下文增強,通過配置上下文提高識別準確率
- 支援時間戳記輸出,產生結構化識別結果
- 靈活採樣率與多種音頻格式,適配不同錄音環境
批量情境(會議轉寫、通話分析、字幕產生等)可使用非即時語音辨識。各模型選型建議請參見語音辨識。
前提條件
- 已擷取與配置 API Key並將其配置到環境變數。
- 如果通過 DashScope SDK 調用,需要安裝最新版SDK。
- 如果通過 AOQ 協議接入 Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime,需要下載並整合 AOQ 用戶端 SDK,詳見AOQ SDK 簡介。
快速開始
以下樣本展示如何通過 DashScope SDK 快速調用即時語音辨識服務。
Qwen-Audio-3.0-ASR-Flash-Streaming/ Fun-ASR -Realtime
該模型除 WebSocket 通訊協定外,還支援通過 AOQ 協議接入;如果是用戶端對接,且更看重穩定的延遲、弱網下的互動能力、即時雙工的降噪與回聲消除,可優先考慮 AOQ,協議對比與選型請參見模型/應用支援力度。
識別傳入麥克風的語音
識別麥克風傳入的語音並即時輸出文本,實現"邊說邊出字"的效果。
Java
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());
}
}
Python
運行Python樣本前,需要通過pip install pyaudio命令安裝第三方音頻播放與採集套件。
import os
import signal # for keyboard events handling (press "Ctrl+C" to terminate recording)
import sys
import dashscope
import pyaudio
from dashscope.audio.asr import *
mic = None
stream = None
# Set recording parameters
sample_rate = 16000 # sampling rate (Hz)
channels = 1 # mono channel
dtype = 'int16' # data type
format_pcm = 'pcm' # the format of the audio data
block_size = 3200 # number of frames per buffer
# Real-time speech recognition callback
class Callback(RecognitionCallback):
def on_open(self) -> None:
global mic
global stream
print('RecognitionCallback open.')
mic = pyaudio.PyAudio()
stream = mic.open(format=pyaudio.paInt16,
channels=1,
rate=16000,
input=True)
def on_close(self) -> None:
global mic
global stream
print('RecognitionCallback close.')
stream.stop_stream()
stream.close()
mic.terminate()
stream = None
mic = None
def on_complete(self) -> None:
print('RecognitionCallback completed.') # recognition completed
def on_error(self, message) -> None:
print('RecognitionCallback task_id: ', message.request_id)
print('RecognitionCallback error: ', message.message)
# Stop and close the audio stream if it is running
if 'stream' in globals() and stream.is_active():
stream.stop_stream()
stream.close()
# Forcefully exit the program
sys.exit(1)
def on_event(self, result: RecognitionResult) -> None:
sentence = result.get_sentence()
if 'text' in sentence:
print('RecognitionCallback text: ', sentence['text'])
if RecognitionResult.is_sentence_end(sentence):
print(
'RecognitionCallback sentence end, request_id:%s, usage:%s'
% (result.get_request_id(), result.get_usage(sentence)))
def signal_handler(sig, frame):
print('Ctrl+C pressed, stop recognition ...')
# Stop recognition
recognition.stop()
print('Recognition stopped.')
print(
'[Metric] requestId: {}, first package delay ms: {}, last package delay ms: {}'
.format(
recognition.get_last_request_id(),
recognition.get_first_package_delay(),
recognition.get_last_package_delay(),
))
# Forcefully exit the program
sys.exit(0)
# main function
if __name__ == '__main__':
# 新加坡地區和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# 若沒有配置環境變數,請用百鍊API Key將下行替換為:dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
# Create the recognition callback
callback = Callback()
# Call recognition service by async mode, you can customize the recognition parameters, like model, format,
# sample_rate
recognition = Recognition(
model='qwen-audio-3.0-asr-flash-streaming',
format=format_pcm,
# 'pcm'、'wav'、'opus'、'speex'、'aac'、'amr', you can check the supported formats in the document
sample_rate=sample_rate,
# support 8000, 16000
semantic_punctuation_enabled=False,
callback=callback)
# Start recognition
recognition.start()
signal.signal(signal.SIGINT, signal_handler)
print("Press 'Ctrl+C' to stop recording and recognition...")
# Create a keyboard listener until "Ctrl+C" is pressed
while True:
if stream:
data = stream.read(3200, exception_on_overflow=False)
recognition.send_audio_frame(data)
else:
break
recognition.stop()
識別本地音頻檔案
識別本地音頻檔案並輸出結果,適用於對話聊天、控制口令、語音輸入法、語音搜尋等較短的准即時情境。
import com.alibaba.dashscope.api.GeneralApi;
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.base.HalfDuplexParamBase;
import com.alibaba.dashscope.common.GeneralListParam;
import com.alibaba.dashscope.common.ResultCallback;
import com.alibaba.dashscope.protocol.GeneralServiceOption;
import com.alibaba.dashscope.protocol.HttpMethod;
import com.alibaba.dashscope.protocol.Protocol;
import com.alibaba.dashscope.protocol.StreamingMode;
import com.alibaba.dashscope.utils.Constants;
import java.io.FileInputStream;
import java.nio.ByteBuffer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
class TimeUtils {
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
public static String getTimestamp() {
return LocalDateTime.now().format(formatter);
}
}
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";
// 實際應用中,該方法僅在程式最開始執行一次即可,不必多次執行該方法。
warmUp();
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new RealtimeRecognitionTask(Paths.get(System.getProperty("user.dir"), "{YOUR_AUDIO_FILE}")));
executorService.shutdown();
// wait for all tasks to complete
executorService.awaitTermination(1, TimeUnit.MINUTES);
System.exit(0);
}
public static void warmUp() {
try {
// Lightweight GET request to establish connection
GeneralServiceOption warmupOption = GeneralServiceOption.builder()
.protocol(Protocol.HTTP)
.httpMethod(HttpMethod.GET)
.streamingMode(StreamingMode.OUT)
.path("assistants")
.build();
warmupOption.setBaseHttpUrl(Constants.baseHttpApiUrl);
GeneralApi<HalfDuplexParamBase> api = new GeneralApi<>();
api.get(GeneralListParam.builder().limit(1L).build(), warmupOption);
} catch (Exception e) {
// Reset flag to allow retry if pre-warming failed
}
}
}
class RealtimeRecognitionTask implements Runnable {
private Path filepath;
public RealtimeRecognitionTask(Path filepath) {
this.filepath = filepath;
}
@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("wav")
.sampleRate(16000)
.build();
Recognition recognizer = new Recognition();
String threadName = Thread.currentThread().getName();
ResultCallback<RecognitionResult> callback = new ResultCallback<RecognitionResult>() {
@Override
public void onEvent(RecognitionResult message) {
if (message.isSentenceEnd()) {
System.out.println(TimeUtils.getTimestamp()+" "+
"[process " + threadName + "] Final Result:" + message.getSentence().getText());
} else {
System.out.println(TimeUtils.getTimestamp()+" "+
"[process " + threadName + "] Intermediate Result: " + message.getSentence().getText());
}
}
@Override
public void onComplete() {
System.out.println(TimeUtils.getTimestamp()+" "+"[" + threadName + "] Recognition complete");
}
@Override
public void onError(Exception e) {
System.out.println(TimeUtils.getTimestamp()+" "+
"[" + threadName + "] RecognitionCallback error: " + e.getMessage());
}
};
try {
recognizer.call(param, callback);
// Please replace the path with your audio file path
System.out.println(TimeUtils.getTimestamp()+" "+"[" + threadName + "] Input file_path is: " + this.filepath);
// Read file and send audio by chunks
FileInputStream fis = new FileInputStream(this.filepath.toFile());
byte[] allData = new byte[fis.available()];
int ret = fis.read(allData);
fis.close();
int sendFrameLength = 3200;
for (int i = 0; i * sendFrameLength < allData.length; i ++) {
int start = i * sendFrameLength;
int end = Math.min(start + sendFrameLength, allData.length);
ByteBuffer byteBuffer = ByteBuffer.wrap(allData, start, end - start);
recognizer.sendAudioFrame(byteBuffer);
Thread.sleep(100);
}
System.out.println(TimeUtils.getTimestamp()+" "+LocalDateTime.now());
recognizer.stop();
} catch (Exception e) {
e.printStackTrace();
} finally {
// 任務結束後關閉 Websocket 串連
recognizer.getDuplexApi().close(1000, "bye");
}
System.out.println(
"["
+ threadName
+ "][Metric] requestId: "
+ recognizer.getLastRequestId()
+ ", first package delay ms: "
+ recognizer.getFirstPackageDelay()
+ ", last package delay ms: "
+ recognizer.getLastPackageDelay());
}
}
import os
import time
import dashscope
from dashscope.audio.asr import *
# 新加坡地區和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# 若沒有配置環境變數,請用百鍊API Key將下行替換為:dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
from datetime import datetime
def get_timestamp():
now = datetime.now()
formatted_timestamp = now.strftime("[%Y-%m-%d %H:%M:%S.%f]")
return formatted_timestamp
class Callback(RecognitionCallback):
def on_complete(self) -> None:
print(get_timestamp() + ' Recognition completed') # recognition complete
def on_error(self, result: RecognitionResult) -> None:
print('Recognition task_id: ', result.request_id)
print('Recognition error: ', result.message)
exit(0)
def on_event(self, result: RecognitionResult) -> None:
sentence = result.get_sentence()
if 'text' in sentence:
print(get_timestamp() + ' RecognitionCallback text: ', sentence['text'])
if RecognitionResult.is_sentence_end(sentence):
print(get_timestamp() +
'RecognitionCallback sentence end, request_id:%s, usage:%s'
% (result.get_request_id(), result.get_usage(sentence)))
callback = Callback()
recognition = Recognition(model='qwen-audio-3.0-asr-flash-streaming',
format='wav',
sample_rate=16000,
callback=callback)
try:
audio_data: bytes = None
f = open("{YOUR_AUDIO_FILE}", 'rb')
if os.path.getsize("{YOUR_AUDIO_FILE}"):
# 一次性將檔案資料全部讀入buffer
file_buffer = f.read()
f.close()
print("Start Recognition")
recognition.start()
# 從buffer中間隔3200位元組發送一次
buffer_size = len(file_buffer)
offset = 0
chunk_size = 3200
while offset < buffer_size:
# 計算本次要發送的資料區塊大小
remaining_bytes = buffer_size - offset
current_chunk_size = min(chunk_size, remaining_bytes)
# 從buffer中提取當前資料區塊
audio_data = file_buffer[offset:offset + current_chunk_size]
# 發送音頻資料幀
recognition.send_audio_frame(audio_data)
# 更新位移量
offset += current_chunk_size
# 添加延遲類比即時傳輸
time.sleep(0.1)
recognition.stop()
else:
raise Exception(
'The supplied file was empty (zero bytes long)')
except Exception as e:
raise e
print(
'[Metric] requestId: {}, first package delay ms: {}, last package delay ms: {}'
.format(
recognition.get_last_request_id(),
recognition.get_first_package_delay(),
recognition.get_last_package_delay(),
))
Qwen3-ASR-Flash-Realtime
說明範例程式碼讀取 your_audio_file.pcm(PCM16、16 kHz、單聲道)。如僅有 MP3/WAV 等格式,可使用 ffmpeg 轉換:
ffmpeg -i your_audio.mp3 -ar 16000 -ac 1 -f s16le your_audio_file.pcm
import com.alibaba.dashscope.audio.omni.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.sound.sampled.LineUnavailableException;
import java.io.File;
import java.io.FileInputStream;
import java.util.Base64;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
public class Qwen3AsrRealtimeUsage {
private static final Logger log = LoggerFactory.getLogger(Qwen3AsrRealtimeUsage.class);
private static final int AUDIO_CHUNK_SIZE = 1024; // Audio chunk size in bytes
private static final int SLEEP_INTERVAL_MS = 30; // Sleep interval in milliseconds
public static void main(String[] args) throws InterruptedException, LineUnavailableException {
CountDownLatch finishLatch = new CountDownLatch(1);
OmniRealtimeParam param = OmniRealtimeParam.builder()
.model("qwen3-asr-flash-realtime")
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
.url("wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime")
// 新加坡和北京地區的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"))
.build();
OmniRealtimeConversation conversation = null;
final AtomicReference<OmniRealtimeConversation> conversationRef = new AtomicReference<>(null);
conversation = new OmniRealtimeConversation(param, new OmniRealtimeCallback() {
@Override
public void onOpen() {
System.out.println("connection opened");
}
@Override
public void onEvent(JsonObject message) {
String type = message.get("type").getAsString();
switch(type) {
case "session.created":
System.out.println("start session: " + message.get("session").getAsJsonObject().get("id").getAsString());
break;
case "conversation.item.input_audio_transcription.completed":
System.out.println("transcription: " + message.get("transcript").getAsString());
finishLatch.countDown();
break;
case "input_audio_buffer.speech_started":
System.out.println("======VAD Speech Start======");
break;
case "input_audio_buffer.speech_stopped":
System.out.println("======VAD Speech Stop======");
break;
case "conversation.item.input_audio_transcription.text":
System.out.println("transcription: " + message.get("text").getAsString() + message.get("stash").getAsString());
break;
default:
break;
}
}
@Override
public void onClose(int code, String reason) {
System.out.println("connection closed code: " + code + ", reason: " + reason);
}
});
conversationRef.set(conversation);
try {
conversation.connect();
} catch (NoApiKeyException e) {
throw new RuntimeException(e);
}
OmniRealtimeTranscriptionParam transcriptionParam = new OmniRealtimeTranscriptionParam();
transcriptionParam.setLanguage("zh");
transcriptionParam.setInputAudioFormat("pcm");
transcriptionParam.setInputSampleRate(16000);
OmniRealtimeConfig config = OmniRealtimeConfig.builder()
.modalities(Collections.singletonList(OmniRealtimeModality.TEXT))
.transcriptionConfig(transcriptionParam)
.build();
conversation.updateSession(config);
String filePath = "your_audio_file.pcm";
File audioFile = new File(filePath);
if (!audioFile.exists()) {
log.error("Audio file not found: {}", filePath);
return;
}
try (FileInputStream audioInputStream = new FileInputStream(audioFile)) {
byte[] audioBuffer = new byte[AUDIO_CHUNK_SIZE];
int bytesRead;
int totalBytesRead = 0;
log.info("Starting to send audio data from: {}", filePath);
// Read and send audio data in chunks
while ((bytesRead = audioInputStream.read(audioBuffer)) != -1) {
totalBytesRead += bytesRead;
byte[] chunk = new byte[bytesRead];
System.arraycopy(audioBuffer, 0, chunk, 0, bytesRead);
String audioB64 = Base64.getEncoder().encodeToString(chunk);
// Send audio chunk to conversation
conversation.appendAudio(audioB64);
// Add small delay to simulate real-time audio streaming
Thread.sleep(SLEEP_INTERVAL_MS);
}
log.info("Finished sending audio data. Total bytes sent: {}", totalBytesRead);
} catch (Exception e) {
log.error("Error sending audio from file: {}", filePath, e);
}
//send session.finish and wait for finish and close
conversation.endSession();
log.info("task finished");
System.exit(0);
}
}
Constants.baseHttpApiUrl = "https://{WorkspaceId}.cn-beijing.maas.aliyuncs.com/api/v1";
import logging
import os
import base64
import signal
import sys
import time
import dashscope
from dashscope.audio.qwen_omni import *
from dashscope.audio.qwen_omni.omni_realtime import TranscriptionParams
def setup_logging():
"""配置日誌輸出"""
logger = logging.getLogger('dashscope')
logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
logger.propagate = False
return logger
def init_api_key():
"""初始化 API Key"""
# 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY', 'YOUR_API_KEY')
if dashscope.api_key == 'YOUR_API_KEY':
print('[Warning] Using placeholder API key, set DASHSCOPE_API_KEY environment variable.')
class MyCallback(OmniRealtimeCallback):
"""即時識別回調處理"""
def __init__(self, conversation):
self.conversation = conversation
self.handlers = {
'session.created': self._handle_session_created,
'conversation.item.input_audio_transcription.completed': self._handle_final_text,
'conversation.item.input_audio_transcription.text': self._handle_transcription_text,
'input_audio_buffer.speech_started': lambda r: print('======Speech Start======'),
'input_audio_buffer.speech_stopped': lambda r: print('======Speech Stop======')
}
def on_open(self):
print('Connection opened')
def on_close(self, code, msg):
print(f'Connection closed, code: {code}, msg: {msg}')
def on_event(self, response):
try:
handler = self.handlers.get(response['type'])
if handler:
handler(response)
except Exception as e:
print(f'[Error] {e}')
def _handle_session_created(self, response):
print(f"Start session: {response['session']['id']}")
def _handle_final_text(self, response):
print(f"Final recognized text: {response['transcript']}")
def _handle_transcription_text(self, response):
print(f"Got transcription result: {response['text'] + response['stash']}")
def read_audio_chunks(file_path, chunk_size=3200):
"""按塊讀取音頻檔案"""
with open(file_path, 'rb') as f:
while chunk := f.read(chunk_size):
yield chunk
def send_audio(conversation, file_path, delay=0.1):
"""發送音頻資料"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"Audio file {file_path} does not exist.")
print("Processing audio file... Press 'Ctrl+C' to stop.")
for chunk in read_audio_chunks(file_path):
audio_b64 = base64.b64encode(chunk).decode('ascii')
conversation.append_audio(audio_b64)
time.sleep(delay)
def main():
setup_logging()
init_api_key()
audio_file_path = "./your_audio_file.pcm"
callback = MyCallback(conversation=None)
conversation = OmniRealtimeConversation(
model='qwen3-asr-flash-realtime',
# 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime',
callback=callback,
)
callback.conversation = conversation # 把 conversation 注入回調,用於回調中調用其方法
def handle_exit(sig, frame):
print('Ctrl+C pressed, exiting...')
conversation.close()
sys.exit(0)
signal.signal(signal.SIGINT, handle_exit)
conversation.connect()
transcription_params = TranscriptionParams(
language='zh',
sample_rate=16000,
input_audio_format="pcm"
)
conversation.update_session(
output_modalities=[MultiModality.TEXT],
enable_input_audio_transcription=True,
transcription_params=transcription_params
)
try:
send_audio(conversation, audio_file_path)
# send session.finish and wait for finished and close
conversation.end_session()
except Exception as e:
print(f"Error occurred: {e}")
finally:
conversation.close()
print("Audio processing completed.")
if __name__ == '__main__':
main()
Paraformer
Paraformer範例程式碼和Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime相似,將model替換成Paraformer模型名即可。
識別配置
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 SDK和Java 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_time與payload.output.sentence.end_time,標識整句在音頻中的起止時刻。中間結果中end_time可能為null,待句子結束(sentence_end = true)時填充最終值。 - 字級:
payload.output.sentence.words數組,每個元素包含begin_time、end_time、text(該字/詞文本)以及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.text 與 conversation.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_tag 與 payload.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_tag 與 emo_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參考。
點擊查看 WebSocket 原始協議調用樣本
Qwen-Audio-3.0-ASR-Flash-Streaming/ Fun-ASR-Realtime
Python
在運行樣本前,請確保已使用以下命令安裝依賴:
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()
Java
在運行樣本前,請確保已安裝Java-WebSocket依賴:
<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>
implementation 'org.java-websocket:Java-WebSocket:1.5.6'
implementation 'org.json:json:20240303'
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONObject;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
public class FunASRRealtimeClient {
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:private static final String API_KEY = "sk-xxx";
private static final String API_KEY = System.getenv().getOrDefault("DASHSCOPE_API_KEY", "sk-xxx");
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
private static final String URL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
private static final String AUDIO_FILE = "{YOUR_AUDIO_FILE}"; // 替換為您的音頻檔案路徑
private static final String MODEL = "qwen-audio-3.0-asr-flash-streaming";
// 產生32位隨機ID
private static final String TASK_ID = UUID.randomUUID().toString().replace("-", "").substring(0, 32);
private static final AtomicBoolean taskStarted = new AtomicBoolean(false);
private static WebSocketClient client;
public static void main(String[] args) throws Exception {
client = new WebSocketClient(new URI(URL)) {
@Override
public void onOpen(ServerHandshake handshake) {
System.out.println("串連到伺服器");
sendRunTask();
}
@Override
public void onMessage(String data) {
JSONObject message = new JSONObject(data);
String event = message.getJSONObject("header").getString("event");
switch (event) {
case "task-started":
System.out.println("任務開始");
taskStarted.set(true);
new Thread(FunASRRealtimeClient::sendAudioStream).start();
break;
case "result-generated":
JSONObject payload = message.getJSONObject("payload");
String text = payload.getJSONObject("output").getJSONObject("sentence").getString("text");
System.out.println("識別結果:" + text);
if (payload.has("usage")) {
System.out.println("任務計費時間長度(秒):" + payload.getJSONObject("usage").get("duration"));
}
break;
case "task-finished":
System.out.println("任務完成");
close();
break;
case "task-failed":
String errMsg = message.getJSONObject("header").optString("error_message");
System.err.println("任務失敗:" + errMsg);
close();
break;
default:
System.out.println("未知事件:" + event);
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
if (!taskStarted.get()) {
System.err.println("任務未啟動,關閉串連");
}
}
@Override
public void onError(Exception ex) {
System.err.println("WebSocket錯誤:" + ex.getMessage());
}
};
client.addHeader("Authorization", "bearer " + API_KEY);
client.connectBlocking();
}
// 發送run-task指令
private static void sendRunTask() {
JSONObject runTask = new JSONObject()
.put("header", new JSONObject()
.put("action", "run-task")
.put("task_id", TASK_ID)
.put("streaming", "duplex"))
.put("payload", new JSONObject()
.put("task_group", "audio")
.put("task", "asr")
.put("function", "recognition")
.put("model", MODEL)
.put("parameters", new JSONObject()
.put("sample_rate", 16000)
.put("format", "wav"))
.put("input", new JSONObject()));
client.send(runTask.toString());
}
// 發送音頻流(每100ms發送一個二進位chunk)
private static void sendAudioStream() {
int chunkSize = 3200; // 100ms @ 16kHz 16bit 單聲道
try {
byte[] audio = Files.readAllBytes(Paths.get(AUDIO_FILE));
int offset = 0;
while (offset < audio.length) {
int end = Math.min(offset + chunkSize, audio.length);
byte[] chunk = new byte[end - offset];
System.arraycopy(audio, offset, chunk, 0, end - offset);
client.send(ByteBuffer.wrap(chunk));
offset = end;
Thread.sleep(100);
}
System.out.println("音頻流結束");
sendFinishTask();
} catch (Exception e) {
System.err.println("讀取音頻檔案錯誤:" + e.getMessage());
client.close();
}
}
// 發送finish-task指令
private static void sendFinishTask() {
JSONObject finishTask = new JSONObject()
.put("header", new JSONObject()
.put("action", "finish-task")
.put("task_id", TASK_ID)
.put("streaming", "duplex"))
.put("payload", new JSONObject()
.put("input", new JSONObject()));
client.send(finishTask.toString());
}
}
Node.js
需安裝相關依賴:
npm install ws
npm install uuid
範例程式碼如下:
const fs = require('fs');
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid'); // 用於產生UUID
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
const url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'; // WebSocket伺服器位址
const audioFile = '{YOUR_AUDIO_FILE}'; // 替換為您的音頻檔案路徑
// 產生32位隨機ID
const TASK_ID = uuidv4().replace(/-/g, '').slice(0, 32);
// 建立WebSocket用戶端
const ws = new WebSocket(url, {
headers: {
Authorization: `bearer ${apiKey}`
}
});
let taskStarted = false; // 標記任務是否已啟動
// 串連開啟時發送run-task指令
ws.on('open', () => {
console.log('串連到伺服器');
sendRunTask();
});
// 接收訊息處理
ws.on('message', (data) => {
const message = JSON.parse(data);
switch (message.header.event) {
case 'task-started':
console.log('任務開始');
taskStarted = true;
sendAudioStream();
break;
case 'result-generated':
console.log('識別結果:', message.payload.output.sentence.text);
if (message.payload.usage) {
console.log('任務計費時間長度(秒):', message.payload.usage.duration);
}
break;
case 'task-finished':
console.log('任務完成');
ws.close();
break;
case 'task-failed':
console.error('任務失敗:', message.header.error_message);
ws.close();
break;
default:
console.log('未知事件:', message.header.event);
}
});
// 如果沒有收到task-started事件,關閉串連
ws.on('close', () => {
if (!taskStarted) {
console.error('任務未啟動,關閉串連');
}
});
// 發送run-task指令
function sendRunTask() {
const runTaskMessage = {
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.stringify(runTaskMessage));
}
// 發送音頻流
function sendAudioStream() {
const audioStream = fs.createReadStream(audioFile);
let chunkCount = 0;
function sendNextChunk() {
const chunk = audioStream.read();
if (chunk) {
ws.send(chunk);
chunkCount++;
setTimeout(sendNextChunk, 100); // 每100ms發送一次
}
}
audioStream.on('readable', () => {
sendNextChunk();
});
audioStream.on('end', () => {
console.log('音頻流結束');
sendFinishTask();
});
audioStream.on('error', (err) => {
console.error('讀取音頻檔案錯誤:', err);
ws.close();
});
}
// 發送finish-task指令
function sendFinishTask() {
const finishTaskMessage = {
header: {
action: 'finish-task',
task_id: TASK_ID,
streaming: 'duplex'
},
payload: {
input: {}
}
};
ws.send(JSON.stringify(finishTaskMessage));
}
// 錯誤處理
ws.on('error', (error) => {
console.error('WebSocket錯誤:', error);
});
C#
範例程式碼如下:
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
class Program {
private static ClientWebSocket _webSocket = new ClientWebSocket();
private static CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource();
private static bool _taskStartedReceived = false;
private static bool _taskFinishedReceived = false;
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:private static readonly string ApiKey = "sk-xxx"
private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
private const string WebSocketUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
// 替換為您的音頻檔案路徑
private const string AudioFilePath = "{YOUR_AUDIO_FILE}";
static async Task Main(string[] args) {
// 建立WebSocket串連,配置headers進行鑒權
_webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
await _webSocket.ConnectAsync(new Uri(WebSocketUrl), _cancellationTokenSource.Token);
// 啟動線程非同步接收WebSocket訊息
var receiveTask = ReceiveMessagesAsync();
// 發送run-task指令
string _taskId = Guid.NewGuid().ToString("N"); // 產生32位隨機ID
var runTaskJson = GenerateRunTaskJson(_taskId);
await SendAsync(runTaskJson);
// 等待task-started事件
while (!_taskStartedReceived) {
await Task.Delay(100, _cancellationTokenSource.Token);
}
// 讀取本地檔案,向伺服器發送待識別音頻流
await SendAudioStreamAsync(AudioFilePath);
// 發送finish-task指令結束任務
var finishTaskJson = GenerateFinishTaskJson(_taskId);
await SendAsync(finishTaskJson);
// 等待task-finished事件
while (!_taskFinishedReceived && !_cancellationTokenSource.IsCancellationRequested) {
try {
await Task.Delay(100, _cancellationTokenSource.Token);
} catch (OperationCanceledException) {
// 任務已被取消,退出迴圈
break;
}
}
// 關閉串連
if (!_cancellationTokenSource.IsCancellationRequested) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cancellationTokenSource.Token);
}
_cancellationTokenSource.Cancel();
try {
await receiveTask;
} catch (OperationCanceledException) {
// 忽略操作取消異常
}
}
private static async Task ReceiveMessagesAsync() {
try {
while (_webSocket.State == WebSocketState.Open && !_cancellationTokenSource.IsCancellationRequested) {
var message = await ReceiveMessageAsync(_cancellationTokenSource.Token);
if (message != null) {
var eventValue = message["header"]?["event"]?.GetValue<string>();
switch (eventValue) {
case "task-started":
Console.WriteLine("任務開啟成功");
_taskStartedReceived = true;
break;
case "result-generated":
Console.WriteLine($"識別結果:{message["payload"]?["output"]?["sentence"]?["text"]?.GetValue<string>()}");
if (message["payload"]?["usage"] != null && message["payload"]?["usage"]?["duration"] != null) {
Console.WriteLine($"任務計費時間長度(秒):{message["payload"]?["usage"]?["duration"]?.GetValue<int>()}");
}
break;
case "task-finished":
Console.WriteLine("任務完成");
_taskFinishedReceived = true;
_cancellationTokenSource.Cancel();
break;
case "task-failed":
Console.WriteLine($"任務失敗:{message["header"]?["error_message"]?.GetValue<string>()}");
_cancellationTokenSource.Cancel();
break;
}
}
}
} catch (OperationCanceledException) {
// 忽略操作取消異常
}
}
private static async Task<JsonNode?> ReceiveMessageAsync(CancellationToken cancellationToken) {
var buffer = new byte[1024 * 4];
var segment = new ArraySegment<byte>(buffer);
var sb = new StringBuilder();
WebSocketReceiveResult result;
// 迴圈接收並拼接分區,直到 EndOfMessage,避免長訊息被截斷導致 JSON 解析失敗
do {
result = await _webSocket.ReceiveAsync(segment, cancellationToken);
if (result.MessageType == WebSocketMessageType.Close) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
return null;
}
sb.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
} while (!result.EndOfMessage);
return JsonNode.Parse(sb.ToString());
}
private static async Task SendAsync(string message) {
var buffer = Encoding.UTF8.GetBytes(message);
var segment = new ArraySegment<byte>(buffer);
await _webSocket.SendAsync(segment, WebSocketMessageType.Text, true, _cancellationTokenSource.Token);
}
private static async Task SendAudioStreamAsync(string filePath) {
using (var audioStream = File.OpenRead(filePath)) {
var buffer = new byte[1024]; // 每次發送100ms的音頻資料
int bytesRead;
while ((bytesRead = await audioStream.ReadAsync(buffer, 0, buffer.Length)) > 0) {
var segment = new ArraySegment<byte>(buffer, 0, bytesRead);
await _webSocket.SendAsync(segment, WebSocketMessageType.Binary, true, _cancellationTokenSource.Token);
await Task.Delay(100); // 間隔100ms
}
}
}
private static string GenerateRunTaskJson(string taskId) {
var runTask = new JsonObject {
["header"] = new JsonObject {
["action"] = "run-task",
["task_id"] = taskId,
["streaming"] = "duplex"
},
["payload"] = new JsonObject {
["task_group"] = "audio",
["task"] = "asr",
["function"] = "recognition",
["model"] = "qwen-audio-3.0-asr-flash-streaming",
["parameters"] = new JsonObject {
["format"] = "wav",
["sample_rate"] = 16000,
},
["input"] = new JsonObject()
}
};
return JsonSerializer.Serialize(runTask);
}
private static string GenerateFinishTaskJson(string taskId) {
var finishTask = new JsonObject {
["header"] = new JsonObject {
["action"] = "finish-task",
["task_id"] = taskId,
["streaming"] = "duplex"
},
["payload"] = new JsonObject {
["input"] = new JsonObject()
}
};
return JsonSerializer.Serialize(finishTask);
}
}
PHP
範例程式碼目錄結構為:
my-php-project/
├── composer.json
├── vendor/
└── index.php
composer.json內容如下,相關依賴的版本號碼請根據實際情況自行決定:
{
"require": {
"react/event-loop": "^1.3",
"react/socket": "^1.11",
"react/stream": "^1.2",
"react/http": "^1.1",
"ratchet/pawl": "^0.4"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}
index.php內容如下:
<?php
require __DIR__ . '/vendor/autoload.php';
use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;
use Ratchet\rfc6455\Messaging\Frame;
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:$api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference';
$audio_file_path = '{YOUR_AUDIO_FILE}'; // 替換為您的音頻檔案路徑
$loop = Loop::get();
// 建立自訂的連接器
$socketConnector = new SocketConnector($loop, [
'tcp' => [
'bindto' => '0.0.0.0:0',
],
// 警告:關閉 TLS 認證校正存在中間人攻擊風險,僅限本地調試;生產環境務必將 verify_peer/verify_peer_name 設為 true
'tls' => [
'verify_peer' => false,
'verify_peer_name' => false,
],
]);
$connector = new Connector($loop, $socketConnector);
$headers = [
'Authorization' => 'bearer ' . $api_key
];
$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $audio_file_path) {
echo "串連到WebSocket伺服器\n";
// 啟動非同步接收WebSocket訊息的線程
$conn->on('message', function($msg) use ($conn, $loop, $audio_file_path) {
$response = json_decode($msg, true);
if (isset($response['header']['event'])) {
handleEvent($conn, $response, $loop, $audio_file_path);
} else {
echo "未知的訊息格式\n";
}
});
// 監聽串連關閉
$conn->on('close', function($code = null, $reason = null) {
echo "串連已關閉\n";
if ($code !== null) {
echo "關閉代碼: " . $code . "\n";
}
if ($reason !== null) {
echo "關閉原因:" . $reason . "\n";
}
});
// 產生任務ID
$taskId = generateTaskId();
// 發送 run-task 指令
sendRunTaskMessage($conn, $taskId);
}, function ($e) {
echo "無法串連:{$e->getMessage()}\n";
});
$loop->run();
/**
* 產生任務ID
* @return string
*/
function generateTaskId(): string {
return bin2hex(random_bytes(16));
}
/**
* 發送 run-task 指令
* @param $conn
* @param $taskId
*/
function sendRunTaskMessage($conn, $taskId) {
$runTaskMessage = json_encode([
"header" => [
"action" => "run-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"task_group" => "audio",
"task" => "asr",
"function" => "recognition",
"model" => "qwen-audio-3.0-asr-flash-streaming",
"parameters" => [
"format" => "wav",
"sample_rate" => 16000
],
"input" => []
]
]);
echo "準備發送run-task指令:" . $runTaskMessage . "\n";
$conn->send($runTaskMessage);
echo "run-task指令已發送\n";
}
/**
* 讀取音頻檔案
* @param string $filePath
* @return bool|string
*/
function readAudioFile(string $filePath) {
$voiceData = file_get_contents($filePath);
if ($voiceData === false) {
echo "無法讀取音頻檔案\n";
}
return $voiceData;
}
/**
* 分割音頻資料
* @param string $data
* @param int $chunkSize
* @return array
*/
function splitAudioData(string $data, int $chunkSize): array {
return str_split($data, $chunkSize);
}
/**
* 發送 finish-task 指令
* @param $conn
* @param $taskId
*/
function sendFinishTaskMessage($conn, $taskId) {
$finishTaskMessage = json_encode([
"header" => [
"action" => "finish-task",
"task_id" => $taskId,
"streaming" => "duplex"
],
"payload" => [
"input" => []
]
]);
echo "準備發送finish-task指令: " . $finishTaskMessage . "\n";
$conn->send($finishTaskMessage);
echo "finish-task指令已發送\n";
}
/**
* 處理事件
* @param $conn
* @param $response
* @param $loop
* @param $audio_file_path
*/
function handleEvent($conn, $response, $loop, $audio_file_path) {
static $taskId;
static $chunks;
static $allChunksSent = false;
if (is_null($taskId)) {
$taskId = generateTaskId();
}
switch ($response['header']['event']) {
case 'task-started':
echo "任務開始,發送音頻資料...\n";
// 讀取音頻檔案
$voiceData = readAudioFile($audio_file_path);
if ($voiceData === false) {
echo "無法讀取音頻檔案\n";
$conn->close();
return;
}
// 分割音頻資料
$chunks = splitAudioData($voiceData, 1024);
// 定義發送函數
$sendChunk = function() use ($conn, &$chunks, $loop, &$sendChunk, &$allChunksSent, $taskId) {
if (!empty($chunks)) {
$chunk = array_shift($chunks);
$binaryMsg = new Frame($chunk, true, Frame::OP_BINARY);
$conn->send($binaryMsg);
// 100ms後發送下一個片段
$loop->addTimer(0.1, $sendChunk);
} else {
echo "所有資料區塊已發送\n";
$allChunksSent = true;
// 發送 finish-task 指令
sendFinishTaskMessage($conn, $taskId);
}
};
// 開始發送音頻資料
$sendChunk();
break;
case 'result-generated':
$result = $response['payload']['output']['sentence'];
echo "識別結果:" . $result['text'] . "\n";
if (isset($response['payload']['usage']['duration'])) {
echo "任務計費時間長度(秒):" . $response['payload']['usage']['duration'] . "\n";
}
break;
case 'task-finished':
echo "任務完成\n";
$conn->close();
break;
case 'task-failed':
echo "任務失敗\n";
echo "錯誤碼:" . $response['header']['error_code'] . "\n";
echo "錯誤資訊:" . $response['header']['error_message'] . "\n";
$conn->close();
break;
case 'error':
echo "錯誤:" . $response['payload']['message'] . "\n";
break;
default:
echo "未知事件:" . $response['header']['event'] . "\n";
break;
}
// 如果所有資料已發送且任務已完成,關閉串連
if ($allChunksSent && $response['header']['event'] == 'task-finished') {
// 等待1秒以確保所有資料都已傳輸完畢
$loop->addTimer(1, function() use ($conn) {
$conn->close();
echo "用戶端關閉串連\n";
});
}
}
Go
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
const (
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
wsURL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference" // WebSocket伺服器位址
audioFile = "{YOUR_AUDIO_FILE}" // 替換為您的音頻檔案路徑
)
var dialer = websocket.DefaultDialer
func main() {
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:apiKey := "sk-xxx"
apiKey := os.Getenv("DASHSCOPE_API_KEY")
// 串連WebSocket服務
conn, err := connectWebSocket(apiKey)
if err != nil {
log.Fatal("串連WebSocket失敗:", err)
}
defer closeConnection(conn)
// 啟動一個goroutine來接收結果
taskStarted := make(chan bool)
taskDone := make(chan bool)
startResultReceiver(conn, taskStarted, taskDone)
// 發送run-task指令
taskID, err := sendRunTaskCmd(conn)
if err != nil {
log.Fatal("發送run-task指令失敗:", err)
}
// 等待task-started事件
waitForTaskStarted(taskStarted)
// 發送待識別音頻檔案流
if err := sendAudioData(conn); err != nil {
log.Fatal("發送音頻失敗:", err)
}
// 發送finish-task指令
if err := sendFinishTaskCmd(conn, taskID); err != nil {
log.Fatal("發送finish-task指令失敗:", err)
}
// 等待任務完成或失敗
<-taskDone
}
// 定義結構體來表示JSON資料
type Header struct {
Action string `json:"action"`
TaskID string `json:"task_id"`
Streaming string `json:"streaming"`
Event string `json:"event"`
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
Attributes map[string]interface{} `json:"attributes"`
}
type Output struct {
Sentence struct {
BeginTime int64 `json:"begin_time"`
EndTime *int64 `json:"end_time"`
Text string `json:"text"`
Words []struct {
BeginTime int64 `json:"begin_time"`
EndTime *int64 `json:"end_time"`
Text string `json:"text"`
Punctuation string `json:"punctuation"`
} `json:"words"`
} `json:"sentence"`
}
type Payload struct {
TaskGroup string `json:"task_group"`
Task string `json:"task"`
Function string `json:"function"`
Model string `json:"model"`
Parameters Params `json:"parameters"`
Input Input `json:"input"`
Output Output `json:"output,omitempty"`
Usage *struct {
Duration int `json:"duration"`
} `json:"usage,omitempty"`
}
type Params struct {
Format string `json:"format"`
SampleRate int `json:"sample_rate"`
DisfluencyRemovalEnabled bool `json:"disfluency_removal_enabled"`
}
type Input struct {
}
type Event struct {
Header Header `json:"header"`
Payload Payload `json:"payload"`
}
// 串連WebSocket服務
func connectWebSocket(apiKey string) (*websocket.Conn, error) {
header := make(http.Header)
header.Add("Authorization", fmt.Sprintf("bearer %s", apiKey))
conn, _, err := dialer.Dial(wsURL, header)
return conn, err
}
// 啟動一個goroutine非同步接收WebSocket訊息
func startResultReceiver(conn *websocket.Conn, taskStarted chan<- bool, taskDone chan<- bool) {
go func() {
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("解析伺服器訊息失敗:", err)
return
}
var event Event
err = json.Unmarshal(message, &event)
if err != nil {
log.Println("解析事件失敗:", err)
continue
}
if handleEvent(conn, event, taskStarted, taskDone) {
return
}
}
}()
}
// 發送run-task指令
func sendRunTaskCmd(conn *websocket.Conn) (string, error) {
runTaskCmd, taskID, err := generateRunTaskCmd()
if err != nil {
return "", err
}
err = conn.WriteMessage(websocket.TextMessage, []byte(runTaskCmd))
return taskID, err
}
// 產生run-task指令
func generateRunTaskCmd() (string, string, error) {
taskID := uuid.New().String()
runTaskCmd := Event{
Header: Header{
Action: "run-task",
TaskID: taskID,
Streaming: "duplex",
},
Payload: Payload{
TaskGroup: "audio",
Task: "asr",
Function: "recognition",
Model: "qwen-audio-3.0-asr-flash-streaming",
Parameters: Params{
Format: "wav",
SampleRate: 16000,
},
Input: Input{},
},
}
runTaskCmdJSON, err := json.Marshal(runTaskCmd)
return string(runTaskCmdJSON), taskID, err
}
// 等待task-started事件
func waitForTaskStarted(taskStarted chan bool) {
select {
case <-taskStarted:
fmt.Println("任務開啟成功")
case <-time.After(10 * time.Second):
log.Fatal("等待task-started逾時,任務開啟失敗")
}
}
// 發送音頻資料
func sendAudioData(conn *websocket.Conn) error {
file, err := os.Open(audioFile)
if err != nil {
return err
}
defer file.Close()
buf := make([]byte, 1024)
for {
n, err := file.Read(buf)
if n == 0 {
break
}
if err != nil && err != io.EOF {
return err
}
err = conn.WriteMessage(websocket.BinaryMessage, buf[:n])
if err != nil {
return err
}
time.Sleep(100 * time.Millisecond)
}
return nil
}
// 發送finish-task指令
func sendFinishTaskCmd(conn *websocket.Conn, taskID string) error {
finishTaskCmd, err := generateFinishTaskCmd(taskID)
if err != nil {
return err
}
err = conn.WriteMessage(websocket.TextMessage, []byte(finishTaskCmd))
return err
}
// 產生finish-task指令
func generateFinishTaskCmd(taskID string) (string, error) {
finishTaskCmd := Event{
Header: Header{
Action: "finish-task",
TaskID: taskID,
Streaming: "duplex",
},
Payload: Payload{
Input: Input{},
},
}
finishTaskCmdJSON, err := json.Marshal(finishTaskCmd)
return string(finishTaskCmdJSON), err
}
// 處理事件
func handleEvent(conn *websocket.Conn, event Event, taskStarted chan<- bool, taskDone chan<- bool) bool {
switch event.Header.Event {
case "task-started":
fmt.Println("收到task-started事件")
taskStarted <- true
case "result-generated":
if event.Payload.Output.Sentence.Text != "" {
fmt.Println("識別結果:", event.Payload.Output.Sentence.Text)
}
if event.Payload.Usage != nil {
fmt.Println("任務計費時間長度(秒):", event.Payload.Usage.Duration)
}
case "task-finished":
fmt.Println("任務完成")
taskDone <- true
return true
case "task-failed":
handleTaskFailed(event, conn)
taskDone <- true
return true
default:
log.Printf("預料之外的事件:%v", event)
}
return false
}
// 處理任務失敗事件
func handleTaskFailed(event Event, conn *websocket.Conn) {
if event.Header.ErrorMessage != "" {
log.Fatalf("任務失敗:%s", event.Header.ErrorMessage)
} else {
log.Fatal("未知原因導致任務失敗")
}
}
// 關閉串連
func closeConnection(conn *websocket.Conn) {
if conn != nil {
conn.Close()
}
}
Qwen3-ASR-Flash-Realtime
說明範例程式碼讀取 your_audio_file.pcm(PCM16、16 kHz、單聲道)。如僅有 MP3/WAV 等格式,可使用 ffmpeg 轉換:
ffmpeg -i your_audio.mp3 -ar 16000 -ac 1 -f s16le your_audio_file.pcm
說明使用非 VAD(Manual)模式時,建議單次會話持續發送的音頻時間長度累加不超過 60 秒。
Python
在運行樣本前,請確保已使用以下命令安裝依賴:
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 time
import json
import threading
import base64
import websocket
import logging
import logging.handlers
from datetime import datetime
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
# 新加坡和北京地區的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", "sk-xxx")
QWEN_MODEL = "qwen3-asr-flash-realtime"
# 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
baseUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime"
url = f"{baseUrl}?model={QWEN_MODEL}"
print(f"Connecting to server: {url}")
# 注意: 如果是非vad模式,建議持續發送的音頻時間長度累加不超過60s
enableServerVad = True
is_running = True # 增加運行標誌位
headers = [
"Authorization: Bearer " + API_KEY,
"OpenAI-Beta: realtime=v1"
]
def init_logger():
formatter = logging.Formatter('%(asctime)s|%(levelname)s|%(message)s')
f_handler = logging.handlers.RotatingFileHandler(
"omni_tester.log", maxBytes=100 * 1024 * 1024, backupCount=3
)
f_handler.setLevel(logging.DEBUG)
f_handler.setFormatter(formatter)
console = logging.StreamHandler()
console.setLevel(logging.DEBUG)
console.setFormatter(formatter)
logger.addHandler(f_handler)
logger.addHandler(console)
def on_open(ws):
logger.info("Connected to server.")
# 會話更新事件
event_manual = {
"event_id": "event_123",
"type": "session.update",
"session": {
"modalities": ["text"],
"input_audio_format": "pcm",
"sample_rate": 16000,
# "input_audio_transcription": {
# # 語種標識,可選,如果有明確的語種資訊,建議設定
# "language": "zh"
# },
"turn_detection": None
}
}
event_vad = {
"event_id": "event_123",
"type": "session.update",
"session": {
"modalities": ["text"],
"input_audio_format": "pcm",
"sample_rate": 16000,
# "input_audio_transcription": {
# "language": "zh"
# },
"turn_detection": {
"type": "server_vad",
"threshold": 0.2,
"silence_duration_ms": 400
}
}
}
if enableServerVad:
logger.info(f"Sending event: {json.dumps(event_vad, indent=2)}")
ws.send(json.dumps(event_vad))
else:
logger.info(f"Sending event: {json.dumps(event_manual, indent=2)}")
ws.send(json.dumps(event_manual))
def on_message(ws, message):
global is_running
try:
data = json.loads(message)
logger.info(f"Received event: {json.dumps(data, ensure_ascii=False, indent=2)}")
if data.get("type") == "conversation.item.input_audio_transcription.completed":
logger.info(f"Final transcript: {data.get('transcript')}")
elif data.get("type") == "session.finished":
logger.info("Closing WebSocket connection after session finished...")
is_running = False # 停止音頻發送線程
ws.close()
except json.JSONDecodeError:
logger.error(f"Failed to parse message: {message}")
def on_error(ws, error):
logger.error(f"Error: {error}")
def on_close(ws, close_status_code, close_msg):
logger.info(f"Connection closed: {close_status_code} - {close_msg}")
def send_audio(ws, local_audio_path):
time.sleep(3) # 等待會話更新完成
global is_running
with open(local_audio_path, 'rb') as audio_file:
logger.info(f"檔案讀取開始: {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}")
while is_running:
audio_data = audio_file.read(3200) # ~0.1s PCM16/16kHz
if not audio_data:
logger.info(f"檔案讀取完畢: {datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}")
if ws.sock and ws.sock.connected:
if not enableServerVad:
commit_event = {
"event_id": "event_789",
"type": "input_audio_buffer.commit"
}
ws.send(json.dumps(commit_event))
finish_event = {
"event_id": "event_987",
"type": "session.finish"
}
ws.send(json.dumps(finish_event))
break
if not ws.sock or not ws.sock.connected:
logger.info("WebSocket已關閉,停止發送音頻。")
break
encoded_data = base64.b64encode(audio_data).decode('utf-8')
eventd = {
"event_id": f"event_{int(time.time() * 1000)}",
"type": "input_audio_buffer.append",
"audio": encoded_data
}
ws.send(json.dumps(eventd))
logger.info(f"Sending audio event: {eventd['event_id']}")
time.sleep(0.1) # 類比即時採集
# 初始化日誌
init_logger()
logger.info(f"Connecting to WebSocket server at {url}...")
local_audio_path = "your_audio_file.pcm"
ws = websocket.WebSocketApp(
url,
header=headers,
on_open=on_open,
on_message=on_message,
on_error=on_error,
on_close=on_close
)
thread = threading.Thread(target=send_audio, args=(ws, local_audio_path))
thread.start()
ws.run_forever()
Java
在運行樣本前,請確保已安裝Java-WebSocket依賴:
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.6</version>
</dependency>
implementation 'org.java-websocket:Java-WebSocket:1.5.6'
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());
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:private static final String API_KEY = "sk-xxx"
private static final String API_KEY = System.getenv().getOrDefault("DASHSCOPE_API_KEY", "sk-xxx");
private static final String MODEL = "qwen3-asr-flash-realtime";
// 控制是否使用 VAD 模式
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();
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
String baseUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/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));
// 最終識別結果在 transcription.completed 事件中
if ("conversation.item.input_audio_transcription.completed".equals(eventType)) {
logger.info("Final transcript: " + data.optString("transcript"));
}
// 收到結束事件 → 停止發送線程並關閉串連
if ("session.finished".equals(eventType)) {
logger.info("Closing WebSocket connection after session finished...");
isRunning.set(false); // 停止發送音頻線程
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());
}
};
// 添加要求標頭
client.addHeader("Authorization", "Bearer " + API_KEY);
client.addHeader("OpenAI-Beta", "realtime=v1");
client.connectBlocking(); // 阻塞直到串連建立
// 替換為待識別的音頻檔案路徑
String localAudioPath = "your_audio_file.pcm";
Thread audioThread = new Thread(() -> {
try {
sendAudio(localAudioPath);
} catch (Exception e) {
logger.severe("Audio sending thread error: " + e.getMessage());
}
});
audioThread.start();
}
/** 會話更新事件(開啟/關閉 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) // 手動模式
);
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());
}
}
/** 發送音頻檔案流 */
private static void sendAudio(String localAudioPath) throws Exception {
Thread.sleep(3000); // 等會話準備
byte[] allBytes = Files.readAllBytes(Paths.get(localAudioPath));
logger.info("檔案讀取開始");
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; // 避免在斷開後繼續發送
}
Thread.sleep(100); // 類比即時發送
}
logger.info("檔案讀取完畢");
if (client != null && client.isOpen()) {
// 非 VAD 模式下需要 commit
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.");
}
}
/** 初始化日誌 */
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);
}
}
Node.js
在運行樣本前,請確保已使用以下命令安裝依賴:
npm install ws
/**
* Qwen-ASR Realtime WebSocket 用戶端(Node.js版)
* 功能:
* - 支援 VAD 模式和 Manual 模式
* - 發送 session.update 啟動會話
* - 持續發送音頻塊 input_audio_buffer.append
* - 如果是Manual模式,需要發送 input_audio_buffer.commit
* - 發送session.finish事件
* - 收到 session.finished 事件後關閉串連
*/
import WebSocket from 'ws';
import fs from 'fs';
// ===== 配置 =====
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:const API_KEY = "sk-xxx"
const API_KEY = process.env.DASHSCOPE_API_KEY || 'sk-xxx';
const MODEL = 'qwen3-asr-flash-realtime';
const enableServerVad = true; // true為VAD模式,false為Manual模式
const localAudioPath = 'your_audio_file.pcm'; // PCM16、16kHz音頻檔案路徑
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
const baseUrl = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime';
const url = `${baseUrl}?model=${MODEL}`;
console.log(`Connecting to server: ${url}`);
// ===== 狀態控制 =====
let isRunning = true;
// ===== 建立串連 =====
const ws = new WebSocket(url, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'OpenAI-Beta': 'realtime=v1'
}
});
// ===== 事件綁定 =====
ws.on('open', () => {
console.log('[WebSocket] Connected to server.');
sendSessionUpdate();
// 啟動音頻發送線程
sendAudio(localAudioPath);
});
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
console.log('[Received Event]:', JSON.stringify(data, null, 2));
// 最終識別結果在 transcription.completed 事件中
if (data.type === 'conversation.item.input_audio_transcription.completed') {
console.log(`[Final Transcript] ${data.transcript}`);
}
// 收到結束事件
if (data.type === 'session.finished') {
console.log('[Action] Closing WebSocket connection after session finished...');
if (ws.readyState === WebSocket.OPEN) {
ws.close(1000, 'ASR finished');
}
}
} catch (e) {
console.error('[Error] Failed to parse message:', message);
}
});
ws.on('close', (code, reason) => {
console.log(`[WebSocket] Connection closed: ${code} - ${reason}`);
});
ws.on('error', (err) => {
console.error('[WebSocket Error]', err);
});
// ===== 會話更新 =====
function sendSessionUpdate() {
const eventNoVad = {
event_id: 'event_123',
type: 'session.update',
session: {
modalities: ['text'],
input_audio_format: 'pcm',
sample_rate: 16000,
// input_audio_transcription: {
// language: 'zh'
// },
turn_detection: null
}
};
const eventVad = {
event_id: 'event_123',
type: 'session.update',
session: {
modalities: ['text'],
input_audio_format: 'pcm',
sample_rate: 16000,
// input_audio_transcription: {
// language: 'zh'
// },
turn_detection: {
type: 'server_vad',
threshold: 0.2,
silence_duration_ms: 400
}
}
};
if (enableServerVad) {
console.log('[Send Event] VAD Mode:\n', JSON.stringify(eventVad, null, 2));
ws.send(JSON.stringify(eventVad));
} else {
console.log('[Send Event] Manual Mode:\n', JSON.stringify(eventNoVad, null, 2));
ws.send(JSON.stringify(eventNoVad));
}
}
// ===== 發送音頻檔案流 =====
function sendAudio(audioPath) {
setTimeout(() => {
console.log(`[File Read Start] ${audioPath}`);
const buffer = fs.readFileSync(audioPath);
let offset = 0;
const chunkSize = 3200; // 約0.1s的PCM16音頻
function sendChunk() {
if (!isRunning) return;
if (offset >= buffer.length) {
isRunning = false; // 停止發送音頻
console.log('[File Read End]');
if (ws.readyState === WebSocket.OPEN) {
if (!enableServerVad) {
const commitEvent = {
event_id: 'event_789',
type: 'input_audio_buffer.commit'
};
ws.send(JSON.stringify(commitEvent));
console.log('[Send Commit Event]');
}
const finishEvent = {
event_id: 'event_987',
type: 'session.finish'
};
ws.send(JSON.stringify(finishEvent));
console.log('[Send Finish Event]');
}
return;
}
if (ws.readyState !== WebSocket.OPEN) {
console.log('[Stop] WebSocket is not open.');
return;
}
const chunk = buffer.slice(offset, offset + chunkSize);
offset += chunkSize;
const encoded = chunk.toString('base64');
const appendEvent = {
event_id: `event_${Date.now()}`,
type: 'input_audio_buffer.append',
audio: encoded
};
ws.send(JSON.stringify(appendEvent));
console.log(`[Send Audio Event] ${appendEvent.event_id}`);
setTimeout(sendChunk, 100); // 類比即時發送
}
sendChunk();
}, 3000); // 等待會話配置完成
}
C#
範例程式碼如下:
using System.Net.WebSockets;
using System.Text;
using System.Text.Json.Nodes;
class Program {
private static ClientWebSocket _webSocket = new ClientWebSocket();
private static CancellationTokenSource _cts = new CancellationTokenSource();
private static bool _sessionFinished = false;
private static bool _isRunning = true;
// 控制是否使用 VAD 模式
private const bool EnableServerVad = true;
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:private static readonly string ApiKey = "sk-xxx"
private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");
private const string Model = "qwen3-asr-flash-realtime";
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
private const string BaseUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime";
private const string AudioFilePath = "your_audio_file.pcm"; // 替換為您的PCM音頻檔案路徑
static async Task Main(string[] args) {
var url = $"{BaseUrl}?model={Model}";
Console.WriteLine($"Connecting to server: {url}");
// 設定鑒權 headers
_webSocket.Options.SetRequestHeader("Authorization", $"Bearer {ApiKey}");
_webSocket.Options.SetRequestHeader("OpenAI-Beta", "realtime=v1");
await _webSocket.ConnectAsync(new Uri(url), _cts.Token);
Console.WriteLine("Connected to server.");
// 啟動訊息接收任務
var receiveTask = ReceiveMessagesAsync();
// 發送 session.update 配置
await SendSessionUpdateAsync();
// 發送音頻流
await SendAudioStreamAsync();
// 等待 session.finished 事件
while (!_sessionFinished && !_cts.IsCancellationRequested) {
await Task.Delay(100);
}
if (_webSocket.State == WebSocketState.Open) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "ASR finished", _cts.Token);
}
}
private static async Task SendAsync(string text) {
var bytes = Encoding.UTF8.GetBytes(text);
await _webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, _cts.Token);
}
// 發送 session.update 事件
private static async Task SendSessionUpdateAsync() {
var session = new JsonObject {
["modalities"] = new JsonArray { "text" },
["input_audio_format"] = "pcm",
["sample_rate"] = 16000,
// ["input_audio_transcription"] = new JsonObject { ["language"] = "zh" }
};
if (EnableServerVad) {
session["turn_detection"] = new JsonObject {
["type"] = "server_vad",
["threshold"] = 0.2,
["silence_duration_ms"] = 400
};
} else {
session["turn_detection"] = null;
}
var payload = new JsonObject {
["event_id"] = "event_123",
["type"] = "session.update",
["session"] = session
};
Console.WriteLine($"Sending session.update: {payload.ToJsonString()}");
await SendAsync(payload.ToJsonString());
}
// 發送音頻流(每100ms發送一個PCM chunk)
private static async Task SendAudioStreamAsync() {
await Task.Delay(3000); // 等待會話配置完成
const int chunkSize = 3200; // 100ms @ 16kHz 16bit 單聲道
using var fs = new FileStream(AudioFilePath, FileMode.Open, FileAccess.Read);
var buffer = new byte[chunkSize];
int read;
while (_isRunning && (read = await fs.ReadAsync(buffer, 0, chunkSize)) > 0) {
if (_webSocket.State != WebSocketState.Open) break;
string b64 = Convert.ToBase64String(buffer, 0, read);
var append = new JsonObject {
["event_id"] = $"event_{DateTimeOffset.Now.ToUnixTimeMilliseconds()}",
["type"] = "input_audio_buffer.append",
["audio"] = b64
};
await SendAsync(append.ToJsonString());
await Task.Delay(100);
}
Console.WriteLine("File read end.");
if (_webSocket.State == WebSocketState.Open) {
if (!EnableServerVad) {
var commit = new JsonObject {
["event_id"] = "event_789",
["type"] = "input_audio_buffer.commit"
};
await SendAsync(commit.ToJsonString());
}
var finish = new JsonObject {
["event_id"] = "event_987",
["type"] = "session.finish"
};
await SendAsync(finish.ToJsonString());
}
}
// 接收並處理服務端事件
private static async Task ReceiveMessagesAsync() {
var buffer = new byte[16384];
var sb = new StringBuilder();
while (_webSocket.State == WebSocketState.Open && !_cts.IsCancellationRequested) {
try {
var result = await _webSocket.ReceiveAsync(new ArraySegment<byte>(buffer), _cts.Token);
if (result.MessageType == WebSocketMessageType.Close) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cts.Token);
break;
}
sb.Append(Encoding.UTF8.GetString(buffer, 0, result.Count));
if (!result.EndOfMessage) continue;
string text = sb.ToString();
sb.Clear();
var data = JsonNode.Parse(text);
string? type = data?["type"]?.GetValue<string>();
Console.WriteLine($"Received event: {type}");
if (type == "conversation.item.input_audio_transcription.completed") {
Console.WriteLine($"Final transcript: {data!["transcript"]}");
} else if (type == "session.finished") {
Console.WriteLine("Session finished, closing...");
_sessionFinished = true;
_isRunning = false;
break;
}
} catch (Exception ex) {
Console.WriteLine($"Receive error: {ex.Message}");
break;
}
}
}
}
PHP
範例程式碼目錄結構為:
my-php-project/
├── composer.json
├── vendor/
└── index.php
composer.json內容如下,相關依賴的版本號碼請根據實際情況自行決定:
{
"require": {
"react/event-loop": "^1.3",
"react/socket": "^1.11",
"ratchet/pawl": "^0.4"
}
}
index.php內容如下:
<?php
require __DIR__ . '/vendor/autoload.php';
use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:$api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
$model = 'qwen3-asr-flash-realtime';
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
$base_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime';
$websocket_url = $base_url . '?model=' . $model;
$audio_file_path = 'your_audio_file.pcm'; // 替換為您的PCM音頻檔案路徑
// 控制是否使用 VAD 模式
$enable_server_vad = true;
$loop = Loop::get();
$socketConnector = new SocketConnector($loop, [
// 警告:關閉 TLS 認證校正存在中間人攻擊風險,僅限本地調試;生產環境務必將 verify_peer/verify_peer_name 設為 true
'tls' => ['verify_peer' => false, 'verify_peer_name' => false],
]);
$connector = new Connector($loop, $socketConnector);
$headers = [
'Authorization' => 'Bearer ' . $api_key,
'OpenAI-Beta' => 'realtime=v1',
];
$is_running = true;
$connector($websocket_url, [], $headers)->then(function ($conn) use ($loop, $audio_file_path, $enable_server_vad, &$is_running) {
echo "串連到WebSocket伺服器\n";
// 監聽服務端事件
$conn->on('message', function($msg) use ($conn, &$is_running) {
$event = json_decode($msg, true);
if (!isset($event['type'])) {
return;
}
echo "Received event: {$event['type']}\n";
if ($event['type'] === 'conversation.item.input_audio_transcription.completed') {
echo "Final transcript: {$event['transcript']}\n";
} elseif ($event['type'] === 'session.finished') {
echo "Session finished, closing...\n";
$is_running = false;
$conn->close();
}
});
$conn->on('close', function() {
echo "串連已關閉\n";
});
// 發送 session.update 事件
sendSessionUpdate($conn, $enable_server_vad);
// 等待會話配置完成後開始發送音頻
$loop->addTimer(3, function () use ($conn, $audio_file_path, $enable_server_vad, $loop, &$is_running) {
sendAudioStream($conn, $audio_file_path, $enable_server_vad, $loop, $is_running);
});
}, function ($e) {
echo "無法串連:{$e->getMessage()}\n";
});
$loop->run();
// 發送 session.update 事件
function sendSessionUpdate($conn, $enable_server_vad) {
$session = [
'modalities' => ['text'],
'input_audio_format' => 'pcm',
'sample_rate' => 16000,
// 'input_audio_transcription' => ['language' => 'zh'],
'turn_detection' => $enable_server_vad ? [
'type' => 'server_vad',
'threshold' => 0.2,
'silence_duration_ms' => 400,
] : null,
];
$event = [
'event_id' => 'event_123',
'type' => 'session.update',
'session' => $session,
];
$conn->send(json_encode($event));
echo "Sent session.update\n";
}
// 發送音頻流(每100ms發送一個PCM chunk)
function sendAudioStream($conn, $audio_file_path, $enable_server_vad, $loop, &$is_running) {
$fp = fopen($audio_file_path, 'rb');
if (!$fp) {
echo "無法開啟音頻檔案\n";
return;
}
$send_chunk = function() use ($conn, $fp, $enable_server_vad, $loop, &$send_chunk, &$is_running) {
if (!$is_running) {
fclose($fp);
return;
}
$chunk = fread($fp, 3200); // 100ms @ 16kHz 16bit 單聲道
if ($chunk === false || strlen($chunk) === 0) {
fclose($fp);
echo "音頻流結束\n";
if (!$enable_server_vad) {
$conn->send(json_encode([
'event_id' => 'event_789',
'type' => 'input_audio_buffer.commit',
]));
}
$conn->send(json_encode([
'event_id' => 'event_987',
'type' => 'session.finish',
]));
return;
}
$append = [
'event_id' => 'event_' . round(microtime(true) * 1000),
'type' => 'input_audio_buffer.append',
'audio' => base64_encode($chunk),
];
$conn->send(json_encode($append));
$loop->addTimer(0.1, $send_chunk);
};
$send_chunk();
}
Go
在運行樣本前,請確保已安裝相關依賴:
go get github.com/gorilla/websocket
package main
import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/gorilla/websocket"
)
const (
// 以下為新加坡地區的配置,調用時請將"{WorkspaceId}"替換為真實的業務空間ID,各地區的配置不同。
baseURL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime"
model = "qwen3-asr-flash-realtime"
audioFile = "your_audio_file.pcm" // 替換為您的PCM音頻檔案路徑
enableServerVad = true // 控制是否使用 VAD 模式
)
// 服務端事件結構
type ServerEvent struct {
Type string `json:"type"`
Transcript string `json:"transcript,omitempty"`
}
func main() {
// 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// 若沒有配置環境變數,請用阿里雲百鍊API Key將下行替換為:apiKey := "sk-xxx"
apiKey := os.Getenv("DASHSCOPE_API_KEY")
url := baseURL + "?model=" + model
log.Printf("Connecting to server: %s", url)
conn, err := connect(url, apiKey)
if err != nil {
log.Fatal("串連WebSocket失敗:", err)
}
defer conn.Close()
// 啟動接收訊息的 goroutine
sessionFinished := make(chan bool, 1)
go receiveMessages(conn, sessionFinished)
// 發送 session.update
if err := sendSessionUpdate(conn); err != nil {
log.Fatal("發送 session.update 失敗:", err)
}
// 等待會話配置完成
time.Sleep(3 * time.Second)
// 發送音頻流
if err := sendAudioStream(conn); err != nil {
log.Fatal("發送音頻失敗:", err)
}
// 等待 session.finished
<-sessionFinished
}
// 建立 WebSocket 串連
func connect(url, apiKey string) (*websocket.Conn, error) {
headers := http.Header{}
headers.Set("Authorization", "Bearer "+apiKey)
headers.Set("OpenAI-Beta", "realtime=v1")
conn, _, err := websocket.DefaultDialer.Dial(url, headers)
return conn, err
}
// 發送 session.update 事件
func sendSessionUpdate(conn *websocket.Conn) error {
session := map[string]interface{}{
"modalities": []string{"text"},
"input_audio_format": "pcm",
"sample_rate": 16000,
// "input_audio_transcription": map[string]interface{}{
// "language": "zh",
// },
}
if enableServerVad {
session["turn_detection"] = map[string]interface{}{
"type": "server_vad",
"threshold": 0.2,
"silence_duration_ms": 400,
}
} else {
session["turn_detection"] = nil
}
event := map[string]interface{}{
"event_id": "event_123",
"type": "session.update",
"session": session,
}
payload, _ := json.Marshal(event)
log.Printf("Sending session.update: %s", string(payload))
return conn.WriteMessage(websocket.TextMessage, payload)
}
// 發送音頻流(每100ms發送一個PCM chunk)
func sendAudioStream(conn *websocket.Conn) error {
f, err := os.Open(audioFile)
if err != nil {
return err
}
defer f.Close()
chunk := make([]byte, 3200) // 100ms @ 16kHz 16bit 單聲道
for {
n, err := f.Read(chunk)
if n > 0 {
event := map[string]interface{}{
"event_id": fmt.Sprintf("event_%d", time.Now().UnixMilli()),
"type": "input_audio_buffer.append",
"audio": base64.StdEncoding.EncodeToString(chunk[:n]),
}
payload, _ := json.Marshal(event)
if err := conn.WriteMessage(websocket.TextMessage, payload); err != nil {
return err
}
time.Sleep(100 * time.Millisecond)
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
log.Println("音頻流結束")
if !enableServerVad {
commitEvt := map[string]interface{}{
"event_id": "event_789",
"type": "input_audio_buffer.commit",
}
payload, _ := json.Marshal(commitEvt)
if err := conn.WriteMessage(websocket.TextMessage, payload); err != nil {
return err
}
}
finishEvt := map[string]interface{}{
"event_id": "event_987",
"type": "session.finish",
}
payload, _ := json.Marshal(finishEvt)
return conn.WriteMessage(websocket.TextMessage, payload)
}
// 接收並處理服務端事件
func receiveMessages(conn *websocket.Conn, sessionFinished chan<- bool) {
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Println("讀取訊息錯誤:", err)
sessionFinished <- true
return
}
var evt ServerEvent
if err := json.Unmarshal(msg, &evt); err != nil {
log.Println("解析訊息錯誤:", err)
continue
}
log.Printf("Received event: %s", evt.Type)
switch evt.Type {
case "conversation.item.input_audio_transcription.completed":
log.Printf("Final transcript: %s", evt.Transcript)
case "session.finished":
log.Println("Session finished")
sessionFinished <- true
return
}
}
}
Paraformer
Paraformer範例程式碼和Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime相似,將model替換成Paraformer模型名即可。
應用於生產環境
串連複用(WebSocket)
Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime 和 Paraformer 的 WebSocket 串連支援複用:一個識別任務結束後,無需重建立立串連即可開啟下一個任務。
複用流程:用戶端發送 finish-task,服務端返回 task-finished 後,可重新發送 run-task 開啟新任務。
重要
- 必須等服務端返回
task-finished事件後才可發起新任務。 - 複用串連中的不同任務需要使用不同的
task_id。 - 任務失敗時服務端返回錯誤事件並關閉串連,該串連不可複用。
- 任務結束後 60 秒無新任務,串連自動斷開。
Qwen3-ASR-Flash-Realtime 採用會話模式,每次會話結束後需主動中斷連線,不支援串連複用。
各模型事件說明請參見對應的API參考。
高並發最佳實務
DashScope SDK 內建池化機制,可複用 WebSocket 串連和識別對象,避免頻繁建立銷毀帶來的開銷。
重要目前僅 Paraformer Java SDK 支援此功能。
點擊查看高並發最佳實務
前提條件
- 擷取與配置 API Key
- 已安裝符合版本要求的 DashScope SDK,建議安裝最新版:Java SDK 版本≥2.16.9
Java SDK 通過內建的串連池和自訂的對象池協同工作,實現最佳效能:
- 串連池:SDK 內部整合的 OkHttp3 串連池,負責管理和複用底層的 WebSocket 串連,減少網路握手開銷。此功能預設開啟。
- 對象池:基於
commons-pool2實現,用於維護一組已預先建立好串連的Recognition對象。從池中擷取對象可消除串連建立的延遲,顯著降低首包延遲。
實現步驟
-
添加依賴
根據專案構建工具,在依賴設定檔中添加 dashscope-sdk-java 和 commons-pool2。
以 Maven 和 Gradle 為例,配置如下:
Maven
- 開啟 Maven 專案的
pom.xml檔案。 - 在
<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>- 儲存
pom.xml檔案。 - 使用 Maven 命令(如
mvn clean install或mvn compile)來更新專案依賴。
Gradle
- 開啟 Gradle 專案的
build.gradle檔案。 - 在
dependencies塊內添加以下依賴資訊。
dependencies { // 請將 'the-latest-version' 替換為2.16.9及以上版本,可在如下連結查詢相關版本號碼:https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java implementation group: 'com.alibaba', name: 'dashscope-sdk-java', version: 'the-latest-version' // 請將 'the-latest-version' 替換為最新版本,可在如下連結查詢相關版本號碼:https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 implementation group: 'org.apache.commons', name: 'commons-pool2', version: 'the-latest-version' }- 儲存
build.gradle檔案。 - 在命令列中,切換到專案根目錄,執行以下 Gradle 命令來更新專案依賴。
./gradlew build --refresh-dependencies如果使用 Windows 系統,命令應為:
gradlew build --refresh-dependencies - 開啟 Maven 專案的
-
配置串連池
通過環境變數配置串連池關鍵參數:
環境變數
描述
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。
-
設定物件池
通過環境變數設定物件池大小:
環境變數
描述
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;
}
}
-
從對象池中擷取 Recognition 對象
未歸還的對象數量超過對象池上限時,系統會額外建立新的
Recognition對象。這類新對象需重建立立 WebSocket 串連,無法複用。
recognizer = RecognitionObjectPool.getInstance().borrowObject();
-
進行語音辨識
調用
Recognition對象的 call 或 streamCall 方法進行語音辨識。 -
歸還 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 內部或商務邏輯拋出異常導致任務中斷時,必須執行以下兩個操作:
- 主動關閉底層的 WebSocket 串連
- 從對象池中廢棄該對象,防止被再次使用
// 關閉串連
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~2 分鐘),以充分填充串連池。
- 確認串連池已建立並維持足夠的活躍串連後,再開始正式的效能資料採集。
提升識別效果
- 選擇匹配採樣率的模型:8kHz 電話音頻直接使用 8kHz 模型,避免升採樣到 16kHz 造成的資訊失真。
- 最佳化輸入音頻品質:使用高品質麥克風,確保錄音環境信噪比高、無回聲。可在應用程式層整合降噪(如 RNNoise)、回聲消除(AEC)等演算法做預先處理。
設定容錯策略
-
用戶端重連:用戶端應實現斷線自動重連機制,以應對網路抖動。Python SDK 參考實現如下:
- 捕獲異常:在
Callback類中實現on_error方法。當dashscopeSDK遇到網路錯誤或其他問題時,會調用該方法。 - 狀態通知:當
on_error被觸發時,設定重連訊號。在Python中可以使用threading.Event,它是一種安全執行緒的訊號標誌。 - 重連迴圈:將主邏輯包裹在一個
for迴圈中(例如重試3次)。當檢測到重連訊號後,當前輪次的識別會中斷,清理資源,然後等待幾秒鐘,再次進入迴圈,建立一個全新的串連。
- 捕獲異常:在
-
設定心跳防止串連斷開:當需要與服務端保持長串連時,可將參數heartbeat設定為
true,即使音頻中長時間沒有聲音,與服務端的串連也不會中斷。 -
模型限流:在調用模型介面時請注意模型的限流規則。
支援的模型與地區
新加坡
調用以下模型時,請選擇新加坡地區的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(快照版)
華北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-2026-02-28(最新快照版)、fun-asr-realtime-2025-11-07(快照版)、fun-asr-realtime-2025-09-15(快照版)
- fun-asr-flash-8k-realtime(穩定版,當前等同fun-asr-flash-8k-realtime-2026-01-28)、fun-asr-flash-8k-realtime-2026-01-28
-
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(快照版)
-
Paraformer:paraformer-realtime-v2、paraformer-realtime-v1、paraformer-realtime-8k-v2、paraformer-realtime-8k-v1
API參考
- 即時語音辨識-Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime API參考
- 即時語音辨識-Qwen3-ASR-Flash-Realtime API參考
- 即時語音辨識-Paraformer API參考
- AOQ用戶端API(適用於 Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime)
常見問題
即時語音辨識支援哪些音頻格式?
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)防止長時間無音頻導致串連斷開。詳細的容錯策略請參見應用於生產環境。