All Products
Search
Document Center

Alibaba Cloud Model Studio:Pengenalan ucapan real-time - Qwen

Last Updated:Sep 09, 2026

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

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, atur session.turn_detection ke null.

Beralih mode interaksi:

  • WebSocket: Atur field turn_detection dalam event session.update.
{
    "type": "session.update",
    "session": {
        "turn_detection": null
    }
}
  • SDK Python: Atur parameter enable_turn_detection dalam metode update_session.
conversation.update_session(
    enable_turn_detection=False
)
  • SDK Java: Atur parameter enableTurnDetection melalui OmniRealtimeConfig.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 mencakup silence_duration_ms (durasi ambang batas keheningan yang mengakhiri giliran ketika dilewati; default server 800, dengan 400 direkomendasikan untuk skenario percakapan dan obrolan yang memerlukan segmentasi cepat) dan threshold (sensitivitas deteksi VAD; default server 0.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_time dan payload.output.sentence.end_time menandai awal dan akhir kalimat lengkap dalam audio. Dalam hasil antara, end_time mungkin null dan diisi dengan nilai akhir ketika kalimat berakhir (sentence_end = true).
  • Tingkat kata: Array payload.output.sentence.words, di mana setiap elemen berisi begin_time, end_time, text (teks kata atau karakter), dan punctuation (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-nya false. 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

  1. Tunggu server mengembalikan event task-finished sebelum memulai tugas baru.
  2. Tugas berbeda melalui koneksi yang digunakan kembali harus menggunakan nilai task_id yang berbeda.
  3. Saat tugas gagal, server mengembalikan event error dan menutup koneksi. Koneksi tersebut tidak dapat digunakan kembali.
  4. 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

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 objek Recognition yang koneksi-nya sudah dibuat. Meminjam objek dari pool menghilangkan latensi pengaturan koneksi dan secara signifikan mengurangi latensi paket pertama.

Langkah implementasi

  1. 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

    1. Buka file pom.xml proyek Maven Anda.
    2. 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>
    
    1. Simpan file pom.xml.
    2. Jalankan perintah Maven (seperti mvn clean install atau mvn compile) untuk memperbarui dependensi proyek.

    Gradle

    1. Buka file build.gradle proyek Gradle Anda.
    2. 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'
    }
    
    1. Simpan file build.gradle.
    2. Di command line, beralih ke direktori root proyek dan jalankan perintah Gradle berikut untuk memperbarui dependensi proyek.
    ./gradlew build --refresh-dependencies
    

    Di Windows, gunakan perintah berikut:

    gradlew build --refresh-dependencies
    
  2. 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.

  3. 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:

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;
    }
}
  1. Pinjam objek Recognition dari pool objek

    Saat jumlah objek yang belum dikembalikan melebihi batas pool objek, sistem membuat objek Recognition tambahan. Objek baru ini harus membuat koneksi WebSocket baru dan tidak dapat digunakan kembali.

recognizer = RecognitionObjectPool.getInstance().borrowObject();
  1. Lakukan pengenalan ucapan

    Panggil metode call atau streamCall objek Recognition untuk melakukan pengenalan ucapan.

  2. 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:

    1. Tutup secara aktif koneksi WebSocket dasar.
    2. 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:

  1. Simulasikan tingkat konkurensi pengujian formal dengan mengirim sejumlah pemanggilan terlebih dahulu (misalnya, selama 1 hingga 2 menit) untuk sepenuhnya mengisi pool koneksi.
  2. 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:

    1. Tangkap pengecualian: Implementasikan metode on_error dalam kelas Callback. SDK dashscope memanggil metode ini saat mengalami error jaringan atau masalah lain.
    2. Beri sinyal status: Saat on_error dipicu, atur sinyal koneksi ulang. Di Python, Anda dapat menggunakan threading.Event, flag sinyal aman thread.
    3. 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.
  • 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

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.