全部產品
Search
文件中心

Alibaba Cloud Model Studio:即時多模態(Qwen-Omni-Realtime)

更新時間:Dec 05, 2025

Qwen-Omni-Realtime 是通義千問推出的一款即時音視訊交談模型。它能夠同時理解流式的音頻與映像輸入(例如從視頻流中即時抽取的連續映像幀),並即時輸出高品質的文本與音頻。

如何使用

1. 建立串連

Qwen-Omni-Realtime 模型通過 WebSocket 通訊協定接入,可通過以下 Python 範例程式碼建立串連。也可通過DashScope SDK 建立串連。

說明

請注意,Qwen-Omni-Realtime 的單次 WebSocket 會話最長可持續 30 分鐘。達到此上限後,服務將主動關閉串連。

WebSocket 原生串連

串連時需要以下配置項:

配置項

說明

調用地址

中國大陸(北京):wss://dashscope.aliyuncs.com/api-ws/v1/realtime

國際(新加坡):wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime

查詢參數

查詢參數為model,需指定為訪問的模型名。樣本:?model=qwen3-omni-flash-realtime

要求標頭

使用 Bearer Token 鑒權:Authorization: Bearer DASHSCOPE_API_KEY

DASHSCOPE_API_KEY 是您在百鍊上申請的API Key
# pip install websocket-client
import json
import websocket
import os

API_KEY=os.getenv("DASHSCOPE_API_KEY")
API_URL = "wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime?model=qwen3-omni-flash-realtime"

headers = [
    "Authorization: Bearer " + API_KEY
]

def on_open(ws):
    print(f"Connected to server: {API_URL}")
def on_message(ws, message):
    data = json.loads(message)
    print("Received event:", json.dumps(data, indent=2))
def on_error(ws, error):
    print("Error:", error)

ws = websocket.WebSocketApp(
    API_URL,
    header=headers,
    on_open=on_open,
    on_message=on_message,
    on_error=on_error
)

ws.run_forever()

DashScope SDK

# SDK 版本不低於1.23.9
import os
import json
from dashscope.audio.qwen_omni import OmniRealtimeConversation,OmniRealtimeCallback
import dashscope
# 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
# 若沒有配置 API Key,請將下行改為 dashscope.api_key = "sk-xxx"
dashscope.api_key = os.getenv("DASHSCOPE_API_KEY")

class PrintCallback(OmniRealtimeCallback):
    def on_open(self) -> None:
        print("Connected Successfully")
    def on_event(self, response: dict) -> None:
        print("Received event:")
        print(json.dumps(response, indent=2, ensure_ascii=False))
    def on_close(self, close_status_code: int, close_msg: str) -> None:
        print(f"Connection closed (code={close_status_code}, msg={close_msg}).")

callback = PrintCallback()
conversation = OmniRealtimeConversation(
    model="qwen3-omni-flash-realtime",
    callback=callback,
    # 以下為新加坡地區url,若使用北京地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/realtime
    url="wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime"
)
try:
    conversation.connect()
    print("Conversation started. Press Ctrl+C to exit.")
    conversation.thread.join()
except KeyboardInterrupt:
    conversation.close()
// SDK 版本不低於 2.20.9
import com.alibaba.dashscope.audio.omni.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import java.util.concurrent.CountDownLatch;

public class Main {
    public static void main(String[] args) throws InterruptedException, NoApiKeyException {
        CountDownLatch latch = new CountDownLatch(1);
        OmniRealtimeParam param = OmniRealtimeParam.builder()
                .model("qwen3-omni-flash-realtime")
                .apikey(System.getenv("DASHSCOPE_API_KEY"))
                // 以下為新加坡地區url,若使用北京地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/realtime
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                .build();

        OmniRealtimeConversation conversation = new OmniRealtimeConversation(param, new OmniRealtimeCallback() {
            @Override
            public void onOpen() {
                System.out.println("Connected Successfully");
            }
            @Override
            public void onEvent(JsonObject message) {
                System.out.println(message);
            }
            @Override
            public void onClose(int code, String reason) {
                System.out.println("connection closed code: " + code + ", reason: " + reason);
                latch.countDown();
            }
        });
        conversation.connect();
        latch.await();
        conversation.close(1000, "bye");
        System.exit(0);
    }
}

2. 配置會話

發送用戶端事件session.update

{
    // 該事件的id,由用戶端產生
    "event_id": "event_ToPZqeobitzUJnt3QqtWg",
    // 事件類型,固定為session.update
    "type": "session.update",
    // 會話配置
    "session": {
        // 輸出模態,支援設定為["text"](僅輸出文本)或["text","audio"](輸出文本與音頻)。
        "modalities": [
            "text",
            "audio"
        ],
        // 輸出音訊音色
        "voice": "Cherry",
        // 輸入音頻格式,僅支援設為pcm16。
        "input_audio_format": "pcm16",
        // 輸出音頻格式,僅支援設為pcm24。
        "output_audio_format": "pcm24",
        // 系統訊息,用於設定模型的目標或角色。
        "instructions": "你是某五星級酒店的AI客服專員,請準確且友好地解答客戶關於房型、設施、價格、預訂政策的諮詢。請始終以專業和樂於助人的態度回應,杜絕提供未經證實或超出酒店服務涵蓋範圍的資訊。",
        // 是否開啟語音活動檢測。若需啟用,需傳入一個設定物件,服務端將據此自動檢測語音起止。
        // 設定為null表示由用戶端決定何時發起模型響應。
        "turn_detection": {
            // VAD類型,需設定為server_vad。
            "type": "server_vad",
            // VAD檢測閾值。建議在嘈雜的環境中增加,在安靜的環境中降低。
            "threshold": 0.5,
            // 檢測語音停止的靜音期間,超過此值後會觸發模型響應
            "silence_duration_ms": 800
        }
    }
}

3. 輸入音頻與圖片

用戶端通過input_audio_buffer.append和 input_image_buffer.append 事件發送 Base 64 編碼的音頻和圖片資料到服務端緩衝區。音頻輸入是必需的;圖片輸入是可選的。

圖片可以來自本地檔案,或從視頻流中即時採集。
啟用服務端VAD時,服務端會在檢測到語音結束時自動認可資料並觸發響應。禁用VAD時(手動模式),用戶端必須在發送完資料後,主動調用input_audio_buffer.commit事件來提交。

4. 接收模型響應

模型的響應格式取決於配置的輸出模態。

模型列表

Qwen3-Omni-Flash-Realtime 是通義千問最新推出的即時多模態模型,相比於上一代的 Qwen-Omni-Turbo-Realtime(後續不再更新):

  • 支援的語言

    增加至 10 種,包括漢語(支援普通話及多種主流方言,如上海話、粵語、四川話等)、英語,法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語,Qwen-Omni-Turbo-Realtime 僅支援 2 種(漢語(普通話)和英語)。

  • 支援的音色

    qwen3-omni-flash-realtime-2025-12-01支援的音色增加至49種,qwen3-omni-flash-realtime-2025-09-15、qwen3-omni-realtime-flash增加至 17 種,Qwen-Omni-Turbo-Realtime 僅支援 4 種;具體可查看音色列表

國際(新加坡)

模型名稱

版本

上下文長度

最大輸入

最大輸出

免費額度

(注)

(Token數)

qwen3-omni-flash-realtime

當前能力等同 qwen3-omni-flash-realtime-2025-09-15

穩定版

65,536

49,152

16,384

各100萬Token(不區分模態)

有效期間:百鍊開通後90天內

qwen3-omni-flash-realtime-2025-12-01

快照版

qwen3-omni-flash-realtime-2025-09-15

更多模型

模型名稱

版本

上下文長度

最大輸入

最大輸出

免費額度

(注)

(Token數)

qwen-omni-turbo-realtime

當前能力等同 qwen-omni-turbo-realtime-2025-05-08

穩定版

32,768

30,720

2,048

各100萬Token(不區分模態)

有效期間:百鍊開通後90天內

qwen-omni-turbo-realtime-latest

能力始終等同最新快照版

最新版

qwen-omni-turbo-realtime-2025-05-08

快照版

中國大陸(北京)

模型名稱

版本

上下文長度

最大輸入

最大輸出

免費額度

(注)

(Token數)

qwen3-omni-flash-realtime

當前能力等同 qwen3-omni-flash-realtime-2025-09-15

穩定版

65,536

49,152

16,384

無免費額度

qwen3-omni-flash-realtime-2025-12-01

快照版

qwen3-omni-flash-realtime-2025-09-15

更多模型

模型名稱

版本

上下文長度

最大輸入

最大輸出

免費額度

(注)

(Token數)

qwen-omni-turbo-realtime

當前與qwen-omni-turbo-2025-05-08能力相同

穩定版

32,768

30,720

2,048

無免費額度

qwen-omni-turbo-realtime-latest

始終與最新快照版能力相同

最新版

qwen-omni-turbo-realtime-2025-05-08

快照版

快速開始

您需要擷取與配置 API Key配置API Key到環境變數(準備下線,併入配置 API Key)

請選擇您熟悉的程式設計語言,通過以下步驟快速體驗與 Realtime 模型即時對話的功能。

DashScope Python SDK

  • 準備運行環境

您的 Python 版本需要不低於 3.10。

首先根據您的作業系統安裝 pyaudio。

macOS

brew install portaudio && pip install pyaudio

