TokenToSea开发者文档
控制台

实时音频WS

通过 WebSocket 进行实时语音对话、流式语音识别、流式语音合成与同声传译。每个会话最长 60 秒、最多 8 轮,按厂商实际用量结算。

本页内容

WS wss://api.tokentosea.com/v1/realtime?model={model}

建立一个 WebSocket 会话,平台在你与厂商的实时接口之间双向转发事件。你发送的事件与收到的事件均使用厂商的原生实时协议;平台只校验会话配置、限制会话规模,并在会话结束时追加一条 tokentosea.session.closed 事件。

一个会话就是一次计费调用:连接时预留费用,每完成一轮按厂商返回的用量结算,会话结束后释放剩余预留。

前提:全能密钥或模型密钥,账户可用余额足以覆盖一个会话的预留(按 60 秒音频与 32,768 个输出 token 等上限计算)。Python 示例需要 pip install "websockets>=13" requests,Node.js 示例需要 Node.js 18+ 与 npm install ws。

支持的型号#

截至 2026-10-21,26 个型号开放本接口,以 GET /v1/models 中 verified_endpoints 包含 /v1/realtime 的型号为准。不同型号使用不同的会话类型:

会话类型型号输入采样率
语音对话(session)qwen3.8-omni-flash-realtime、qwen3.5-omni-plus-realtime、qwen3.5-omni-plus-realtime-2026-03-15、qwen3.5-omni-flash-realtime、qwen3.5-omni-flash-realtime-2026-03-15、qwen3-omni-flash-realtime-2025-09-15、qwen-audio-3.1-realtime-plus、qwen-audio-3.0-realtime-plus、qwen-audio-3.0-realtime-flash16 kHz
语音对话(session)step-audio-2、stepaudio-2.5-realtime、stepaudio-3-realtime-preview24 kHz
语音识别(asr-session)qwen3-asr-flash-realtime16 kHz
语音识别(inference)fun-asr-realtime、fun-asr-realtime-2026-02-28、paraformer-realtime-v2、qwen-audio-3.1-asr-flash-streaming、qwen-audio-3.1-asr-flash-message、qwen-audio-3.0-asr-flash-streaming16 kHz
语音识别(inference)fun-asr-flash-8k-realtime、fun-asr-flash-8k-realtime-2026-01-28、paraformer-realtime-8k-v28 kHz
语音合成(tts-session)qwen3-tts-flash-realtime无音频输入
同声传译(translate-commit)qwen3.5-livetranslate-flash-realtime、qwen3.5-livetranslate-flash-realtime-2026-05-1916 kHz
同声传译(translate-auto)qwen3.8-livetranslate-flash-realtime16 kHz

连接#

项目规则
地址wss://api.tokentosea.com/v1/realtime?model=<型号>,查询字符串只能包含 model
Authorization必填,Bearer sk-relay_…。实时接口不接受 x-api-key
Idempotency-Key可选,格式同 HTTP 接口(16–128 个 [A-Za-z0-9_.:-] 字符)。不传时平台生成 tts-auto-…;握手响应头 X-Request-Id 返回本次使用的键
调用方只能从服务端连接:浏览器的 WebSocket 无法设置 Authorization 请求头

握手被拒绝时直接返回 HTTP 状态码、不附带正文:路径错误为 404,其他原因(密钥无效、型号不可用、查询字符串不合法、幂等键已被使用、余额不足、并发超限等)均为 403。

会话规则#

规则值
会话时长60 秒。到时平台请求厂商结束当前输出(最多再等待 20 秒),随后关闭会话
轮数最多 8 轮;同一时间只能有一轮在生成
音频格式PCM16(16 位有符号小端)、单声道,采样率见型号表
音频总量不超过 60 秒的 PCM 数据(采样率 × 2 字节 × 60)
文本输入input_text_buffer.append 与 conversation.item.create 事件按整条消息的字节数累计:语音合成型号不超过 5,000 字节,其他型号不超过 32 KiB
图片 / 视频帧累计不超过 8 个
单条消息客户端发来的单条消息不超过 1 MiB;排队中的输入不超过 4 MiB
第一条事件发送音频前必须先发送 session.update(inference 类型为 run-task)

