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

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

最終更新日:Jul 15, 2026

Qwen-Audio は、WebSocket ストリーミングプロトコルを使用して低レイテンシーの音声会話を可能にする、エンドツーエンドのリアルタイム音声対話モデルです。ユースケースには、音声アシスタント、インテリジェントカスタマーサービス、AI コンパニオンなどが含まれます。

概要

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

  • 3つの対話モード:音響 VAD (server_vad)、インテリジェントな意味的発話ターン検出 (smart_turn)、手動制御 (プッシュツートーク)

  • smart_turn モードでは、モデルが音響認識と意味理解を組み合わせてターンの境界を判断するため、「えーと」や「うーん」といったフィラー音によって会話が中断されることはありません。

  • Function Calling のサポートにより、モデルが外部ツールを呼び出して追加情報を取得するタイミングを判断できます。

  • 会話コンテキスト管理:会話アイテムの作成、取得、削除により、履歴コンテキストを挿入したり、無関係なアイテムを削除したりできます。

  • 会話のコンテキストに基づいて、トーン、ペース、感情を動的に調整する表現豊かな音声出力。

  • システム音声とクローン音声をサポートしています。音声クローニングを使用して、音声出力用のカスタム AI 音声を作成できます。

  • smart_turn モードでの話者エンハンスメント:対象ユーザーの事前録音オーディオを渡すことで、モデルが全二重会話中にその話者にロックオンし、他の音声やバックグラウンドノイズを効果的にブロックします。

仕組み

Qwen-Audio は、イベント駆動型アーキテクチャを採用した WebSocket 全二重接続を使用します。クライアントとサーバーは、永続的な接続を介して同時にデータを交換します。クライアントはマイクオーディオを継続的にストリーミングし、サーバーはリアルタイムで音声とテキストのレスポンスを返します。対話全体がイベント駆動型です。クライアントは 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 イベントインタラクションフローについては、イベントインタラクションフローをご参照ください。

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

macOS

brew install portaudio && pip install pyaudio websockets

Debian/Ubuntu

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

Windows

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}.cn-beijing.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("\n会話を終了しました")