Debian/Ubuntu

  • 若未使用虛擬環境,可直接通過系統包管理器安裝:

    sudo apt-get install python3-pyaudio
  • 若使用虛擬環境,需先安裝編譯依賴:

    sudo apt update
    sudo apt install -y python3-dev portaudio19-dev

    然後在已啟用的虛擬環境中使用 pip 安裝:

    pip install pyaudio

CentOS

sudo yum install -y portaudio portaudio-devel && pip install pyaudio

Windows

pip install pyaudio

安裝完成後,通過 pip 安裝依賴:

pip install websocket-client dashscope
  • 選擇互動模式

    • VAD 模式(Voice Activity Detection,自動檢測語音起止)

      服務端自動判斷使用者何時開始與停止說話並作出回應。

    • Manual 模式(按下即說,鬆開即發送)

      用戶端控制語音起止。使用者說話結束後,用戶端需主動發送訊息至服務端。

    VAD 模式

    建立一個 python 檔案,命名為vad_dash.py,並將以下代碼複製到檔案中:

    vad_dash.py

    # 依賴:dashscope >= 1.23.9,pyaudio
    import os
    import base64
    import time
    import pyaudio
    from dashscope.audio.qwen_omni import MultiModality, AudioFormat,OmniRealtimeCallback,OmniRealtimeConversation
    import dashscope
    
    # 配置參數:地址、API Key、音色、模型、模型角色
    # 指定地區,設為intl表示國際(新加坡),設為cn表示中國大陸(北京)
    region = 'intl'
    base_domain = 'dashscope-intl.aliyuncs.com' if region == 'intl' else 'dashscope.aliyuncs.com'
    url = f'wss://{base_domain}/api-ws/v1/realtime'
    # 配置 API Key,若沒有設定環境變數,請用 API Key 將下行替換為 dashscope.api_key = "sk-xxx"
    dashscope.api_key = os.getenv('DASHSCOPE_API_KEY')
    # 指定音色
    voice = 'Cherry'
    # 指定模型
    model = 'qwen3-omni-flash-realtime'
    # 指定模型角色
    instructions = "你是個人助理小雲,請用幽默風趣的方式回答使用者的問題"
    class SimpleCallback(OmniRealtimeCallback):
        def __init__(self, pya):
            self.pya = pya
            self.out = None
        def on_open(self):
            # 初始化音訊輸出流
            self.out = self.pya.open(
                format=pyaudio.paInt16,
                channels=1,
                rate=24000,
                output=True
            )
        def on_event(self, response):
            if response['type'] == 'response.audio.delta':
                # 播放音頻
                self.out.write(base64.b64decode(response['delta']))
            elif response['type'] == 'conversation.item.input_audio_transcription.completed':
                # 列印轉錄文本
                print(f"[User] {response['transcript']}")
            elif response['type'] == 'response.audio_transcript.done':
                # 列印助手回複文本
                print(f"[LLM] {response['transcript']}")
    
    # 1. 初始化音訊裝置
    pya = pyaudio.PyAudio()
    # 2. 建立回呼函數和會話
    callback = SimpleCallback(pya)
    conv = OmniRealtimeConversation(model=model, callback=callback, url=url)
    # 3. 建立串連並配置會話
    conv.connect()
    conv.update_session(output_modalities=[MultiModality.AUDIO, MultiModality.TEXT], voice=voice, instructions=instructions)
    # 4. 初始化音頻輸入資料流
    mic = pya.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
    # 5. 主迴圈處理音頻輸入
    print("對話已開始,對著麥克風說話 (Ctrl+C 退出)...")
    try:
        while True:
            audio_data = mic.read(3200, exception_on_overflow=False)
            conv.append_audio(base64.b64encode(audio_data).decode())
            time.sleep(0.01)
    except KeyboardInterrupt:
        # 清理資源
        conv.close()
        mic.close()
        callback.out.close()
        pya.terminate()
        print("\n對話結束")

    運行vad_dash.py,通過麥克風即可與 Qwen-Omni-Realtime 模型即時對話,系統會檢測您的音頻起始位置並自動發送到伺服器,無需您手動發送。

    Manual 模式

    建立一個 python 檔案,命名為manual_dash.py,並將以下代碼複製進檔案中:

    manual_dash.py

    # 依賴:dashscope >= 1.23.9,pyaudio。
    import os
    import base64
    import sys
    import threading
    import pyaudio
    from dashscope.audio.qwen_omni import *
    import dashscope
    
    # 如果沒有設定環境變數,請用您的 API Key 將下行替換為 dashscope.api_key = "sk-xxx"
    dashscope.api_key = os.getenv('DASHSCOPE_API_KEY')
    voice = 'Cherry'
    
    class MyCallback(OmniRealtimeCallback):
        """最簡回調:建立串連時初始化擴音器,事件中直接播放返迴音頻。"""
        def __init__(self, ctx):
            super().__init__()
            self.ctx = ctx
    
        def on_open(self) -> None:
            # 串連建立後初始化 PyAudio 與擴音器(24k/mono/16bit)
            print('connection opened')
            try:
                self.ctx['pya'] = pyaudio.PyAudio()
                self.ctx['out'] = self.ctx['pya'].open(
                    format=pyaudio.paInt16,
                    channels=1,
                    rate=24000,
                    output=True
                )
                print('audio output initialized')
            except Exception as e:
                print('[Error] audio init failed: {}'.format(e))
    
        def on_close(self, close_status_code, close_msg) -> None:
            print('connection closed with code: {}, msg: {}'.format(close_status_code, close_msg))
            sys.exit(0)
    
        def on_event(self, response: str) -> None:
            try:
                t = response['type']
                handlers = {
                    'session.created': lambda r: print('start session: {}'.format(r['session']['id'])),
                    'conversation.item.input_audio_transcription.completed': lambda r: print('question: {}'.format(r['transcript'])),
                    'response.audio_transcript.delta': lambda r: print('llm text: {}'.format(r['delta'])),
                    'response.audio.delta': self._play_audio,
                    'response.done': self._response_done,
                }
                h = handlers.get(t)
                if h:
                    h(response)
            except Exception as e:
                print('[Error] {}'.format(e))
    
        def _play_audio(self, response):
            # 直接解碼base64並寫入輸出資料流進行播放
            if self.ctx['out'] is None:
                return
            try:
                data = base64.b64decode(response['delta'])
                self.ctx['out'].write(data)
            except Exception as e:
                print('[Error] audio playback failed: {}'.format(e))
    
        def _response_done(self, response):
            # 標記本輪對話完成,用於主迴圈等待
            if self.ctx['conv'] is not None:
                print('[Metric] response: {}, first text delay: {}, first audio delay: {}'.format(
                    self.ctx['conv'].get_last_response_id(),
                    self.ctx['conv'].get_last_first_text_delay(),
                    self.ctx['conv'].get_last_first_audio_delay(),
                ))
            if self.ctx['resp_done'] is not None:
                self.ctx['resp_done'].set()
    
    def shutdown_ctx(ctx):
        """安全釋放音頻與PyAudio資源。"""
        try:
            if ctx['out'] is not None:
                ctx['out'].close()
                ctx['out'] = None
        except Exception:
            pass
        try:
            if ctx['pya'] is not None:
                ctx['pya'].terminate()
                ctx['pya'] = None
        except Exception:
            pass
    
    
    def record_until_enter(pya_inst: pyaudio.PyAudio, sample_rate=16000, chunk_size=3200):
        """按 Enter 停止錄音,返回PCM位元組。"""
        frames = []
        stop_evt = threading.Event()
    
        stream = pya_inst.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=sample_rate,
            input=True,
            frames_per_buffer=chunk_size
        )
    
        def _reader():
            while not stop_evt.is_set():
                try:
                    frames.append(stream.read(chunk_size, exception_on_overflow=False))
                except Exception:
                    break
    
        t = threading.Thread(target=_reader, daemon=True)
        t.start()
        input()  # 使用者再次按 Enter 停止錄音
        stop_evt.set()
        t.join(timeout=1.0)
        try:
            stream.close()
        except Exception:
            pass
        return b''.join(frames)
    
    
    if __name__  == '__main__':
        print('Initializing ...')
        # 運行時上下文:存放音頻與交談控制代碼
        ctx = {'pya': None, 'out': None, 'conv': None, 'resp_done': threading.Event()}
        callback = MyCallback(ctx)
        conversation = OmniRealtimeConversation(
            model='qwen3-omni-flash-realtime',
            callback=callback,
            # 以下為國際(新加坡)地區url,若使用中國大陸(北京)地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/realtime
            url="wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime",
        )
        try:
            conversation.connect()
        except Exception as e:
            print('[Error] connect failed: {}'.format(e))
            sys.exit(1)
    
        ctx['conv'] = conversation
        # 會話配置:啟用文本+音訊輸出(禁用服務端VAD,改為手動錄音)
        conversation.update_session(
            output_modalities=[MultiModality.AUDIO, MultiModality.TEXT],
            voice=voice,
            input_audio_format=AudioFormat.PCM_16000HZ_MONO_16BIT,
            output_audio_format=AudioFormat.PCM_24000HZ_MONO_16BIT,
            enable_input_audio_transcription=True,
            # 對輸入音頻做語音轉錄的模型,僅支援gummy-realtime-v1
            input_audio_transcription_model='gummy-realtime-v1',
            enable_turn_detection=False,
            instructions="你是個人助理小雲,請你準確且友好地解答使用者的問題,始終以樂於助人的態度回應。"
        )
    
        try:
            turn = 1
            while True:
                print(f"\n--- 第 {turn} 輪對話 ---")
                print("按 Enter 開始錄音(輸入 q 斷行符號退出)...")
                user_input = input()
                if user_input.strip().lower() in ['q', 'quit']:
                    print("使用者請求退出...")
                    break
                print("錄音中... 再次按 Enter 停止錄音。")
                if ctx['pya'] is None:
                    ctx['pya'] = pyaudio.PyAudio()
                recorded = record_until_enter(ctx['pya'])
                if not recorded:
                    print("未錄製到有效音頻,請重試。")
                    continue
                print(f"成功錄製音頻: {len(recorded)} 位元組,發送中...")
    
                # 以3200位元組為塊發送(對應16k/16bit/100ms)
                chunk_size = 3200
                for i in range(0, len(recorded), chunk_size):
                    chunk = recorded[i:i+chunk_size]
                    conversation.append_audio(base64.b64encode(chunk).decode('ascii'))
    
                print("發送完成,等待模型響應...")
                ctx['resp_done'].clear()
                conversation.commit()
                conversation.create_response()
                ctx['resp_done'].wait()
                print('播放音頻完成')
                turn += 1
        except KeyboardInterrupt:
            print("\n程式被使用者中斷")
        finally:
            shutdown_ctx(ctx)
            print("程式退出")

    運行manual_dash.py,按 Enter 鍵開始說話,再按一次擷取模型響應的音頻。

