All Products
Search
Document Center

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

Last Updated:Jul 07, 2026

Pengenalan ucapan real-time mentranskripsikan aliran audio menjadi teks ber-tanda baca melalui WebSocket dengan latensi rendah, cocok untuk keterangan langsung (live captions), rapat daring, obrolan suara, dan asisten cerdas.

Ikhtisar

Protokol streaming WebSocket mengirimkan audio ke layanan dan mengembalikan teks hasil transkripsi dengan latensi rendah.

  • Pengenalan akurasi tinggi untuk bahasa Mandarin dan dialeknya, termasuk Kanton dan Sichuan.

  • Kinerja tangguh di lingkungan akustik kompleks, dengan deteksi bahasa otomatis dan penyaringan non-ucapan.

  • Pengenalan emosi dalam berbagai kondisi seperti terkejut, tenang, gembira, sedih, jijik, marah, dan takut.

  • Kata kunci khusus (hotwords) yang meningkatkan akurasi pengenalan untuk istilah tertentu.

  • Output timestamp untuk hasil pengenalan terstruktur.

  • Laju sampel dan format audio yang dapat dikonfigurasi untuk menyesuaikan berbagai lingkungan perekaman.

Untuk skenario batch seperti transkripsi rapat, analisis panggilan, dan pembuatan subtitel, gunakan Pengenalan ucapan non-real-time. Untuk panduan pemilihan model, lihat Speech-to-text.

Prasyarat

Mulai cepat

Contoh berikut menunjukkan cara memanggil pengenalan ucapan real-time melalui SDK DashScope.

Fun-ASR

Kenali audio mikrofon

Tangkap audio dari mikrofon dan alirkan teks hasil transkripsi secara real-time sehingga teks 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 {
        // Konfigurasi berikut ini untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja aktual Anda. Konfigurasi berbeda-beda per 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("fun-asr-realtime")
                // Kunci API untuk wilayah Singapura dan Beijing berbeda. Untuk mendapatkan Kunci API, lihat https://www.alibabacloud.com/help/en/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();

        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 tersebut.
            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 tinggi.
                    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

Contoh Python memerlukan library pyaudio untuk penangkapan audio. Instal dengan pip install pyaudio sebelum menjalankan contoh.

import os
import signal  # for keyboard events handling (press "Ctrl+C" to terminate recording)
import sys

import dashscope
import pyaudio
from dashscope.audio.asr import *

mic = None
stream = None

# Set recording parameters
sample_rate = 16000  # sampling rate (Hz)
channels = 1  # mono channel
dtype = 'int16'  # data type
format_pcm = 'pcm'  # the format of the audio data
block_size = 3200  # number of frames per buffer

# Real-time speech recognition callback
class Callback(RecognitionCallback):
    def on_open(self) -> None:
        global mic
        global stream
        print('RecognitionCallback open.')
        mic = pyaudio.PyAudio()
        stream = mic.open(format=pyaudio.paInt16,
                          channels=1,
                          rate=16000,
                          input=True)

    def on_close(self) -> None:
        global mic
        global stream
        print('RecognitionCallback close.')
        stream.stop_stream()
        stream.close()
        mic.terminate()
        stream = None
        mic = None

    def on_complete(self) -> None:
        print('RecognitionCallback completed.')  # recognition completed

    def on_error(self, message) -> None:
        print('RecognitionCallback task_id: ', message.request_id)
        print('RecognitionCallback error: ', message.message)
        # Stop and close the audio stream if it is running
        if 'stream' in globals() and stream.is_active():
            stream.stop_stream()
            stream.close()
        # Forcefully exit the program
        sys.exit(1)

    def on_event(self, result: RecognitionResult) -> None:
        sentence = result.get_sentence()
        if 'text' in sentence:
            print('RecognitionCallback text: ', sentence['text'])
            if RecognitionResult.is_sentence_end(sentence):
                print(
                    'RecognitionCallback sentence end, request_id:%s, usage:%s'
                    % (result.get_request_id(), result.get_usage(sentence)))

def signal_handler(sig, frame):
    print('Ctrl+C pressed, stop recognition ...')
    # Stop recognition
    recognition.stop()
    print('Recognition stopped.')
    print(
        '[Metric] requestId: {}, first package delay ms: {}, last package delay ms: {}'
        .format(
            recognition.get_last_request_id(),
            recognition.get_first_package_delay(),
            recognition.get_last_package_delay(),
        ))
    # Forcefully exit the program
    sys.exit(0)

# main function
if __name__ == '__main__':
    # The API keys for the Singapore and Beijing regions are different. To get an API key, see https://www.alibabacloud.com/help/en/model-studio/get-api-key
    # If you have not configured an environment variable, replace the following line with your Model Studio API key: dashscope.api_key = "sk-xxx"
    dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')

    # The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
    dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'

    # Create the recognition callback
    callback = Callback()

    # Call recognition service by async mode, you can customize the recognition parameters, like model, format,
    # sample_rate
    recognition = Recognition(
        model='fun-asr-realtime',
        format=format_pcm,
        # 'pcm', 'wav', 'opus', 'speex', 'aac', 'amr'. You can check the supported formats in the document.
        sample_rate=sample_rate,
        # Supports 16000.
        semantic_punctuation_enabled=False,
        callback=callback)

    # Start recognition
    recognition.start()

    signal.signal(signal.SIGINT, signal_handler)
    print("Press 'Ctrl+C' to stop recording and recognition...")
    # Create a keyboard listener until "Ctrl+C" is pressed

    while True:
        if stream:
            data = stream.read(3200, exception_on_overflow=False)
            recognition.send_audio_frame(data)
        else:
            break

    recognition.stop()

Kenali file audio lokal

Transkripsikan file audio lokal. Ini cocok untuk skenario real-time singkat seperti obrolan suara, perintah suara, input ucapan, dan pencarian suara.

Java

File audio yang digunakan dalam contoh adalah asr_example.wav.

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 {
        // The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
        Constants.baseWebsocketApiUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference";
        // In a real application, call this method only once at program startup.
        warmUp();

        ExecutorService executorService = Executors.newSingleThreadExecutor();
        executorService.submit(new RealtimeRecognitionTask(Paths.get(System.getProperty("user.dir"), "asr_example.wav")));
        executorService.shutdown();

        // Wait for all tasks to complete.
        executorService.awaitTermination(1, TimeUnit.MINUTES);
        System.exit(0);
    }

    public static void warmUp() {
        try {
            // Lightweight GET request to establish a connection.
            GeneralServiceOption warmupOption = GeneralServiceOption.builder()
                    .protocol(Protocol.HTTP)
                    .httpMethod(HttpMethod.GET)
                    .streamingMode(StreamingMode.OUT)
                    .path("assistants")
                    .build();

            warmupOption.setBaseHttpUrl(Constants.baseHttpApiUrl);
            GeneralApi<HalfDuplexParamBase> api = new GeneralApi<>();
            api.get(GeneralListParam.builder().limit(1L).build(), warmupOption);
        } catch (Exception e) {
            // Allow retry if warm-up fails.
        }
    }
}

class RealtimeRecognitionTask implements Runnable {
    private Path filepath;

    public RealtimeRecognitionTask(Path filepath) {
        this.filepath = filepath;
    }