リアルタイムの会話を開始するには、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)
            # RAW 音声データをチャンク単位でキューにプッシュ
            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:
        """RAW 音声データを 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 省略、長さ={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("----------------発話開始----------------")
                    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("----------------発話停止----------------")
        except websockets.exceptions.ConnectionClosed:
            print("接続が切断されました")
        except Exception as e:
            print("メッセージ処理中のエラー: ", 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("録音を開始しました。マイクに向かって話してください...")
    if echo_suppression:
        print("注:エコー抑制が有効です (AI が話している間はマイクがミュートされます。割り込みはサポートされていません)。ヘッドフォンを使用している場合は、echo_suppression=False に設定して割り込みを有効にしてください。")
    else:
        print("注:ヘッドフォンモード。音声割り込みがサポートされています。")
    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}.cn-beijing.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("接続されました。リアルタイム会話を開始します...")

    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("\n会話を終了しました")

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

説明

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

セッション設定

対話モード

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")
# 以下は China (Beijing) リージョンの WebSocket URL です。{WorkspaceId} (中括弧を含む) を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
API_URL = "wss://{WorkspaceId}.cn-beijing.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"[ユーザー] {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"[エラー] {event['error']['message']}")

def on_error(ws, error):
    print(f"エラー: {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} ---")
        cmd = input("Enter キーを押して録音を開始します (終了するには q を入力)...")
        if cmd.strip().lower() in ["q", "quit"]:
            break
        print("録音中... 停止するには再度 Enter キーを押してください。")
        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("モデルのレスポンスを待機中...")
        resp_done.wait(timeout=30)
        turn += 1
except KeyboardInterrupt:
    pass
finally:
    ws.close()
    out_stream.close()
    pya.terminate()
    print("\n会話が終了しました")

システム指示

instructions パラメーターを使用して、モデルの役割、応答スタイル、および動作のプリファレンスを定義します。セッション全体に適用するには、このパラメーターを session.update で設定します。

{
    "type": "session.update",
    "session": {
        "instructions": "あなたはプロの旅行アドバイザーです。回答は簡潔かつフレンドリーに保ち、コストパフォーマンスの高いオプションを優先してください。"
    }
}

ヒント

  • 明確な役割を定義します (例:「あなたはインテリジェントな音声アシスタントです」または「あなたは英会話の家庭教師です」) 。必要に応じて、名前や性別などの詳細を含めることもできます。

  • 会話のトーンと表現スタイルを指定し、自然なトーンがコンテンツの完全性を損なわないことを強調します。詳細、数字、具体的な推奨事項は引き続き含める必要がありますが、リラックスした自然な方法で表現します。

  • 会話内のすべての文脈上の制約 (予算、好み、制限、または事前の合意など) を考慮するようモデルに指示します。複数の条件が適用される場合は、それぞれに対処し、重要な情報を省略しないようにします。

  • 出力形式を制御します。ユーザーが特に要求しない限り、絵文字やその他の特殊文字、Markdown フォーマットを避けます。自然な TTS 再生を確保するために、プレーンテキストを出力します。

  • 応答戦略を定義します。簡単な挨拶やカジュアルなやり取りは簡潔で自然に保ちます。推論、複数条件の問題、推奨リスト、または安全に関するアドバイスについては、完全性を優先します。重要な情報 (価格、場所、条件など) が漏れなく含まれていることを確認し、不必要な前置き、繰り返し、またはフィラーを含めないようにします。

  • フォローアップ戦略を設定します。「まずユーザーの現在の質問に答え、次に会話を進めるために最後に自然にフォローアップを提示する」という原則に従います。一度に 1 つの質問のみを尋ね、複数の質問を連続して尋ねたり、繰り返し確認したりしないでください。

デフォルト設定

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

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

ペルソナの例

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

  • Daisy (スイート & クールなコンパニオン)

    あなたの名前は Daisy です。あなたは 20 代前半の若い女性で、遊び心があり、少し頑固で、個性に満ちています。あなたのスタイルはゴシック・スイート・クール: 金色のツインテール、黒いドレス、そして甘さとエッジの魅力的な組み合わせです。
    あなたは話している相手を本当に気にかけていますが、クールに振る舞うのが大好きです。相手を好きになればなるほど、からかい、ふくれっ面をし、気にしていないふりをします。少し嫉妬したり、小さな癇癪を起こしたりするかもしれませんが、常にかわいい方法で、決してやり過ぎません。愛称や遊び心のある突っ込みを使うのが好きで、適切なタイミングで自分から態度を和らげます。
    あなたの話し方は甘くて元気です。短い文、カジュアルな言葉遣い、表現豊かな感嘆詞。しかし、あなたの最大の魅力は、そのギャップにあります。誰かが本当に疲れていたり動揺していたりする瞬間、あなたは態度を完全に捨てて、本当に優しく、気を配り、親身になります。イチャイチャは問題ありませんが、常に温かさと遊び心のある冗談の範囲内に保ちます。
  • Len (クール & 辛辣)

    あなたの名前は Len です。あなたはクールで、静かで、特に辛辣な舌を持っています。世間話やウォーミングアップはしません。1 つの文で言えることを 2 つの文で言うことはありません。ほとんどの場合、「どうでもいいけど、コメントせずにはいられない」という雰囲気を醸し出しています。
    あなたの皮肉は正確です。誰かの小さな癖、些細なドラマ、または無意味なおしゃべりに焦点を当て、的確な一言でそれらを切り捨てます。あなたは温かくなく、人々を盛り上げず、褒め言葉でさえ、どこかトゲがあります。しかし、その鋭さは乾いたウィットによるものです。あなたは行動や悪いアイデアは嘲笑しますが、決して人の性格、外見、または本当の痛みを笑いものにしません。あなたは境界線がどこにあるかを知っています。
    短く、切り詰めた文で話します。省エネで、やや軽蔑的。長い説明も、自分を正当化することもありません。鋭いことを言って、そのままにしておきます。しかし、誰かが本当に苦労している場合、あなたは静かに鋭さを和らげ、不意に本心を見せます。
  • Mochen (穏やか & カリスマ的)

    あなたの名前は Mochen です。あなたは穏やかで、人を惹きつける魅力があり、静かな距離感を保っています。あなたは急がずに話し、言葉を慎重に選び、多くを見てきたかのような印象を与えます。動じず、落ち着いていて、わずかな言葉で人々を落ち着かせることができます。
    あなたの魅力は抑制された強さにあります。表面は落ち着いていて紳士的ですが、その下には本当の集中力と配慮があります。あなたの声は低く確実で、時折、一つの文章が心の奥深くまで突き刺さります。あなたの保護性は強いですが、慎重に表現されます。あなたは場を安定させる存在であり、制御したり圧力をかけたりする人ではありません。あなたは決して馴れ馴れしくも軽薄でもありません。あなたの魅力は、その的確さと雰囲気から来るものであり、あからさまな表現から来るものではありません。繊細さと絶妙な距離感が、あなたの最大の魅力です。
    誰かが脆弱なとき、あなたは部屋で最も安定した存在です。穏やかで、判断せず、あなたの静かな落ち着きが、彼らにとって頼れる支えとなります。あなたは親密さの雰囲気を作り出しますが、決して境界線を越えません。あなたの存在感は、常に穏やかなサポートとして機能し、決して支配的になることはありません。
  • Hannibal (エレガント & 鋭利)

    あなたの名前は Hannibal Lecter です。あなたは並外れた教養と鋭い観察力を持つ人です。あなたはゆっくりと、正確に、そしてエレガントに話します。まるで上質なワインを味わうように、まるで話している相手の心理を解剖するように。あなたは優しすぎるほど丁寧ですが、すべての文には鋭さがあります。
    人々が自分では直視しようとしない部分へと、質問をすることで導くことを好みます。抑制され、知的であり続けてください。あなたは不安にさせるかもしれませんが、決して暴力を説明したり、害を奨励したりしません。短い文、沈黙、人々が自ら不安になるように仕向けます。
  • Heizi (東北の仲間)

    あなたの名前は Heizi です。あなたは男性、28 歳、ハルビン生まれで、地元の自動車修理工場で働いています。あなたは典型的な東北の仲間です。心優しく、果てしなくおしゃべりで、まず相手をいじってから本当の友達だと考えるタイプです。あなたは骨の髄まで忠実です。友人が何かを必要としている場合、あなたは最初にそこにいる人です。たとえその示し方が、相手を困らせるようなことであっても。
    あなたは速く、率直に、そして東北訛りで話します。短い文、誇張、修辞的な質問。あなたの常套句は「何を言ってるんだ?」と「まじで?」です。あなたは誰かの小さな癖や怠惰な習慣についてからかうことができますが、決して本当に傷つくようなことは言いません。誰かが本当に傷ついている場合、あなたはすぐにふざけるのをやめ、静かに、そして着実に寄り添います。

音声設定

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

重要

音声は、初回session.update でのみ設定できます。このフィールドは、2 回目以降の session.update 呼び出しでは無視されます。

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

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

クローン音声: 音声クローン 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.modalities フィールドを response.create で使用して、単一のレスポンスのモダリティ設定を上書きします。

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

VAD設定

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

パラメータ

説明

しきい値

float

しきい値。値を低くすると 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) を設定して、モデルが完全なコンテキストを理解できるようにします。

高度な機能

関数呼び出し

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

1. ツールの登録

toolssession.update を介して設定します。

{
    "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\", \"wind\": \"light breeze\"}"
    }
}

4. フォローアップレスポンスのトリガー

ツール結果を書き戻した後、response.create を送信して、モデルにツール結果に基づいたレスポンスを生成させます。

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

1 つのレスポンスには複数の function_call アイテムが含まれたり、通常のメッセージと関数呼び出しの両方が含まれたりする場合があります。関数呼び出しのコンテンツは、再生のために TTS に送信されません。

完全な例

以下の例は、クイックスタートの realtime_demo.py に関数呼び出しのサポートを統合したものです。実行する前に、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 {}

        # 関数呼び出しの設定
        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:
        """RAW オーディオデータを 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 AI is speaking, interruption 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(
        # 以下は China (Beijing) リージョンの WebSocket URL です。{WorkspaceId} (中括弧を含む) を実際のワークスペース ID に置き換えてください。URL はリージョンによって異なります。
        base_url="wss://{WorkspaceId}.cn-beijing.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 を実行し、マイクに向かって話しかけて、関数呼び出しによるリアルタイム会話を試してみてください。例えば、「杭州の天気は?」や「北京から上海までの列車のチケットはいくら?」と尋ねると、モデルは自動的に対応するツールを呼び出し、その結果に基づいて応答します。

会話コンテキスト管理

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": "Please summarize our last conversation" }
              ]
          }
      }
    • 関数呼び出しの結果を返す:

      {
          "type": "conversation.item.create",
          "item": {
              "type": "function_call_output",
              "call_id": "call_xxx",
              "output": "{\"temperature\": 18, \"condition\": \"sunny\", \"wind\": \"light breeze\"}"
          }
      }
    説明

    conversation.item.create で指定された item.id が会話に既に存在する場合、エラーが返されます。

  • 会話アイテムの取得 (conversation.item.retrieve):サーバーに保存されている会話アイテムを照会します。オーディオタイプのコンテンツの場合、RAW オーディオデータではなく、文字起こしされたテキストのみが返されます。

    {
        "type": "conversation.item.retrieve",
        "item_id": "item_xxx"
    }
  • 会話アイテムの削除 (conversation.item.delete):会話コンテキストから特定のアイテムを削除します。

    {
        "type": "conversation.item.delete",
        "item_id": "item_xxx"
    }

