Skip to main content

リアルタイム音声対話モデルの統合

Agent ツールに音声クライアントの作成と実行を任せ、Token Plan のリアルタイム音声対話モデルに接続します。

対応モデル

モデル機能
qwen-audio-3.0-realtime-plusリアルタイム音声対話
その他のモデルは Token Plan が対応するリアルタイム音声対話モデル を参照してください。

前提

  • Token Plan を購読している。個人版は My Subscription(個人版) で sk-sp- で始まる API Key を取得する。チーム版は メンバー管理 でシートを割り当ててから API Key を取得する。
  • Python 3.9 以降、マイク、スピーカーが利用可能である。

ステップ 1:Agent ツールに接続指示を送る

以下の接続指示を展開してコピーし、使用している Agent ツールに送ります。設定済みの Token Plan Key を読み取り、依存関係をインストールし、qwen_realtime_voice.py を作成して実行します。Key の手動設定や Python の記述は不要です。
現在のプロジェクトに Token Plan のリアルタイム音声対話モデル qwen-audio-3.0-realtime-plus を導入してください:

1. 現在の Agent ツールに設定済みの Token Plan API Key を使用する(TOKEN_PLAN_API_KEY を優先し、次に ANTHROPIC_AUTH_TOKEN を読み取る)。どちらもない場合は、先に Agent ツールで Token Plan Key を設定するよう私に促す。Key を表示したり、ファイルやログに書き込んだりしない。
2. 依存関係をインストールする:pip install "websockets>=15" sounddevice certifi
3. 以下の内容で qwen_realtime_voice.py を作成する:

```python
#!/usr/bin/env python3
import asyncio
import base64
import json
import os
import queue
import ssl
import threading

import certifi
import sounddevice as sd
import websockets

API_KEY = os.environ.get("TOKEN_PLAN_API_KEY") or os.environ.get("ANTHROPIC_AUTH_TOKEN")
if not API_KEY:
    raise SystemExit("Token Plan API Key not found. Configure it in the Agent tool first.")
WS_URL = "wss://token-plan.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/realtime?model=qwen-audio-3.0-realtime-plus"

play_queue = queue.Queue()


def play_audio():
    with sd.RawOutputStream(samplerate=24000, channels=1, dtype="int16") as stream:
        while True:
            chunk = play_queue.get()
            if chunk is None:
                break
            stream.write(chunk)


async def receive(ws):
    transcript = []
    async for message in ws:
        event = json.loads(message)
        event_type = event["type"]
        if event_type == "conversation.item.input_audio_transcription.completed":
            print(f"\n[You] {event.get('transcript', '')}", flush=True)
        elif event_type == "response.audio.delta":
            play_queue.put(base64.b64decode(event["delta"]))
        elif event_type == "response.audio_transcript.delta":
            transcript.append(event.get("delta", ""))
        elif event_type == "response.audio_transcript.done":
            print(f"[AI] {''.join(transcript)}", flush=True)
            transcript.clear()
        elif event_type == "error":
            print(f"[Error] {event}", flush=True)


async def send_mic(ws):
    loop = asyncio.get_running_loop()
    audio_queue = asyncio.Queue()

    def callback(indata, frames, time_info, status):
        loop.call_soon_threadsafe(audio_queue.put_nowait, bytes(indata))

    with sd.RawInputStream(
        samplerate=16000,
        channels=1,
        dtype="int16",
        blocksize=1600,
        callback=callback,
    ):
        print("Start speaking. Press Ctrl+C to stop.", flush=True)
        while True:
            chunk = await audio_queue.get()
            await ws.send(
                json.dumps(
                    {
                        "type": "input_audio_buffer.append",
                        "audio": base64.b64encode(chunk).decode(),
                    }
                )
            )


async def main():
    context = ssl.create_default_context(cafile=certifi.where())
    async with websockets.connect(
        WS_URL,
        ssl=context,
        additional_headers={"Authorization": f"Bearer {API_KEY}"},
        max_size=None,
    ) as ws:
        await ws.recv()
        await ws.send(
            json.dumps(
                {
                    "type": "session.update",
                    "session": {
                        "modalities": ["audio", "text"],
                        "voice": "longanqian",
                        "instructions": "You are a voice assistant. Keep answers concise and natural.",
                        "input_audio_format": "pcm",
                        "output_audio_format": "pcm",
                        "turn_detection": {
                            "type": "server_vad",
                            "threshold": 0.5,
                            "silence_duration_ms": 600,
                        },
                    },
                }
            )
        )
        threading.Thread(target=play_audio, daemon=True).start()
        await asyncio.gather(receive(ws), send_mic(ws))


if __name__ == "__main__":
    try:
        asyncio.run(main())
    except KeyboardInterrupt:
        pass
    finally:
        play_queue.put(None)
```

4. Bash ツールで python3 qwen_realtime_voice.py を実行する。timeout は 600000 ミリ秒に設定する。
5. 起動後、「話し始めてください。Ctrl+C で終了します」と表示する。
初回実行時に、システム設定で Agent ツールを実行するアプリ(Cursor や Terminal など)にマイクへのアクセスを許可してください。許可しないとスクリプトに音声が届きません。スピーカーの音声がマイクに入り、応答が中断されるのを防ぐため、ヘッドホンの使用を推奨します。

ステップ 2:音声対話を開始する

Agent ツールがスクリプトを起動したら、開始メッセージが表示されたときに話し始めます。ターミナルに認識結果とモデルの応答が表示され、スピーカーからモデルの音声が再生されます。Ctrl+C でセッションを終了します。
Agent ツールが依存関係のインストール、スクリプトの作成、セッションの開始を行います。音声データはローカルクライアントと Token Plan の間で送受信され、Agent ツールのテキストモデルは経由しません。

ツール拡張として保存する

再度使用する場合は、Agent ツールに上の指示を現在のツールの Skill、Slash Command、または Agent 設定として保存させ、そのツールの起動方法で実行します。主なツールの拡張メカニズムとパスは次のとおりです:
ツール拡張メカニズム設定ファイルのパス
Claude CodeSlash Command.claude/commands/voice-chat.md
CodexSkill~/.codex/skills/token-plan-voice/SKILL.md
Qwen CodeSkill~/.qwen/skills/voice-chat/SKILL.md
OpenCodeAgent.opencode/agents/voice-chat.md
OpenClawSkill~/.openclaw/workspace/skills/token-plan-voice/SKILL.md
Hermes AgentSkill~/.hermes/skills/media/voice-chat/SKILL.md
QoderSkill~/.qoder/skills/voice-chat/SKILL.md

制限

  • スピーカーを使用すると、モデルの音声がマイクに拾われて応答が中断される場合があります。ヘッドホンを使用してください。
  • Agent ツールの Bash 呼び出しにはタイムアウトがあります。長時間の対話では、スクリプトを独立したプロセスとして実行するか、MCP サーバーとしてラップしてください。
  • 現在の構成は音声入力と出力のみを処理します。