    @Override
    public void run() {
        RecognitionParam param = RecognitionParam.builder()
                .model("fun-asr-realtime")
                // The API keys for the Singapore and Beijing regions are different. To get an API key, see https://www.alibabacloud.com/help/en/model-studio/get-api-key.
                // If you have not configured an environment variable, replace the following line with your Model Studio API key: .apiKey("sk-xxx")
                .apiKey(System.getenv("DASHSCOPE_API_KEY"))
                .format("wav")
                .sampleRate(16000)
                .build();
        Recognition recognizer = new Recognition();

        String threadName = Thread.currentThread().getName();

        ResultCallback<RecognitionResult> callback = new ResultCallback<RecognitionResult>() {
            @Override
            public void onEvent(RecognitionResult message) {
                if (message.isSentenceEnd()) {

                    System.out.println(TimeUtils.getTimestamp()+" "+
                            "[process " + threadName + "] Final Result:" + message.getSentence().getText());
                } else {
                    System.out.println(TimeUtils.getTimestamp()+" "+
                            "[process " + threadName + "] Intermediate Result: " + message.getSentence().getText());
                }
            }

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

            @Override
            public void onError(Exception e) {
                System.out.println(TimeUtils.getTimestamp()+" "+
                        "[" + threadName + "] RecognitionCallback error: " + e.getMessage());
            }
        };

        try {
            recognizer.call(param, callback);
            // Replace the path with your audio file path.
            System.out.println(TimeUtils.getTimestamp()+" "+"[" + threadName + "] Input file_path is: " + this.filepath);
            // Read the file and send audio in chunks.
            FileInputStream fis = new FileInputStream(this.filepath.toFile());
            byte[] allData = new byte[fis.available()];
            int ret = fis.read(allData);
            fis.close();

            int sendFrameLength = 3200;
            for (int i = 0; i * sendFrameLength < allData.length; i ++) {
                int start = i * sendFrameLength;
                int end = Math.min(start + sendFrameLength, allData.length);
                ByteBuffer byteBuffer = ByteBuffer.wrap(allData, start, end - start);
                recognizer.sendAudioFrame(byteBuffer);
                Thread.sleep(100);
            }

            System.out.println(TimeUtils.getTimestamp()+" "+LocalDateTime.now());
            recognizer.stop();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // Close the WebSocket connection after the task is complete.
            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());
    }
}

Python

File audio yang digunakan dalam contoh adalah asr_example.wav.

import os
import time
import dashscope
from dashscope.audio.asr import *

# API keys differ between the Singapore and Beijing regions. Get an API key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
# If you have not set an environment variable, replace the next line with your Model Studio API key: dashscope.api_key = "sk-xxx"
dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY')

# The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. The configuration varies by region.
dashscope.base_websocket_api_url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference'

from datetime import datetime

def get_timestamp():
    now = datetime.now()
    formatted_timestamp = now.strftime("[%Y-%m-%d %H:%M:%S.%f]")
    return formatted_timestamp

class Callback(RecognitionCallback):
    def on_complete(self) -> None:
        print(get_timestamp() + ' Recognition completed')  # recognition complete

    def on_error(self, result: RecognitionResult) -> None:
        print('Recognition task_id: ', result.request_id)
        print('Recognition error: ', result.message)
        exit(0)

    def on_event(self, result: RecognitionResult) -> None:
        sentence = result.get_sentence()
        if 'text' in sentence:
            print(get_timestamp() + ' RecognitionCallback text: ', sentence['text'])
        if RecognitionResult.is_sentence_end(sentence):
            print(get_timestamp() +
                  'RecognitionCallback sentence end, request_id:%s, usage:%s'
                  % (result.get_request_id(), result.get_usage(sentence)))

callback = Callback()

recognition = Recognition(model='fun-asr-realtime',
                          format='wav',
                          sample_rate=16000,
                          callback=callback)

try:
    audio_data: bytes = None
    f = open("asr_example.wav", 'rb')
    if os.path.getsize("asr_example.wav"):
        # Read the entire file into a buffer
        file_buffer = f.read()
        f.close()
        print("Start Recognition")
        recognition.start()

        # Send data in chunks of 3200 bytes
        buffer_size = len(file_buffer)
        offset = 0
        chunk_size = 3200

        while offset < buffer_size:
            # Calculate the size of the current chunk
            remaining_bytes = buffer_size - offset
            current_chunk_size = min(chunk_size, remaining_bytes)

            # Extract the current chunk from the buffer
            audio_data = file_buffer[offset:offset + current_chunk_size]

            # Send the audio frame
            recognition.send_audio_frame(audio_data)
            # Update the offset
            offset += current_chunk_size

            # Add a delay to simulate real-time transmission
            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(),
    ))

Qwen-ASR

Catatan

Berikut contoh membaca file your_audio_file.pcm (PCM16, 16 kHz, mono). Untuk mengonversi dari format MP3, WAV, atau lainnya, gunakan ffmpeg:

ffmpeg -i your_audio.mp3 -ar 16000 -ac 1 -f s16le your_audio_file.pcm

Java

import com.alibaba.dashscope.audio.omni.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sound.sampled.LineUnavailableException;
import java.io.File;
import java.io.FileInputStream;
import java.util.Base64;
import java.util.Collections;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

public class Qwen3AsrRealtimeUsage {
    private static final Logger log = LoggerFactory.getLogger(Qwen3AsrRealtimeUsage.class);
    private static final int AUDIO_CHUNK_SIZE = 1024; // Audio chunk size in bytes
    private static final int SLEEP_INTERVAL_MS = 30;  // Sleep interval in milliseconds

    public static void main(String[] args) throws InterruptedException, LineUnavailableException {
        CountDownLatch finishLatch = new CountDownLatch(1);

        OmniRealtimeParam param = OmniRealtimeParam.builder()
                .model("qwen3-asr-flash-realtime")
                // The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
                .url("wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime")
                // The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
                // If you have not configured the environment variable, replace the line below with your Model Studio API Key: .apikey("sk-xxx")
                .apikey(System.getenv("DASHSCOPE_API_KEY"))
                .build();

        OmniRealtimeConversation conversation = null;
        final AtomicReference<OmniRealtimeConversation> conversationRef = new AtomicReference<>(null);
        conversation = new OmniRealtimeConversation(param, new OmniRealtimeCallback() {
            @Override
            public void onOpen() {
                System.out.println("connection opened");
            }
            @Override
            public void onEvent(JsonObject message) {
                String type = message.get("type").getAsString();
                switch(type) {
                    case "session.created":
                        System.out.println("start session: " + message.get("session").getAsJsonObject().get("id").getAsString());
                        break;
                    case "conversation.item.input_audio_transcription.completed":
                        System.out.println("transcription: " + message.get("transcript").getAsString());
                        finishLatch.countDown();
                        break;
                    case "input_audio_buffer.speech_started":
                        System.out.println("======VAD Speech Start======");
                        break;
                    case "input_audio_buffer.speech_stopped":
                        System.out.println("======VAD Speech Stop======");
                        break;
                    case "conversation.item.input_audio_transcription.text":
                        System.out.println("transcription: " + message.get("text").getAsString() + message.get("stash").getAsString());
                        break;
                    default:
                        break;
                }
            }
            @Override
            public void onClose(int code, String reason) {
                System.out.println("connection closed code: " + code + ", reason: " + reason);
            }
        });
        conversationRef.set(conversation);
        try {
            conversation.connect();
        } catch (NoApiKeyException e) {
            throw new RuntimeException(e);
        }

        OmniRealtimeTranscriptionParam transcriptionParam = new OmniRealtimeTranscriptionParam();
        transcriptionParam.setLanguage("en");
        transcriptionParam.setInputAudioFormat("pcm");
        transcriptionParam.setInputSampleRate(16000);

        OmniRealtimeConfig config = OmniRealtimeConfig.builder()
                .modalities(Collections.singletonList(OmniRealtimeModality.TEXT))
                .transcriptionConfig(transcriptionParam)
                .build();
        conversation.updateSession(config);

        String filePath = "your_audio_file.pcm";
        File audioFile = new File(filePath);
        if (!audioFile.exists()) {
            log.error("Audio file not found: {}", filePath);
            return;
        }

        try (FileInputStream audioInputStream = new FileInputStream(audioFile)) {
            byte[] audioBuffer = new byte[AUDIO_CHUNK_SIZE];
            int bytesRead;
            int totalBytesRead = 0;

            log.info("Starting to send audio data from: {}", filePath);

            // Read and send audio data in chunks
            while ((bytesRead = audioInputStream.read(audioBuffer)) != -1) {
                totalBytesRead += bytesRead;
                String audioB64 = Base64.getEncoder().encodeToString(audioBuffer);
                // Send audio chunk to conversation
                conversation.appendAudio(audioB64);

                // Add small delay to simulate real-time audio streaming
                Thread.sleep(SLEEP_INTERVAL_MS);
            }

            log.info("Finished sending audio data. Total bytes sent: {}", totalBytesRead);

        } catch (Exception e) {
            log.error("Error sending audio from file: {}", filePath, e);
        }

        //send session.finish and wait for finish and close
        conversation.endSession();
        log.info("task finished");

        System.exit(0);
    }
}

