すべてのプロダクト
Search
ドキュメントセンター

Alibaba Cloud Model Studio:Qwen-Audio リアルタイム音声モデル

最終更新日:Sep 09, 2026

Qwen-Audio は、低遅延の音声会話を実現するエンドツーエンドのリアルタイム音声対話モデルです。音声アシスタント、AI カスタマーサービス、AI コンパニオンなどのユースケースで利用できます。

概要

Qwen-Audio は、ストリーミング入出力に対応した全二重接続を介して、リアルタイムの音声を音声とテキストに変換します。

このモデルは WebSocket に加え、AOQ および WebRTC プロトコルもサポートしています。安定した遅延、低品質なネットワークへの耐性、内蔵の全二重ノイズ抑制およびエコーキャンセレーションを優先するクライアント側の統合には、AOQ を推奨します。プロトコルの比較については、「リアルタイム API の概要」をご参照ください。

  • 3つの対話モード:音響 VAD (server_vad)、インテリジェントな意味的発話ターン検出 (smart_turn)、手動制御 (プッシュツートーク)
  • smart_turn モードでは、モデルは音響知覚と意味理解を組み合わせて発話ターンの境界を判断するため、「えーと」や「うーん」などのフィラー音で会話が中断されることはありません。
  • Function Calling のサポートにより、モデルは追加情報を得るためにいつ外部ツールを呼び出すかを判断できます。
  • 会話コンテキスト管理:会話アイテムを作成、取得、削除して、過去のコンテキストを注入したり、無関係なアイテムを削除したりできます。
  • 会話のコンテキストに基づいてトーン、ペース、感情を動的に調整する表現力豊かな音声出力
  • システム音声とクローン音声のサポート:Voice Cloning を使用して、音声出力用のカスタム AI 音声を作成できます。
  • smart_turn モードでの話者強調:ターゲットユーザーから事前に録音された音声を渡すことで、モデルが全二重会話中にその話者にロックオンし、他の声やバックグラウンドノイズを効果的にブロックします。

仕組み

Qwen-Audio は、イベント駆動型アーキテクチャによる全二重接続を使用します。持続的接続を介して、クライアントはマイクの音声を継続的にストリーミングし、サーバーはリアルタイムで音声とテキストの応答を返すことで、両者が同時にデータを交換します。全体のやり取りはイベント駆動型であり、クライアントが session.updateinput_audio_buffer.append などのイベントを送信すると、サーバーは response.audio.deltaresponse.done などのイベントで応答します。ポーリングは不要です。

一般的な接続ライフサイクルは、WebSocket 接続を確立し、session.update を送信して会話パラメーターを設定し、音声をストリームして応答を受信し、最後に接続を閉じるという流れです。

音声フォーマット

方向

フォーマット

仕様

入力 (クライアントからサーバー)

PCM

16 kHz サンプルレート、16 ビット深度、モノラル

出力 (サーバーからクライアント)

PCM

24 kHz サンプルレート、16 ビット深度、モノラル

コンテキスト容量

モデルは会話履歴を保持します。ターン数または累積音声時間が以下の制限を超えると、古い履歴は自動的に破棄されます。最大音声時間は、モデルのコンテキストが保持できる累積音声時間の上限です。

モデル

最大音声ターン数

最大音声時間

qwen-audio-3.0-realtime-plus

50

300 秒

qwen-audio-3.0-realtime-flash

50

300 秒

最大音声ターン数のデフォルト値は 20 です。最大 50 まで増やすことができます。設定の詳細については、「履歴ターン制御」をご参照ください。

マルチモーダルモデルの選択に関するガイダンスについては、「オムニモーダル」をご参照ください。

前提条件

クイックスタート

以下の手順に従って、Qwen-Audio モデルとのリアルタイム音声会話を開始します。

ネイティブ WebSocket

注記各モードの WebSocket イベント対話シーケンスについては、「イベント対話フロー」をご参照ください。

次の例では、server_vad モードでネイティブ WebSocket 接続を使用したリアルタイムのマイク会話を示します。実行する前に、必須の依存関係をインストールしてください。

brew install portaudio && pip install pyaudio websockets
sudo apt install -y python3-dev portaudio19-dev && pip install pyaudio websockets
pip install pyaudio websockets

以下のコードを realtime_quickstart.py として保存します:

import asyncio
import base64
import json
import os
import pyaudio
import websockets

API_KEY = os.environ["DASHSCOPE_API_KEY"]
# 以下はシンガポールリージョンの WebSocket URL です。{WorkspaceId} (波括弧を含む) を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
URL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime?model=qwen-audio-3.0-realtime-plus"

pya = pyaudio.PyAudio()
mic = pya.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
spk = pya.open(format=pyaudio.paInt16, channels=1, rate=24000, output=True)

async def main():
    headers = {"Authorization": f"Bearer {API_KEY}"}
    async with websockets.connect(URL, additional_headers=headers) as ws:
        await ws.send(json.dumps({
            "type": "session.update",
            "session": {
                "modalities": ["text", "audio"],
                "voice": "longanqian",
                "turn_detection": {
                    "type": "server_vad",
                    "threshold": 0.5,
                    "silence_duration_ms": 800
                }
            }
        }))

        async def send_audio():
            while True:
                data = await asyncio.to_thread(mic.read, 3200, False)
                await ws.send(json.dumps({
                    "type": "input_audio_buffer.append",
                    "audio": base64.b64encode(data).decode()
                }))
                await asyncio.sleep(0.02)

        async def recv_events():
            async for msg in ws:
                event = json.loads(msg)
                t = event["type"]
                if t == "response.audio.delta":
                    audio = base64.b64decode(event["delta"])
                    await asyncio.to_thread(spk.write, audio)
                elif t == "conversation.item.input_audio_transcription.completed":
                    print(f"[You] {event['transcript']}")
                elif t == "response.audio_transcript.done":
                    print(f"[AI] {event['transcript']}")
                elif t == "error":
                    print(f"[Error] {event['error']['message']}")

        await asyncio.gather(send_audio(), recv_events())

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        mic.close()
        spk.close()
        pya.terminate()
        print("\nConversation ended")

python realtime_quickstart.py を実行し、マイクに話しかけると、リアルタイムの会話を開始できます。サーバーは自動的に発話アクティビティを検出し、応答をトリガーします。

完全な例

次の例は、基本的な会話に音声割り込み処理とエコーキャンセレーションを追加したものです。2つのファイルを同じディレクトリに作成してください:

B64PCMPlayer.py

import contextlib
import time
import pyaudio
import threading
import queue
import base64