session.update 中以下配置会被拒绝:

  • turn_detection 不为 null:平台只支持手动控制轮次(你发送 commit / response.create),不支持服务端 VAD 自动断句;
  • tools、联网搜索、问候语等附加功能;
  • 非 standard / default 的 service_tier;
  • 与型号采样率不一致的音频格式;
  • 语音对话中的 input_audio_transcription(输入转写另行计价);
  • qwen3.8-livetranslate-flash-realtime 顶层的 turn_detection 与 modalities(使用嵌套的 audio 配置)。

违反规则时平台发送一条错误事件并关闭会话:

JSON
{ "type": "error", "error": { "code": "session_limit_or_invalid_event" } }

各会话类型的事件流程#

类型开始发送输入触发一轮输出一轮结束结束会话
sessionsession.updateinput_audio_buffer.append(base64 PCM)input_audio_buffer.commit,然后 response.createresponse.done关闭连接
asr-sessionsession.updateinput_audio_buffer.appendinput_audio_buffer.commitconversation.item.input_audio_transcription.completed关闭连接
inferencerun-task(JSON)二进制 PCM 帧自动task-finished发送 finish-task;平台在任务结束后关闭会话
tts-sessionsession.update(mode: commit 或 server_commit)input_text_buffer.appendinput_text_buffer.commit(commit 模式)response.donesession.finish,收到 session.finished
translate-commitsession.updateinput_audio_buffer.appendinput_audio_buffer.commitresponse.donesession.finish
translate-autosession.updateinput_audio_buffer.append自动response.donesession.finish

inference 类型的 run-task 事件:

JSON
{
  "header": { "action": "run-task", "task_id": "你生成的任务 ID(1–128 个字母、数字、_ 或 -)", "streaming": "duplex" },
  "payload": { "model": "fun-asr-realtime", "parameters": { "format": "pcm", "sample_rate": 16000 }, "input": {} }
}

一个会话只能有一个任务。音频发送完后发送 {"header":{"action":"finish-task","task_id":"…","streaming":"duplex"},"payload":{"input":{}}}。

服务端事件名称与字段由厂商定义(例如语音合成的音频增量事件),请参考千问实时接口文档与阶跃星辰实时接口文档。

会话结束#

会话结束时平台结算,并在关闭连接前发送:

JSON
{
  "type": "tokentosea.session.closed",
  "result": {
    "id": "result_…",
    "run_id": "cm2…",
    "status": "succeeded",
    "billing_status": "succeeded",
    "output": { "model": "qwen3-tts-flash-realtime", "events": ["…"], "usage": { "…": "累计用量" } },
    "status_code": 200,
    "response_type": "application/json"
  }
}

result 与 GET /v1/results/{key} 返回的结果信封相同。如果你先关闭了连接、没有收到这条事件,用握手时的 Idempotency-Key 查询结果即可。

情况表现
厂商报错(error、task-failed 事件)原样转发后关闭会话,结果进入待核对
厂商未返回可核验的用量发送 {"type":"error","error":{"code":"supplier_usage_pending_reconciliation"}} 后关闭
发送过快、缓冲积压过多关闭会话
平台内部错误、服务重启关闭会话

关闭码不作保证。执行节点主动关闭会话(包括积压、内部错误与重启)时,api.tokentosea.com 转发关闭但不带关闭码,客户端通常看到 1005(无状态码)。只有入口自身触发的关闭才带码:1013(入口缓冲超过 8 MiB)、1011(入口到执行节点的连接出错)、1012(入口重启)。不要按关闭码判断结果:除了正常收到 tokentosea.session.closed 之外,任何关闭都按异常处理,用握手时的 Idempotency-Key 查询 /v1/results/{key}。

断线后不要自动重连

同一个 Idempotency-Key 只能建立一个会话,重连会被拒绝;换新键重连则是一次新的计费会话。断线后先查询 /v1/results/{key} 确认原会话的状态与用量。

示例#

流式语音合成#

qwen3-tts-flash-realtime:发送文字,收到 24 kHz PCM 音频增量。