DashScope Java SDK

選擇互動模式

  • VAD 模式(Voice Activity Detection,自動檢測語音起止)

    Realtime API 自動判斷使用者何時開始與停止說話並作出回應。

  • Manual 模式(按下即說,鬆開即發送)

    用戶端控制語音起止。使用者說話結束後,用戶端需主動發送訊息至服務端。

VAD 模式

OmniServerVad.java

import com.alibaba.dashscope.audio.omni.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.*;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Base64;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;

public class OmniServerVad {
    static class SequentialAudioPlayer {
        private final SourceDataLine line;
        private final Queue<byte[]> audioQueue = new ConcurrentLinkedQueue<>();
        private final Thread playerThread;
        private final AtomicBoolean shouldStop = new AtomicBoolean(false);

        public SequentialAudioPlayer() throws LineUnavailableException {
            AudioFormat format = new AudioFormat(24000, 16, 1, true, false);
            line = AudioSystem.getSourceDataLine(format);
            line.open(format);
            line.start();

            playerThread = new Thread(() -> {
                while (!shouldStop.get()) {
                    byte[] audio = audioQueue.poll();
                    if (audio != null) {
                        line.write(audio, 0, audio.length);
                    } else {
                        try { Thread.sleep(10); } catch (InterruptedException ignored) {}
                    }
                }
            }, "AudioPlayer");
            playerThread.start();
        }

        public void play(String base64Audio) {
            try {
                byte[] audio = Base64.getDecoder().decode(base64Audio);
                audioQueue.add(audio);
            } catch (Exception e) {
                System.err.println("音頻解碼失敗: " + e.getMessage());
            }
        }

        public void cancel() {
            audioQueue.clear();
            line.flush();
        }

        public void close() {
            shouldStop.set(true);
            try { playerThread.join(1000); } catch (InterruptedException ignored) {}
            line.drain();
            line.close();
        }
    }

    public static void main(String[] args) {
        try {
            SequentialAudioPlayer player = new SequentialAudioPlayer();
            AtomicBoolean userIsSpeaking = new AtomicBoolean(false);
            AtomicBoolean shouldStop = new AtomicBoolean(false);

            OmniRealtimeParam param = OmniRealtimeParam.builder()
                    .model("qwen3-omni-flash-realtime")
                    .apikey(System.getenv("DASHSCOPE_API_KEY"))
                    // 以下為國際(新加坡)(北京)地區url,若使用中國大陸地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/realtime
                    .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                    .build();

            OmniRealtimeConversation conversation = new OmniRealtimeConversation(param, new OmniRealtimeCallback() {
                @Override public void onOpen() {
                    System.out.println("串連已建立");
                }
                @Override public void onClose(int code, String reason) {
                    System.out.println("串連已關閉 (" + code + "): " + reason);
                    shouldStop.set(true);
                }
                @Override public void onEvent(JsonObject event) {
                    handleEvent(event, player, userIsSpeaking);
                }
            });

            conversation.connect();
            conversation.updateSession(OmniRealtimeConfig.builder()
                    .modalities(Arrays.asList(OmniRealtimeModality.AUDIO, OmniRealtimeModality.TEXT))
                    .voice("Cherry")
                    .enableTurnDetection(true)
                    .enableInputAudioTranscription(true)
                    .parameters(Map.of("instructions",
                            "你是五星級酒店的AI客服專員,請準確且友好地解答客戶關於房型、設施、價格、預訂政策的諮詢。請始終以專業和樂於助人的態度回應,杜絕提供未經證實或超出酒店服務涵蓋範圍的資訊。"))
                    .build()
            );

            System.out.println("請開始說話(自動檢測語音開始/結束,按Ctrl+C退出)...");
            AudioFormat format = new AudioFormat(16000, 16, 1, true, false);
            TargetDataLine mic = AudioSystem.getTargetDataLine(format);
            mic.open(format);
            mic.start();

            ByteBuffer buffer = ByteBuffer.allocate(3200);
            while (!shouldStop.get()) {
                int bytesRead = mic.read(buffer.array(), 0, buffer.capacity());
                if (bytesRead > 0) {
                    try {
                        conversation.appendAudio(Base64.getEncoder().encodeToString(buffer.array()));
                    } catch (Exception e) {
                        if (e.getMessage() != null && e.getMessage().contains("closed")) {
                            System.out.println("對話已關閉,停止錄音");
                            break;
                        }
                    }
                }
                Thread.sleep(20);
            }

            conversation.close(1000, "正常結束");
            player.close();
            mic.close();
            System.out.println("\n程式已退出");

        } catch (NoApiKeyException e) {
            System.err.println("未找到API KEY: 請設定環境變數 DASHSCOPE_API_KEY");
            System.exit(1);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static void handleEvent(JsonObject event, SequentialAudioPlayer player, AtomicBoolean userIsSpeaking) {
        String type = event.get("type").getAsString();
        switch (type) {
            case "input_audio_buffer.speech_started":
                System.out.println("\n[使用者開始說話]");
                player.cancel();
                userIsSpeaking.set(true);
                break;
            case "input_audio_buffer.speech_stopped":
                System.out.println("[使用者停止說話]");
                userIsSpeaking.set(false);
                break;
            case "response.audio.delta":
                if (!userIsSpeaking.get()) {
                    player.play(event.get("delta").getAsString());
                }
                break;
            case "conversation.item.input_audio_transcription.completed":
                System.out.println("使用者: " + event.get("transcript").getAsString());
                break;
            case "response.audio_transcript.delta":
                System.out.print(event.get("delta").getAsString());
                break;
            case "response.done":
                System.out.println("回複完成");
                break;
        }
    }
}

運行OmniServerVad.main()方法,通過麥克風即可與 Realtime 模型即時對話,系統會檢測您的音頻起始位置並自動發送到伺服器,無需您手動發送。

Manual 模式

OmniWithoutServerVad.java

// DashScope Java SDK 版本不低於2.20.9

import com.alibaba.dashscope.audio.omni.*;
import com.alibaba.dashscope.exception.NoApiKeyException;
import com.google.gson.JsonObject;
import javax.sound.sampled.*;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Base64;
import java.util.HashMap;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

public class Main {
    // RealtimePcmPlayer 類定義開始
    public static class RealtimePcmPlayer {
        private int sampleRate;
        private SourceDataLine line;
        private AudioFormat audioFormat;
        private Thread decoderThread;
        private Thread playerThread;
        private AtomicBoolean stopped = new AtomicBoolean(false);
        private Queue<String> b64AudioBuffer = new ConcurrentLinkedQueue<>();
        private Queue<byte[]> RawAudioBuffer = new ConcurrentLinkedQueue<>();

        // 建構函式初始化音頻格式和音頻線路
        public RealtimePcmPlayer(int sampleRate) throws LineUnavailableException {
            this.sampleRate = sampleRate;
            this.audioFormat = new AudioFormat(this.sampleRate, 16, 1, true, false);
            DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
            line = (SourceDataLine) AudioSystem.getLine(info);
            line.open(audioFormat);
            line.start();
            decoderThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!stopped.get()) {
                        String b64Audio = b64AudioBuffer.poll();
                        if (b64Audio != null) {
                            byte[] rawAudio = Base64.getDecoder().decode(b64Audio);
                            RawAudioBuffer.add(rawAudio);
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            });
            playerThread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (!stopped.get()) {
                        byte[] rawAudio = RawAudioBuffer.poll();
                        if (rawAudio != null) {
                            try {
                                playChunk(rawAudio);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        } else {
                            try {
                                Thread.sleep(100);
                            } catch (InterruptedException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    }
                }
            });
            decoderThread.start();
            playerThread.start();
        }

        // 播放一個音頻塊並阻塞直到播放完成
        private void playChunk(byte[] chunk) throws IOException, InterruptedException {
            if (chunk == null || chunk.length == 0) return;

            int bytesWritten = 0;
            while (bytesWritten < chunk.length) {
                bytesWritten += line.write(chunk, bytesWritten, chunk.length - bytesWritten);
            }
            int audioLength = chunk.length / (this.sampleRate*2/1000);
            // 等待緩衝區中的音頻播放完成
            Thread.sleep(audioLength - 10);
        }

        public void write(String b64Audio) {
            b64AudioBuffer.add(b64Audio);
        }

        public void cancel() {
            b64AudioBuffer.clear();
            RawAudioBuffer.clear();
        }

        public void waitForComplete() throws InterruptedException {
            while (!b64AudioBuffer.isEmpty() || !RawAudioBuffer.isEmpty()) {
                Thread.sleep(100);
            }
            line.drain();
        }

        public void shutdown() throws InterruptedException {
            stopped.set(true);
            decoderThread.join();
            playerThread.join();
            if (line != null && line.isRunning()) {
                line.drain();
                line.close();
            }
        }
    } // RealtimePcmPlayer 類定義結束
    // 新增錄音方法
    private static void recordAndSend(TargetDataLine line, OmniRealtimeConversation conversation) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        byte[] buffer = new byte[3200];
        AtomicBoolean stopRecording = new AtomicBoolean(false);

        // 啟動監聽Enter鍵的線程
        Thread enterKeyListener = new Thread(() -> {
            try {
                System.in.read();
                stopRecording.set(true);
            } catch (IOException e) {
                e.printStackTrace();
            }
        });
        enterKeyListener.start();

        // 錄音迴圈
        while (!stopRecording.get()) {
            int count = line.read(buffer, 0, buffer.length);
            if (count > 0) {
                out.write(buffer, 0, count);
            }
        }

        // 發送錄音資料
        byte[] audioData = out.toByteArray();
        String audioB64 = Base64.getEncoder().encodeToString(audioData);
        conversation.appendAudio(audioB64);
        out.close();
    }

