Layanan pengenalan ucapan real-time menerima aliran audio dan mentranskripsinya menjadi teks berbobot tanda baca secara real-time. Gunakan layanan ini untuk subtitel langsung, rapat daring, obrolan suara, asisten cerdas, dan skenario serupa.
Ikhtisar
Layanan ini mengalirkan audio dan mengembalikan teks hasil transkripsi dengan latensi rendah.
- Mengenali bahasa Mandarin dengan akurasi tinggi, serta dialek Kanton, Sichuan, dan lainnya.
- Menangani lingkungan akustik kompleks, dengan deteksi bahasa otomatis dan penyaringan cerdas terhadap audio non-ucapan.
- Mengenali berbagai keadaan emosional, termasuk kaget, tenang, senang, sedih, jijik, marah, dan takut.
- Mendukung hotword kustom untuk meningkatkan akurasi pengenalan istilah tertentu.
- Mendukung peningkatan konteks untuk meningkatkan akurasi pengenalan dengan meneruskan riwayat percakapan atau istilah domain.
- Menghasilkan timestamp untuk menghasilkan hasil pengenalan terstruktur.
- Menerima laju sampel fleksibel dan berbagai format audio agar sesuai dengan berbagai lingkungan perekaman.
Untuk skenario batch seperti transkripsi rapat, analisis panggilan, dan pembuatan subtitel, gunakan Pengenalan ucapan non-real-time. Untuk panduan memilih model, lihat Speech-to-text.
Prasyarat
- Kunci API harus Dapatkan kunci API dan ditetapkan sebagai Variabel lingkungan.
- Untuk memanggil layanan melalui SDK DashScope, instal SDK terbaru.
Mulai cepat
Contoh berikut menunjukkan cara memanggil layanan pengenalan ucapan real-time melalui SDK DashScope.
Qwen-Audio-3.0-ASR-Flash-Streaming/ Fun-ASR -Realtime
Selain WebSocket, model ini juga mendukung protokol AOQ. Untuk integrasi sisi klien yang memprioritaskan latensi stabil, ketahanan pada jaringan lemah, serta penekanan noise full-duplex dan pembatalan gema bawaan, AOQ direkomendasikan. Untuk perbandingan protokol, lihat Ikhtisar API Realtime.
Kenali ucapan dari mikrofon
Kenali ucapan dari mikrofon dan tampilkan teks secara real-time, sehingga kata-kata muncul saat pembicara berbicara.
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 {
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
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")
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio Anda: .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);
// Buat format audio
AudioFormat audioFormat = new AudioFormat(16000, 16, 1, true, false);
// Sesuaikan perangkat perekaman default berdasarkan format
TargetDataLine targetDataLine =
AudioSystem.getTargetDataLine(audioFormat);
targetDataLine.open(audioFormat);
// Mulai merekam
targetDataLine.start();
ByteBuffer buffer = ByteBuffer.allocate(1024);
long start = System.currentTimeMillis();
// Rekam selama 50 detik dan lakukan transkripsi real-time
while (System.currentTimeMillis() - start < 50000) {
int read = targetDataLine.read(buffer.array(), 0, buffer.capacity());
if (read > 0) {
buffer.limit(read);
// Kirim data audio yang direkam ke layanan pengenalan streaming
recognizer.sendAudioFrame(buffer);
buffer = ByteBuffer.allocate(1024);
// Laju perekaman dibatasi; tidur sejenak untuk mencegah penggunaan CPU berlebihan
Thread.sleep(20);
}
}
recognizer.stop();
} catch (Exception e) {
e.printStackTrace();
} finally {
// Tutup koneksi WebSocket setelah tugas selesai
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
Sebelum menjalankan contoh Python, instal toolkit pihak ketiga untuk pemutaran dan perekaman audio dengan pip install pyaudio.
import os
import signal # untuk penanganan event keyboard (tekan "Ctrl+C" untuk menghentikan perekaman)
import sys
import dashscope
import pyaudio
from dashscope.audio.asr import *
mic = None
stream = None
# Atur parameter perekaman
sample_rate = 16000 # laju pengambilan sampel (Hz)
channels = 1 # saluran mono
dtype = 'int16' # tipe data
format_pcm = 'pcm' # format data audio
block_size = 3200 # jumlah frame per buffer
# Callback pengenalan ucapan real-time
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.') # pengenalan selesai
def on_error(self, message) -> None:
print('RecognitionCallback task_id: ', message.request_id)
print('RecognitionCallback error: ', message.message)
# Hentikan dan tutup aliran audio jika sedang berjalan
if 'stream' in globals() and stream.active:
stream.stop()
stream.close()
# Keluar dari program secara paksa
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 ditekan, hentikan pengenalan ...')
# Hentikan pengenalan
recognition.stop()
print('Pengenalan dihentikan.')
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(),
))
# Keluar dari program secara paksa
sys.exit(0)
# fungsi utama
if __name__ == '__main__':
# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio Anda: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
# Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'
# Buat callback pengenalan
callback = Callback()
# Panggil layanan pengenalan dalam mode async, Anda dapat menyesuaikan parameter pengenalan, seperti model, format,
# sample_rate
recognition = Recognition(
model='qwen-audio-3.0-asr-flash-streaming',
format=format_pcm,
# 'pcm'、'wav'、'opus'、'speex'、'aac'、'amr', Anda dapat memeriksa format yang didukung di dokumen
sample_rate=sample_rate,
# mendukung 8000, 16000
semantic_punctuation_enabled=False,
callback=callback)
# Mulai pengenalan
recognition.start()
signal.signal(signal.SIGINT, signal_handler)
print("Tekan 'Ctrl+C' untuk menghentikan perekaman dan pengenalan...")
# Buat pendengar keyboard hingga "Ctrl+C" ditekan
while True:
if stream:
data = stream.read(3200, exception_on_overflow=False)
recognition.send_audio_frame(data)
else:
break
recognition.stop()
Kenali file audio lokal
Kenali file audio lokal dan tampilkan hasilnya. Ini cocok untuk skenario real-time singkat seperti percakapan obrolan, perintah suara, metode input suara, dan pencarian suara.
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 {
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
// Dalam aplikasi nyata, metode ini hanya perlu dijalankan sekali di awal program; tidak perlu dijalankan berulang kali.
warmUp();
ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(new RealtimeRecognitionTask(Paths.get(System.getProperty("user.dir"), "{YOUR_AUDIO_FILE}")));
executorService.shutdown();
// tunggu semua tugas selesai
executorService.awaitTermination(1, TimeUnit.MINUTES);
System.exit(0);
}
public static void warmUp() {
try {
// Permintaan GET ringan untuk membuat koneksi
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) {
// Setel ulang flag untuk memungkinkan percobaan ulang jika pemanasan gagal
}
}
}
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")
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio Anda: .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);
// Harap ganti path dengan path file audio Anda
System.out.println(TimeUtils.getTimestamp()+" "+"[" + threadName + "] Input file_path is: " + this.filepath);
// Baca file dan kirim audio per chunk
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 {
// Tutup koneksi WebSocket setelah tugas selesai
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 *
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Model Studio Anda: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
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') // pengenalan selesai
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}"):
// Baca semua data file ke buffer sekaligus
file_buffer = f.read()
f.close()
print("Start Recognition")
recognition.start()
// Kirim 3200 byte dari buffer setiap kali
buffer_size = len(file_buffer)
offset = 0
chunk_size = 3200
while offset < buffer_size:
// Hitung ukuran chunk data yang akan dikirim kali ini
remaining_bytes = buffer_size - offset
current_chunk_size = min(chunk_size, remaining_bytes)
// Ekstrak chunk data saat ini dari buffer
audio_data = file_buffer[offset:offset + current_chunk_size]
// Kirim frame data audio
recognition.send_audio_frame(audio_data)
// Perbarui offset
offset += current_chunk_size
// Tambahkan jeda untuk mensimulasikan transmisi real-time
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
CatatanKode contoh membaca your_audio_file.pcm (PCM16, 16 kHz, mono). Jika Anda hanya memiliki format MP3, WAV, atau serupa, konversi dengan 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; // Ukuran chunk audio dalam byte
private static final int SLEEP_INTERVAL_MS = 30; // Interval tidur dalam milidetik
public static void main(String[] args) throws InterruptedException, LineUnavailableException {
CountDownLatch finishLatch = new CountDownLatch(1);
OmniRealtimeParam param = OmniRealtimeParam.builder()
.model("qwen3-asr-flash-realtime")
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
.url("wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime")
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: .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);
// Baca dan kirim data audio per chunk
while ((bytesRead = audioInputStream.read(audioBuffer)) != -1) {
totalBytesRead += bytesRead;
String audioB64 = Base64.getEncoder().encodeToString(audioBuffer);
// Kirim chunk audio ke percakapan
conversation.appendAudio(audioB64);
// Tambahkan jeda kecil untuk mensimulasikan streaming audio real-time
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);
}
//kirim session.finish dan tunggu selesai dan tutup
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():
"""Konfigurasi output log"""
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():
"""Inisialisasi Kunci API"""
# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: 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('[Peringatan] Menggunakan placeholder kunci API, atur variabel lingkungan DASHSCOPE_API_KEY.')
class MyCallback(OmniRealtimeCallback):
"""Penanganan callback pengenalan waktu nyata"""
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('======Mulai Bicara======'),
'input_audio_buffer.speech_stopped': lambda r: print('======Berhenti Bicara======')
}
def on_open(self):
print('Koneksi dibuka')
def on_close(self, code, msg):
print(f'Koneksi ditutup, kode: {code}, pesan: {msg}')
def on_event(self, response):
try:
handler = self.handlers.get(response['type'])
if handler:
handler(response)
except Exception as e:
print(f'[Kesalahan] {e}')
def _handle_session_created(self, response):
print(f"Mulai sesi: {response['session']['id']}")
def _handle_final_text(self, response):
print(f"Teks yang dikenali akhir: {response['transcript']}")
def _handle_transcription_text(self, response):
print(f"Mendapatkan hasil transkripsi: {response['text'] + response['stash']}")
def read_audio_chunks(file_path, chunk_size=3200):
"""Membaca file audio dalam chunk"""
with open(file_path, 'rb') as f:
while chunk := f.read(chunk_size):
yield chunk
def send_audio(conversation, file_path, delay=0.1):
"""Kirim data audio"""
if not os.path.exists(file_path):
raise FileNotFoundError(f"File audio {file_path} tidak ada.")
print("Memproses file audio... Tekan 'Ctrl+C' untuk berhenti.")
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',
# Berikut ini adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi berbeda berdasarkan wilayah.
url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime',
callback=callback,
)
callback.conversation = conversation # Suntikkan percakapan ke dalam callback agar metodenya dapat dipanggil dalam callback
def handle_exit(sig, frame):
print('Ctrl+C ditekan, keluar...')
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)
# kirim session.finish dan tunggu hingga selesai dan tutup
conversation.end_session()
except Exception as e:
print(f"Terjadi kesalahan: {e}")
finally:
conversation.close()
print("Pemrosesan audio selesai.")
if __name__ == '__main__':
main()
Paraformer
Kode contoh Paraformer mirip dengan Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime. Ganti nama model dengan model Paraformer.
Konfigurasi pengenalan
Mode interaksi Qwen3-ASR-Flash-Realtime
API Realtime Qwen3-ASR-Flash-Realtime menawarkan dua mode interaksi:
- Mode VAD (default): Server secara otomatis mendeteksi awal dan akhir ucapan (segmentasi). Mode ini cocok untuk percakapan real-time, catatan rapat, dan skenario serupa. Untuk mengaktifkannya, konfigurasikan parameter
session.turn_detection(diaktifkan secara default). - Mode manual: Klien mengontrol segmentasi dengan mengirim
input_audio_buffer.commit. Mode ini cocok untuk skenario yang memerlukan kontrol eksplisit atas kapan audio dikirim, seperti mengirim pesan suara di aplikasi obrolan. Untuk mengaktifkannya, atursession.turn_detectionke null.
Beralih mode interaksi:
- WebSocket: Atur field
turn_detectiondalam eventsession.update.
{
"type": "session.update",
"session": {
"turn_detection": null
}
}
- SDK Python: Atur parameter
enable_turn_detectiondalam metodeupdate_session.
conversation.update_session(
enable_turn_detection=False
)
- SDK Java: Atur parameter
enableTurnDetectionmelaluiOmniRealtimeConfig.builder().
OmniRealtimeConfig config = OmniRealtimeConfig.builder()
.enableTurnDetection(false)
.build();
conversation.updateSession(config);
Untuk contoh kode SDK lengkap, lihat Referensi API SDK Python Qwen-ASR-Realtime dan SDK Java. Untuk siklus hidup event WebSocket, lihat Alur interaksi event.
Konfigurasi segmentasi VAD
Voice Activity Detection (VAD) menentukan kapan segmen ucapan berkelanjutan berakhir, yang memicu event hasil pengenalan akhir. Ketiga keluarga model mengaktifkan VAD sisi server secara default, tetapi nama parameter dan granularitas penyetelannya berbeda:
- Qwen-Audio-3.0-ASR-Flash-Streaming / Fun-ASR-Realtime / Paraformer: Dikonfigurasi melalui
max_sentence_silence(ambang batas diam VAD untuk segmentasi, dalam milidetik). Ketika keheningan setelah segmen ucapan melebihi ambang batas ini, sistem menganggap kalimat tersebut selesai. - Qwen3-ASR-Flash-Realtime: Dikonfigurasi melalui
session.turn_detection, yang mencakupsilence_duration_ms(durasi ambang batas keheningan yang mengakhiri giliran ketika dilewati; default server800, dengan400direkomendasikan untuk skenario percakapan dan obrolan yang memerlukan segmentasi cepat) danthreshold(sensitivitas deteksi VAD; default server0.2). Qwen3-ASR-Flash-Realtime juga mendukung Mode Manual, yang menonaktifkan VAD dan menggunakan commit sisi klien untuk segmentasi. Untuk detailnya, lihat Mode interaksi Qwen3-ASR-Flash-Realtime di atas.
Nama parameter bervariasi berdasarkan protokol: konsep yang sama disebut max_sentence_silence di Qwen-Audio-3.0-ASR-Flash-Streaming / Fun-ASR-Realtime / Paraformer, dan silence_duration_ms di Qwen3-ASR-Flash-Realtime. Untuk definisi field lengkap, lihat Referensi API.
Fitur lanjutan
Tingkatkan akurasi dengan hotword
Gunakan hotword untuk meningkatkan akurasi pengenalan istilah tertentu, seperti nama merek, nama pribadi, dan terminologi khusus.
Untuk konfigurasi dan penggunaan hotword terperinci, lihat Tingkatkan akurasi pengenalan.
Tingkatkan akurasi dengan peningkatan konteks
Peningkatan konteks meneruskan riwayat percakapan atau terminologi domain ke model ASR untuk secara signifikan meningkatkan akurasi transkripsi istilah khusus. Untuk penggunaan terperinci dan contoh hasil, lihat Peningkatan konteks.
Dapatkan timestamp
Keluarga model Qwen-Audio-3.0-ASR-Flash-Streaming, Fun-ASR-Realtime, dan Paraformer menghasilkan timestamp baik di tingkat kalimat maupun kata secara default, yang mendukung penyelarasan subtitel, penyorotan kata kunci, pembacaan karaoke, dan skenario serupa. Qwen3-ASR-Flash-Realtime (qwen3-asr-flash-realtime) saat ini tidak mengembalikan timestamp. Jika Anda memerlukan timestamp, gunakan Qwen-Audio-3.0-ASR-Flash-Streaming, Fun-ASR-Realtime, atau Paraformer. Untuk transkripsi file, model transkripsi file rekaman Qwen ASR qwen3-asr-flash-filetrans mendukung timestamp tingkat kata. Untuk detailnya, lihat Pengenalan ucapan non-real-time.
Timestamp dikembalikan dalam milidetik pada dua tingkat:
- Tingkat kalimat:
payload.output.sentence.begin_timedanpayload.output.sentence.end_timemenandai awal dan akhir kalimat lengkap dalam audio. Dalam hasil antara,end_timemungkinnulldan diisi dengan nilai akhir ketika kalimat berakhir (sentence_end = true). - Tingkat kata: Array
payload.output.sentence.words, di mana setiap elemen berisibegin_time,end_time,text(teks kata atau karakter), danpunctuation(tanda baca yang mengikuti kata, atau string kosong jika tidak ada).
Kutipan berikut menunjukkan struktur respons:
{
"payload": {
"output": {
"sentence": {
"begin_time": 170,
"end_time": 920,
"text": "OK, I got it",
"sentence_end": true,
"words": [
{ "begin_time": 170, "end_time": 295, "text": "OK", "punctuation": "," },
{ "begin_time": 295, "end_time": 503, "text": "I", "punctuation": "" },
{ "begin_time": 503, "end_time": 711, "text": "got", "punctuation": "" },
{ "begin_time": 711, "end_time": 920, "text": "it", "punctuation": "" }
]
}
}
}
}
Nama field di atas mengikuti jalur JSON WebSocket. SDK berbeda mengekspos field ini dengan konvensi penamaan mereka sendiri (kunci kamus, properti objek, metode getter, dan sebagainya). Untuk pemetaan field lengkap, lihat referensi API untuk setiap SDK.
Untuk definisi field lengkap, lihat Referensi API.
Pengenalan emosi
Qwen3-ASR-Flash-Realtime dan beberapa model Paraformer dapat menyertakan keadaan emosional pembicara dalam hasil transkripsi, tetapi keduanya berbeda dalam granularitas output dan cara fitur diaktifkan.
Qwen3-ASR-Flash-Realtime (qwen3-asr-flash-realtime): Selalu aktif, tidak perlu konfigurasi. Emosi dikembalikan melalui field tingkat atas emotion baik dalam event conversation.item.input_audio_transcription.text maupun conversation.item.input_audio_transcription.completed. Nilainya adalah salah satu dari tujuh emosi detail halus: surprised, neutral, happy, sad, disgusted, angry, dan fearful.
{
"type": "conversation.item.input_audio_transcription.text",
"emotion": "neutral",
"text": "The weather is nice today",
"stash": ""
}
Paraformer (paraformer-realtime-8k-v2): Ini adalah satu-satunya model Paraformer yang mendukung pengenalan emosi. Hasilnya dikembalikan melalui payload.output.sentence.emo_tag dan payload.output.sentence.emo_confidence. Nilainya adalah salah satu dari tiga polaritas: positive (seperti senang atau puas), negative (seperti marah atau murung), dan neutral (tidak ada emosi jelas). Keyakinan berkisar dari 0,0 hingga 1,0.
Pengenalan emosi dikembalikan hanya jika semua kondisi berikut terpenuhi:
- Modelnya adalah
paraformer-realtime-8k-v2. - Segmentasi semantik dimatikan:
semantic_punctuation_enabled = false(false adalah default, jadi tidak perlu pengaturan khusus). - Hasil dikembalikan hanya dalam event akhir kalimat, di mana
sentence_end = true.
Untuk berhenti mengembalikan field emosi, atur semantic_punctuation_enabled ke true. Ini mengaktifkan segmentasi semantik dan tidak lagi mengembalikan field emo_tag dan emo_confidence.
Nama field di atas mengikuti jalur JSON WebSocket. SDK berbeda mengekspos field ini dengan konvensi penamaan mereka sendiri (kunci kamus, properti objek, metode getter, dan sebagainya). Untuk pemetaan field lengkap, lihat referensi API untuk setiap SDK.
Untuk definisi field lengkap, batasan nilai, dan contoh, lihat Referensi API.
Penyaringan kata sensitif
Penyaringan kata sensitif mengganti atau menghapus kata sensitif dalam hasil pengenalan. Gunakan untuk inspeksi kualitas call-center, kepatuhan konten, tinjauan subtitel, dan skenario serupa.
Model yang didukung: Hanya Qwen-Audio-3.0-ASR-Flash-Streaming dan Fun-ASR-Realtime.
Batas: Anda dapat mengatur hingga 32 kata sensitif.
Perilaku default: Ketika parameter special_word_filter tidak diteruskan, tidak ada kata sensitif yang disaring.
Cara mengonfigurasi: special_word_filter adalah objek JSON dengan tiga subfield:
filter_with_signed.word_list: Array string yang mencantumkan kata sensitif untuk diganti dengan string karakter*sepanjang yang sama. Misalnya, dengan["test"], "Help me test it" menjadi "Help me **** it".filter_with_empty.word_list: Array string yang mencantumkan kata sensitif untuk dihapus sepenuhnya dari hasil. Misalnya, dengan["start"], "Is the game about to start" menjadi "Is the game about to".system_reserved_filter: Boolean yang default-nyafalse. Ini menentukan apakah penyaringan kata sensitif diaktifkan.
Contoh konfigurasi:
{
"special_word_filter": {
"filter_with_signed": {
"word_list": ["test"]
},
"filter_with_empty": {
"word_list": ["start", "occur"]
},
"system_reserved_filter": true
}
}
SDK berbeda mengekspos parameter ini dengan konvensi penamaan mereka sendiri (kunci kamus, properti objek, metode, dan sebagainya). Untuk pemetaan field lengkap, lihat referensi API.
Panggil protokol WebSocket mentah
Contoh berikut menunjukkan cara menghubungkan langsung ke server melalui protokol WebSocket mentah, untuk skenario yang tidak menggunakan SDK DashScope. Setiap contoh adalah implementasi minimal yang dapat dijalankan. Untuk protokol WebSocket, lihat referensi API masing-masing model.
Klik untuk melihat contoh protokol WebSocket mentah
Qwen-Audio-3.0-ASR-Flash-Streaming/ Fun-ASR-Realtime
Python
Sebelum menjalankan contoh, instal dependensi dengan perintah berikut:
pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client
Jangan beri nama file contoh websocket.py. Nama ini bertentangan dengan pustaka websocket dan menyebabkan error berikut: 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
# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: api_key = "sk-xxx"
api_key = os.environ.get('DASHSCOPE_API_KEY')
# Berikut ini adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi berbeda berdasarkan wilayah.
url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference' # Alamat server WebSocket
audio_file = '{YOUR_AUDIO_FILE}' # Ganti dengan path ke file audio Anda
# Hasilkan ID acak 32 karakter
TASK_ID = uuid.uuid4().hex[:32]
task_started = False # Bendera yang menunjukkan apakah tugas telah dimulai
# Kirim instruksi 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))
# Kirim instruksi 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))
# Kirim aliran audio (kirim satu chunk biner setiap 100ms)
def send_audio_stream(ws):
chunk_size = 3200 # 100ms @ 16kHz 16bit mono
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('Audio stream ended')
send_finish_task(ws)
except Exception as e:
print('Error reading audio file:', e)
ws.close()
# Kirim instruksi run-task saat koneksi terbuka
def on_open(ws):
print('Connected to server')
send_run_task(ws)
# Tangani pesan yang diterima
def on_message(ws, data):
global task_started
message = json.loads(data)
event = message['header']['event']
if event == 'task-started':
print('Task started')
task_started = True
threading.Thread(target=send_audio_stream, args=(ws,), daemon=True).start()
elif event == 'result-generated':
print('Recognition result:', message['payload']['output']['sentence']['text'])
if message['payload'].get('usage'):
print('Task billing duration (seconds):', message['payload']['usage']['duration'])
elif event == 'task-finished':
print('Task finished')
ws.close()
elif event == 'task-failed':
print('Task failed:', message['header'].get('error_message'))
ws.close()
else:
print('Unknown event:', event)
# Tutup koneksi jika event task-started tidak diterima
def on_close(ws, close_status_code, close_msg):
if not task_started:
print('Task not started, closing connection')
# Penanganan error
def on_error(ws, error):
print('WebSocket error:', 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
Sebelum menjalankan contoh, instal dependensi 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 {
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: private static final String API_KEY = "sk-xxx";
private static final String API_KEY = System.getenv().getOrDefault("DASHSCOPE_API_KEY", "sk-xxx");
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
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}"; // Ganti dengan path ke file audio Anda
private static final String MODEL = "qwen-audio-3.0-asr-flash-streaming";
// Hasilkan ID acak 32 karakter
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("Connected to server");
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("Task started");
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("Recognition result: " + text);
if (payload.has("usage")) {
System.out.println("Task billing duration (seconds): " + payload.getJSONObject("usage").get("duration"));
}
break;
case "task-finished":
System.out.println("Task finished");
close();
break;
case "task-failed":
String errMsg = message.getJSONObject("header").optString("error_message");
System.err.println("Task failed: " + errMsg);
close();
break;
default:
System.out.println("Unknown event: " + event);
}
}
@Override
public void onClose(int code, String reason, boolean remote) {
if (!taskStarted.get()) {
System.err.println("Task not started, closing connection");
}
}
@Override
public void onError(Exception ex) {
System.err.println("WebSocket error: " + ex.getMessage());
}
};
client.addHeader("Authorization", "bearer " + API_KEY);
client.connectBlocking();
}
// Kirim instruksi 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());
}
// Kirim aliran audio (kirim satu chunk biner setiap 100ms)
private static void sendAudioStream() {
int chunkSize = 3200; // 100ms @ 16kHz 16bit mono
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("Audio stream ended");
sendFinishTask();
} catch (Exception e) {
System.err.println("Error reading audio file: " + e.getMessage());
client.close();
}
}
// Kirim instruksi 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
Instal dependensi yang diperlukan:
npm install ws
npm install uuid
Kode contohnya sebagai berikut:
const fs = require('fs');
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid'); // Digunakan untuk menghasilkan UUID
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
const url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'; // alamat server WebSocket
const audioFile = '{YOUR_AUDIO_FILE}'; // Ganti dengan path ke file audio Anda
// Hasilkan ID acak 32 karakter
const TASK_ID = uuidv4().replace(/-/g, '').slice(0, 32);
// Buat klien WebSocket
const ws = new WebSocket(url, {
headers: {
Authorization: `bearer ${apiKey}`
}
});
let taskStarted = false; // Flag yang menunjukkan apakah tugas telah dimulai
// Kirim instruksi run-task saat koneksi terbuka
ws.on('open', () => {
console.log('Connected to server');
sendRunTask();
});
// Tangani pesan yang diterima
ws.on('message', (data) => {
const message = JSON.parse(data);
switch (message.header.event) {
case 'task-started':
console.log('Task started');
taskStarted = true;
sendAudioStream();
break;
case 'result-generated':
console.log('Recognition result:', message.payload.output.sentence.text);
if (message.payload.usage) {
console.log('Task billing duration (seconds):', message.payload.usage.duration);
}
break;
case 'task-finished':
console.log('Task finished');
ws.close();
break;
case 'task-failed':
console.error('Task failed:', message.header.error_message);
ws.close();
break;
default:
console.log('Unknown event:', message.header.event);
}
});
// Tutup koneksi jika event task-started tidak diterima
ws.on('close', () => {
if (!taskStarted) {
console.error('Task not started, closing connection');
}
});
// Kirim instruksi 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));
}
// Kirim aliran audio
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); // Kirim sekali setiap 100ms
}
}
audioStream.on('readable', () => {
sendNextChunk();
});
audioStream.on('end', () => {
console.log('Audio stream ended');
sendFinishTask();
});
audioStream.on('error', (err) => {
console.error('Error reading audio file:', err);
ws.close();
});
}
// Kirim instruksi finish-task
function sendFinishTask() {
const finishTaskMessage = {
header: {
action: 'finish-task',
task_id: TASK_ID,
streaming: 'duplex'
},
payload: {
input: {}
}
};
ws.send(JSON.stringify(finishTaskMessage));
}
// Penanganan error
ws.on('error', (error) => {
console.error('WebSocket error:', error);
});
C#
Kode contohnya sebagai berikut:
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;
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: 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.");
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
private const string WebSocketUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
// Ganti dengan path ke file audio Anda
private const string AudioFilePath = "{YOUR_AUDIO_FILE}";
static async Task Main(string[] args) {
// Membuat koneksi WebSocket dan mengonfigurasi header untuk autentikasi
_webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");
await _webSocket.ConnectAsync(new Uri(WebSocketUrl), _cancellationTokenSource.Token);
// Memulai thread untuk menerima pesan WebSocket secara asinkron
var receiveTask = ReceiveMessagesAsync();
// Mengirim instruksi run-task
string _taskId = Guid.NewGuid().ToString("N"); // Menghasilkan ID acak 32 karakter
var runTaskJson = GenerateRunTaskJson(_taskId);
await SendAsync(runTaskJson);
// Menunggu event task-started
while (!_taskStartedReceived) {
await Task.Delay(100, _cancellationTokenSource.Token);
}
// Membaca file lokal dan mengirim aliran audio untuk dikenali ke server
await SendAudioStreamAsync(AudioFilePath);
// Mengirim instruksi finish-task untuk mengakhiri tugas
var finishTaskJson = GenerateFinishTaskJson(_taskId);
await SendAsync(finishTaskJson);
// Menunggu event task-finished
while (!_taskFinishedReceived && !_cancellationTokenSource.IsCancellationRequested) {
try {
await Task.Delay(100, _cancellationTokenSource.Token);
} catch (OperationCanceledException) {
// Tugas telah dibatalkan, keluar dari loop
break;
}
}
// Menutup koneksi
if (!_cancellationTokenSource.IsCancellationRequested) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cancellationTokenSource.Token);
}
_cancellationTokenSource.Cancel();
try {
await receiveTask;
} catch (OperationCanceledException) {
// Mengabaikan exception operasi dibatalkan
}
}
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("Task started successfully");
_taskStartedReceived = true;
break;
case "result-generated":
Console.WriteLine($"Recognition result: {message["payload"]?["output"]?["sentence"]?["text"]?.GetValue<string>()}");
if (message["payload"]?["usage"] != null && message["payload"]?["usage"]?["duration"] != null) {
Console.WriteLine($"Task billing duration (seconds): {message["payload"]?["usage"]?["duration"]?.GetValue<int>()}");
}
break;
case "task-finished":
Console.WriteLine("Task finished");
_taskFinishedReceived = true;
_cancellationTokenSource.Cancel();
break;
case "task-failed":
Console.WriteLine($"Task failed: {message["header"]?["error_message"]?.GetValue<string>()}");
_cancellationTokenSource.Cancel();
break;
}
}
}
} catch (OperationCanceledException) {
// Mengabaikan exception operasi dibatalkan
}
}
private static async Task<JsonNode?> ReceiveMessageAsync(CancellationToken cancellationToken) {
var buffer = new byte[1024 * 4];
var segment = new ArraySegment<byte>(buffer);
var result = await _webSocket.ReceiveAsync(segment, cancellationToken);
if (result.MessageType == WebSocketMessageType.Close) {
await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", cancellationToken);
return null;
}
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
return JsonNode.Parse(message);
}
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]; // Mengirim 100ms data audio setiap kali
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); // interval 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
Proyek contoh memiliki struktur direktori berikut:
my-php-project/
├── composer.json
├── vendor/
└── index.php
Isi composer.json sebagai berikut. Sesuaikan versi dependensi sesuai kebutuhan:
{
"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/"
}
}
}
Isi index.php sebagai berikut:
<?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;
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference';
$audio_file_path = '{YOUR_AUDIO_FILE}'; // Ganti dengan path ke file audio Anda
$loop = Loop::get();
// Membuat konektor kustom
$socketConnector = new SocketConnector($loop, [
'tcp' => [
'bindto' => '0.0.0.0:0',
],
'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 "Connected to WebSocket server\n";
// Memulai thread untuk menerima pesan WebSocket secara asinkron
$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 "Unknown message format\n";
}
});
// Mendengarkan penutupan koneksi
$conn->on('close', function($code = null, $reason = null) {
echo "Connection closed\n";
if ($code !== null) {
echo "Close code: " . $code . "\n";
}
if ($reason !== null) {
echo "Close reason: " . $reason . "\n";
}
});
// Menghasilkan ID tugas
$taskId = generateTaskId();
// Mengirim instruksi run-task
sendRunTaskMessage($conn, $taskId);
}, function ($e) {
echo "Unable to connect: {$e->getMessage()}\n";
});
$loop->run();
/**
* Menghasilkan ID tugas
* @return string
*/
function generateTaskId(): string {
return bin2hex(random_bytes(16));
}
/**
* Mengirim instruksi 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 "Preparing to send the run-task instruction: " . $runTaskMessage . "\n";
$conn->send($runTaskMessage);
echo "run-task instruction sent\n";
}
/**
* Membaca file audio
* @param string $filePath
* @return bool|string
*/
function readAudioFile(string $filePath) {
$voiceData = file_get_contents($filePath);
if ($voiceData === false) {
echo "Unable to read the audio file\n";
}
return $voiceData;
}
/**
* Membagi data audio
* @param string $data
* @param int $chunkSize
* @return array
*/
function splitAudioData(string $data, int $chunkSize): array {
return str_split($data, $chunkSize);
}
/**
* Mengirim instruksi 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 "Preparing to send the finish-task instruction: " . $finishTaskMessage . "\n";
$conn->send($finishTaskMessage);
echo "finish-task instruction sent\n";
}
/**
* Menangani event
* @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 "Task started, sending audio data...\n";
// Membaca file audio
$voiceData = readAudioFile($audio_file_path);
if ($voiceData === false) {
echo "Unable to read the audio file\n";
$conn->close();
return;
}
// Membagi data audio
$chunks = splitAudioData($voiceData, 1024);
// Mendefinisikan fungsi kirim
$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);
// Kirim chunk berikutnya setelah 100ms
$loop->addTimer(0.1, $sendChunk);
} else {
echo "All data chunks sent\n";
$allChunksSent = true;
// Mengirim instruksi finish-task
sendFinishTaskMessage($conn, $taskId);
}
};
// Mulai mengirim data audio
$sendChunk();
break;
case 'result-generated':
$result = $response['payload']['output']['sentence'];
echo "Recognition result: " . $result['text'] . "\n";
if (isset($response['payload']['usage']['duration'])) {
echo "Task billing duration (seconds): " . $response['payload']['usage']['duration'] . "\n";
}
break;
case 'task-finished':
echo "Task finished\n";
$conn->close();
break;
case 'task-failed':
echo "Task failed\n";
echo "Error code: " . $response['header']['error_code'] . "\n";
echo "Error message: " . $response['header']['error_message'] . "\n";
$conn->close();
break;
case 'error':
echo "Error: " . $response['payload']['message'] . "\n";
break;
default:
echo "Unknown event: " . $response['header']['event'] . "\n";
break;
}
// Jika semua data telah dikirim dan tugas selesai, tutup koneksi
if ($allChunksSent && $response['header']['event'] == 'task-finished') {
// Tunggu 1 detik untuk memastikan semua data telah ditransmisikan
$loop->addTimer(1, function() use ($conn) {
$conn->close();
echo "Client closed the connection\n";
});
}
}
Go
package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/google/uuid"
"github.com/gorilla/websocket"
)
const (
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
wsURL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference" // alamat server WebSocket
audioFile = "{YOUR_AUDIO_FILE}" // Ganti dengan path ke file audio Anda
)
var dialer = websocket.DefaultDialer
func main() {
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: apiKey := "sk-xxx"
apiKey := os.Getenv("DASHSCOPE_API_KEY")
// Menghubungkan ke layanan WebSocket
conn, err := connectWebSocket(apiKey)
if err != nil {
log.Fatal("Failed to connect to WebSocket: ", err)
}
defer closeConnection(conn)
// Memulai goroutine untuk menerima hasil
taskStarted := make(chan bool)
taskDone := make(chan bool)
startResultReceiver(conn, taskStarted, taskDone)
// Mengirim instruksi run-task
taskID, err := sendRunTaskCmd(conn)
if err != nil {
log.Fatal("Failed to send the run-task instruction: ", err)
}
// Menunggu event task-started
waitForTaskStarted(taskStarted)
// Mengirim aliran file audio untuk dikenali
if err := sendAudioData(conn); err != nil {
log.Fatal("Failed to send audio: ", err)
}
// Mengirim instruksi finish-task
if err := sendFinishTaskCmd(conn, taskID); err != nil {
log.Fatal("Failed to send the finish-task instruction: ", err)
}
// Menunggu tugas selesai atau gagal
<-taskDone
}
// Mendefinisikan struct untuk merepresentasikan data 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"`
}
// Menghubungkan ke layanan 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
}
// Memulai goroutine untuk menerima pesan WebSocket secara asinkron
func startResultReceiver(conn *websocket.Conn, taskStarted chan<- bool, taskDone chan<- bool) {
go func() {
for {
_, message, err := conn.ReadMessage()
if err != nil {
log.Println("Failed to parse server message: ", err)
return
}
var event Event
err = json.Unmarshal(message, &event)
if err != nil {
log.Println("Failed to parse event: ", err)
continue
}
if handleEvent(conn, event, taskStarted, taskDone) {
return
}
}
}()
}
// Mengirim instruksi 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
}
// Menghasilkan instruksi 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
}
// Menunggu event task-started
func waitForTaskStarted(taskStarted chan bool) {
select {
case <-taskStarted:
fmt.Println("Task started successfully")
case <-time.After(10 * time.Second):
log.Fatal("Timed out waiting for task-started; failed to start the task")
}
}
// Mengirim data audio
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
}
// Mengirim instruksi 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
}
// Menghasilkan instruksi 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
}
// Menangani event
func handleEvent(conn *websocket.Conn, event Event, taskStarted chan<- bool, taskDone chan<- bool) bool {
switch event.Header.Event {
case "task-started":
fmt.Println("Received the task-started event")
taskStarted <- true
case "result-generated":
if event.Payload.Output.Sentence.Text != "" {
fmt.Println("Recognition result: ", event.Payload.Output.Sentence.Text)
}
if event.Payload.Usage != nil {
fmt.Println("Task billing duration (seconds): ", event.Payload.Usage.Duration)
}
case "task-finished":
fmt.Println("Task finished")
taskDone <- true
return true
case "task-failed":
handleTaskFailed(event, conn)
taskDone <- true
return true
default:
log.Printf("Unexpected event: %v", event)
}
return false
}
// Menangani event task-failed
func handleTaskFailed(event Event, conn *websocket.Conn) {
if event.Header.ErrorMessage != "" {
log.Fatalf("Task failed: %s", event.Header.ErrorMessage)
} else {
log.Fatal("The task failed for an unknown reason")
}
}
// Menutup koneksi
func closeConnection(conn *websocket.Conn) {
if conn != nil {
conn.Close()
}
}
Qwen3-ASR-Flash-Realtime
CatatanKode contoh membaca your_audio_file.pcm (PCM16, 16 kHz, mono). Jika Anda hanya memiliki format MP3, WAV, atau serupa, konversi dengan ffmpeg:
ffmpeg -i your_audio.mp3 -ar 16000 -ac 1 -f s16le your_audio_file.pcm
Python
Sebelum menjalankan contoh, instal dependensi dengan perintah berikut:
pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client
Jangan beri nama file contoh websocket.py. Nama ini bertentangan dengan pustaka websocket dan menyebabkan error berikut: 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)
# Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: API_KEY="sk-xxx"
API_KEY = os.environ.get("DASHSCOPE_API_KEY", "sk-xxx")
QWEN_MODEL = "qwen3-asr-flash-realtime"
# Berikut ini adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi berbeda berdasarkan wilayah.
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}")
# Catatan: Dalam mode non-VAD, disarankan agar durasi kumulatif audio yang dikirim secara terus-menerus tidak melebihi 60 detik
enableServerVad = True
is_running = True # Tambahkan bendera berjalan
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 pembaruan sesi
event_manual = {
"event_id": "event_123",
"type": "session.update",
"session": {
"modalities": ["text"],
"input_audio_format": "pcm",
"sample_rate": 16000,
# "input_audio_transcription": {
# # Pengidentifikasi bahasa, opsional; disarankan untuk mengaturnya jika bahasa diketahui
# "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.0,
"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 # Hentikan thread pengiriman audio
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) # Tunggu hingga pembaruan sesi selesai
global is_running
with open(local_audio_path, 'rb') as audio_file:
logger.info(f"File reading started: {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"File reading finished: {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 is closed, stopping audio sending.")
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) # Simulasikan pengambilan waktu nyata
# Inisialisasi pencatatan log
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
Sebelum menjalankan contoh, instal dependensi 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());
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: 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";
// Mengontrol apakah akan menggunakan mode 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();
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
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));
// Hasil pengenalan akhir ada di event transcription.completed
if ("conversation.item.input_audio_transcription.completed".equals(eventType)) {
logger.info("Final transcript: " + data.optString("transcript"));
}
// Saat menerima event finished -> hentikan thread pengiriman dan tutup koneksi
if ("session.finished".equals(eventType)) {
logger.info("Closing WebSocket connection after session finished...");
isRunning.set(false); // Menghentikan thread pengiriman audio
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());
}
};
// Menambahkan header permintaan
client.addHeader("Authorization", "Bearer " + API_KEY);
client.addHeader("OpenAI-Beta", "realtime=v1");
client.connectBlocking(); // Blokir hingga koneksi terbentuk
// Ganti dengan path ke file audio yang akan dikenali
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();
}
/** Event pembaruan sesi (mengaktifkan/menonaktifkan 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) // Mode manual
);
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.0)
.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());
}
}
/** Mengirim aliran file audio */
private static void sendAudio(String localAudioPath) throws Exception {
Thread.sleep(3000); // Menunggu sesi siap
byte[] allBytes = Files.readAllBytes(Paths.get(localAudioPath));
logger.info("File reading started");
int offset = 0;
while (isRunning.get() && offset < allBytes.length) {
int chunkSize = Math.min(3200, allBytes.length - offset);
byte[] chunk = new byte[chunkSize];
System.arraycopy(allBytes, offset, chunk, 0, chunkSize);
offset += chunkSize;
if (client != null && client.isOpen()) {
String encoded = Base64.getEncoder().encodeToString(chunk);
JSONObject eventd = new JSONObject()
.put("event_id", "event_" + System.currentTimeMillis())
.put("type", "input_audio_buffer.append")
.put("audio", encoded);
client.send(eventd.toString());
logger.info("Sending audio event: " + eventd.getString("event_id"));
} else {
break; // Hindari melanjutkan pengiriman setelah putus
}
Thread.sleep(100); // Mensimulasikan pengiriman real-time
}
logger.info("File reading finished");
if (client != null && client.isOpen()) {
// Commit diperlukan dalam mode non-VAD
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.");
}
}
/** Inisialisasi logging */
private static void initLogger() {
logger.setLevel(Level.ALL);
Logger rootLogger = Logger.getLogger("");
for (Handler h : rootLogger.getHandlers()) {
rootLogger.removeHandler(h);
}
Handler consoleHandler = new ConsoleHandler();
consoleHandler.setLevel(Level.ALL);
consoleHandler.setFormatter(new SimpleFormatter());
logger.addHandler(consoleHandler);
}
}
Node.js
Sebelum menjalankan contoh, instal dependensi dengan perintah berikut:
npm install ws
/**
* Klien WebSocket Qwen-ASR Realtime (versi Node.js)
* Fitur:
* - Mendukung mode VAD dan mode Manual
* - Mengirim session.update untuk memulai sesi
* - Terus-menerus mengirim chunk audio melalui input_audio_buffer.append
* - Dalam mode Manual, mengirim input_audio_buffer.commit
* - Mengirim event session.finish
* - Menutup koneksi setelah menerima event session.finished
*/
import WebSocket from 'ws';
import fs from 'fs';
// ===== Konfigurasi =====
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: 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 untuk mode VAD, false untuk mode Manual
const localAudioPath = 'your_audio_file.pcm'; // Path ke file audio PCM16, 16kHz
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
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}`);
// ===== Kontrol status =====
let isRunning = true;
// ===== Membuat koneksi =====
const ws = new WebSocket(url, {
headers: {
'Authorization': `Bearer ${API_KEY}`,
'OpenAI-Beta': 'realtime=v1'
}
});
// ===== Binding event =====
ws.on('open', () => {
console.log('[WebSocket] Connected to server.');
sendSessionUpdate();
// Memulai thread pengiriman audio
sendAudio(localAudioPath);
});
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
console.log('[Received Event]:', JSON.stringify(data, null, 2));
// Hasil pengenalan akhir ada di event transcription.completed
if (data.type === 'conversation.item.input_audio_transcription.completed') {
console.log(`[Final Transcript] ${data.transcript}`);
}
// Menerima event finished
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);
});
// ===== Pembaruan sesi =====
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.0,
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));
}
}
// ===== Mengirim aliran file audio =====
function sendAudio(audioPath) {
setTimeout(() => {
console.log(`[File Read Start] ${audioPath}`);
const buffer = fs.readFileSync(audioPath);
let offset = 0;
const chunkSize = 3200; // Sekitar 0.1s audio PCM16
function sendChunk() {
if (!isRunning) return;
if (offset >= buffer.length) {
isRunning = false; // Menghentikan pengiriman audio
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); // Mensimulasikan pengiriman real-time
}
sendChunk();
}, 3000); // Menunggu konfigurasi sesi selesai
}
C#
Kode contohnya sebagai berikut:
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;
// Mengontrol apakah akan menggunakan mode VAD
private const bool EnableServerVad = true;
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: 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";
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
private const string BaseUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime";
private const string AudioFilePath = "your_audio_file.pcm"; // Ganti dengan path ke file audio PCM Anda
static async Task Main(string[] args) {
var url = $"{BaseUrl}?model={Model}";
Console.WriteLine($"Connecting to server: {url}");
// Mengatur header autentikasi
_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.");
// Memulai tugas penerima pesan
var receiveTask = ReceiveMessagesAsync();
// Mengirim konfigurasi session.update
await SendSessionUpdateAsync();
// Mengirim aliran audio
await SendAudioStreamAsync();
// Menunggu event 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);
}
// Mengirim event 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.0,
["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());
}
// Mengirim aliran audio (kirim satu chunk PCM setiap 100ms)
private static async Task SendAudioStreamAsync() {
await Task.Delay(3000); // Menunggu konfigurasi sesi selesai
const int chunkSize = 3200; // 100ms @ 16kHz 16bit mono
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());
}
}
// Menerima dan menangani event sisi server
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
Proyek contoh memiliki struktur direktori berikut:
my-php-project/
├── composer.json
├── vendor/
└── index.php
Isi composer.json sebagai berikut. Sesuaikan versi dependensi sesuai kebutuhan:
{
"require": {
"react/event-loop": "^1.3",
"react/socket": "^1.11",
"ratchet/pawl": "^0.4"
}
}
Isi index.php sebagai berikut:
<?php
require __DIR__ . '/vendor/autoload.php';
use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
$model = 'qwen3-asr-flash-realtime';
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
$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'; // Ganti dengan path ke file audio PCM Anda
// Mengontrol apakah akan menggunakan mode VAD
$enable_server_vad = true;
$loop = Loop::get();
$socketConnector = new SocketConnector($loop, [
'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 "Connected to WebSocket server\n";
// Mendengarkan event sisi server
$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 "Connection closed\n";
});
// Mengirim event session.update
sendSessionUpdate($conn, $enable_server_vad);
// Mulai mengirim audio setelah konfigurasi sesi selesai
$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 "Unable to connect: {$e->getMessage()}\n";
});
$loop->run();
// Mengirim event 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.0,
'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";
}
// Mengirim aliran audio (kirim satu chunk PCM setiap 100ms)
function sendAudioStream($conn, $audio_file_path, $enable_server_vad, $loop, &$is_running) {
$fp = fopen($audio_file_path, 'rb');
if (!$fp) {
echo "Unable to open the audio file\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 mono
if ($chunk === false || strlen($chunk) === 0) {
fclose($fp);
echo "Audio stream ended\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
Sebelum menjalankan contoh, instal dependensi yang diperlukan:
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 (
// Berikut adalah konfigurasi untuk wilayah Singapura. Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
baseURL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime"
model = "qwen3-asr-flash-realtime"
audioFile = "your_audio_file.pcm" // Ganti dengan path ke file audio PCM Anda
enableServerVad = true // Mengontrol apakah akan menggunakan mode VAD
)
// Struktur event sisi server
type ServerEvent struct {
Type string `json:"type"`
Transcript string `json:"transcript,omitempty"`
}
func main() {
// Kunci API berbeda antara wilayah Singapura dan Beijing. Dapatkan Kunci API Anda: https://www.alibabacloud.com/help/zh/model-studio/get-api-key
// Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris berikut dengan Kunci API Alibaba Cloud Model Studio Anda: 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("Failed to connect to WebSocket: ", err)
}
defer conn.Close()
// Memulai goroutine untuk menerima pesan
sessionFinished := make(chan bool, 1)
go receiveMessages(conn, sessionFinished)
// Mengirim session.update
if err := sendSessionUpdate(conn); err != nil {
log.Fatal("Failed to send session.update: ", err)
}
// Menunggu konfigurasi sesi selesai
time.Sleep(3 * time.Second)
// Mengirim aliran audio
if err := sendAudioStream(conn); err != nil {
log.Fatal("Failed to send audio: ", err)
}
// Menunggu session.finished
<-sessionFinished
}
// Membuat koneksi 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
}
// Mengirim event 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.0,
"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)
}
// Mengirim aliran audio (kirim satu chunk PCM setiap 100ms)
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 mono
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("Audio stream ended")
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)
}
// Menerima dan menangani event sisi server
func receiveMessages(conn *websocket.Conn, sessionFinished chan<- bool) {
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Println("Error reading message: ", err)
sessionFinished <- true
return
}
var evt ServerEvent
if err := json.Unmarshal(msg, &evt); err != nil {
log.Println("Error parsing message: ", 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
Kode contoh Paraformer mirip dengan Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime. Ganti nama model dengan model Paraformer.
Terapkan di produksi
Gunakan kembali koneksi (WebSocket)
Koneksi WebSocket untuk Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime dan Paraformer mendukung penggunaan kembali: setelah satu tugas pengenalan selesai, Anda dapat memulai tugas berikutnya tanpa membuat koneksi baru.
Alur penggunaan kembali: Klien mengirim finish-task. Setelah server mengembalikan task-finished, klien dapat mengirim run-task lagi untuk memulai tugas baru.
Penting
- Tunggu server mengembalikan event
task-finishedsebelum memulai tugas baru. - Tugas berbeda melalui koneksi yang digunakan kembali harus menggunakan nilai
task_idyang berbeda. - Saat tugas gagal, server mengembalikan event error dan menutup koneksi. Koneksi tersebut tidak dapat digunakan kembali.
- Jika tidak ada tugas baru yang dimulai dalam waktu 60 detik setelah tugas berakhir, koneksi akan ditutup secara otomatis.
Qwen3-ASR-Flash-Realtime menggunakan model sesi dan tidak mendukung penggunaan kembali koneksi. Tutup koneksi setelah setiap sesi berakhir.
Untuk event masing-masing model, lihat referensi API yang sesuai.
Praktik terbaik konkurensi tinggi
SDK DashScope mencakup mekanisme pooling bawaan yang menggunakan kembali koneksi WebSocket dan objek pengenalan, yang menghindari overhead pembuatan dan penghancuran yang sering.
PentingSaat ini, hanya SDK Java Paraformer yang mendukung fitur ini.
Klik untuk melihat praktik terbaik konkurensi tinggi
Prasyarat
- Dapatkan kunci API
- SDK DashScope telah diinstal dan memenuhi persyaratan versi. Kami menyarankan Anda menginstal versi terbaru: SDK Java versi 2.16.9 atau lebih baru.
SDK Java menggabungkan pool koneksi bawaan dengan pool objek kustom untuk mencapai kinerja optimal:
- Pool koneksi: Pool koneksi OkHttp3 yang terintegrasi dalam SDK mengelola dan menggunakan kembali koneksi WebSocket dasar, yang mengurangi overhead handshake jaringan. Fitur ini diaktifkan secara default.
- Pool objek: Dibangun di atas
commons-pool2, pool objek mempertahankan serangkaian objekRecognitionyang koneksi-nya sudah dibuat. Meminjam objek dari pool menghilangkan latensi pengaturan koneksi dan secara signifikan mengurangi latensi paket pertama.
Langkah implementasi
-
Tambahkan dependensi
Tambahkan dashscope-sdk-java dan commons-pool2 ke file konfigurasi dependensi Anda, berdasarkan alat build proyek Anda.
Contoh berikut menunjukkan konfigurasi untuk Maven dan Gradle:
Maven
- Buka file
pom.xmlproyek Maven Anda. - Tambahkan dependensi berikut di dalam tag
<dependencies>.
<dependency> <groupId>com.alibaba</groupId> <artifactId>dashscope-sdk-java</artifactId> <!-- Ganti 'the-latest-version' dengan versi 2.16.9 atau lebih baru. Anda dapat mencari nomor versi di: 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> <!-- Ganti 'the-latest-version' dengan versi terbaru. Anda dapat mencari nomor versi di: https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 --> <version>the-latest-version</version> </dependency>- Simpan file
pom.xml. - Jalankan perintah Maven (seperti
mvn clean installataumvn compile) untuk memperbarui dependensi proyek.
Gradle
- Buka file
build.gradleproyek Gradle Anda. - Tambahkan dependensi berikut di dalam blok
dependencies.
dependencies { // Ganti 'the-latest-version' dengan versi 2.16.9 atau lebih baru. Anda dapat mencari nomor versi di: https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java implementation group: 'com.alibaba', name: 'dashscope-sdk-java', version: 'the-latest-version' // Ganti 'the-latest-version' dengan versi terbaru. Anda dapat mencari nomor versi di: https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 implementation group: 'org.apache.commons', name: 'commons-pool2', version: 'the-latest-version' }- Simpan file
build.gradle. - Di command line, beralih ke direktori root proyek dan jalankan perintah Gradle berikut untuk memperbarui dependensi proyek.
./gradlew build --refresh-dependenciesDi Windows, gunakan perintah berikut:
gradlew build --refresh-dependencies - Buka file
-
Konfigurasikan pool koneksi
Konfigurasikan parameter utama pool koneksi melalui variabel lingkungan:
Variabel lingkungan
Deskripsi
DASHSCOPE_CONNECTION_POOL_SIZE
Ukuran pool koneksi.
Nilai yang direkomendasikan: minimal dua kali konkurensi puncak.
Nilai default: 32.
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS
Jumlah maksimum permintaan asinkron.
Nilai yang direkomendasikan: sama dengan
DASHSCOPE_CONNECTION_POOL_SIZE.Nilai default: 32.
DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST
Jumlah maksimum permintaan asinkron per host.
Nilai yang direkomendasikan: sama dengan
DASHSCOPE_CONNECTION_POOL_SIZE.Nilai default: 32.
-
Konfigurasikan pool objek
Konfigurasikan ukuran pool objek melalui variabel lingkungan:
Variabel lingkungan
Deskripsi
RECOGNITION_OBJECTPOOL_SIZE
Ukuran pool objek.
Nilai yang direkomendasikan: 1,5 hingga 2 kali konkurensi puncak.
Nilai default: 500.
Penting
- Ukuran pool objek (
RECOGNITION_OBJECTPOOL_SIZE) harus kurang dari atau sama dengan ukuran pool koneksi (DASHSCOPE_CONNECTION_POOL_SIZE). Jika tidak, saat pool objek meminta objek dan pool koneksi penuh, thread pemanggil akan diblokir. - Ukuran pool objek tidak boleh melebihi batas permintaan per detik (QPS) akun Anda.
Buat pool objek dengan kode berikut:
- Ukuran pool objek (
class RecognitionObjectPool {
// ... Untuk contoh lengkap, lihat kode lengkap.
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;
}
}
-
Pinjam objek Recognition dari pool objek
Saat jumlah objek yang belum dikembalikan melebihi batas pool objek, sistem membuat objek
Recognitiontambahan. Objek baru ini harus membuat koneksi WebSocket baru dan tidak dapat digunakan kembali.
recognizer = RecognitionObjectPool.getInstance().borrowObject();
-
Lakukan pengenalan ucapan
Panggil metode call atau streamCall objek
Recognitionuntuk melakukan pengenalan ucapan. -
Kembalikan objek Recognition
Setelah tugas pengenalan ucapan selesai, kembalikan objek Recognition agar dapat digunakan kembali. Jangan mengembalikan objek dengan tugas yang belum selesai atau gagal.
RecognitionObjectPool.getInstance().returnObject(recognizer);
Kode lengkap
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 {
// Berikut adalah konfigurasi untuk wilayah China (Beijing). Saat memanggil, ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi berbeda tiap wilayah.
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);
// ukuran chunk diatur ke 100 ms untuk laju sampel 16KHz
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();
}
}
Konfigurasi yang direkomendasikan
Konfigurasi berikut didasarkan pada hasil pengujian dari menjalankan hanya layanan pengenalan ucapan real-time Paraformer pada server Alibaba Cloud dengan spesifikasi yang ditentukan. Konkurensi mesin tunggal adalah jumlah tugas pengenalan ucapan real-time Paraformer yang berjalan secara bersamaan (yaitu, jumlah thread pekerja).
Spesifikasi mesin (Alibaba Cloud) | Konkurensi maksimum mesin tunggal | Ukuran pool objek | Ukuran pool koneksi |
|---|---|---|---|
4 vCPU, 8 GiB | 100 | 500 | 2000 |
8 vCPU, 16 GiB | 200 | 500 | 2000 |
16 vCPU, 32 GiB | 400 | 500 | 2000 |
Manajemen sumber daya dan penanganan error
-
Tugas berhasil: Panggil
GenericObjectPool.returnObject()untuk mengembalikan objek Recognition ke pool untuk digunakan kembali.PentingJangan mengembalikan objek Recognition dengan tugas yang belum selesai atau gagal.
-
Tugas gagal: Saat SDK atau logika bisnis Anda melemparkan pengecualian yang mengganggu tugas, lakukan dua tindakan berikut:
- Tutup secara aktif koneksi WebSocket dasar.
- Invalidasi objek dalam pool objek untuk mencegahnya digunakan kembali.
// Tutup koneksi.
recognizer.getDuplexApi().close(1000, "bye");
// Invalidasi recognizer yang gagal dalam pool objek.
RecognitionObjectPool.getInstance().invalidateObject(recognizer);
- Saat layanan mengembalikan error TaskFailed, tidak diperlukan penanganan tambahan.
Pemanasan dan pengukuran latensi
Saat mengevaluasi kinerja seperti latensi pemanggilan konkuren untuk SDK Java DashScope, kami menyarankan Anda menjalankan pemanasan yang cukup sebelum pengujian formal.
Mekanisme penggunaan kembali koneksi
SDK Java DashScope mengelola dan menggunakan kembali koneksi WebSocket melalui pool koneksi singleton global. Mekanisme ini bekerja sebagai berikut:
-
Pembuatan sesuai permintaan: SDK tidak membuat koneksi WebSocket sebelumnya saat startup layanan. Sebaliknya, SDK membuat koneksi sesuai permintaan pada pemanggilan pertama.
-
Penggunaan kembali berbatas waktu: Setelah permintaan selesai, koneksi tetap berada di pool hingga 60 detik untuk digunakan kembali.
- Jika permintaan baru tiba dalam waktu 60 detik, SDK menggunakan kembali koneksi yang ada dan menghindari overhead handshake berulang.
- Jika koneksi tetap tidak aktif selama lebih dari 60 detik, SDK menutupnya secara otomatis untuk melepaskan sumber daya.
Mengapa pemanasan penting
Dalam skenario berikut, pool koneksi mungkin tidak memiliki koneksi aktif untuk digunakan kembali, sehingga permintaan harus membuat koneksi baru:
- Aplikasi baru saja dimulai dan belum melakukan pemanggilan apa pun.
- Layanan telah tidak aktif selama lebih dari 60 detik, sehingga koneksi dalam pool telah ditutup karena timeout.
Dalam skenario ini, permintaan pertama atau awal memicu proses koneksi WebSocket lengkap (termasuk handshake TCP, negosiasi TLS, dan peningkatan protokol). Latensi end-to-end-nya jauh lebih tinggi dibandingkan permintaan berikutnya yang menggunakan kembali koneksi.
Pendekatan yang direkomendasikan
Sebelum menjalankan pengujian beban formal atau mengukur latensi, ikuti langkah pemanasan berikut:
- Simulasikan tingkat konkurensi pengujian formal dengan mengirim sejumlah pemanggilan terlebih dahulu (misalnya, selama 1 hingga 2 menit) untuk sepenuhnya mengisi pool koneksi.
- Setelah Anda memastikan bahwa pool koneksi telah membuat dan mempertahankan koneksi aktif yang cukup, mulailah mengumpulkan data kinerja formal.
Tingkatkan akurasi pengenalan
- Pilih model yang sesuai dengan laju sampel: Untuk audio telepon 8 kHz, gunakan model 8 kHz secara langsung. Ini menghindari kehilangan informasi yang disebabkan oleh upsampling ke 16 kHz.
- Tingkatkan kualitas input audio: Gunakan mikrofon berkualitas tinggi dan rekam di lingkungan dengan rasio signal-to-noise tinggi dan tanpa gema. Di lapisan aplikasi, Anda dapat mengintegrasikan algoritma seperti pengurangan noise (misalnya, RNNoise) dan pembatalan gema akustik (AEC) untuk pra-pemrosesan.
Siapkan strategi toleransi kesalahan
-
Koneksi ulang sisi klien: Klien harus mengimplementasikan koneksi ulang otomatis untuk menangani fluktuasi jaringan. Berikut adalah implementasi referensi untuk SDK Python:
- Tangkap pengecualian: Implementasikan metode
on_errordalam kelasCallback. SDKdashscopememanggil metode ini saat mengalami error jaringan atau masalah lain. - Beri sinyal status: Saat
on_errordipicu, atur sinyal koneksi ulang. Di Python, Anda dapat menggunakanthreading.Event, flag sinyal aman thread. - Loop koneksi ulang: Bungkus logika utama dalam loop
for(misalnya, coba 3 kali). Saat sinyal koneksi ulang terdeteksi, putaran pengenalan saat ini dihentikan, sumber daya dibersihkan, dan setelah beberapa detik loop dijalankan lagi untuk membuat koneksi baru.
- Tangkap pengecualian: Implementasikan metode
-
Atur heartbeat untuk menjaga koneksi tetap aktif: Untuk mempertahankan koneksi jangka panjang dengan server, atur parameter heartbeat ke
true. Koneksi ke server kemudian tetap terbuka meskipun audio tidak mengandung suara untuk waktu yang lama. -
Batas laju model: Saat memanggil API model, perhatikan aturan Pembatasan laju model.
Model dan wilayah yang didukung
Singapura
Untuk memanggil model berikut, gunakan Kunci API untuk wilayah Singapura:
- Qwen-Audio-3.0-ASR-Flash-Streaming: qwen-audio-3.0-asr-flash-streaming
- Fun-ASR-Realtime: fun-asr-realtime (versi stabil, saat ini setara dengan fun-asr-realtime-2025-11-07), fun-asr-realtime-2025-11-07 (versi snapshot)
- Qwen3-ASR-Flash-Realtime: qwen3-asr-flash-realtime (versi stabil, saat ini setara dengan qwen3-asr-flash-realtime-2025-10-27), qwen3-asr-flash-realtime-2026-02-10 (versi snapshot terbaru), qwen3-asr-flash-realtime-2025-10-27 (versi snapshot)
China (Beijing)
Untuk memanggil model berikut, gunakan Kunci API untuk wilayah China (Beijing):
-
Qwen-Audio-3.0-ASR-Flash-Streaming: qwen-audio-3.0-asr-flash-streaming
-
Fun-ASR-Realtime: fun-asr-realtime (versi stabil, saat ini setara dengan fun-asr-realtime-2025-11-07), fun-asr-realtime-2026-02-28 (versi snapshot terbaru), fun-asr-realtime-2025-11-07 (versi snapshot), fun-asr-realtime-2025-09-15 (versi snapshot)
- fun-asr-flash-8k-realtime (versi stabil, saat ini setara dengan fun-asr-flash-8k-realtime-2026-01-28), fun-asr-flash-8k-realtime-2026-01-28
-
Qwen3-ASR-Flash-Realtime: qwen3-asr-flash-realtime (versi stabil, saat ini setara dengan qwen3-asr-flash-realtime-2025-10-27), qwen3-asr-flash-realtime-2026-02-10 (versi snapshot terbaru), qwen3-asr-flash-realtime-2025-10-27 (versi snapshot)
-
Paraformer: paraformer-realtime-v2, paraformer-realtime-v1, paraformer-realtime-8k-v2, paraformer-realtime-8k-v1
Referensi API
- Referensi API Pengenalan ucapan real-time - Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime
- Referensi API Pengenalan ucapan real-time - Qwen3-ASR-Flash-Realtime
- Referensi API Pengenalan ucapan real-time - Paraformer
- SDK Klien AOQ (untuk Qwen-Audio-3.0-ASR-Flash-Streaming/Fun-ASR-Realtime)
FAQ
Format audio apa saja yang didukung oleh pengenalan ucapan real-time?
Model Qwen-Audio-3.0-ASR-Flash-Streaming, Fun-ASR-Realtime, dan Paraformer mendukung format pcm, wav, mp3, opus, speex, aac, dan amr. Untuk model Qwen3-ASR-Flash-Realtime, kami merekomendasikan format pcm atau opus. Format lain (seperti wav, aac, dan amr) diterima oleh lapisan validasi session.update, tetapi decoding sisi server mungkin gagal. Pastikan aliran audio menggunakan format yang direkomendasikan sebelum mengirimnya.
Apa perbedaan antara SDK dan API WebSocket, dan bagaimana cara memilih?
SDK DashScope menyembunyikan detail seperti manajemen koneksi WebSocket, autentikasi, dan koneksi ulang, yang menjadikannya pilihan tepat untuk integrasi cepat. Menghubungkan langsung ke API WebSocket memberikan kontrol lebih rinci dan cocok untuk bahasa pemrograman yang tidak didukung SDK atau skenario yang memerlukan manajemen koneksi kustom. Kami menyarankan Anda menggunakan SDK terlebih dahulu.
Bagaimana cara meningkatkan akurasi pengenalan untuk nama diri?
Gunakan hotword atau peningkatan konteks. Untuk metode konfigurasi dan catatan penggunaan terperinci, lihat Tingkatkan akurasi pengenalan.
Apa yang harus saya lakukan ketika koneksi sering terputus?
Implementasikan koneksi ulang sisi klien dan aktifkan parameter heartbeat (heartbeat=true) untuk mencegah koneksi terputus saat tidak ada audio untuk waktu yang lama. Untuk strategi toleransi kesalahan terperinci, lihat Terapkan di produksi.