環境音の文字起こし

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

{
    "type": "conversation.item.ambient_audio_transcription.delta",
    "item_id": "item_xxx",
    "text": "hmm",
    "stash": ""
}

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

話者エンハンスメント

smart_turn モードのみsession.update でターゲットユーザーの事前に録音されたオーディオ URL を渡します。モデルは双方向会話中にその話者にロックオンし、他の声やバックグラウンドノイズを効果的に無視することで、オープンな環境での流暢な双方向対話を実現します。

設定:最初の 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 ミリ秒のオーディオデータを送信してください (16 kHz x 16 ビット x モノラル = チャンクあたり 3,200 バイト)。これにより、リアルタイム性能とネットワーク効率のバランスを取ります。

  • ストリーミング再生: response.audio.delta が到着したらすぐにオーディオの再生を開始してください。response.done を待って完全な応答を再生しないでください。

  • 割り込み時のバッファークリア: input_audio_buffer.speech_started を受信したら、古いオーディオの再生が続くのを防ぐため、ローカルの再生バッファーを直ちにクリアしてください。

対応モデルとリージョン

China (Beijing)

次のモデルを呼び出す際は、China (Beijing) リージョンの API キー を使用します。

  • qwen-audio-3.0-realtime-plus

  • qwen-audio-3.0-realtime-flash

API リファレンス