    public static void main(String[] args) throws InterruptedException, LineUnavailableException {
        OmniRealtimeParam param = OmniRealtimeParam.builder()
                .model("qwen3-omni-flash-realtime")
                // 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
                // 若沒有配置環境變數,請用百鍊API Key將下行替換為:.apikey("sk-xxx")
                .apikey(System.getenv("DASHSCOPE_API_KEY"))
                //以下為新加坡地區url,若使用北京地區的模型,需將url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/realtime
                .url("wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime")
                .build();
        AtomicReference<CountDownLatch> responseDoneLatch = new AtomicReference<>(null);
        responseDoneLatch.set(new CountDownLatch(1));

        RealtimePcmPlayer audioPlayer = new RealtimePcmPlayer(24000);
        final AtomicReference<OmniRealtimeConversation> conversationRef = new AtomicReference<>(null);
        OmniRealtimeConversation 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("question: " + message.get("transcript").getAsString());
                        break;
                    case "response.audio_transcript.delta":
                        System.out.println("got llm response delta: " + message.get("delta").getAsString());
                        break;
                    case "response.audio.delta":
                        String recvAudioB64 = message.get("delta").getAsString();
                        audioPlayer.write(recvAudioB64);
                        break;
                    case "response.done":
                        System.out.println("======RESPONSE DONE======");
                        if (conversationRef.get() != null) {
                            System.out.println("[Metric] response: " + conversationRef.get().getResponseId() +
                                    ", first text delay: " + conversationRef.get().getFirstTextDelay() +
                                    " ms, first audio delay: " + conversationRef.get().getFirstAudioDelay() + " ms");
                        }
                        responseDoneLatch.get().countDown();
                        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);
        }
        OmniRealtimeConfig config = OmniRealtimeConfig.builder()
                .modalities(Arrays.asList(OmniRealtimeModality.AUDIO, OmniRealtimeModality.TEXT))
                .voice("Cherry")
                .enableTurnDetection(false)
                // 設定模型角色
                .parameters(new HashMap<String, Object>() {{
                    put("instructions","你是個人助理小雲,請你準確且友好地解答使用者的問題,始終以樂於助人的態度回應。");
                }})
                .build();
        conversation.updateSession(config);

        // 新增麥克風錄音功能
        AudioFormat format = new AudioFormat(16000, 16, 1, true, false);
        DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);

        if (!AudioSystem.isLineSupported(info)) {
            System.out.println("Line not supported");
            return;
        }

        TargetDataLine line = null;
        try {
            line = (TargetDataLine) AudioSystem.getLine(info);
            line.open(format);
            line.start();

            while (true) {
                System.out.println("按Enter開始錄音...");
                System.in.read();
                System.out.println("開始錄音,請說話...再次按Enter停止錄音並發送");
                recordAndSend(line, conversation);
                conversation.commit();
                conversation.createResponse(null, null);
                // 重設latch以便下次等待
                responseDoneLatch.set(new CountDownLatch(1));
            }
        } catch (LineUnavailableException | IOException e) {
            e.printStackTrace();
        } finally {
            if (line != null) {
                line.stop();
                line.close();
            }
        }
    }}

運行OmniWithoutServerVad.main()方法,按 Enter 鍵開始錄音,錄音過程中再次按 Enter 鍵停止錄音並發送,隨後將接收並播放模型響應。