Python

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():
    """Configure logging output"""
    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():
    """Initialize API Key"""
    # The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    # If you have not configured the environment variable, replace the line below with your Model Studio API Key: dashscope.api_key = "sk-xxx"
    dashscope.api_key = os.environ.get('DASHSCOPE_API_KEY', 'YOUR_API_KEY')
    if dashscope.api_key == 'YOUR_API_KEY':
        print('[Warning] Using placeholder API key, set DASHSCOPE_API_KEY environment variable.')

class MyCallback(OmniRealtimeCallback):
    """Real-time recognition callback handler"""
    def __init__(self, conversation):
        self.conversation = conversation
        self.handlers = {
            'session.created': self._handle_session_created,
            'conversation.item.input_audio_transcription.completed': self._handle_final_text,
            'conversation.item.input_audio_transcription.text': self._handle_transcription_text,
            'input_audio_buffer.speech_started': lambda r: print('======Speech Start======'),
            'input_audio_buffer.speech_stopped': lambda r: print('======Speech Stop======')
        }

    def on_open(self):
        print('Connection opened')

    def on_close(self, code, msg):
        print(f'Connection closed, code: {code}, msg: {msg}')

    def on_event(self, response):
        try:
            handler = self.handlers.get(response['type'])
            if handler:
                handler(response)
        except Exception as e:
            print(f'[Error] {e}')

    def _handle_session_created(self, response):
        print(f"Start session: {response['session']['id']}")

    def _handle_final_text(self, response):
        print(f"Final recognized text: {response['transcript']}")

    def _handle_transcription_text(self, response):
        print(f"Got transcription result: {response['text'] + response['stash']}")

def read_audio_chunks(file_path, chunk_size=3200):
    """Read audio file in chunks"""
    with open(file_path, 'rb') as f:
        while chunk := f.read(chunk_size):
            yield chunk

def send_audio(conversation, file_path, delay=0.1):
    """Send audio data"""
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"Audio file {file_path} does not exist.")

    print("Processing audio file... Press 'Ctrl+C' to stop.")
    for chunk in read_audio_chunks(file_path):
        audio_b64 = base64.b64encode(chunk).decode('ascii')
        conversation.append_audio(audio_b64)
        time.sleep(delay)

def main():
    setup_logging()
    init_api_key()

    audio_file_path = "./your_audio_file.pcm"
    callback = MyCallback(conversation=None)
    conversation = OmniRealtimeConversation(
        model='qwen3-asr-flash-realtime',
        # The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
        url='wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime',
        callback=callback,
    )
    callback.conversation = conversation  # Inject conversation into the callback so its methods can be invoked from the callback

    def handle_exit(sig, frame):
        print('Ctrl+C pressed, exiting...')
        conversation.close()
        sys.exit(0)

    signal.signal(signal.SIGINT, handle_exit)

    conversation.connect()

    transcription_params = TranscriptionParams(
        language='en',
        sample_rate=16000,
        input_audio_format="pcm"
    )

    conversation.update_session(
        output_modalities=[MultiModality.TEXT],
        enable_input_audio_transcription=True,
        transcription_params=transcription_params
    )

    try:
        send_audio(conversation, audio_file_path)
        # send session.finish and wait for finished and close
        conversation.end_session()
    except Exception as e:
        print(f"Error occurred: {e}")
    finally:
        conversation.close()
        print("Audio processing completed.")

if __name__ == '__main__':
    main()

Paraformer

Paraformer menggunakan kembali contoh kode dari Fun-ASR. Ganti parameter model dengan nama model Paraformer.

Konfigurasi pengenalan

Mode interaksi Qwen-ASR

API Realtime Qwen-ASR mendukung dua mode interaksi:

  • Mode VAD (default): Server secara otomatis mendeteksi awal dan akhir ucapan (deteksi giliran). Mode ini cocok untuk percakapan real-time, transkripsi rapat, dan skenario serupa. Untuk mengaktifkannya, konfigurasikan session.turn_detection (diaktifkan secara default).

  • Mode manual: Klien mengontrol deteksi giliran dengan mengirim input_audio_buffer.commit. Mode ini cocok untuk skenario yang memerlukan kontrol eksplisit atas waktu pengiriman, seperti pesan suara di aplikasi obrolan. Untuk mengaktifkannya, atur session.turn_detection ke null.

Beralih antar mode:

  • WebSocket: Atur bidang 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 event.

Deteksi giliran VAD

Voice Activity Detection (VAD) menentukan kapan ucapan berkelanjutan berakhir, yang memicu event hasil akhir. Ketiga keluarga model mengaktifkan VAD sisi server secara default, tetapi nama parameter dan kemampuan penyetelannya berbeda:

  • Qwen-ASR: Dikonfigurasi melalui session.turn_detection, yang mencakup silence_duration_ms (ambang batas durasi diam; giliran berakhir ketika diam melebihi nilai ini; default server 800; direkomendasikan 400 untuk skenario percakapan dan obrolan yang memerlukan deteksi giliran cepat) dan threshold (sensitivitas deteksi VAD; default server 0.2). Qwen-ASR juga mendukung Mode Manual yang menonaktifkan VAD dan memungkinkan klien mengontrol deteksi giliran melalui commit. Lihat Mode interaksi Qwen-ASR di atas.

  • Fun-ASR dan Paraformer: Dikonfigurasi melalui max_sentence_silence (ambang batas deteksi giliran VAD, dalam milidetik). Ketika diam setelah segmen ucapan melebihi ambang batas ini, kalimat dianggap berakhir.

Nama parameter berbeda per protokol: konsep yang sama dinyatakan sebagai silence_duration_ms di Qwen-ASR dan max_sentence_silence di Fun-ASR dan Paraformer. Untuk definisi bidang lengkap, lihat Referensi API.

Fitur lanjutan

Tingkatkan akurasi dengan hotwords

Fun-ASR dan Paraformer mendukung hotwords untuk meningkatkan akurasi pengenalan istilah tertentu, seperti nama merek, nama pribadi, dan kata benda khusus.

Untuk konfigurasi dan penggunaan hotword, lihat Tingkatkan akurasi pengenalan.

Timestamp

Fun-ASR dan Paraformer mengembalikan timestamp pada dua granularitas secara default: tingkat kalimat dan tingkat kata. Fitur ini mendukung penyelarasan subtitel, penyorotan kata kunci, tampilan lirik bergaya karaoke, serta kasus penggunaan serupa. Qwen-ASR Realtime (qwen3-asr-flash-realtime) saat ini tidak mengembalikan timestamp. Untuk mendapatkan timestamp, gunakan Fun-ASR atau Paraformer. Model transkripsi file Qwen-ASR qwen3-asr-flash-filetrans mendukung timestamp tingkat kata. Lihat Pengenalan ucapan non-real-time.

Timestamp dilaporkan dalam milidetik pada dua tingkat:

  • Tingkat kalimat: payload.output.sentence.begin_time dan payload.output.sentence.end_time menandai awal dan akhir kalimat dalam audio. Pada hasil antara, end_time mungkin null. Nilai akhir diisi ketika kalimat berakhir (sentence_end = true).

  • Tingkat kata: Larik payload.output.sentence.words. Setiap elemen berisi begin_time, end_time, text (teks karakter atau kata), dan punctuation (tanda baca yang mengikuti karakter; string kosong jika tidak ada).

Contoh respons (cuplikan):