# pip install "websockets>=13" requests
import asyncio
import base64
import json
import os
import uuid

import requests
from websockets.asyncio.client import connect

API_KEY = os.environ["TOKENTOSEA_API_KEY"]
MODEL = "qwen3-tts-flash-realtime"


async def main() -> None:
    request_key = str(uuid.uuid4())
    audio = bytearray()
    async with connect(
        f"wss://api.tokentosea.com/v1/realtime?model={MODEL}",
        additional_headers={"Authorization": f"Bearer {API_KEY}", "Idempotency-Key": request_key},
        max_size=8 * 1024 * 1024,
    ) as ws:
        await ws.send(json.dumps({"type": "session.update", "session": {
            "voice": "Cherry", "mode": "commit", "language_type": "Chinese",
            "response_format": "pcm", "sample_rate": 24000,
        }}))
        async for raw in ws:
            event = json.loads(raw)
            kind = event.get("type")
            if kind == "session.updated":
                await ws.send(json.dumps({"type": "input_text_buffer.append", "text": "你好,欢迎使用语音服务。"}))
                await ws.send(json.dumps({"type": "input_text_buffer.commit"}))
            elif kind == "response.audio.delta":  # 事件名以厂商文档为准
                audio += base64.b64decode(event["delta"])
            elif kind == "response.done":
                await ws.send(json.dumps({"type": "session.finish"}))
            elif kind in ("session.finished", "error"):
                print(event)
                break
    with open("speech.pcm", "wb") as file:  # ffmpeg -f s16le -ar 24000 -ac 1 -i speech.pcm speech.wav
        file.write(audio)
    result = requests.get(
        f"https://api.tokentosea.com/v1/results/{request_key}",
        headers={"Authorization": f"Bearer {API_KEY}"}, timeout=30,
    ).json()
    print("result:", result["status"], result.get("billing_status"))


asyncio.run(main())

流式语音识别#

qwen3-asr-flash-realtime:把音频转成 16 kHz 单声道 PCM(ffmpeg -i input.wav -ac 1 -ar 16000 -f s16le audio.pcm),每 100 毫秒发送一段。

# pip install "websockets>=13" requests
import asyncio
import base64
import json
import os
import uuid

import requests
from websockets.asyncio.client import connect

API_KEY = os.environ["TOKENTOSEA_API_KEY"]
MODEL = "qwen3-asr-flash-realtime"
CHUNK = 3200  # 100 ms:16000 Hz × 2 字节 × 0.1 s


async def stream_audio(ws) -> None:
    pcm = open("audio.pcm", "rb").read()
    for offset in range(0, len(pcm), CHUNK):
        chunk = base64.b64encode(pcm[offset:offset + CHUNK]).decode()
        await ws.send(json.dumps({"type": "input_audio_buffer.append", "audio": chunk}))
        await asyncio.sleep(0.1)
    await ws.send(json.dumps({"type": "input_audio_buffer.commit"}))


async def main() -> None:
    request_key = str(uuid.uuid4())
    async with connect(
        f"wss://api.tokentosea.com/v1/realtime?model={MODEL}",
        additional_headers={"Authorization": f"Bearer {API_KEY}", "Idempotency-Key": request_key},
        max_size=8 * 1024 * 1024,
    ) as ws:
        await ws.send(json.dumps({"type": "session.update", "session": {"input_audio_format": "pcm", "sample_rate": 16000}}))
        sender = None
        async for raw in ws:
            event = json.loads(raw)
            kind = event.get("type")
            if kind == "session.updated" and sender is None:
                sender = asyncio.create_task(stream_audio(ws))
            elif kind == "conversation.item.input_audio_transcription.completed":
                print("transcript:", event.get("transcript"))
                break
            elif kind == "error":
                print(event)
                break
    result = requests.get(
        f"https://api.tokentosea.com/v1/results/{request_key}",
        headers={"Authorization": f"Bearer {API_KEY}"}, timeout=30,
    ).json()
    print("result:", result["status"], result.get("billing_status"))


asyncio.run(main())

每个实时型号的完整示例(含 inference 类型的二进制帧发送)见对应的模型页面。

下一步#

    输入关键词搜索全部文档与模型。