WebSocket(Python)

  • 準備運行環境

    您的 Python 版本需要不低於 3.10。

    首先根據您的作業系統來安裝 pyaudio。

    macOS

    brew install portaudio && pip install pyaudio

    Debian/Ubuntu

    sudo apt-get install python3-pyaudio
    
    或者
    
    pip install pyaudio
    推薦使用pip install pyaudio。如果安裝失敗,請先根據您的作業系統安裝portaudio依賴。

    CentOS

    sudo yum install -y portaudio portaudio-devel && pip install pyaudio

    Windows

    pip install pyaudio

    安裝完成後,通過 pip 安裝 websocket 相關的依賴:

    pip install websockets==15.0.1
  • 建立用戶端

    在本地建立一個 python 檔案,命名為omni_realtime_client.py,並將以下代碼複製進檔案中:

    omni_realtime_client.py

    import asyncio
    import websockets
    import json
    import base64
    import time
    from typing import Optional, Callable, List, Dict, Any
    from enum import Enum
    
    class TurnDetectionMode(Enum):
        SERVER_VAD = "server_vad"
        MANUAL = "manual"
    
    class OmniRealtimeClient:
    
        def __init__(
                self,
                base_url,
                api_key: str,
                model: str = "",
                voice: str = "Ethan",
                instructions: str = "You are a helpful assistant.",
                turn_detection_mode: TurnDetectionMode = TurnDetectionMode.SERVER_VAD,
                on_text_delta: Optional[Callable[[str], None]] = None,
                on_audio_delta: Optional[Callable[[bytes], None]] = None,
                on_input_transcript: Optional[Callable[[str], None]] = None,
                on_output_transcript: Optional[Callable[[str], None]] = None,
                extra_event_handlers: Optional[Dict[str, Callable[[Dict[str, Any]], None]]] = None
        ):
            self.base_url = base_url
            self.api_key = api_key
            self.model = model
            self.voice = voice
            self.instructions = instructions
            self.ws = None
            self.on_text_delta = on_text_delta
            self.on_audio_delta = on_audio_delta
            self.on_input_transcript = on_input_transcript
            self.on_output_transcript = on_output_transcript
            self.turn_detection_mode = turn_detection_mode
            self.extra_event_handlers = extra_event_handlers or {}
    
            # 當前回複狀態
            self._current_response_id = None
            self._current_item_id = None
            self._is_responding = False
            # 輸入/輸出轉錄列印狀態
            self._print_input_transcript = True
            self._output_transcript_buffer = ""
    
        async def connect(self) -> None:
            """與 Realtime API 建立 WebSocket 串連。"""
            url = f"{self.base_url}?model={self.model}"
            headers = {
                "Authorization": f"Bearer {self.api_key}"
            }
            self.ws = await websockets.connect(url, additional_headers=headers)
    
            # 會話配置
            session_config = {
                "modalities": ["text", "audio"],
                "voice": self.voice,
                "instructions": self.instructions,
                "input_audio_format": "pcm16",
                "output_audio_format": "pcm24",
                "input_audio_transcription": {
                    "model": "gummy-realtime-v1"
                }
            }
    
            if self.turn_detection_mode == TurnDetectionMode.MANUAL:
                session_config['turn_detection'] = None
                await self.update_session(session_config)
            elif self.turn_detection_mode == TurnDetectionMode.SERVER_VAD:
                session_config['turn_detection'] = {
                    "type": "server_vad",
                    "threshold": 0.1,
                    "prefix_padding_ms": 500,
                    "silence_duration_ms": 900
                }
                await self.update_session(session_config)
            else:
                raise ValueError(f"Invalid turn detection mode: {self.turn_detection_mode}")
    
        async def send_event(self, event) -> None:
            event['event_id'] = "event_" + str(int(time.time() * 1000))
            await self.ws.send(json.dumps(event))
    
        async def update_session(self, config: Dict[str, Any]) -> None:
            """更新會話配置。"""
            event = {
                "type": "session.update",
                "session": config
            }
            await self.send_event(event)
    
        async def stream_audio(self, audio_chunk: bytes) -> None:
            """向 API 流式發送原始音頻資料。"""
            # 僅支援 16bit 16kHz 單聲道 PCM
            audio_b64 = base64.b64encode(audio_chunk).decode()
            append_event = {
                "type": "input_audio_buffer.append",
                "audio": audio_b64
            }
            await self.send_event(append_event)
    
        async def commit_audio_buffer(self) -> None:
            """提交音頻緩衝區以觸發處理。"""
            event = {
                "type": "input_audio_buffer.commit"
            }
            await self.send_event(event)
    
        async def append_image(self, image_chunk: bytes) -> None:
            """向映像緩衝區追加映像資料。
            映像資料可以來自本地檔案,也可以來自即時視頻流。
            注意:
                - 映像格式必須為 JPG 或 JPEG。推薦解析度為 480P 或 720P,最高支援 1080P。
                - 單張圖片大小不應超過 500KB。
                - 將映像資料編碼為 Base64 後再發送。
                - 建議以不超過每秒 2 幀的頻率向伺服器發送映像。
                - 在發送映像資料之前,需要至少發送過一次音頻資料。
            """
            image_b64 = base64.b64encode(image_chunk).decode()
            event = {
                "type": "input_image_buffer.append",
                "image": image_b64
            }
            await self.send_event(event)
    
        async def create_response(self) -> None:
            """向 API 請求產生回複(僅在手動模式下需要調用)。"""
            event = {
                "type": "response.create"
            }
            await self.send_event(event)
    
        async def cancel_response(self) -> None:
            """取消當前回複。"""
            event = {
                "type": "response.cancel"
            }
            await self.send_event(event)
    
        async def handle_interruption(self):
            """處理使用者對當前回複的打斷。"""
            if not self._is_responding:
                return
            # 1. 取消當前回複
            if self._current_response_id:
                await self.cancel_response()
    
            self._is_responding = False
            self._current_response_id = None
            self._current_item_id = None
    
        async def handle_messages(self) -> None:
            try:
                async for message in self.ws:
                    event = json.loads(message)
                    event_type = event.get("type")
                    if event_type == "error":
                        print(" Error: ", event['error'])
                        continue
                    elif event_type == "response.created":
                        self._current_response_id = event.get("response", {}).get("id")
                        self._is_responding = True
                    elif event_type == "response.output_item.added":
                        self._current_item_id = event.get("item", {}).get("id")
                    elif event_type == "response.done":
                        self._is_responding = False
                        self._current_response_id = None
                        self._current_item_id = None
                    elif event_type == "input_audio_buffer.speech_started":
                        print("檢測到語音開始")
                        if self._is_responding:
                            print("處理打斷")
                            await self.handle_interruption()
                    elif event_type == "input_audio_buffer.speech_stopped":
                        print("檢測到語音結束")
                    elif event_type == "response.text.delta":
                        if self.on_text_delta:
                            self.on_text_delta(event["delta"])
                    elif event_type == "response.audio.delta":
                        if self.on_audio_delta:
                            audio_bytes = base64.b64decode(event["delta"])
                            self.on_audio_delta(audio_bytes)
                    elif event_type == "conversation.item.input_audio_transcription.completed":
                        transcript = event.get("transcript", "")
                        print(f"使用者: {transcript}")
                        if self.on_input_transcript:
                            await asyncio.to_thread(self.on_input_transcript, transcript)
                            self._print_input_transcript = True
                    elif event_type == "response.audio_transcript.delta":
                        if self.on_output_transcript:
                            delta = event.get("delta", "")
                            if not self._print_input_transcript:
                                self._output_transcript_buffer += delta
                            else:
                                if self._output_transcript_buffer:
                                    await asyncio.to_thread(self.on_output_transcript, self._output_transcript_buffer)
                                    self._output_transcript_buffer = ""
                                await asyncio.to_thread(self.on_output_transcript, delta)
                    elif event_type == "response.audio_transcript.done":
                        print(f"大模型: {event.get('transcript', '')}")
                        self._print_input_transcript = False
                    elif event_type in self.extra_event_handlers:
                        self.extra_event_handlers[event_type](event)
            except websockets.exceptions.ConnectionClosed:
                print(" Connection closed")
            except Exception as e:
                print(" Error in message handling: ", str(e))
        async def close(self) -> None:
            """關閉 WebSocket 串連。"""
            if self.ws:
                await self.ws.close()
  • 選擇互動模式

    • VAD 模式(Voice Activity Detection,自動檢測語音起止)

      Realtime API 自動判斷使用者何時開始與停止說話並作出回應。

    • Manual 模式(按下即說,鬆開即發送)

      用戶端控制語音起止。使用者說話結束後,用戶端需主動發送訊息至服務端。

    VAD 模式

    omni_realtime_client.py的同級目錄下建立另一個 python 檔案,命名為vad_mode.py,並將以下代碼複製進檔案中:

    vad_mode.py

    # -- coding: utf-8 --
    import os, asyncio, pyaudio, queue, threading
    from omni_realtime_client import OmniRealtimeClient, TurnDetectionMode
    
    # 音頻播放器類(處理中斷)
    class AudioPlayer:
        def __init__(self, pyaudio_instance, rate=24000):
            self.stream = pyaudio_instance.open(format=pyaudio.paInt16, channels=1, rate=rate, output=True)
            self.queue = queue.Queue()
            self.stop_evt = threading.Event()
            self.interrupt_evt = threading.Event()
            threading.Thread(target=self._run, daemon=True).start()
    
        def _run(self):
            while not self.stop_evt.is_set():
                try:
                    data = self.queue.get(timeout=0.5)
                    if data is None: break
                    if not self.interrupt_evt.is_set(): self.stream.write(data)
                    self.queue.task_done()
                except queue.Empty: continue
    
        def add_audio(self, data): self.queue.put(data)
        def handle_interrupt(self): self.interrupt_evt.set(); self.queue.queue.clear()
        def stop(self): self.stop_evt.set(); self.queue.put(None); self.stream.stop_stream(); self.stream.close()
    
    # 麥克風錄音並發送
    async def record_and_send(client):
        p = pyaudio.PyAudio()
        stream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True, frames_per_buffer=3200)
        print("開始錄音,請講話...")
        try:
            while True:
                audio_data = stream.read(3200)
                await client.stream_audio(audio_data)
                await asyncio.sleep(0.02)
        finally:
            stream.stop_stream(); stream.close(); p.terminate()
    
    async def main():
        p = pyaudio.PyAudio()
        player = AudioPlayer(pyaudio_instance=p)
    
        client = OmniRealtimeClient(
            # 以下是國際(新加坡)地區 base_url,中國大陸(北京)地區base_url為wss://dashscope.aliyuncs.com/api-ws/v1/realtime
            base_url="wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime",
            api_key=os.environ.get("DASHSCOPE_API_KEY"),
            model="qwen3-omni-flash-realtime",
            voice="Cherry",
            instructions="你是小雲,風趣幽默的好助手",
            turn_detection_mode=TurnDetectionMode.SERVER_VAD,
            on_text_delta=lambda t: print(f"\nAssistant: {t}", end="", flush=True),
            on_audio_delta=player.add_audio,
        )
    
        await client.connect()
        print("串連成功,開始即時對話...")
    
        # 並發運行
        await asyncio.gather(client.handle_messages(), record_and_send(client))
    
    if __name__ == "__main__":
        try:
            asyncio.run(main())
        except KeyboardInterrupt:
            print("\n程式已退出。")

    運行vad_mode.py,通過麥克風即可與 Realtime 模型即時對話,系統會檢測您的音頻起始位置並自動發送到伺服器,無需您手動發送。

    Manual 模式

    omni_realtime_client.py的同級目錄下建立另一個 python 檔案,命名為manual_mode.py,並將以下代碼複製進檔案中:

    manual_mode.py

    # -- coding: utf-8 --
    import os
    import asyncio
    import time
    import threading
    import queue
    import pyaudio
    from omni_realtime_client import OmniRealtimeClient, TurnDetectionMode
    
    
    class AudioPlayer:
        """即時音頻播放器類"""
    
        def __init__(self, sample_rate=24000, channels=1, sample_width=2):
            self.sample_rate = sample_rate
            self.channels = channels
            self.sample_width = sample_width  # 2 bytes for 16-bit
            self.audio_queue = queue.Queue()
            self.is_playing = False
            self.play_thread = None
            self.pyaudio_instance = None
            self.stream = None
            self._lock = threading.Lock()  # 添加鎖來同步訪問
            self._last_data_time = time.time()  # 記錄最後接收資料的時間
            self._response_done = False  # 添加響應完成標誌
            self._waiting_for_response = False  # 標記是否正在等待伺服器響應
            # 記錄最後一次向音頻流寫入資料的時間及最近一次音頻塊的時間長度,用於更精確地判斷播放結束
            self._last_play_time = time.time()
            self._last_chunk_duration = 0.0
    
        def start(self):
            """啟動音頻播放器"""
            with self._lock:
                if self.is_playing:
                    return
    
                self.is_playing = True
    
                try:
                    self.pyaudio_instance = pyaudio.PyAudio()
    
                    # 建立音訊輸出流
                    self.stream = self.pyaudio_instance.open(
                        format=pyaudio.paInt16,  # 16-bit
                        channels=self.channels,
                        rate=self.sample_rate,
                        output=True,
                        frames_per_buffer=1024
                    )
    
                    # 啟動播放線程
                    self.play_thread = threading.Thread(target=self._play_audio)
                    self.play_thread.daemon = True
                    self.play_thread.start()
    
                    print("音頻播放器已啟動")
                except Exception as e:
                    print(f"啟動音頻播放器失敗: {e}")
                    self._cleanup_resources()
                    raise
    
        def stop(self):
            """停止音頻播放器"""
            with self._lock:
                if not self.is_playing:
                    return
    
                self.is_playing = False
    
            # 清空隊列
            while not self.audio_queue.empty():
                try:
                    self.audio_queue.get_nowait()
                except queue.Empty:
                    break
    
            # 等待播放線程結束(在鎖外面等待,避免死結)
            if self.play_thread and self.play_thread.is_alive():
                self.play_thread.join(timeout=2.0)
    
            # 再次擷取鎖來清理資源
            with self._lock:
                self._cleanup_resources()
    
            print("音頻播放器已停止")
    
        def _cleanup_resources(self):
            """清理音頻資源(必須在鎖內調用)"""
            try:
                # 關閉音頻流
                if self.stream:
                    if not self.stream.is_stopped():
                        self.stream.stop_stream()
                    self.stream.close()
                    self.stream = None
            except Exception as e:
                print(f"關閉音頻流時出錯: {e}")
    
            try:
                if self.pyaudio_instance:
                    self.pyaudio_instance.terminate()
                    self.pyaudio_instance = None
            except Exception as e:
                print(f"終止PyAudio時出錯: {e}")
    
        def add_audio_data(self, audio_data):
            """添加音頻資料到播放隊列"""
            if self.is_playing and audio_data:
                self.audio_queue.put(audio_data)
                with self._lock:
                    self._last_data_time = time.time()  # 更新最後接收資料的時間
                    self._waiting_for_response = False  # 收到資料,不再等待
    
        def stop_receiving_data(self):
            """標記不再接收新的音頻資料"""
            with self._lock:
                self._response_done = True
                self._waiting_for_response = False  # 響應結束,不再等待
    
        def prepare_for_next_turn(self):
            """為下一輪對話重設播放器狀態。"""
            with self._lock:
                self._response_done = False
                self._last_data_time = time.time()
                self._last_play_time = time.time()
                self._last_chunk_duration = 0.0
                self._waiting_for_response = True  # 開始等待下一輪響應
    
            # 清空上一輪可能殘留的音頻資料
            while not self.audio_queue.empty():
                try:
                    self.audio_queue.get_nowait()
                except queue.Empty:
                    break
    
        def is_finished_playing(self):
            """檢查是否已經播放完所有音頻資料"""
            with self._lock:
                queue_size = self.audio_queue.qsize()
                time_since_last_data = time.time() - self._last_data_time
                time_since_last_play = time.time() - self._last_play_time
    
                # ---------------------- 智能結束判定 ----------------------
                # 1. 首選:如果伺服器已標記完成且播放隊列為空白
                #    進一步等待最近一塊音頻播放完畢(音頻塊時間長度 + 0.1s 容錯)。
                if self._response_done and queue_size == 0:
                    min_wait = max(self._last_chunk_duration + 0.1, 0.5)  # 至少等待 0.5s
                    if time_since_last_play >= min_wait:
                        return True
    
                # 2. 備用:如果長時間沒有新資料且播放隊列為空白
                #    當伺服器沒有明確發出 `response.done` 時,此邏輯作為保障
                if not self._waiting_for_response and queue_size == 0 and time_since_last_data > 1.0:
                    print("\n(逾時未收到新音頻,判定播放結束)")
                    return True
    
                return False
    
        def _play_audio(self):
            """播放音頻資料的背景工作執行緒"""
            while True:
                # 檢查是否應該停止
                with self._lock:
                    if not self.is_playing:
                        break
                    stream_ref = self.stream  # 擷取流的引用
    
                try:
                    # 從隊列中擷取音頻資料,逾時0.1秒
                    audio_data = self.audio_queue.get(timeout=0.1)
    
                    # 再次檢查狀態和流的有效性
                    with self._lock:
                        if self.is_playing and stream_ref and not stream_ref.is_stopped():
                            try:
                                # 播放音頻資料
                                stream_ref.write(audio_data)
                                # 更新最近播放資訊
                                self._last_play_time = time.time()
                                self._last_chunk_duration = len(audio_data) / (
                                            self.channels * self.sample_width) / self.sample_rate
                            except Exception as e:
                                print(f"寫入音頻流時出錯: {e}")
                                break
    
                    # 標記該資料區塊已處理完成
                    self.audio_queue.task_done()
    
                except queue.Empty:
                    # 隊列為空白時繼續等待
                    continue
                except Exception as e:
                    print(f"播放音頻時出錯: {e}")
                    break
    
    
    class MicrophoneRecorder:
        """即時麥克風錄音器"""
    
        def __init__(self, sample_rate=16000, channels=1, chunk_size=3200):
            self.sample_rate = sample_rate
            self.channels = channels
            self.chunk_size = chunk_size
            self.pyaudio_instance = None
            self.stream = None
            self.frames = []
            self._is_recording = False
            self._record_thread = None
    
        def _recording_thread(self):
            """錄音背景工作執行緒"""
            # 在 _is_recording 為 True 期間,持續從音頻流中讀取資料
            while self._is_recording:
                try:
                    # 使用 exception_on_overflow=False 避免因緩衝區溢位而崩潰
                    data = self.stream.read(self.chunk_size, exception_on_overflow=False)
                    self.frames.append(data)
                except (IOError, OSError) as e:
                    # 當流被關閉時,讀取操作可能會引發錯誤
                    print(f"錄音流讀取錯誤,可能已關閉: {e}")
                    break
    
        def start(self):
            """開始錄音"""
            if self._is_recording:
                print("錄音已在進行中。")
                return
    
            self.frames = []
            self._is_recording = True
    
            try:
                self.pyaudio_instance = pyaudio.PyAudio()
                self.stream = self.pyaudio_instance.open(
                    format=pyaudio.paInt16,
                    channels=self.channels,
                    rate=self.sample_rate,
                    input=True,
                    frames_per_buffer=self.chunk_size
                )
    
                self._record_thread = threading.Thread(target=self._recording_thread)
                self._record_thread.daemon = True
                self._record_thread.start()
                print("麥克風錄音已開始...")
            except Exception as e:
                print(f"啟動麥克風失敗: {e}")
                self._is_recording = False
                self._cleanup()
                raise
    
        def stop(self):
            """停止錄音並返迴音頻資料"""
            if not self._is_recording:
                return None
    
            self._is_recording = False
    
            # 等待錄音安全執行緒退出
            if self._record_thread:
                self._record_thread.join(timeout=1.0)
    
            self._cleanup()
    
            print("麥克風錄音已停止。")
            return b''.join(self.frames)
    
        def _cleanup(self):
            """安全地清理 PyAudio 資源"""
            if self.stream:
                try:
                    if self.stream.is_active():
                        self.stream.stop_stream()
                    self.stream.close()
                except Exception as e:
                    print(f"關閉音頻流時出錯: {e}")
    
            if self.pyaudio_instance:
                try:
                    self.pyaudio_instance.terminate()
                except Exception as e:
                    print(f"終止 PyAudio 執行個體時出錯: {e}")
    
            self.stream = None
            self.pyaudio_instance = None
    
    
    async def interactive_test():
        """
        互動式測試指令碼:允許多輪連續對話,每輪可以發送音頻和圖片。
        """
        # ------------------- 1. 初始化和串連 (一次性) -------------------
        # 新加坡和北京地區的API Key不同。擷取API Key:https://www.alibabacloud.com/help/zh/model-studio/get-api-key
        api_key = os.environ.get("DASHSCOPE_API_KEY")
        if not api_key:
            print("請設定DASHSCOPE_API_KEY環境變數")
            return
    
        print("--- 即時多輪音視訊交談用戶端 ---")
        print("正在初始化音頻播放器和用戶端...")
    
        audio_player = AudioPlayer()
        audio_player.start()
    
        def on_audio_received(audio_data):
            audio_player.add_audio_data(audio_data)
    
        def on_response_done(event):
            print("\n(收到響應結束標記)")
            audio_player.stop_receiving_data()
    
        realtime_client = OmniRealtimeClient(
            # 以下為新加坡地區base_url,若使用北京地區的模型,需將base_url替換為:wss://dashscope.aliyuncs.com/api-ws/v1/realtime
            base_url="wss://dashscope-intl.aliyuncs.com/api-ws/v1/realtime",
            api_key=api_key,
            model="qwen3-omni-flash-realtime",
            voice="Ethan",
            instructions="你是個人助理小雲,請你準確且友好地解答使用者的問題,始終以樂於助人的態度回應。", # 設定模型角色
            on_text_delta=lambda text: print(f"助手回複: {text}", end="", flush=True),
            on_audio_delta=on_audio_received,
            turn_detection_mode=TurnDetectionMode.MANUAL,
            extra_event_handlers={"response.done": on_response_done}
        )
    
        message_handler_task = None
        try:
            await realtime_client.connect()
            print("已串連到伺服器。輸入 'q' 或 'quit' 可隨時退出程式。")
            message_handler_task = asyncio.create_task(realtime_client.handle_messages())
            await asyncio.sleep(0.5)
    
            turn_counter = 1
            # ------------------- 2. 多輪對話迴圈 -------------------
            while True:
                print(f"\n--- 第 {turn_counter} 輪對話 ---")
                audio_player.prepare_for_next_turn()
    
                recorded_audio = None
                image_paths = []
    
                # --- 擷取使用者輸入:從麥克風錄音 ---
                loop = asyncio.get_event_loop()
                recorder = MicrophoneRecorder(sample_rate=16000)  # 推薦使用16k採樣率進行語音辨識
    
                print("準備錄音。按 Enter 鍵開始錄音 (或輸入 'q' 退出)...")
                user_input = await loop.run_in_executor(None, input)
                if user_input.strip().lower() in ['q', 'quit']:
                    print("使用者請求退出...")
                    return
    
                try:
                    recorder.start()
                except Exception:
                    print("無法啟動錄音,請檢查您的麥克風許可權和裝置。跳過本輪。")
                    continue
    
                print("錄音中... 再次按 Enter 鍵停止錄音。")
                await loop.run_in_executor(None, input)
    
                recorded_audio = recorder.stop()
    
                if not recorded_audio or len(recorded_audio) == 0:
                    print("未錄製到有效音頻,請重新開始本輪對話。")
                    continue
    
                # --- 擷取圖片輸入 (可選) ---
                # 以下圖片輸入功能已被注釋,暫時禁用。若需啟用請取消下方代碼注釋。
                # print("\n請逐行輸入【圖片檔案】的絕對路徑 (可選)。完成後,輸入 's' 或按 Enter 發送請求。")
                # while True:
                #     path = input("圖片路徑: ").strip()
                #     if path.lower() == 's' or path == '':
                #         break
                #     if path.lower() in ['q', 'quit']:
                #         print("使用者請求退出...")
                #         return
                #
                #     if not os.path.isabs(path):
                #         print("錯誤: 請輸入絕對路徑。")
                #         continue
                #     if not os.path.exists(path):
                #         print(f"錯誤: 檔案不存在 -> {path}")
                #         continue
                #     image_paths.append(path)
                #     print(f"已添加圖片: {os.path.basename(path)}")
    
                # --- 3. 發送資料並擷取響應 ---
                print("\n--- 輸入確認 ---")
                print(f"待處理音頻: 1個 (來自麥克風), 圖片: {len(image_paths)}個")
                print("------------------")
    
                # 3.1 發送錄製的音頻
                try:
                    print(f"發送麥克風錄音 ({len(recorded_audio)}位元組)")
                    await realtime_client.stream_audio(recorded_audio)
                    await asyncio.sleep(0.1)
                except Exception as e:
                    print(f"發送麥克風錄音失敗: {e}")
                    continue
    
                # 3.2 發送所有圖片檔案
                # 以下圖片發送代碼已被注釋,暫時禁用。
                # for i, path in enumerate(image_paths):
                #     try:
                #         with open(path, "rb") as f:
                #             data = f.read()
                #         print(f"發送圖片 {i+1}: {os.path.basename(path)} ({len(data)}位元組)")
                #         await realtime_client.append_image(data)
                #         await asyncio.sleep(0.1)
                #     except Exception as e:
                #         print(f"發送圖片 {os.path.basename(path)} 失敗: {e}")
    
                # 3.3 提交並等待響應
                print("提交所有輸入,請求伺服器響應...")
                await realtime_client.commit_audio_buffer()
                await realtime_client.create_response()
    
                print("等待並播放伺服器響應音頻...")
                start_time = time.time()
                max_wait_time = 60
                while not audio_player.is_finished_playing():
                    if time.time() - start_time > max_wait_time:
                        print(f"\n等待逾時 ({max_wait_time}秒), 進入下一輪。")
                        break
                    await asyncio.sleep(0.2)
    
                print("\n本輪音頻播放完成!")
                turn_counter += 1
    
        except (asyncio.CancelledError, KeyboardInterrupt):
            print("\n程式被中斷。")
        except Exception as e:
            print(f"發生未處理的錯誤: {e}")
        finally:
            # ------------------- 4. 清理資源 -------------------
            print("\n正在關閉串連並清理資源...")
            if message_handler_task and not message_handler_task.done():
                message_handler_task.cancel()
    
            if 'realtime_client' in locals() and realtime_client.ws and not realtime_client.ws.close:
                await realtime_client.close()
                print("串連已關閉。")
    
            audio_player.stop()
            print("程式退出。")
    
    
    if __name__ == "__main__":
        try:
            asyncio.run(interactive_test())
        except KeyboardInterrupt:
            print("\n程式被使用者強制退出。")

    運行manual_mode.py,按 Enter 鍵開始說話,再按一次擷取模型響應的音頻。