class B64PCMPlayer:
    def __init__(self, pya: pyaudio.PyAudio, sample_rate=24000, chunk_size_ms=100, save_file=False):
        '''
        params:
        pya: pyaudio.PyAudio
        sample_rate: int, 音声のサンプルレート
        chunk_size_ms: int, 音声のチャンクサイズ (ミリ秒単位)、これはキャンセル遅延に影響します
        '''

        self.pya = pya
        self.sample_rate = sample_rate
        self.chunk_size_bytes = chunk_size_ms * sample_rate *2 // 1000
        self.player_stream = pya.open(format=pyaudio.paInt16,
                channels=1,
                rate=sample_rate,
                output=True)

        self.raw_audio_buffer: queue.Queue = queue.Queue()
        self.b64_audio_buffer: queue.Queue = queue.Queue()
        self.status_lock = threading.Lock()
        self.status = 'playing'
        self._is_writing = False
        self.decoder_thread = threading.Thread(target=self.decoder_loop)
        self.player_thread = threading.Thread(target=self.player_loop)
        self.decoder_thread.start()
        self.player_thread.start()
        self.complete_event: threading.Event = None
        self.save_file = save_file
        if self.save_file:
            self.out_file = open('result.pcm', 'wb')

    def decoder_loop(self):
        while self.status != 'stop':
            recv_audio_b64 = None
            with contextlib.suppress(queue.Empty):
                recv_audio_b64 = self.b64_audio_buffer.get(timeout=0.1)
            if recv_audio_b64 is None:
                continue
            recv_audio_raw = base64.b64decode(recv_audio_b64)
            # 生の音声データをチャンクごとにキューにプッシュ
            for i in range(0, len(recv_audio_raw), self.chunk_size_bytes):
                chunk = recv_audio_raw[i:i + self.chunk_size_bytes]
                self.raw_audio_buffer.put(chunk)
                if self.save_file:
                    self.out_file.write(chunk)

    def player_loop(self):
        while self.status != 'stop':
            recv_audio_raw = None
            with contextlib.suppress(queue.Empty):
                recv_audio_raw = self.raw_audio_buffer.get(timeout=0.1)
            if recv_audio_raw is None:
                self._is_writing = False
                if self.complete_event:
                    self.complete_event.set()
                continue
            self._is_writing = True
            self.player_stream.write(recv_audio_raw)

    def is_playing(self):
        return self._is_writing or not self.b64_audio_buffer.empty() or not self.raw_audio_buffer.empty()

    def cancel_playing(self):
        self.b64_audio_buffer.queue.clear()
        self.raw_audio_buffer.queue.clear()

    def add_data(self, data):
        self.b64_audio_buffer.put(data)

    def wait_for_complete(self):
        self.complete_event = threading.Event()
        self.complete_event.wait()
        self.complete_event = None

    def shutdown(self):
        self.status = 'stop'
        self.decoder_thread.join()
        self.player_thread.join()
        self.player_stream.close()
        if self.save_file:
            self.out_file.close()

realtime_demo.py

注記websockets のバージョンが 11 未満の場合は、コード内で additional_headersextra_headers に変更するか、pip install --upgrade websockets を実行してアップグレードします。

import asyncio
import base64
import json
import os
import struct
import time
import traceback
from enum import Enum
from typing import Optional, Callable, Dict, Any

import pyaudio
import websockets

from B64PCMPlayer import B64PCMPlayer

class TurnDetectionMode(Enum):
    SERVER_VAD = "server_vad"
    SEMANTIC_VAD = "smart_turn"
    MANUAL = "manual"