{
  "payload": {
    "output": {
      "sentence": {
        "begin_time": 170,
        "end_time": 920,
        "text": "Okay, I got it.",
        "sentence_end": true,
        "words": [
          { "begin_time": 170, "end_time": 295, "text": "Okay", "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 bidang di atas menggunakan jalur JSON WebSocket. Setiap SDK mengekspos bidang-bidang ini sesuai konvensi penamaannya sendiri (kunci kamus, properti objek, metode getter, dan sebagainya). Untuk pemetaan bidang lengkap, lihat referensi API setiap SDK.

Untuk definisi bidang lengkap, lihat Referensi API.

Pengenalan emosi

Beberapa model Qwen-ASR dan Paraformer menyertakan kondisi emosional pembicara dalam hasil transkripsi. Granularitas output dan cara mengaktifkannya berbeda antara keduanya.

Qwen-ASR (qwen3-asr-flash-realtime): Selalu aktif tanpa perlu konfigurasi. Bidang tingkat atas emotion dikembalikan 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): Hanya model Paraformer ini yang mendukung pengenalan emosi. Hasil dikembalikan melalui payload.output.sentence.emo_tag dan payload.output.sentence.emo_confidence. Nilainya adalah salah satu dari tiga polaritas: positive (emosi positif seperti kebahagiaan atau kepuasan), negative (emosi negatif seperti kemarahan atau semangat rendah), dan neutral (tidak ada emosi jelas). Rentang kepercayaan adalah [0.0, 1.0].

Pengenalan emosi dikembalikan hanya jika semua kondisi berikut terpenuhi:

  • Modelnya adalah paraformer-realtime-8k-v2.

  • Deteksi giliran semantik dinonaktifkan: semantic_punctuation_enabled = false (nilai default; tidak perlu pengaturan khusus).

  • Hasil dikembalikan hanya dalam event akhir kalimat di mana sentence_end = true.

Untuk menonaktifkan bidang pengenalan emosi, atur semantic_punctuation_enabled ke true. Pengaturan ini mengaktifkan deteksi giliran semantik, sehingga bidang emo_tag dan emo_confidence tidak lagi dikembalikan.

Nama bidang di atas menggunakan jalur JSON WebSocket. Setiap SDK mengekspos bidang-bidang ini sesuai konvensi penamaannya sendiri (kunci kamus, properti objek, metode getter, dan sebagainya). Untuk pemetaan bidang lengkap, lihat referensi API setiap SDK.

Untuk definisi bidang lengkap, batasan nilai, dan contoh, lihat Referensi API.

Protokol WebSocket mentah

Contoh berikut menunjukkan cara menghubungkan ke server melalui protokol WebSocket mentah, cocok untuk skenario yang tidak menggunakan SDK DashScope. Ini merupakan implementasi minimal yang dapat dijalankan. Untuk protokol WebSocket tiap model, lihat Referensi API.

Tampilkan contoh protokol WebSocket mentah

Fun-ASR

Contoh menggunakan file audio asr_example.wav.

Python

Sebelum menjalankan contoh, instal dependensi yang diperlukan:

pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client

Jangan beri nama file contoh Anda websocket.py. Nama ini bertentangan dengan library websocket dan memicu 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

# The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
# If you have not configured the environment variable, replace the line below with your Model Studio API Key: api_key = "sk-xxx"
api_key = os.environ.get('DASHSCOPE_API_KEY')
# The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/'  # WebSocket server address
audio_file = 'asr_example.wav'  # Replace with your audio file path

# Generate a 32-character random ID
TASK_ID = uuid.uuid4().hex[:32]

task_started = False  # Tracks whether the task has started

# Send the run-task command
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': 'fun-asr-realtime',
            'parameters': {
                'sample_rate': 16000,
                'format': 'wav'
            },
            'input': {}
        }
    }
    ws.send(json.dumps(run_task_message))

# Send the finish-task command
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))

# Send the audio stream (a binary chunk every 100 ms)
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('Failed to read audio file: ', e)
        ws.close()

# When the connection opens, send the run-task command
def on_open(ws):
    print('Connected to server')
    send_run_task(ws)

# Handle received messages
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)

# Close the connection if task-started is not received
def on_close(ws, close_status_code, close_msg):
    if not task_started:
        print('Task not started, closing connection')

# Error handling
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:

Maven

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

Gradle

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 {

    // The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    // If you have not configured the environment variable, replace the line below with your Model Studio API Key: private static final String API_KEY = "sk-xxx";
    private static final String API_KEY = System.getenv().getOrDefault("DASHSCOPE_API_KEY", "sk-xxx");
    // The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
    private static final String URL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/";
    private static final String AUDIO_FILE = "asr_example.wav"; // Replace with your audio file path
    private static final String MODEL = "fun-asr-realtime";

    // Generate a 32-character random ID
    private static final String TASK_ID = UUID.randomUUID().toString().replace("-", "").substring(0, 32);

    private static final AtomicBoolean taskStarted = new AtomicBoolean(false);
    private static WebSocketClient client;

    public static void main(String[] args) throws Exception {
        client = new WebSocketClient(new URI(URL)) {
            @Override
            public void onOpen(ServerHandshake handshake) {
                System.out.println("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();
    }

    // Send the run-task command
    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());
    }

    // Send the audio stream (a binary chunk every 100 ms)
    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("Failed to read audio file: " + e.getMessage());
            client.close();
        }
    }

    // Send the finish-task command
    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

Contoh kode:

const fs = require('fs');
const WebSocket = require('ws');
const { v4: uuidv4 } = require('uuid'); // Used to generate UUID

// The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
// If you have not configured the environment variable, replace the line below with your Model Studio API Key: const apiKey = "sk-xxx"
const apiKey = process.env.DASHSCOPE_API_KEY;
// The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
const url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/'; // WebSocket server address
const audioFile = 'asr_example.wav'; // Replace with your audio file path

// Generate a 32-character random ID
const TASK_ID = uuidv4().replace(/-/g, '').slice(0, 32);

// Create a WebSocket client
const ws = new WebSocket(url, {
  headers: {
    Authorization: `bearer ${apiKey}`
  }
});

let taskStarted = false; // Flag indicating whether the task has started

// Send the run-task command when the connection is opened
ws.on('open', () => {
  console.log('Connected to server');
  sendRunTask();
});

// Handle received messages
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);
  }
});

// Close the connection if the task-started event is not received
ws.on('close', () => {
  if (!taskStarted) {
    console.error('Task not started, closing connection');
  }
});

// Send the run-task command
function sendRunTask() {
  const runTaskMessage = {
    header: {
      action: 'run-task',
      task_id: TASK_ID,
      streaming: 'duplex'
    },
    payload: {
      task_group: 'audio',
      task: 'asr',
      function: 'recognition',
      model: 'fun-asr-realtime',
      parameters: {
        sample_rate: 16000,
        format: 'wav'
      },
      input: {}
    }
  };
  ws.send(JSON.stringify(runTaskMessage));
}

// Send the audio stream
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); // Send every 100 ms
    }
  }

  audioStream.on('readable', () => {
    sendNextChunk();
  });

  audioStream.on('end', () => {
    console.log('Audio stream ended');
    sendFinishTask();
  });

  audioStream.on('error', (err) => {
    console.error('Failed to read audio file: ', err);
    ws.close();
  });
}

// Send the finish-task command
function sendFinishTask() {
  const finishTaskMessage = {
    header: {
      action: 'finish-task',
      task_id: TASK_ID,
      streaming: 'duplex'
    },
    payload: {
      input: {}
    }
  };
  ws.send(JSON.stringify(finishTaskMessage));
}

// Error handling
ws.on('error', (error) => {
  console.error('WebSocket error: ', error);
});

C#

Contoh kode:

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;
    // The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    // If you have not configured the environment variable, replace the line below with your Model Studio API Key: private static readonly string ApiKey = "sk-xxx"
    private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");

    // The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
    private const string WebSocketUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/";
    // Replace with your audio file path
    private const string AudioFilePath = "asr_example.wav";

    static async Task Main(string[] args) {
        // Establish the WebSocket connection and configure headers for authentication
        _webSocket.Options.SetRequestHeader("Authorization", $"bearer {ApiKey}");

        await _webSocket.ConnectAsync(new Uri(WebSocketUrl), _cancellationTokenSource.Token);

        // Start a thread to asynchronously receive WebSocket messages
        var receiveTask = ReceiveMessagesAsync();

        // Send the run-task command
        string _taskId = Guid.NewGuid().ToString("N"); // Generate a 32-character random ID
        var runTaskJson = GenerateRunTaskJson(_taskId);
        await SendAsync(runTaskJson);

        // Wait for the task-started event
        while (!_taskStartedReceived) {
            await Task.Delay(100, _cancellationTokenSource.Token);
        }

        // Read the local file and send the audio stream to be recognized to the server
        await SendAudioStreamAsync(AudioFilePath);

        // Send the finish-task command to end the task
        var finishTaskJson = GenerateFinishTaskJson(_taskId);
        await SendAsync(finishTaskJson);

        // Wait for the task-finished event
        while (!_taskFinishedReceived && !_cancellationTokenSource.IsCancellationRequested) {
            try {
                await Task.Delay(100, _cancellationTokenSource.Token);
            } catch (OperationCanceledException) {
                // Task has been cancelled, exit the loop
                break;
            }
        }

        // Close the connection
        if (!_cancellationTokenSource.IsCancellationRequested) {
            await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", _cancellationTokenSource.Token);
        }

        _cancellationTokenSource.Cancel();
        try {
            await receiveTask;
        } catch (OperationCanceledException) {
            // Ignore the operation cancelled exception
        }
    }

    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) {
            // Ignore the operation cancelled exception
        }
    }

    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]; // Send 100 ms of audio data each time
            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); // 100 ms interval
            }
        }
    }

    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"] = "fun-asr-realtime",
                ["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

Kode contoh menggunakan struktur direktori berikut:

my-php-project/

├── composer.json

├── vendor/

└── index.php

File composer.json ditunjukkan di bawah. Atur versi dependensi sesuai dengan proyek Anda:

{
    "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/"
        }
    }
}