互動流程

VAD 模式

session.update事件的session.turn_detection 設為"server_vad"以啟用 VAD 模式。此模式下,服務端自動檢測語音起止並進行響應。適用於語音通話情境。

互動流程如下:

  1. 服務端檢測到語音開始,發送input_audio_buffer.speech_started 事件。

  2. 用戶端隨時發送 input_audio_buffer.appendinput_image_buffer.append 事件追加音頻與圖片至緩衝區。

    發送 input_image_buffer.append 事件前,至少發送過一次 input_audio_buffer.append 事件。
  3. 服務端檢測到語音結束,發送input_audio_buffer.speech_stopped 事件。

  4. 服務端發送input_audio_buffer.committed 事件提交音頻緩衝區。

  5. 服務端發送 conversation.item.created 事件,包含從緩衝區建立的使用者訊息項。

生命週期

用戶端事件

服務端事件

會話初始化

session.update

會話配置

session.created

會話已建立

session.updated

會話配置已更新

使用者音頻輸入

input_audio_buffer.append

添加音頻到緩衝區

input_image_buffer.append

添加圖片到緩衝區

input_audio_buffer.speech_started

檢測到語音開始

input_audio_buffer.speech_stopped

檢測到語音結束

input_audio_buffer.committed

伺服器收到提交的音頻

