实时音频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-flash | 16 kHz |
语音对话(session) | step-audio-2、stepaudio-2.5-realtime、stepaudio-3-realtime-preview | 24 kHz |
语音识别(asr-session) | qwen3-asr-flash-realtime | 16 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-streaming | 16 kHz |
语音识别(inference) | fun-asr-flash-8k-realtime、fun-asr-flash-8k-realtime-2026-01-28、paraformer-realtime-8k-v2 | 8 kHz |
语音合成(tts-session) | qwen3-tts-flash-realtime | 无音频输入 |
同声传译(translate-commit) | qwen3.5-livetranslate-flash-realtime、qwen3.5-livetranslate-flash-realtime-2026-05-19 | 16 kHz |
同声传译(translate-auto) | qwen3.8-livetranslate-flash-realtime | 16 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配置)。
违反规则时平台发送一条错误事件并关闭会话:
{ "type": "error", "error": { "code": "session_limit_or_invalid_event" } }各会话类型的事件流程#
| 类型 | 开始 | 发送输入 | 触发一轮输出 | 一轮结束 | 结束会话 |
|---|---|---|---|---|---|
session | session.update | input_audio_buffer.append(base64 PCM) | input_audio_buffer.commit,然后 response.create | response.done | 关闭连接 |
asr-session | session.update | input_audio_buffer.append | input_audio_buffer.commit | conversation.item.input_audio_transcription.completed | 关闭连接 |
inference | run-task(JSON) | 二进制 PCM 帧 | 自动 | task-finished | 发送 finish-task;平台在任务结束后关闭会话 |
tts-session | session.update(mode: commit 或 server_commit) | input_text_buffer.append | input_text_buffer.commit(commit 模式) | response.done | session.finish,收到 session.finished |
translate-commit | session.update | input_audio_buffer.append | input_audio_buffer.commit | response.done | session.finish |
translate-auto | session.update | input_audio_buffer.append | 自动 | response.done | session.finish |
inference 类型的 run-task 事件:
{
"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":{}}}。
服务端事件名称与字段由厂商定义(例如语音合成的音频增量事件),请参考千问实时接口文档与阶跃星辰实时接口文档。
会话结束#
会话结束时平台结算,并在关闭连接前发送:
{
"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())// npm install ws
import WebSocket from 'ws';
import { randomUUID } from 'node:crypto';
import { writeFile } from 'node:fs/promises';
const apiKey = process.env.TOKENTOSEA_API_KEY;
const model = 'qwen3-tts-flash-realtime';
const requestKey = randomUUID();
const audio = [];
const ws = new WebSocket(`wss://api.tokentosea.com/v1/realtime?model=${model}`, {
headers: { Authorization: `Bearer ${apiKey}`, 'Idempotency-Key': requestKey },
maxPayload: 8 * 1024 * 1024,
});
const send = (event) => ws.send(JSON.stringify(event));
ws.on('open', () => send({
type: 'session.update',
session: { voice: 'Cherry', mode: 'commit', language_type: 'Chinese', response_format: 'pcm', sample_rate: 24000 },
}));
ws.on('message', (raw) => {
const event = JSON.parse(raw.toString());
if (event.type === 'session.updated') {
send({ type: 'input_text_buffer.append', text: '你好,欢迎使用语音服务。' });
send({ type: 'input_text_buffer.commit' });
} else if (event.type === 'response.audio.delta') { // 事件名以厂商文档为准
audio.push(Buffer.from(event.delta, 'base64'));
} else if (event.type === 'response.done') {
send({ type: 'session.finish' });
} else if (['session.finished', 'error', 'tokentosea.session.closed'].includes(event.type)) {
console.log(event);
ws.close();
}
});
ws.on('close', async () => {
await writeFile('speech.pcm', Buffer.concat(audio)); // ffmpeg -f s16le -ar 24000 -ac 1 -i speech.pcm speech.wav
const result = await fetch(`https://api.tokentosea.com/v1/results/${requestKey}`, {
headers: { Authorization: `Bearer ${apiKey}` },
}).then((r) => r.json());
console.log('result:', result.status, result.billing_status);
});
ws.on('error', (error) => console.error(error.message));流式语音识别#
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())// npm install ws
import WebSocket from 'ws';
import { randomUUID } from 'node:crypto';
import { readFile } from 'node:fs/promises';
const apiKey = process.env.TOKENTOSEA_API_KEY;
const model = 'qwen3-asr-flash-realtime';
const requestKey = randomUUID();
const CHUNK = 3200; // 100 ms:16000 Hz × 2 字节 × 0.1 s
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const ws = new WebSocket(`wss://api.tokentosea.com/v1/realtime?model=${model}`, {
headers: { Authorization: `Bearer ${apiKey}`, 'Idempotency-Key': requestKey },
maxPayload: 8 * 1024 * 1024,
});
const send = (event) => ws.send(JSON.stringify(event));
let started = false;
async function streamAudio() {
const pcm = await readFile('audio.pcm');
for (let offset = 0; offset < pcm.length && ws.readyState === WebSocket.OPEN; offset += CHUNK) {
send({ type: 'input_audio_buffer.append', audio: pcm.subarray(offset, offset + CHUNK).toString('base64') });
await sleep(100);
}
send({ type: 'input_audio_buffer.commit' });
}
ws.on('open', () => send({ type: 'session.update', session: { input_audio_format: 'pcm', sample_rate: 16000 } }));
ws.on('message', (raw) => {
const event = JSON.parse(raw.toString());
if (event.type === 'session.updated' && !started) { started = true; void streamAudio(); }
if (event.type === 'conversation.item.input_audio_transcription.completed') {
console.log('transcript:', event.transcript);
ws.close();
}
if (event.type === 'error') { console.error(event); ws.close(); }
});
ws.on('close', async () => {
const result = await fetch(`https://api.tokentosea.com/v1/results/${requestKey}`, {
headers: { Authorization: `Bearer ${apiKey}` },
}).then((r) => r.json());
console.log('result:', result.status, result.billing_status);
});每个实时型号的完整示例(含 inference 类型的二进制帧发送)见对应的模型页面。