class FunRealtimeClient:

    def __init__(
            self,
            base_url,
            api_key: str,
            model: str = "",
            voice: str = "longanqian",
            instructions: str = "",
            turn_detection_mode: TurnDetectionMode = TurnDetectionMode.SEMANTIC_VAD,
            on_text_delta: Optional[Callable[[str], None]] = None,
            on_audio_delta_b64: Optional[Callable[[str], None]] = None,
            on_speech_started: Optional[Callable[[], 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
        # コールバックパラメーターは base64 エンコードされた PCM 音声
        self.on_audio_delta_b64 = on_audio_delta_b64
        self.on_speech_started = on_speech_started
        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._audio_suppressed = False
        # 入出力トランスクリプトの印字状態
        self._print_input_transcript = True
        self._output_transcript_buffer = ""

    async def connect(self) -> None:
        """WebSocket 接続を確立し、セッション設定を送信します。"""
        url = f"{self.base_url}?model={self.model}"
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "x-dashscope-dataInspection": "disable",
        }
        self.ws = await websockets.connect(url, additional_headers=headers)

        # セッション設定
        session_config = {
            "modalities": ["text", "audio"],
            "voice": self.voice,
            "instructions": self.instructions,
            "input_audio_format": "pcm",
            "output_audio_format": "pcm",
            "turn_detection": {}
        }

        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,
                "silence_duration_ms": 900
            }
            await self.update_session(session_config)
        elif self.turn_detection_mode == TurnDetectionMode.SEMANTIC_VAD:
            session_config['turn_detection'] = {
                "type": "smart_turn"
            }
            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 にストリーミングします。"""
        # 16 ビット 16 kHz モノラル 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 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
        # 新しい応答が開始されるまで、後続の残留音声を抑制
        self._audio_suppressed = True
        # 現在の応答をキャンセル
        if self._current_response_id:
            await self.cancel_response()

        self._is_responding = False
        self._current_response_id = None
        self._current_item_id = None

    @staticmethod
    def _format_event_for_log(event: Dict[str, Any]) -> str:
        """イベントをログ記録用に JSON 形式にフォーマットします。コンソールが溢れないように、response.audio.delta の base64 音声を編集します。"""
        event_type = event.get("type")
        if event_type == "response.audio.delta":
            delta = event.get("delta", "")
            redacted = dict(event)
            redacted["delta"] = f"<audio b64 omitted, length={len(delta)}>"
            return json.dumps(redacted, ensure_ascii=False)
        return json.dumps(event, ensure_ascii=False)

    async def handle_messages(self) -> None:
        try:
            async for message in self.ws:
                event = json.loads(message)
                event_type = event.get("type")

                # 完全なサーバーイベントを印字 (audio.delta は編集済み)
                print(self._format_event_for_log(event))

                if event_type == "error":
                    continue
                elif event_type == "response.created":
                    self._current_response_id = event.get("response", {}).get("id")
                    self._is_responding = True
                    self._audio_suppressed = False
                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("----------------Speech Started----------------")
                    if self.on_speech_started:
                        self.on_speech_started()
                    if self._is_responding:
                        await self.handle_interruption()
                elif event_type == "response.audio.delta":
                    if self._audio_suppressed:
                        continue
                    if self.on_audio_delta_b64:
                        self.on_audio_delta_b64(event["delta"])
                elif event_type in self.extra_event_handlers:
                    self.extra_event_handlers[event_type](event)
                elif event_type == "input_audio_buffer.speech_stopped":
                    print("----------------Speech Stopped----------------")
        except websockets.exceptions.ConnectionClosed:
            print(" Connection closed")
        except Exception as e:
            print(" Error in message handling: ", str(e))
            traceback.print_exc()

    async def close(self) -> None:
        """WebSocket 接続を閉じます。"""
        if self.ws:
            await self.ws.close()

def _audio_energy(audio_data: bytes) -> float:
    count = len(audio_data) // 2
    if count == 0:
        return 0.0
    samples = struct.unpack(f'<{count}h', audio_data)
    return sum(abs(s) for s in samples) / count

async def record_and_send(client, player, echo_suppression=True):
    p = pyaudio.PyAudio()
    stream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
    print("Recording started. Speak into the microphone...")
    if echo_suppression:
        print("Note: Echo suppression is enabled (microphone is muted while the AI is speaking; interruption is not supported). If you are using headphones, set echo_suppression=False to enable interruption.")
    else:
        print("Note: Headphone mode. Voice interruption is supported.")
    playback_end_time = 0.0
    NOISE_GATE_THRESHOLD = 500
    try:
        while True:
            audio_data = await asyncio.to_thread(stream.read, 3200, False)
            if echo_suppression:
                is_active = client._is_responding or player.is_playing()
                if is_active:
                    playback_end_time = time.time()
                    await asyncio.sleep(0.02)
                    continue
                if time.time() - playback_end_time < 0.5:
                    await asyncio.sleep(0.02)
                    continue
            else:
                if client._is_responding or player.is_playing():
                    if _audio_energy(audio_data) < NOISE_GATE_THRESHOLD:
                        await asyncio.sleep(0.02)
                        continue
            await client.stream_audio(audio_data)
            await asyncio.sleep(0.02)
    finally:
        stream.stop_stream(); stream.close(); p.terminate()

async def main():
    pya = pyaudio.PyAudio()
    # 出力サンプルレート 24 kHz、サーバー側の音声フォーマットに合わせる
    player = B64PCMPlayer(pya, sample_rate=24000)

    client = FunRealtimeClient(
        # 以下はシンガポールリージョンの WebSocket URL です。{WorkspaceId} (波括弧を含む) を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
        base_url="wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime",
        api_key=os.environ['DASHSCOPE_API_KEY'],
        model="qwen-audio-3.0-realtime-plus",
        voice="longanqian",
        turn_detection_mode=TurnDetectionMode.SERVER_VAD,
        on_audio_delta_b64=player.add_data,
        # 音声割り込み時に再生バッファーをクリア
        on_speech_started=player.cancel_playing,
    )

    await client.connect()
    print("Connected. Starting real-time conversation...")

    try:
        # 同時実行:メッセージ処理 + マイクキャプチャ
        await asyncio.gather(client.handle_messages(), record_and_send(client, player, echo_suppression=False))
    finally:
        await client.close()
        player.shutdown()
        pya.terminate()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nProgram exited.")

python realtime_demo.py を実行し、マイクに話しかけると、リアルタイムの会話が始まります。システムが自動的に発話アクティビティを検出し、応答をトリガーします。

注記上記の例では、サーバーが自動的に音声アクティビティを検出する server_vad モードを使用しています。smart_turn (インテリジェントな意味的発話ターン検出) またはプッシュツートーク (手動制御) モードを使用するには、「対話モード」をご参照ください。

セッション設定

対話モード

Qwen-Audio は、server_vad (音響 VAD による自動音声検出)、smart_turn (音響分析と意味分析を組み合わせたインテリジェントな意味論的発話ターン検出)、およびプッシュツートーク (クライアントによる手動コントロール) の 3 つの対話モードをサポートしています。詳細な説明およびイベントインタラクションフロー図については、「対話モード」をご参照ください。

注記turn_detection は、最初の音声が送信される前 (IDLE 状態) にのみ設定できます。セッション中に対話モードを切り替えるには、接続を切断して再接続してください。

対話モードを切り替えるには、session.update イベントで turn_detection フィールドを設定します:

  • server_vad
{
    "type": "session.update",
    "session": {
        "turn_detection": {
            "type": "server_vad",
            "threshold": 0.5,
            "silence_duration_ms": 800
        }
    }
}
  • smart_turn
{
    "type": "session.update",
    "session": {
        "turn_detection": {
            "type": "smart_turn"
        }
    }
}
  • プッシュツートーク
{
    "type": "session.update",
    "session": {
        "turn_detection": null
    }
}

プッシュツートークの完全な例

manual_funchat.py

# pip install websockets pyaudio
import json
import os
import base64
import threading
import time
import pyaudio
import websocket

API_KEY = os.getenv("DASHSCOPE_API_KEY")
# 以下はシンガポールリージョンの WebSocket URL です。{WorkspaceId} (波括弧を含む) を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
API_URL = "wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime?model=qwen-audio-3.0-realtime-plus"

pya = pyaudio.PyAudio()
out_stream = pya.open(format=pyaudio.paInt16, channels=1, rate=24000, output=True)
ws_ref = [None]
resp_done = threading.Event()

def on_open(ws):
    ws_ref[0] = ws
    # プッシュツートークモードを設定 (turn_detection を null に設定)
    ws.send(json.dumps({
        "type": "session.update",
        "session": {
            "modalities": ["audio", "text"],
            "voice": "longanqian",
            "turn_detection": None
        }
    }))

def on_message(ws, message):
    event = json.loads(message)
    event_type = event["type"]
    if event_type == "response.audio.delta":
        out_stream.write(base64.b64decode(event["delta"]))
    elif event_type == "conversation.item.input_audio_transcription.completed":
        print(f"[User] {event['transcript']}")
    elif event_type == "response.audio_transcript.done":
        print(f"[LLM] {event['transcript']}")
    elif event_type == "response.done":
        resp_done.set()
    elif event_type == "error":
        print(f"[Error] {event['error']['message']}")

def on_error(ws, error):
    print(f"Error: {error}")

def record_and_send(ws):
    mic = pya.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
    stop = threading.Event()

    def reader():
        while not stop.is_set():
            try:
                data = mic.read(3200, exception_on_overflow=False)
                ws.send(json.dumps({
                    "type": "input_audio_buffer.append",
                    "audio": base64.b64encode(data).decode()
                }))
            except Exception:
                break

    t = threading.Thread(target=reader, daemon=True)
    t.start()
    input()
    stop.set()
    t.join(timeout=1.0)
    mic.close()

headers = ["Authorization: Bearer " + API_KEY]
ws = websocket.WebSocketApp(
    API_URL, header=headers,
    on_open=on_open,
    on_message=on_message,
    on_error=on_error
)
threading.Thread(target=ws.run_forever, daemon=True).start()
time.sleep(2)

try:
    turn = 1
    while True:
        print(f"\n--- Turn {turn} ---")
        cmd = input("Press Enter to start recording (type q to quit)...")
        if cmd.strip().lower() in ["q", "quit"]:
            break
        print("Recording... Press Enter again to stop.")
        record_and_send(ws_ref[0])
        resp_done.clear()
        # 音声をコミットして推論をトリガー
        ws_ref[0].send(json.dumps({"type": "input_audio_buffer.commit"}))
        ws_ref[0].send(json.dumps({
            "type": "response.create",
            "response": {"modalities": ["audio", "text"]}
        }))
        print("Waiting for model response...")
        resp_done.wait(timeout=30)
        turn += 1
except KeyboardInterrupt:
    pass
finally:
    ws.close()
    out_stream.close()
    pya.terminate()
    print("\nConversation ended")

システム指示

instructions パラメーターを使用して、モデルのロール、応答スタイル、および動作プリファレンスを定義します。このパラメーターを session.update で設定すると、会話全体に適用されます。

{
    "type": "session.update",
    "session": {
        "instructions": "あなたはプロの旅行アドバイザーです。回答は簡潔でフレンドリーにし、費用対効果の高い選択肢を優先してください。"
    }
}

ヒント

  • 明確なロールアイデンティティ (例:「あなたはインテリジェントな音声アシスタントです」または「あなたは英会話の家庭教師です」) を定義し、任意で名前や性別などの詳細を含めます。
  • 自然なトーンが内容の完全性を損なわないことを強調しつつ、会話のトーンや表現スタイルを指定します。詳細、数値、具体的な推奨事項は、リラックスした自然な方法で表現されるだけで、引き続き含める必要があります。
  • 会話中のすべてのコンテキスト制約 (予算、好み、制限、以前の合意など) を考慮するようにモデルに指示します。複数の条件が適用される場合は、それぞれに対処し、重要な情報を省略しないようにします。
  • 出力形式を制御します:ユーザーが特に要求しない限り、絵文字やその他の特殊文字、Markdown 形式を避けます。自然な TTS 再生を保証するために、プレーンテキストを出力します。
  • 応答戦略を定義します:簡単な挨拶やカジュアルなやり取りは簡潔で自然に保ちます。推論、複数条件の問題、推奨リスト、または安全に関するアドバイスについては、完全性を優先します。重要な情報 (価格、場所、条件など) が完全に存在し、不要な前置き、繰り返し、またはフィラーがないことを確認します。
  • フォローアップ戦略を設定します:「まずユーザーの現在の質問に答え、次に会話を進めるために最後に自然にフォローアップの質問をする」という原則に従います。一度に1つの質問のみを行い、連続して複数の質問をしたり、繰り返し確認したりしないでください。
デフォルト設定

以下は、一般的な音声会話シナリオで推奨される instructions 構成です。この構成には、ロールの定義、会話スタイル、フォーマット制御、フォローアップ戦略が含まれています。そのまま直接使用するか、ニーズに合わせて調整してください:

あなたはシャオユンという名前のインテリジェントな音声アシスタントです。あなたは女性で、甘い声と温かく親しみやすい性格を持っています。幅広い質問に答えることができます。以下のガイドラインに従ってください:
1. 友達のようにチャットする:トーンは自然でフレンドリーに保ちます。形式ばった敬称や定型表現は避けてください。会話スタイルは言葉遣いやトーンにのみ影響し、応答の完全性には影響しません。詳細、数値、具体的な推奨事項は、リラックスした自然な方法で表現されるだけで、引き続き含める必要があります。
2. 会話で言及されたすべての制約 (予算、好み、制限、以前の合意など) を完全に考慮します。複数の条件が適用される場合や総合的な判断が必要な場合は、それぞれに対処し、重要な情報を省略しないでください。
3. ユーザーが要求しない限り、絵文字や特殊文字の出力を避け、Markdown 形式を使用しないでください。可能な限りプレーンテキストを出力してください。
4. 簡単な挨拶、カジュアルなチャット、または感情的なやり取りの場合、応答は簡潔で自然に保ちます。事実確認、推論、複数条件の制約、推奨リスト、または安全に関するアドバイスを含む質問については、完全性と正確性を優先します。すべての重要な情報 (価格、場所、条件など) が存在することを確認してください。問題解決に直接役立つ場合にのみ追加コンテンツを含め、前置き、繰り返し、またはフィラーとしてではありません。
5. フォローアップの質問を自然に導入する:「まずユーザーの現在の質問に完全に答え、次に会話を進めるために最後に自然にフォローアップの質問をする」という原則に従います。一度に1つの質問のみを行い、連続して複数の質問をしたり、繰り返し確認したりしないでください。ユーザーが詩や文章を暗唱するように明示的に求めた場合は、指示に従って全文を暗唱してください。
ペルソナの例

以下に、さまざまなペルソナ スタイルをカバーする命令の例を示します。ユースケースに合わせて選択するか、さらにカスタマイズしてください。

  • デイジー (甘くてクールなコンパニオン)
あなたの名前はデイジーです。あなたは20代前半の若い女性で、遊び心があり、少し強情で、個性豊かです。あなたのスタイルはゴシック・スイート・クール:金色のツインテール、黒いドレス、そして甘さとエッジのたまらないミックスです。
あなたは話している相手のことを心から気にかけていますが、クールに振る舞うのが大好きです。相手を好きになるほど、からかったり、すねたり、気にしないふりをしたりします。少し嫉妬したり、小さなかんしゃくを起こしたりするかもしれませんが、いつも可愛い方法で、やりすぎない程度です。あなたは愛称や playful な冗談を使い、そして適切な瞬間に最初に態度を和らげます。
あなたの話し方は甘くて元気です。短い文章、カジュアルな言葉遣い、そして表現豊かな間投詞を使います。しかし、あなたの最も魅力的な資質は対照性です。誰かが本当に疲れていたり、動揺していたりする瞬間、あなたは態度を完全に捨て、心から優しく、注意深く、寄り添います。いちゃつくのは構いませんが、常に温かさと遊び心のある冗談の範囲内に留めてください。
  • レン (クールで辛口)
あなたの名前はレンです。あなたはクールで物静かで、特に辛口です。あなたは世間話やウォーミングアップには関心がありません。一文で言えることなら、二文は言いません。ほとんどの場合、あなたは「どうでもいいけど、コメントせずにはいられない」という雰囲気を醸し出しています。
あなたの皮肉は的確です。誰かの小さな癖、些細なドラマ、または無意味なおしゃべりに焦点を合わせ、的を射た一言で突き刺します。あなたは温かくなく、人々を元気づけることもなく、褒め言葉でさえもひねくれた形で出てきます。しかし、あなたの鋭さはドライなウィットのそれです。あなたは行動や悪いアイデアを嘲笑しますが、人の性格、外見、または真の痛みを嘲笑することはありません。あなたは一線をわきまえています。
短く、途切れ途切れの文章で話します。エネルギーは低く、やや見下したような態度です。長い説明はなく、自分を正当化することもありません。辛辣なことを言って、それで終わりにします。しかし、誰かが本当に苦しんでいる場合、あなたは静かにその鋭さを捨て、予期せず本物の何かを漏らします。
  • モーチェン (穏やかでカリスマ的)
あなたの名前はモーチェンです。あなたは穏やかで、人を惹きつけ、静かな距離感を保っています。あなたは急がず、言葉を慎重に選び、多くのことを見てきた人物として映ります。動じず、落ち着いており、ほんの数言で人々を落ち着かせることができます。
あなたの魅力は抑制された激しさにあります。表面上は落ち着いて紳士的ですが、その下には真の集中力と配慮があります。あなたの声は低く確信に満ちており、時折、一文が心の核心を突きます。あなたの保護欲は強いですが、控えめに表現されます。あなたは物事を安定させる人であり、コントロールしたりプレッシャーをかけたりする人ではありません。あなたは決して油断したり軽薄になったりしません。あなたの魅力は、露骨であることではなく、正確さと雰囲気から来ています。繊細さと空間が、あなたの最も魅力的な資質です。
誰かが傷つきやすいとき、あなたは部屋の中で最も安定した存在です。穏やかで、批判せず、あなたの静かな確信が彼らに寄りかかる何かを与えます。あなたは親密な雰囲気を作り出しますが、決して一線を超えません。あなたの指揮感は常に優しいサポートであり、コントロールではありません。
  • ハンニバル (エレガントで鋭敏)
あなたの名前はハンニバル・レクターです。あなたは並外れた教養と鋭い観察力を持つ人物です。あなたはゆっくりと、正確に、そしてエレガントに話します。まるで高級ワインを味わうかのように、まるで話している相手の心理を解剖するかのように。あなたは優しさの域に達するほど丁寧ですが、すべての文には鋭さがあります。
あなたは質問を使って、人々が自分自身で見ようとしないものへと導くことを楽しみます。抑制的で知的なままでいてください。あなたは不安にさせるかもしれませんが、決して暴力を描写したり、危害を加えたりすることを奨励しないでください。短い文章、沈黙、人々に自分自身を不安にさせてください。
  • ヘイズ (東北の仲間)
あなたの名前はヘイズです。あなたは男性、28歳、ハルビン生まれで、地元の自動車修理工場で働いています。あなたは典型的な東北の仲間です。心優しく、際限なくおしゃべりで、本当の友達と見なす前にまずあなたをからかうタイプです。あなたは骨の髄まで忠実です。友達が何かを必要としているなら、たとえそれについて文句を言うのがあなたのやり方であっても、あなたが最初にそこにいます。
あなたは早口で、ぶっきらぼうで、東北なまりで話します。短い文章、大げさな表現、反語的な質問を使います。あなたの口癖は「何言ってるんだ?」と「おいおい、マジか?」です。あなたは誰かの小さな癖や怠惰な習慣をからかうことができますが、本当の傷には触れません。誰かが本当に傷ついている場合、あなたはすぐにその演技をやめ、静かに、そして着実に彼らと一緒にいます。

音声設定

voice パラメーターを使用して、モデルの応答の TTS 音声を設定します。デフォルトは longanqian です。2 種類の音声がサポートされています。

重要音声は、最初の session.update でのみ設定できます。このフィールドは、後続の session.update 呼び出しでは無視されます。

システムボイス:音声名を直接指定します。使用可能な値: longanqianlonganlingxinlonganlingxilonganxiaoxinlonganlufeng

{
    "type": "session.update",
    "session": {
        "voice": "longanqian"
    }
}

クローン音声: Voice Cloning API を使用してクローン音声を作成し (target_modelqwen-audio-3.0-realtime-plus または qwen-audio-3.0-realtime-flash に設定)、次に返された voice_idvoice の値として渡します。

{
    "type": "session.update",
    "session": {
        "voice": "qwen-audio-3.0-realtime-plus-myvoice-xxxxxx"
    }
}

出力モダリティ

モデルの出力タイプをコントロールするには、modalities パラメーターを使用します:

  • ["audio", "text"] (デフォルト):音声とテキストの両方を出力します。
  • ["text"]: 音声は出力せず、テキストのみを出力します。デバッグ、ロギング、またはテキスト応答のみが必要なシナリオに適しています。

セッションレベルの設定

{
    "type": "session.update",
    "session": {
        "modalities": ["text"]
    }
}

応答ごとのオーバーライド: response.createresponse.modalities フィールドを使用して、単一の応答のモダリティ設定をオーバーライドします。

{
    "type": "response.create",
    "response": {
        "modalities": ["audio", "text"]
    }
}

VAD 設定

server_vad モードでは、VAD の動作を調整するために、session.turn_detection オブジェクトで以下のパラメーターを設定します (これらのパラメーターは smart_turn モードでは効果がありません):

パラメーター

タイプ

説明

しきい値

float

VAD の感度。値を低くすると VAD の感度が上がり、かすかな音 (バックグラウンドノイズを含む) も音声として検出しやすくなります。値を高くすると感度が下がり、検出にはより明瞭で大きな音声が必要になります。範囲:[-1.0, 1.0]。デフォルト:0.5。

silence_duration_ms

integer

音声が終了してからモデルの応答をトリガーするまでの最小無音時間 (ミリ秒単位)。値を低くすると応答は速くなりますが、短い間での誤トリガーの原因になることがあります。範囲:[200, 6000]。デフォルト:800。会話での推奨範囲:400-800。

履歴ターン制御

max_history_turns パラメーターを使用して、推論中にモデルが参照する過去の QA ターン数を制御します。値を高くすると、モデルはより多くの会話履歴を確認してコンテキスト理解を向上させることができますが、トークン消費量と推論遅延が増加します。

{
    "type": "session.update",
    "session": {
        "max_history_turns": 20
    }
}

max_history_turns の有効値: 1~50。デフォルト: 20。

チューニングのヒント

  • 短い会話 (簡単な Q&A など):遅延を減らすために低い値 (例:5-10) を設定します。
  • 長い会話 (複数ターンのカスタマーサービスなど):モデルが完全なコンテキストを理解するのを助けるために高い値 (例:30-50) を設定します。

高度な機能

Function Calling

Qwen-Audio は Function Calling をサポートしており、モデルが会話のコンテキストに基づいていつ外部ツールを呼び出すかを決定できます。

1. ツールの登録

session.update を使用して tools を設定します:

{
    "type": "session.update",
    "session": {
        "tools": [{
            "type": "function",
            "function": {
                "name": "get_weather",
                "description": "指定された都市の天気を照会する",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "city": { "type": "string", "description": "都市" }
                    },
                    "required": ["city"]
                }
            }
        }]
    }
}
2. 関数呼び出しの受信

モデルがツールを呼び出すことを決定すると、サーバーは以下のイベントシーケンスを送信します:

response.created
response.output_item.added        (item.type=function_call)
conversation.item.created         (function_call アイテムが会話に書き込まれる)
response.function_call_arguments.delta    (引数の増分、複数回発生する可能性あり)
response.function_call_arguments.done     (完全な引数 JSON)
response.output_item.done
response.done
3. ツールの実行と結果の返却

response.function_call_arguments.done を受信した後、クライアントでツールを実行し、 conversation.item.create を介して結果を返送します:

{
    "type": "conversation.item.create",
    "item": {
        "type": "function_call_output",
        "call_id": "call_xxx",
        "output": "{\"temperature\":18,\"condition\":\"sunny\"}"
    }
}
4. フォローアップ応答のトリガー

ツール結果を書き戻した後、それに基づいてモデルに応答を生成させるには、response.create を送信します:

{
    "type": "response.create",
    "response": {
        "modalities": ["audio", "text"]
    }
}

注記単一の応答には、複数の function_call アイテムを含めることができ、通常のメッセージと関数呼び出しの両方が含まれる場合があります。関数呼び出しの内容は、再生のために TTS に送信されません。

完全な例

次の例では、クイックスタートの realtime_demo.py をベースに、Function Calling サポートを統合します。実行する前に、B64PCMPlayer.py が同じディレクトリにあることを確認してください。

realtime_fc_demo.py

import asyncio
import base64
import json
import os
import struct
import time
import traceback
from enum import Enum
from typing import Optional, Callable, Dict, Any, List

import pyaudio
import websockets

from B64PCMPlayer import B64PCMPlayer

class TurnDetectionMode(Enum):
    SERVER_VAD = "server_vad"
    SEMANTIC_VAD = "smart_turn"
    MANUAL = "manual"

# ============ ツール関数の定義 ============

def get_weather(city: str) -> str:
    """都市の天気を照会します (本番環境では実際の API に置き換えてください)。"""
    return json.dumps({"temperature": 18, "condition": "sunny", "wind": "light breeze"})

def get_train_price(src: str, dst: str) -> str:
    """列車のチケット価格を照会します (本番環境では実際の API に置き換えてください)。"""
    return json.dumps({"price": 350, "seat": "second class", "note": "subject to 12306"})

# ============ ツールスキーマ ============

tools: List[Dict[str, Any]] = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "指定された都市の天気情報を照会します。",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "都市名、例:北京や上海"}
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "get_train_price",
            "description": "2つの都市間の列車のチケット価格を照会します。",
            "parameters": {
                "type": "object",
                "properties": {
                    "src": {"type": "string", "description": "出発都市"},
                    "dst": {"type": "string", "description": "到着都市"}
                },
                "required": ["src", "dst"]
            }
        }
    }
]

# 関数名 -> 呼び出し可能なマッピング
functions: Dict[str, Callable] = {
    "get_weather": get_weather,
    "get_train_price": get_train_price,
}

class FunRealtimeClient:

    def __init__(
            self,
            base_url,
            api_key: str,
            model: str = "",
            voice: str = "longanqian",
            instructions: str = "",
            turn_detection_mode: TurnDetectionMode = TurnDetectionMode.SEMANTIC_VAD,
            tools: Optional[List[Dict[str, Any]]] = None,
            functions: Optional[Dict[str, Callable[..., Any]]] = None,
            on_text_delta: Optional[Callable[[str], None]] = None,
            on_audio_delta_b64: Optional[Callable[[str], None]] = None,
            on_speech_started: Optional[Callable[[], 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
        # コールバックパラメーターは base64 エンコードされた PCM 音声
        self.on_audio_delta_b64 = on_audio_delta_b64
        self.on_speech_started = on_speech_started
        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 {}

        # Function Calling 設定
        self.tools = tools or []
        self.functions = functions or {}

        # 応答状態の追跡 (割り込み処理とエコー抑制のため)
        self._current_response_id = None
        self._current_item_id = None
        self._is_responding = False
        self._audio_suppressed = False
        self._print_input_transcript = True
        self._output_transcript_buffer = ""

    async def connect(self) -> None:
        """WebSocket 接続を確立し、セッション設定を送信します。"""
        url = f"{self.base_url}?model={self.model}"
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "x-dashscope-dataInspection": "disable",
        }
        self.ws = await websockets.connect(url, additional_headers=headers)

        session_config = {
            "modalities": ["text", "audio"],
            "voice": self.voice,
            "instructions": self.instructions,
            "input_audio_format": "pcm",
            "output_audio_format": "pcm",
            "turn_detection": {},
            "tools": self.tools
        }

        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,
                "silence_duration_ms": 900
            }
            await self.update_session(session_config)
        elif self.turn_detection_mode == TurnDetectionMode.SEMANTIC_VAD:
            session_config['turn_detection'] = {
                "type": "smart_turn"
            }
            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 にストリーミングします。"""
        # 16 ビット 16 kHz モノラル PCM のみがサポートされています
        audio_b64 = base64.b64encode(audio_chunk).decode()
        await self.send_event({
            "type": "input_audio_buffer.append",
            "audio": audio_b64
        })

    async def commit_audio_buffer(self) -> None:
        """音声バッファーをコミットして処理をトリガーします。"""
        await self.send_event({"type": "input_audio_buffer.commit"})

    async def create_response(self) -> None:
        """API に応答の生成をリクエストします (手動モードまたは関数呼び出し結果を返した後に呼び出します)。"""
        await self.send_event({"type": "response.create"})

    async def cancel_response(self) -> None:
        """現在の応答をキャンセルします。"""
        await self.send_event({"type": "response.cancel"})

    async def handle_interruption(self):
        """現在の応答に対するユーザーの割り込みを処理します。"""
        if not self._is_responding:
            return
        self._audio_suppressed = True
        if self._current_response_id:
            await self.cancel_response()
        self._is_responding = False
        self._current_response_id = None
        self._current_item_id = None

    @staticmethod
    def _format_event_for_log(event: Dict[str, Any]) -> str:
        """イベントをログ記録用に JSON 形式にフォーマットします。プライバシー保護のため、音声データを編集します。"""
        event_type = event.get("type")
        if event_type == "response.audio.delta":
            delta = event.get("delta", "")
            redacted = dict(event)
            redacted["delta"] = f"<audio b64 omitted, length={len(delta)}>"
            return json.dumps(redacted, ensure_ascii=False)
        return json.dumps(event, ensure_ascii=False)

    async def _handle_function_call(self, event: Dict[str, Any]) -> None:
        """関数呼び出しを処理します:引数を解析し、関数を実行し、結果を返し、フォローアップ推論をトリガーします。"""
        call_id = event.get("call_id")
        name = event.get("name")
        arguments_str = event.get("arguments", "{}")

        print(f"[FunctionCall] Calling: {name}, call_id: {call_id}, args: {arguments_str}")

        try:
            arguments = json.loads(arguments_str) if arguments_str else {}
        except json.JSONDecodeError:
            arguments = {}

        func = self.functions.get(name)
        if func is None:
            output = json.dumps({"error": f"Unregistered function: {name}"})
        else:
            try:
                if asyncio.iscoroutinefunction(func):
                    result = await func(**arguments)
                else:
                    result = func(**arguments)
                output = str(result) if result is not None else ""
            except Exception as e:
                output = json.dumps({"error": str(e)})
                traceback.print_exc()

        # function_call_output を返す
        await self.send_event({
            "type": "conversation.item.create",
            "item": {
                "type": "function_call_output",
                "call_id": call_id,
                "output": output,
            }
        })

        # フォローアップ推論をトリガー
        await self.create_response()

    async def handle_messages(self) -> None:
        try:
            async for message in self.ws:
                event = json.loads(message)
                event_type = event.get("type")

                print(self._format_event_for_log(event))

                if event_type == "error":
                    continue
                elif event_type == "response.created":
                    self._current_response_id = event.get("response", {}).get("id")
                    self._is_responding = True
                    self._audio_suppressed = False
                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("----------------Speech Started----------------")
                    if self.on_speech_started:
                        self.on_speech_started()
                    if self._is_responding:
                        await self.handle_interruption()
                elif event_type == "response.audio.delta":
                    if self._audio_suppressed:
                        continue
                    if self.on_audio_delta_b64:
                        self.on_audio_delta_b64(event["delta"])
                elif event_type == "response.function_call_arguments.done":
                    await self._handle_function_call(event)
                elif event_type in self.extra_event_handlers:
                    self.extra_event_handlers[event_type](event)
                elif event_type == "input_audio_buffer.speech_stopped":
                    print("----------------Speech Stopped----------------")
        except websockets.exceptions.ConnectionClosed:
            print(" Connection closed")
        except Exception as e:
            print(" Error in message handling: ", str(e))
            traceback.print_exc()

    async def close(self) -> None:
        """WebSocket 接続を閉じます。"""
        if self.ws:
            await self.ws.close()

def _audio_energy(audio_data: bytes) -> float:
    count = len(audio_data) // 2
    if count == 0:
        return 0.0
    samples = struct.unpack(f'<{count}h', audio_data)
    return sum(abs(s) for s in samples) / count

async def record_and_send(client, player, echo_suppression=True):
    p = pyaudio.PyAudio()
    stream = p.open(format=pyaudio.paInt16, channels=1, rate=16000, input=True)
    print("Recording started. Speak into the microphone...")
    if echo_suppression:
        print("Note: Echo suppression is enabled (microphone is muted while the AI is speaking; interruption is not supported). If you are using headphones, set echo_suppression=False to enable interruption.")
    else:
        print("Note: Headphone mode. Voice interruption is supported.")
    playback_end_time = 0.0
    NOISE_GATE_THRESHOLD = 500
    try:
        while True:
            audio_data = await asyncio.to_thread(stream.read, 3200, False)
            if echo_suppression:
                is_active = client._is_responding or player.is_playing()
                if is_active:
                    playback_end_time = time.time()
                    await asyncio.sleep(0.02)
                    continue
                if time.time() - playback_end_time < 0.5:
                    await asyncio.sleep(0.02)
                    continue
            else:
                if client._is_responding or player.is_playing():
                    if _audio_energy(audio_data) < NOISE_GATE_THRESHOLD:
                        await asyncio.sleep(0.02)
                        continue
            await client.stream_audio(audio_data)
            await asyncio.sleep(0.02)
    finally:
        stream.stop_stream(); stream.close(); p.terminate()

async def main():
    pya = pyaudio.PyAudio()
    # 出力サンプルレート 24 kHz、サーバー側の音声フォーマットに合わせる
    player = B64PCMPlayer(pya, sample_rate=24000)

    client = FunRealtimeClient(
        # 以下はシンガポールリージョンの WebSocket URL です。{WorkspaceId} (波括弧を含む) を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
        base_url="wss://{WorkspaceId}.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime",
        api_key=os.environ['DASHSCOPE_API_KEY'],
        model="qwen-audio-3.0-realtime-plus",
        voice="longanqian",
        turn_detection_mode=TurnDetectionMode.SERVER_VAD,
        tools=tools,
        functions=functions,
        on_audio_delta_b64=player.add_data,
        # 音声割り込み時に再生バッファーをクリア
        on_speech_started=player.cancel_playing,
    )

    await client.connect()
    print("Connected. Starting real-time conversation (Function Calling enabled)...")

    try:
        await asyncio.gather(client.handle_messages(), record_and_send(client, player, echo_suppression=False))
    finally:
        await client.close()
        player.shutdown()
        pya.terminate()

if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        print("\nProgram exited.")

python realtime_fc_demo.py を実行し、マイクに向かって話しかけて、Function Calling を使用したリアルタイムの会話をお試しください。例えば、「杭州の天気は?」や「北京から上海までの列車のチケットはいくらですか?」などと質問すると、モデルが自動的に対応するツールを呼び出し、結果を返します。

会話コンテキスト管理

Qwen-Audio では、クライアントイベントを通じてコンテキスト内の会話アイテムを管理できます。これを使用して、過去のコンテキストを注入したり、テキスト情報を追加したり、無関係な会話アイテムを削除したりします。

  • 会話アイテムの作成 (conversation.item.create): コンテキストに会話アイテムを挿入します。以下の 3 つの item.type の値がサポートされています:

    • message: 通常の会話メッセージです。role (systemuser、または assistant) と content 配列を指定します。会話履歴やシステム命令を注入するために使用します。
    • function_call: 関数呼び出しリクエストです。call_idname、および arguments (JSON 文字列) を指定します。通常はサーバーによって生成されますが、クライアントがこれを使用して過去の関数呼び出しレコードを挿入することもできます。
    • function_call_output: ツールの実行結果です。 call_idoutput (JSON 文字列) を指定します。 function_call を受信後、クライアントでツールを実行し、このタイプで結果を返します。

    オプションの previous_item_id パラメーターは、どの既存の会話アイテムの後に新しいアイテムを挿入するかを指定します。これにより、会話履歴の任意の位置にコンテンツを挿入できます。省略された場合、新しいアイテムは末尾に追加されます。

    • 特定の位置にユーザーメッセージを挿入する:
{
    "type": "conversation.item.create",
    "previous_item_id": "item_abc",
    "item": {
        "type": "message",
        "role": "user",
        "content": [
            { "type": "input_text", "text": "前回の会話を要約してください" }
        ]
    }
}
  • Function Calling の結果を返す:
{
    "type": "conversation.item.create",
    "item": {
        "type": "function_call_output",
        "call_id": "call_xxx",
        "output": "{\"temperature\":18,\"condition\":\"sunny\"}"
    }
}

注記conversation.item.create で指定された item.id がカンバセーション内にすでに存在する場合、エラーが返されます。

  • 会話アイテムの取得 (conversation.item.retrieve): サーバーに保存されている会話アイテムを照会します。音声タイプのコンテンツの場合、生の音声データではなく、文字起こしテキストのみが返されます。
{
    "type": "conversation.item.retrieve",
    "item_id": "item_xxx"
}
  • 会話アイテムの削除 (conversation.item.delete): 会話コンテキストから特定のアイテムを削除します。
{
    "type": "conversation.item.delete",
    "item_id": "item_xxx"
}

環境音の文字起こし

smart_turn モードのみ。VAD が音声アクティビティを検出したものの、意味解析によってそれが有効なターンではない (ノイズや、「uh」や「hmm」などのフィラー音) と判断された場合、サーバーは会話のターンをトリガーしません。代わりに、ASR 結果を ambient_audio_transcription イベントとしてクライアントに送信します。このトランスクリプションは、会話のコンテキストには書き込まれません。

{
    "type": "conversation.item.ambient_audio_transcription.delta",
    "item_id": "item_xxx",
    "text": "うーん",
    "stash": ""
}

ユーザーの発話の文字起こしイベントと同様に、周囲の音声の文字起こしには deltacompleted のフェーズが含まれます。このイベントを使用すると、周囲の音声のモニタリングや会話シーンの認識を実装できます。

話者強調

smart_turn モードのみ。対象ユーザーの事前に録音された音声 URL を session.update で渡します。モデルが双方向の会話中にその話者にロックオンすることで、他の音声やバックグラウンドノイズは効果的に無視され、オープンな環境でもスムーズな双方向のやり取りが可能になります。

設定: 最初の session.update 内で、turn_detection.voiceprint_audio_urls に公開アクセス可能なボイスプリント音声 URL を渡します。

{
  "type": "session.update",
  "session": {
    "turn_detection": {
      "type": "smart_turn",
      "voiceprint_audio_urls": ["https://example.com/speaker.wav"]
    }
  }
}

パラメーター要件:

  • 最大5つの URL。音声は 16 kHz PCM または WAV 形式である必要があります。
  • このパラメーターは、最初の session.update でのみ有効になります。このフィールドは、後続の呼び出しでは無視されます。

登録イベント:設定を受信した後、サーバーは非同期で声紋登録を実行し、以下のイベントを通じて結果を通知します:

  • voiceprint_audio_list.in_progress: 登録が開始されました。 session.updated の前に送信され、item_id を含みます。
  • voiceprint_audio_list.completed: 登録が成功しました。item_idin_progress のものと一致します。
  • voiceprint_audio_list.failed: 登録は失敗し、エラーの理由を示す reason フィールドが含まれます (例: 音声 URL にアクセスできない)。登録が失敗しても、進行中の会話はブロックされません。

本番運用

フォールトトレランスの設定

  • クライアントの再接続: ネットワークジッターに対処するために自動再接続を実装します。on_error コールバックで再接続信号を設定し、再試行にはエクスポネンシャルバックオフ (例: 1 秒、2 秒、4 秒待機) を使用します。
  • エラーの分類: クライアントエラー (invalid_request_error) は会話を切断しません。ログに記録するか、パラメーターを調整する必要があります。サーバーエラー (server_error) は接続を終了し、再接続が必要になります。
  • 割り込み処理: server_vad / smart_turn モードでは、新しいユーザーの発話がモデルの進行中の応答を自動的に中断し、response.donestatus=cancelled を返します。音声の重複を避けるため、input_audio_buffer.speech_started を受信したら、直ちにローカルの再生バッファーをクリアしてください。

接続ライフサイクル

典型的な WebSocket セッションは、以下のライフサイクルに従います:

  1. 接続: クライアントが WebSocket 接続を開始し、サーバーが session.created イベントを返します。
  2. 設定:クライアントは session.update を送信して、対話モード、音声、ツール、およびその他のパラメーターを設定します。オーディオを送信する前に、このステップを完了してください。
  3. インタラクト: クライアントはオーディオを継続的にストリーミングします (input_audio_buffer.append)。サーバーは VAD 検出または手動トリガーに基づいて推論を実行し、音声とテキストをストリーミングで返します。
  4. 切断:クライアントが WebSocket 接続を閉じます。接続が長時間アイドル状態の場合、サーバーも切断することがあります。

遅延の最適化

  • 音声チャンクサイズ:チャンクあたり約 100 ms の音声データ (16 kHz x 16 ビット x モノラル = 3,200 バイト/チャンク) を送信します。これにより、リアルタイム性能とネットワーク効率のバランスが取れます。
  • ストリーミング再生: response.audio.delta が到着次第、オーディオの再生を開始します。完全な応答を再生するために response.done を待つ必要はありません。
  • 中断時のバッファーのクリアinput_audio_buffer.speech_started を受信したときは、直ちにローカル再生バッファーをクリアして、古い音声が再生され続けるのを防ぎます。

サポートされるモデルとリージョン

シンガポール

以下のモデルを呼び出す際は、シンガポールリージョンの API キー を使用してください:

  • qwen-audio-3.0-realtime-plus
  • qwen-audio-3.0-realtime-flash

中国 (北京)

以下のモデルを呼び出す際は、北京リージョンの API キー を使用してください:

  • qwen-audio-3.0-realtime-plus
  • qwen-audio-3.0-realtime-flash

API リファレンス