伺服器音訊輸出

response.created

服務端開始產生響應

response.output_item.added

響應時有新的輸出內容

conversation.item.created

對話項被建立

response.content_part.added

新的輸出內容添加到assistant message

response.audio_transcript.delta

增量產生的轉錄文字

response.audio.delta

模型增量產生的音頻

response.audio_transcript.done

文本轉錄完成

response.audio.done

音頻產生完成

response.content_part.done

Assistant message 的文本或音頻內容流式輸出完成

response.output_item.done

Assistant message 的整個輸出項串流完成

response.done

響應完成

Manual 模式

session.update事件的session.turn_detection 設為 null 以啟用 Manual 模式。此模式下,用戶端通過顯式發送input_audio_buffer.commitresponse.create事件請求伺服器響應。適用於按下即說情境,如聊天軟體中的發送語音。

互動流程如下:

  1. 用戶端隨時發送 input_audio_buffer.appendinput_image_buffer.append事件追加音頻與圖片至緩衝區。

    發送 input_image_buffer.append 事件前,至少發送過一次 input_audio_buffer.append 事件。
  2. 用戶端發送input_audio_buffer.commit事件提交音頻緩衝區與映像緩衝區,告知服務端本輪的使用者輸入(音頻及圖片)已全部發送完畢。

  3. 服務端響應 input_audio_buffer.committed事件。

  4. 用戶端發送response.create事件,等待服務端返回模型的輸出。

  5. 服務端響應conversation.item.created事件。

生命週期

用戶端事件

服務端事件

會話初始化

session.update

會話配置

session.created

會話已建立

session.updated

會話配置已更新

使用者音頻輸入

input_audio_buffer.append

添加音頻到緩衝區

input_image_buffer.append

添加圖片到緩衝區

input_audio_buffer.commit

提交音頻與圖片到伺服器

response.create

建立模型響應

input_audio_buffer.committed

伺服器收到提交的音頻

伺服器音訊輸出

input_audio_buffer.clear

清除緩衝區的音頻

response.created

服務端開始產生響應

response.output_item.added

響應時有新的輸出內容

conversation.item.created

對話項被建立

response.content_part.added

新的輸出內容添加到assistant message 項

response.audio_transcript.delta

增量產生的轉錄文字

response.audio.delta

模型增量產生的音頻

response.audio_transcript.done

完成文本轉錄

response.audio.done

完成音頻產生

response.content_part.done

Assistant message 的文本或音頻內容流式輸出完成

response.output_item.done

Assistant message 的整個輸出項串流完成

response.done

響應完成

API 參考

計費與限流

計費規則

Qwen-Omni-Realtime 模型根據不同模態(音頻、映像)對應的Token數計費。計費詳情請參見模型列表

音頻、圖片轉換為Token數的規則

音頻

  • Qwen3-Omni-Flash-Realtime:總 Token 數 = 音頻時間長度(單位:秒)* 12.5

  • Qwen-Omni-Turbo-Realtime:總 Token 數 = 音頻時間長度(單位:秒)* 25,若音頻時間長度不足1秒,則按 1 秒計算。

圖片

  • Qwen3-Omni-Flash-Realtime模型32x32像素對應 1 個 Token

  • Qwen-Omni-Turbo-Realtime模型:每28x28像素對應 1 個 Token

一張圖最少需要 4 個 Token,最多支援 1280 個 Token;可使用以下代碼,傳入映像路徑和會話時間長度即可估算圖片消耗的 Token 總量:

# 使用以下命令安裝Pillow庫:pip install Pillow
from PIL import Image
import math

# Qwen-Omni-Turbo-Realtime模型,縮放因子為28
# factor = 28
# Qwen3-Omni-Flash-Realtime模型,縮放因子為32
factor = 32