File index.php:

<?php

require __DIR__ . '/vendor/autoload.php';

use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;
use Ratchet\rfc6455\Messaging\Frame;

// The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
// If you have not configured the environment variable, replace the line below with your Model Studio API Key: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
// The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
$websocket_url = 'wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/';
$audio_file_path = 'asr_example.wav'; // Replace with your audio file path

$loop = Loop::get();

// Create a custom connector
$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";

    // Start asynchronous thread to receive WebSocket messages
    $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";
        }
    });

    // Listen for connection close
    $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";
        }
    });

    // Generate task ID
    $taskId = generateTaskId();

    // Send the run-task command
    sendRunTaskMessage($conn, $taskId);

}, function ($e) {
    echo "Failed to connect: {$e->getMessage()}\n";
});

$loop->run();

/**
 * Generate task ID
 * @return string
 */
function generateTaskId(): string {
    return bin2hex(random_bytes(16));
}

/**
 * Send the run-task command
 * @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" => "fun-asr-realtime",
            "parameters" => [
                "format" => "wav",
                "sample_rate" => 16000
            ],
            "input" => []
        ]
    ]);
    echo "Preparing to send run-task command: " . $runTaskMessage . "\n";
    $conn->send($runTaskMessage);
    echo "run-task command sent\n";
}

/**
 * Read audio file
 * @param string $filePath
 * @return bool|string
 */
function readAudioFile(string $filePath) {
    $voiceData = file_get_contents($filePath);
    if ($voiceData === false) {
        echo "Failed to read audio file\n";
    }
    return $voiceData;
}

/**
 * Split audio data
 * @param string $data
 * @param int $chunkSize
 * @return array
 */
function splitAudioData(string $data, int $chunkSize): array {
    return str_split($data, $chunkSize);
}

/**
 * Send the finish-task command
 * @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 finish-task command: " . $finishTaskMessage . "\n";
    $conn->send($finishTaskMessage);
    echo "finish-task command sent\n";
}

/**
 * Handle 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";
            // Read audio file
            $voiceData = readAudioFile($audio_file_path);
            if ($voiceData === false) {
                echo "Failed to read audio file\n";
                $conn->close();
                return;
            }

            // Split audio data
            $chunks = splitAudioData($voiceData, 1024);

            // Define the send function
            $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);
                    // Send the next chunk after 100 ms
                    $loop->addTimer(0.1, $sendChunk);
                } else {
                    echo "All data chunks sent\n";
                    $allChunksSent = true;

                    // Send the finish-task command
                    sendFinishTaskMessage($conn, $taskId);
                }
            };

            // Start sending audio data
            $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;
    }

    // If all data has been sent and the task is finished, close the connection
    if ($allChunksSent && $response['header']['event'] == 'task-finished') {
        // Wait 1 second to ensure all data has been transmitted
        $loop->addTimer(1, function() use ($conn) {
            $conn->close();
            echo "Client closes 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 (
	// The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
	wsURL     = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference/" // WebSocket server address
	audioFile = "asr_example.wav"                                   // Replace with your audio file path
)

var dialer = websocket.DefaultDialer

func main() {
	// The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
	// If you have not configured the environment variable, replace the line below with your Model Studio API Key: apiKey := "sk-xxx"
	apiKey := os.Getenv("DASHSCOPE_API_KEY")

	// Connect to the WebSocket service
	conn, err := connectWebSocket(apiKey)
	if err != nil {
		log.Fatal("Failed to connect WebSocket: ", err)
	}
	defer closeConnection(conn)

	// Start a goroutine to receive results
	taskStarted := make(chan bool)
	taskDone := make(chan bool)
	startResultReceiver(conn, taskStarted, taskDone)

	// Send the run-task command
	taskID, err := sendRunTaskCmd(conn)
	if err != nil {
		log.Fatal("Failed to send run-task command: ", err)
	}

	// Wait for the task-started event
	waitForTaskStarted(taskStarted)

	// Send the audio file stream to be recognized
	if err := sendAudioData(conn); err != nil {
		log.Fatal("Failed to send audio: ", err)
	}

	// Send the finish-task command
	if err := sendFinishTaskCmd(conn, taskID); err != nil {
		log.Fatal("Failed to send finish-task command: ", err)
	}

	// Wait for the task to finish or fail
	<-taskDone
}

// Define structs to represent JSON data
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"`
}

// Connect to the WebSocket service
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
}

// Start a goroutine to asynchronously receive WebSocket messages
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
			}
		}
	}()
}

// Send the run-task command
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
}

// Generate the run-task command
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:     "fun-asr-realtime",
			Parameters: Params{
				Format:     "wav",
				SampleRate: 16000,
			},
			Input: Input{},
		},
	}
	runTaskCmdJSON, err := json.Marshal(runTaskCmd)
	return string(runTaskCmdJSON), taskID, err
}

// Wait for the task-started event
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 task")
	}
}

// Send audio data
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
}

// Send the finish-task command
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
}

// Generate the finish-task command
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
}

// Handle 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 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
}

// Handle the task-failed event
func handleTaskFailed(event Event, conn *websocket.Conn) {
	if event.Header.ErrorMessage != "" {
		log.Fatalf("Task failed: %s", event.Header.ErrorMessage)
	} else {
		log.Fatal("Task failed for unknown reason")
	}
}

// Close the connection
func closeConnection(conn *websocket.Conn) {
	if conn != nil {
		conn.Close()
	}
}

Qwen-ASR

Catatan

Contoh membaca your_audio_file.pcm (PCM16, 16 kHz, mono). Untuk mengonversi dari format MP3, WAV, atau lainnya, gunakan ffmpeg:

ffmpeg -i your_audio.mp3 -ar 16000 -ac 1 -f s16le your_audio_file.pcm

Python

Sebelum menjalankan contoh, instal dependensi:

pip uninstall websocket-client
pip uninstall websocket
pip install websocket-client

Jangan beri nama file contoh websocket.py. Nama ini bertentangan dengan library 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 untuk wilayah Singapura dan Beijing. Dapatkan Kunci API: https://www.alibabacloud.com/help/en/model-studio/get-api-key
# Jika Anda belum mengonfigurasi variabel lingkungan, ganti baris di bawah ini dengan Kunci API Model Studio Anda: API_KEY="sk-xxx"
API_KEY = os.environ.get("DASHSCOPE_API_KEY", "sk-xxx")
QWEN_MODEL = "qwen3-asr-flash-realtime"
# Konfigurasi berikut adalah untuk wilayah Singapura. Ganti "{WorkspaceId}" dengan ID ruang kerja Anda yang sebenarnya. Konfigurasi bervariasi menurut 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, durasi audio kumulatif yang dikirim tidak boleh melebihi 60 detik
enableServerVad = True
is_running = True  # Flag 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 session.update
    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 mengatur ini jika bahasa diketahui
                "language": "en"
            },
            "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": "en"
            },
            "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 read start: {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 read complete: {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 real-time

# Inisialisasi logger
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:

Maven

<dependency>
    <groupId>org.java-websocket</groupId>
    <artifactId>Java-WebSocket</artifactId>
    <version>1.5.6</version>
</dependency>

Gradle

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());
    // The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    // If you have not configured the environment variable, replace the line below with your Model Studio API Key: private static final String API_KEY = "sk-xxx"
    private static final String API_KEY = System.getenv().getOrDefault("DASHSCOPE_API_KEY", "sk-xxx");
    private static final String MODEL = "qwen3-asr-flash-realtime";

    // Controls whether VAD mode is enabled
    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();

        // The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
        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));

                    // The final transcript is in the transcription.completed event
                    if ("conversation.item.input_audio_transcription.completed".equals(eventType)) {
                        logger.info("Final transcript: " + data.optString("transcript"));
                    }

                    // On end event, stop the sending thread and close the connection
                    if ("session.finished".equals(eventType)) {
                        logger.info("Closing WebSocket connection after session finished...");

                        isRunning.set(false); // Stop the audio sending thread
                        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());
            }
        };

        // Add request headers
        client.addHeader("Authorization", "Bearer " + API_KEY);
        client.addHeader("OpenAI-Beta", "realtime=v1");

        client.connectBlocking(); // Block until the connection is established

        // Replace with the path of the audio file to be recognized
        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();
    }

    /** session.update event (enable/disable 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", "en"))
                        .put("turn_detection", JSONObject.NULL) // Manual mode
                );

        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", "en"))
                        .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());
        }
    }

    /** Send the audio file stream */
    private static void sendAudio(String localAudioPath) throws Exception {
        Thread.sleep(3000); // Wait for the session to be prepared
        byte[] allBytes = Files.readAllBytes(Paths.get(localAudioPath));
        logger.info("File read start");

        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; // Avoid sending after disconnection
            }

            Thread.sleep(100); // Simulate real-time sending
        }

        logger.info("File read complete");

        if (client != null && client.isOpen()) {
            // commit is required in non-VAD mode
            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.");
        }
    }

    /** Initialize logger */
    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:

npm install ws
/**
 * Qwen-ASR Realtime WebSocket Client (Node.js version)
 * Features:
 * - Supports VAD mode and Manual mode
 * - Sends session.update to start the session
 * - Continuously sends audio chunks via input_audio_buffer.append
 * - In Manual mode, sends input_audio_buffer.commit
 * - Sends session.finish event
 * - Closes the connection after receiving session.finished
 */

import WebSocket from 'ws';
import fs from 'fs';

// ===== Config =====
// The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
// If you have not configured the environment variable, replace the line below with your Model Studio API Key: const API_KEY = "sk-xxx"
const API_KEY = process.env.DASHSCOPE_API_KEY || 'sk-xxx';
const MODEL = 'qwen3-asr-flash-realtime';
const enableServerVad = true; // true for VAD mode, false for Manual mode
const localAudioPath = 'your_audio_file.pcm'; // PCM16, 16 kHz audio file path

// The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
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}`);

// ===== State control =====
let isRunning = true;

// ===== Establish connection =====
const ws = new WebSocket(url, {
    headers: {
        'Authorization': `Bearer ${API_KEY}`,
        'OpenAI-Beta': 'realtime=v1'
    }
});

// ===== Event binding =====
ws.on('open', () => {
    console.log('[WebSocket] Connected to server.');
    sendSessionUpdate();
    // Start the audio sending thread
    sendAudio(localAudioPath);
});

ws.on('message', (message) => {
    try {
        const data = JSON.parse(message);
        console.log('[Received Event]:', JSON.stringify(data, null, 2));

        // The final transcript is in the transcription.completed event
        if (data.type === 'conversation.item.input_audio_transcription.completed') {
            console.log(`[Final Transcript] ${data.transcript}`);
        }

        // On end event
        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);
});

// ===== Session update =====
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: 'en'
            },
            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: 'en'
            },
            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));
    }
}

// ===== Send the audio file stream =====
function sendAudio(audioPath) {
    setTimeout(() => {
        console.log(`[File Read Start] ${audioPath}`);
        const buffer = fs.readFileSync(audioPath);

        let offset = 0;
        const chunkSize = 3200; // ~0.1s of PCM16 audio

        function sendChunk() {
            if (!isRunning) return;
            if (offset >= buffer.length) {
                isRunning = false; // Stop sending 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); // Simulate real-time sending
        }

        sendChunk();
    }, 3000); // Wait for session configuration to complete
}

C#

Contoh berikut menghubungkan ke server dan mentranskripsikan file audio PCM:

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;

    // Controls whether to use VAD mode
    private const bool EnableServerVad = true;

    // The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
    // If you have not configured the environment variable, replace the line below with your Model Studio API Key: private static readonly string ApiKey = "sk-xxx"
    private static readonly string ApiKey = Environment.GetEnvironmentVariable("DASHSCOPE_API_KEY") ?? throw new InvalidOperationException("DASHSCOPE_API_KEY environment variable is not set.");
    private const string Model = "qwen3-asr-flash-realtime";
    // The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
    private const string BaseUrl = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime";
    private const string AudioFilePath = "your_audio_file.pcm"; // Replace with your PCM audio file path

    static async Task Main(string[] args) {
        var url = $"{BaseUrl}?model={Model}";
        Console.WriteLine($"Connecting to server: {url}");

        // Set the authentication headers
        _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {ApiKey}");
        _webSocket.Options.SetRequestHeader("OpenAI-Beta", "realtime=v1");

        await _webSocket.ConnectAsync(new Uri(url), _cts.Token);
        Console.WriteLine("Connected to server.");

        // Start the message receive task
        var receiveTask = ReceiveMessagesAsync();

        // Send the session.update configuration
        await SendSessionUpdateAsync();

        // Send the audio stream
        await SendAudioStreamAsync();

        // Wait for the session.finished event
        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);
    }

    // Send the session.update event
    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());
    }

    // Send the audio stream (a PCM chunk every 100 ms)
    private static async Task SendAudioStreamAsync() {
        await Task.Delay(3000); // Wait for the session to be configured
        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("Audio file read.");
        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());
        }
    }

    // Receive and handle server events
    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

Contoh menggunakan struktur direktori berikut:

my-php-project/
├── composer.json
├── vendor/
└── index.php

Sesuaikan versi dependensi dalam file composer.json agar sesuai dengan lingkungan Anda:

{
    "require": {
        "react/event-loop": "^1.3",
        "react/socket": "^1.11",
        "ratchet/pawl": "^0.4"
    }
}

File index.php ditunjukkan di bawah:

<?php

require __DIR__ . '/vendor/autoload.php';

use Ratchet\Client\Connector;
use React\EventLoop\Loop;
use React\Socket\Connector as SocketConnector;

// The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
// If you have not configured the environment variable, replace the line below with your Model Studio API Key: $api_key = "sk-xxx"
$api_key = getenv("DASHSCOPE_API_KEY");
$model = 'qwen3-asr-flash-realtime';
// The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
$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'; // Replace with your PCM audio file path

// Controls whether to use VAD mode
$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 the WebSocket server\n";

    // Listen for server events
    $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";
    });

    // Send the session.update event
    sendSessionUpdate($conn, $enable_server_vad);

    // Wait for the session to be configured, then send audio
    $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 "Failed to connect: {$e->getMessage()}\n";
});

$loop->run();

// Send the session.update event
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";
}

// Send the audio stream (a PCM chunk every 100 ms)
function sendAudioStream($conn, $audio_file_path, $enable_server_vad, $loop, &$is_running) {
    $fp = fopen($audio_file_path, 'rb');
    if (!$fp) {
        echo "Failed 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:

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 (
	// The following configuration is for the Singapore region. Replace "{WorkspaceId}" with your actual workspace ID. Configurations vary by region.
	baseURL         = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime"
	model           = "qwen3-asr-flash-realtime"
	audioFile       = "your_audio_file.pcm" // Replace with your PCM audio file path
	enableServerVad = true                  // Controls whether to use VAD mode
)

// Server event structure
type ServerEvent struct {
	Type       string `json:"type"`
	Transcript string `json:"transcript,omitempty"`
}

func main() {
	// The API Key is different for the Singapore and Beijing regions. Get an API Key: https://www.alibabacloud.com/help/en/model-studio/get-api-key
	// If you have not configured the environment variable, replace the line below with your Model Studio API Key: apiKey := "sk-xxx"
	apiKey := os.Getenv("DASHSCOPE_API_KEY")

	url := baseURL + "?model=" + model
	log.Printf("Connecting to server: %s", url)

	conn, err := connect(url, apiKey)
	if err != nil {
		log.Fatal("Failed to connect to the WebSocket server: ", err)
	}
	defer conn.Close()

	// Start a goroutine that receives messages
	sessionFinished := make(chan bool, 1)
	go receiveMessages(conn, sessionFinished)

	// Send session.update
	if err := sendSessionUpdate(conn); err != nil {
		log.Fatal("Failed to send session.update: ", err)
	}

	// Wait for the session to be configured
	time.Sleep(3 * time.Second)

	// Send the audio stream
	if err := sendAudioStream(conn); err != nil {
		log.Fatal("Failed to send audio: ", err)
	}

	// Wait for session.finished
	<-sessionFinished
}

// Establish the WebSocket connection
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
}

// Send the session.update event
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)
}

// Send the audio stream (a PCM chunk every 100 ms)
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)
}

// Receive and handle server events
func receiveMessages(conn *websocket.Conn, sessionFinished chan<- bool) {
	for {
		_, msg, err := conn.ReadMessage()
		if err != nil {
			log.Println("Failed to read message: ", err)
			sessionFinished <- true
			return
		}
		var evt ServerEvent
		if err := json.Unmarshal(msg, &evt); err != nil {
			log.Println("Failed to parse 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

Paraformer menggunakan kembali kode contoh Fun-ASR. Ganti parameter model dengan nama model Paraformer.

Terapkan di produksi

Penggunaan ulang koneksi (WebSocket)

Fun-ASR dan Paraformer mendukung penggunaan ulang koneksi WebSocket: setelah satu tugas pengenalan selesai, Anda dapat memulai tugas berikutnya pada koneksi yang sama tanpa perlu terhubung ulang.

Alur penggunaan ulang: Klien mengirim finish-task. Setelah server mengembalikan task-finished, klien mengirim run-task lagi untuk memulai tugas baru.

Penting
  1. Tunggu hingga server mengembalikan event task-finished sebelum memulai tugas baru.

  2. Setiap tugas pada koneksi yang digunakan ulang harus menggunakan task_id yang berbeda.

  3. Jika tugas gagal, server mengembalikan event error dan menutup koneksi. Koneksi tersebut tidak dapat digunakan ulang.

  4. Koneksi ditutup secara otomatis jika tidak ada tugas baru yang dimulai dalam waktu 60 detik setelah tugas berakhir.

Qwen-ASR Realtime menggunakan model sesi. Tutup koneksi setelah setiap sesi berakhir karena penggunaan ulang koneksi tidak didukung.

Untuk deskripsi event setiap model, lihat Referensi API yang sesuai.

Praktik terbaik konkurensi tinggi

SDK DashScope mencakup pooling bawaan yang menggunakan ulang koneksi WebSocket dan objek pengenalan, sehingga menghindari overhead pembuatan dan penghancuran berulang. Saat ini, hanya SDK Java Paraformer yang mendukung fitur ini.

Tampilkan praktik terbaik konkurensi tinggi

Prasyarat

SDK Java mencapai kinerja optimal melalui kumpulan koneksi bawaan yang bekerja bersama kumpulan objek kustom:

  • Kumpulan koneksi: Kumpulan koneksi OkHttp3 bawaan SDK mengelola dan menggunakan ulang koneksi WebSocket dasar, sehingga mengurangi overhead jabat tangan jaringan. Fitur ini diaktifkan secara default.

  • Kumpulan objek: Dibangun di atas commons-pool2 untuk mempertahankan seperangkat objek Recognition yang telah terhubung. Meminjam objek dari kumpulan menghilangkan latensi pembentukan koneksi dan secara signifikan mengurangi latensi paket pertama.

Langkah implementasi

  1. Tambahkan dependensi

    Tambahkan dashscope-sdk-java dan commons-pool2 ke file konfigurasi dependensi proyek Anda, sesuai dengan alat build yang digunakan.

    Konfigurasi Maven dan Gradle ditunjukkan di bawah:

    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>
        <!-- Replace 'the-latest-version' with 2.16.9 or later. See https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java for available versions. -->
        <version>the-latest-version</version>
    </dependency>
    
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-pool2</artifactId>
        <!-- Replace 'the-latest-version' with the latest version. See https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 for available versions. -->
        <version>the-latest-version</version>
    </dependency>
    1. Simpan file pom.xml.

    2. Perbarui dependensi proyek dengan perintah Maven, seperti mvn clean install atau mvn compile.

    Gradle

    1. Buka file build.gradle proyek Gradle Anda.

    2. Tambahkan dependensi berikut di dalam blok dependencies.

      dependencies {
          // Replace 'the-latest-version' with 2.16.9 or later. See https://mvnrepository.com/artifact/com.alibaba/dashscope-sdk-java for available versions.
          implementation group: 'com.alibaba', name: 'dashscope-sdk-java', version: 'the-latest-version'
      
          // Replace 'the-latest-version' with the latest version. See https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 for available versions.
          implementation group: 'org.apache.commons', name: 'commons-pool2', version: 'the-latest-version'
      }
    3. Simpan file build.gradle.

    4. Buka command line, navigasi ke direktori root proyek, lalu jalankan perintah Gradle berikut untuk memperbarui dependensi.

      ./gradlew build --refresh-dependencies

      Di Windows, gunakan:

      gradlew build --refresh-dependencies
  2. Konfigurasikan kumpulan koneksi

    Konfigurasikan kumpulan koneksi melalui variabel lingkungan:

    Variabel lingkungan

    Deskripsi

    DASHSCOPE_CONNECTION_POOL_SIZE

    Ukuran kumpulan koneksi.

    Nilai yang direkomendasikan: setidaknya dua kali konkurensi puncak.

    Nilai default: 32.

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS

    Jumlah maksimum permintaan async bersamaan.

    Nilai yang direkomendasikan: sama dengan DASHSCOPE_CONNECTION_POOL_SIZE.

    Nilai default: 32.

    DASHSCOPE_MAXIMUM_ASYNC_REQUESTS_PER_HOST

    Jumlah maksimum permintaan async per host.

    Nilai yang direkomendasikan: sama dengan DASHSCOPE_CONNECTION_POOL_SIZE.

    Nilai default: 32.

  3. Konfigurasikan kumpulan objek

    Konfigurasikan ukuran kumpulan objek melalui variabel lingkungan:

    Variabel lingkungan

    Deskripsi

    RECOGNITION_OBJECTPOOL_SIZE

    Ukuran kumpulan objek.

    Nilai yang direkomendasikan: 1,5 hingga 2 kali konkurensi puncak.

    Nilai default: 500.

    Penting
    • Ukuran kumpulan objek (RECOGNITION_OBJECTPOOL_SIZE) harus kurang dari atau sama dengan ukuran kumpulan koneksi (DASHSCOPE_CONNECTION_POOL_SIZE). Jika tidak, thread pemanggil akan diblokir saat kumpulan objek meminta objek sementara kumpulan koneksi penuh.

    • Ukuran kumpulan objek tidak boleh melebihi batas QPS (queries per second) akun Anda.

    Gunakan kode berikut untuk membuat kumpulan objek:

    class RecognitionObjectPool {
        // ... See the complete code section for the full example
        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;
        }
    }
  4. Pinjam objek Recognition dari kumpulan

    Ketika jumlah objek yang tidak dikembalikan melebihi ukuran kumpulan, sistem membuat objek Recognition tambahan. Objek tambahan ini harus membentuk koneksi WebSocket baru dan tidak dapat digunakan ulang.

    recognizer = RecognitionObjectPool.getInstance().borrowObject();
  5. Jalankan pengenalan ucapan

    Panggil metode Recognition call atau streamCall untuk menjalankan pengenalan ucapan.

  6. Kembalikan objek Recognition

    Setelah tugas pengenalan ucapan selesai, kembalikan objek Recognition agar dapat digunakan ulang. Jangan mengembalikan objek yang tugasnya 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;

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 {
        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, "asr_example.wav"),
                Paths.get(currentDir, "asr_example.wav"),
                Paths.get(currentDir, "asr_example.wav"),
        };
        ExecutorService executorService = Executors.newFixedThreadPool(threadNums);
        for (int i = 0; i < threadNums; i++) {
            executorService.submit(new RealtimeRecognizeTask(filePaths));
        }
        executorService.shutdown();
        executorService.awaitTermination(10, TimeUnit.MINUTES);
        System.exit(0);
    }
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Konfigurasi yang direkomendasikan

Konfigurasi berikut berasal dari pengujian di mana hanya layanan pengenalan ucapan real-time Paraformer yang berjalan pada server Alibaba Cloud dengan spesifikasi tertentu. Konkurensi per host tunggal merujuk pada jumlah tugas pengenalan ucapan real-time Paraformer yang berjalan secara bersamaan pada satu host (jumlah thread pekerja).

Konfigurasi server (Alibaba Cloud)

Konkurensi maks per host

Ukuran kumpulan objek

Ukuran kumpulan koneksi

4 core, 8 GiB

100

500

2000

8 core, 16 GiB

200

500

2000

16 core, 32 GiB

400

500

2000

Manajemen sumber daya dan penanganan pengecualian

  • Tugas berhasil: Panggil GenericObjectPool.returnObject() untuk mengembalikan objek Recognition ke kumpulan agar dapat digunakan ulang.

    Penting

    Jangan mengembalikan objek Recognition yang tugasnya belum selesai atau gagal.

  • Tugas gagal: Ketika SDK atau logika bisnis melemparkan pengecualian yang mengganggu tugas, lakukan kedua hal berikut:

    1. Tutup koneksi WebSocket dasar.

    2. Buat tidak valid objek dalam kumpulan untuk mencegah penggunaan ulang.

    // Close the connection
    recognizer.getDuplexApi().close(1000, "bye");
    // Invalidate the failed recognizer in the object pool
    RecognitionObjectPool.getInstance().invalidateObject(recognizer);
  • Untuk error TaskFailed di sisi layanan, tidak diperlukan penanganan tambahan.

Pemanasan dan pengukuran latensi

Saat mengukur latensi pemanggilan konkuren atau metrik kinerja lainnya dari SDK Java DashScope, lakukan pemanasan yang cukup sebelum pengujian formal.

Mekanisme penggunaan ulang koneksi

SDK Java DashScope menggunakan kumpulan koneksi singleton global untuk mengelola dan menggunakan ulang koneksi WebSocket. Mekanisme ini berperilaku sebagai berikut:

  • Pembuatan sesuai permintaan: SDK tidak membuat koneksi WebSocket sebelumnya saat startup layanan. Koneksi dibentuk sesuai permintaan pada panggilan pertama.

  • Penggunaan ulang berbatas waktu: Setelah permintaan selesai, koneksi disimpan dalam kumpulan hingga 60 detik untuk penggunaan ulang.

    • Jika permintaan baru tiba dalam waktu 60 detik, koneksi yang ada digunakan ulang, sehingga menghindari overhead jabat tangan.

    • Jika koneksi menganggur lebih dari 60 detik, koneksi tersebut ditutup secara otomatis untuk melepaskan sumber daya.

Mengapa pemanasan penting

Dalam skenario berikut, kumpulan koneksi mungkin tidak memiliki koneksi aktif yang dapat digunakan ulang, sehingga permintaan harus membuat koneksi baru:

  • Aplikasi baru saja dimulai dan belum melakukan panggilan apa pun.

  • Layanan telah menganggur lebih dari 60 detik, sehingga koneksi dalam kumpulan telah ditutup karena waktu habis.

Dalam skenario ini, permintaan pertama atau awal memicu proses koneksi WebSocket penuh (jabat tangan TCP, negosiasi TLS, dan peningkatan protokol). Latensi end-to-end secara signifikan lebih tinggi dibandingkan permintaan berikutnya yang menggunakan ulang koneksi.

Pendekatan yang direkomendasikan

Sebelum pengujian stres kinerja formal atau pengukuran latensi, ikuti langkah pemanasan berikut:

  1. Cocokkan tingkat konkurensi pengujian aktual dengan menjalankan panggilan terlebih dahulu (misalnya, selama 1 hingga 2 menit) untuk sepenuhnya mengisi kumpulan koneksi.

  2. Konfirmasi bahwa kumpulan koneksi telah terbentuk dan mempertahankan koneksi aktif yang cukup, lalu mulai pengumpulan data kinerja formal.

Tingkatkan akurasi pengenalan

  • Cocokkan model dengan laju sampel: Untuk audio telepon 8 kHz, gunakan model 8 kHz secara langsung. Jangan naikkan sampel ke 16 kHz karena hal tersebut mendistorsi sinyal.

  • Tingkatkan kualitas input audio: Gunakan mikrofon berkualitas tinggi di lingkungan perekaman dengan rasio sinyal-terhadap-kebisingan tinggi dan bebas gema. Di lapisan aplikasi, integrasikan pra-pemrosesan seperti pengurangan kebisingan (misalnya, RNNoise) dan pembatalan gema akustik (AEC).

Siapkan ketahanan

  • Rekoneksi sisi klien: Klien harus mengimplementasikan rekoneksi otomatis untuk menangani fluktuasi jaringan. Implementasi referensi SDK Python:

    1. Tangkap pengecualian: Implementasikan metode on_error dalam kelas Callback. SDK dashscope memanggil metode ini ketika mengalami error jaringan atau masalah lain.

    2. Beritahu status: Ketika on_error dipicu, atur sinyal reconnect. Di Python, gunakan threading.Event, yaitu bendera sinyal aman thread.

    3. Putaran reconnect: Bungkus logika utama dalam putaran for (misalnya, coba ulang 3 kali). Ketika sinyal reconnect terdeteksi, pengenalan saat ini dihentikan, sumber daya dibersihkan, lalu setelah beberapa detik putaran membuat koneksi baru.

  • Gunakan heartbeat untuk menjaga koneksi tetap aktif: Saat memerlukan koneksi jangka panjang ke server, atur parameter heartbeat ke true. Koneksi tetap aktif meskipun audio diam untuk waktu yang lama.

  • Batas laju: Saat memanggil antarmuka model, patuhi aturan Pembatasan laju model.

Model dan wilayah yang didukung

Singapura

Untuk memanggil model berikut, gunakan Kunci API dari wilayah Singapura:

  • Fun-ASR: fun-asr-realtime (stabil, saat ini setara dengan fun-asr-realtime-2025-11-07), fun-asr-realtime-2025-11-07 (snapshot)

  • Qwen3-ASR-Flash-Realtime: qwen3-asr-flash-realtime (stabil, saat ini setara dengan qwen3-asr-flash-realtime-2025-10-27), qwen3-asr-flash-realtime-2026-02-10 (snapshot terbaru), qwen3-asr-flash-realtime-2025-10-27 (snapshot)

China (Beijing)

Untuk memanggil model berikut, gunakan Kunci API dari wilayah Beijing:

  • Fun-ASR: fun-asr-realtime (stabil, saat ini setara dengan fun-asr-realtime-2025-11-07), fun-asr-realtime-2026-02-28 (snapshot terbaru), fun-asr-realtime-2025-11-07 (snapshot), fun-asr-realtime-2025-09-15 (snapshot)

    • fun-asr-flash-8k-realtime (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 (stabil, saat ini setara dengan qwen3-asr-flash-realtime-2025-10-27), qwen3-asr-flash-realtime-2026-02-10 (snapshot terbaru), qwen3-asr-flash-realtime-2025-10-27 (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?

Fun-ASR dan Paraformer mendukung pcm, wav, mp3, opus, speex, aac, dan amr. Untuk Qwen-ASR, gunakan pcm atau opus. Format lain (seperti wav, aac, dan amr) lolos validasi session.update, tetapi mungkin gagal saat decoding di sisi server. Pastikan aliran audio menggunakan format yang direkomendasikan sebelum mengirim.

Kapan saya harus menggunakan SDK vs. API WebSocket?

SDK DashScope membungkus manajemen koneksi WebSocket, autentikasi, dan rekoneksi, sehingga menjadi pilihan tercepat untuk integrasi. API WebSocket memberikan kontrol langsung dan detail halus. Gunakan API ini jika SDK tidak mendukung bahasa pemrograman Anda atau jika Anda memerlukan penanganan koneksi khusus. SDK merupakan pilihan yang direkomendasikan untuk sebagian besar kasus penggunaan.

Bagaimana cara meningkatkan akurasi pengenalan untuk kata benda khusus?

Gunakan hotwords (didukung oleh Fun-ASR dan Paraformer). Untuk konfigurasi dan penggunaan hotword, lihat Tingkatkan akurasi pengenalan.

Apa yang dapat saya lakukan ketika koneksi terus terputus?

Implementasikan logika rekoneksi di sisi klien dan aktifkan parameter heartbeat (heartbeat=true) untuk mencegah koneksi terputus selama periode diam yang panjang. Untuk strategi toleransi kesalahan terperinci, lihat Terapkan di produksi.