def token_calculate(image_path='', duration=10):
    """
    :param image_path: 映像路徑
    :param duration: 會話串連時間長度
    :return: 映像的Token數
    """
    if len(image_path) > 0:
        # 開啟指定的PNG圖片檔案
        image = Image.open(image_path)
        # 擷取圖片的原始大小
        height = image.height
        width = image.width
    print(f"縮放前的映像尺寸為:高度為{height},寬度為{width}")
    # 將高度調整為factor的整數倍
    h_bar = round(height / factor) * factor
    # 將寬度調整為factor的整數倍
    w_bar = round(width / factor) * factor
    # 映像的Token下限:4個Token
    min_pixels = factor * factor * 4
    # 映像的Token上限:1280個Token
    max_pixels = 1280 * factor * factor
    # 對映像進行縮放處理,調整像素的總數在範圍[min_pixels,max_pixels]內
    if h_bar * w_bar > max_pixels:
        # 計算縮放因子beta,使得縮放後的映像總像素數不超過max_pixels
        beta = math.sqrt((height * width) / max_pixels)
        # 重新計算調整後的高度,確保為factor的整數倍
        h_bar = math.floor(height / beta / factor) * factor
        # 重新計算調整後的寬度,確保為factor的整數倍
        w_bar = math.floor(width / beta / factor) * factor
    elif h_bar * w_bar < min_pixels:
        # 計算縮放因子beta,使得縮放後的映像總像素數不低於min_pixels
        beta = math.sqrt(min_pixels / (height * width))
        # 重新計算調整後的高度,確保為factor的整數倍
        h_bar = math.ceil(height * beta / factor) * factor
        # 重新計算調整後的寬度,確保為factor的整數倍
        w_bar = math.ceil(width * beta / factor) * factor
    print(f"縮放後的映像尺寸為:高度為{h_bar},寬度為{w_bar}")
    # 計算映像的Token數:總像素除以factor * factor
    token = int((h_bar * w_bar) / (factor * factor))
    print(f"縮放後的token數量為:{token}")
    total_token = token * math.ceil(duration / 2)
    print(f"總Token數為:{total_token}")
    return total_token
if __name__ == "__main__":
    total_token = token_calculate(image_path="xxx/test.jpg", duration=10)

限流

模型限流規則請參見限流

錯誤碼

如果模型調用失敗並返回報錯資訊,請參見錯誤資訊進行解決。

音色列表

使用時將請求參數voice設定為如下表格的“voice參數”列對應的值:

qwen3-omni-flash-realtime-2025-12-01模型

音色名

voice參數

音色效果

描述

支援的語種

芊悅

Cherry

陽光積極、親切自然小姐姐

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

蘇瑤

Serena

溫柔小姐姐

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

晨煦

Ethan

標準普通話,帶部分北方口音。陽光、溫暖、活力、朝氣

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

千雪

Chelsie

二次元虛擬女友

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

茉兔

Momo

撒嬌搞怪,逗你開心

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

十三

Vivian

拽拽的、可愛的小暴躁

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

月白

Moon

率性帥氣的月白

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

四月

Maia

知性與溫柔的碰撞

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

Kai

耳朵的一場SPA

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

不吃魚

Nofish

不會翹舌音的設計師

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

萌寶

Bella

喝酒不打醉拳的小蘿莉

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

詹妮弗

Jennifer

品牌級、電影質感般美語女聲

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

甜茶

Ryan

節奏拉滿,戲感炸裂,真實與張力共舞

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

卡捷琳娜

Katerina

禦姐音色,韻律回味十足

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

艾登

Aiden

精通廚藝的美語大男孩

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

滄明子

Eldric Sage

沉穩睿智的老者,滄桑如松卻心明如鏡

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

乖小妹

Mia

溫順如春水,乖巧如初雪

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

沙小彌

Mochi

聰明伶俐的小大人,童真未泯卻早慧如禪

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

燕錚鶯

Bellona

聲音洪亮,吐字清晰,人物鮮活,聽得人熱血沸騰;

金戈鐵馬入夢來,字正腔圓間盡顯千面人聲的江湖

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

田叔

Vincent

一口獨特的沙啞煙嗓,一開口便道盡了千軍萬馬與江湖豪情

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

萌小姬

Bunny

“萌屬性”爆棚的小蘿莉

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

阿聞

Neil

平直的基準語調,字正腔圓的咬字發音,這就是最專業的新聞主持人

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

墨講師

Elias

既保持學科嚴謹性,又通過敘事技巧將複雜知識轉化為可消化的認知模組

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

徐大爺

Arthur

被歲月和旱煙浸泡過的質樸嗓音,不疾不徐地搖開了滿村的奇聞異事

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

鄰家妹妹

Nini

糯米糍一樣又軟又黏的嗓音,那一聲聲拉長了的“哥哥”,甜得能把人的骨頭都叫酥了

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

詭婆婆

Ebona

她的低語像一把生鏽的鑰匙,緩慢轉動你內心最深處的幽暗角落——那裡藏著所有你不敢承認的童年陰影與未知恐懼

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

小婉

Seren

溫和舒緩的聲線,助你更快地進入睡眠,晚安,好夢

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

頑屁小孩

Pip

調皮搗蛋卻充滿童真的他來了,這是你記憶中的小新嗎

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

少女阿月

Stella

平時是甜到發膩的迷糊少女音,但在喊出“代表月亮消滅你”時,瞬間充滿不容置疑的愛與正義

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

博德加

Bodega

熱情的西班牙大叔

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

索尼莎

Sonrisa

熱情開朗的拉美大姐

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

阿列克

Alek

一開口,是戰鬥民族的冷,也是毛呢大衣下的暖

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

多爾切

Dolce

慵懶的意大利大叔

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

素熙

Sohee

溫柔開朗,情緒豐富的韓國歐尼

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

小野杏

Ono Anna

鬼靈精怪的青梅竹馬

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

萊恩

Lenn

理性是底色,叛逆藏在細節裡——穿西裝也聽後龐克的德國青年

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

埃米爾安

Emilien

浪漫的法國大哥哥

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

安德雷

Andre

聲音磁性,自然舒服、沉穩男生

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

拉迪奧·戈爾

Radio Gol

足球詩人Rádio Gol!今天我要用名字為你們解說足球。

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

上海-阿珍

Jada

風風火火的滬上阿姐

中文(上海話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

北京-曉東

Dylan

北京胡同裡長大的少年

中文(北京話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

南京-老李

Li

耐心的瑜伽老師

中文(南京話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

陝西-秦川

Marcus

面寬話短,心實聲沉——老陝的味道

中文(陝西話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

閩南-阿傑

Roy

詼諧直爽、市井活潑的台灣哥仔形象

中文(閩南語)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

天津-李彼得

Peter

天津相聲,專業捧哏

中文(天津話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

四川-晴兒

Sunny

甜到你心裡的川妹子

中文(四川話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

四川-程川

Eric

一個跳脫市井的四川成都男子

中文(四川話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

粵語-阿強

Rocky

幽默風趣的阿強,線上陪聊

中文(粵語)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

粵語-阿清

Kiki

甜美的港妹閨蜜

中文(粵語)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

qwen3-omni-flash-realtime、qwen3-omni-flash-realtime-2025-09-15模型

音色名

voice參數

音色效果

描述

支援的語種

芊悅

Cherry

陽光積極、親切自然小姐姐

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

晨煦

Ethan

標準普通話,帶部分北方口音。陽光、溫暖、活力、朝氣

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

不吃魚

Nofish

不會翹舌音的設計師

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

詹妮弗

Jennifer

品牌級、電影質感般美語女聲

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

甜茶

Ryan

節奏拉滿,戲感炸裂,真實與張力共舞

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

卡捷琳娜

Katerina

禦姐音色,韻律回味十足

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

墨講師

Elias

既保持學科嚴謹性,又通過敘事技巧將複雜知識轉化為可消化的認知模組

中文、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

上海-阿珍

Jada

風風火火的滬上阿姐

中文(上海話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

北京-曉東

Dylan

北京胡同裡長大的少年

中文(北京話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

四川-晴兒

Sunny

甜到你心裡的川妹子

中文(四川話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

南京-老李

Li

耐心的瑜伽老師

中文(南京話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

陝西-秦川

Marcus

面寬話短,心實聲沉——老陝的味道

中文(陝西話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

閩南-阿傑

Roy

詼諧直爽、市井活潑的台灣哥仔形象

中文(閩南語)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

天津-李彼得

Peter

天津相聲,專業捧哏

中文(天津話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

粵語-阿強

Rocky

幽默風趣的阿強,線上陪聊

中文(粵語)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

粵語-阿清

Kiki

甜美的港妹閨蜜

中文(粵語)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

四川-程川

Eric

一個跳脫市井的四川成都男子

中文(四川話)、英語、法語、德語、俄語、意大利語、西班牙語、葡萄牙語、日語、韓語

Qwen-Omni-Turbo-Realtime模型

音色名

voice參數

音色效果

描述

支援的語種

芊悅

Cherry

陽光積極、親切自然小姐姐

中文、英語

蘇瑤

Serena

溫柔小姐姐

中文、英語

晨煦

Ethan

標準普通話,帶部分北方口音。陽光、溫暖、活力、朝氣

中文、英語

千雪

Chelsie

二次元虛擬女友

中文、英語