实时音频
通过 WebSocket 进行实时语音对话、流式识别、流式合成与同声传译:连接方式、会话类型与事件流程、音频格式、限制、计费与断线恢复。
WS /v1/realtime 提供低延迟的双向音频会话。截至 2026-10-21,目录中有 26 个型号开放该接口,涵盖语音对话、流式语音识别、流式语音合成与同声传译。本页说明连接方式、各类会话的事件流程和平台限制。
前提:已创建密钥并把它设置为环境变量 TOKENTOSEA_API_KEY。Node.js 示例需要 npm install ws,Python 示例需要 pip install "websockets>=14" requests;音频转换使用 FFmpeg。
连接#
wss://api.tokentosea.com/v1/realtime?model=<型号 ID>| 握手请求头 | 说明 |
|---|---|
Authorization: Bearer <密钥> | 必填。WebSocket 只接受这种方式,不接受 x-api-key。 |
Idempotency-Key: <键> | 强烈建议。会话结束后用它查询结果与计费状态;握手响应头 X-Request-Id 回显该键。 |
- URL 中只允许
model一个查询参数。 - 浏览器的 WebSocket API 不能设置请求头,因此请从你的服务器发起连接,再把音频转发给前端。
- 握手失败时:路径错误返回 HTTP
404,其他原因(密钥无效、型号不可用、多余的查询参数、重复的幂等键、余额不足等)统一返回 HTTP403。
会话类型#
平台按型号把会话分为六类。不同类型的首个事件、轮次触发方式和结束方式不同:
| 类型 | 典型型号 | 首个事件 | 客户端输入 | 触发一轮输出 | 结束 |
|---|---|---|---|---|---|
| 语音对话 | qwen3.8-omni-flash-realtime、qwen3.5-omni-plus-realtime、qwen-audio-3.1-realtime-plus、stepaudio-2.5-realtime | session.update | input_audio_buffer.append(base64 PCM) | input_audio_buffer.commit 后发送 response.create | 收到 response.done 后可继续下一轮,结束时关闭连接 |
| 语音识别 | qwen3-asr-flash-realtime | session.update | input_audio_buffer.append | input_audio_buffer.commit | 收到 conversation.item.input_audio_transcription.completed |
| 语音合成 | qwen3-tts-flash-realtime | session.update(mode 为 commit 或 server_commit) | input_text_buffer.append | input_text_buffer.commit(commit 模式) | 发送 session.finish,收到 session.finished |
| 同传(自动) | qwen3.8-livetranslate-flash-realtime | session.update | input_audio_buffer.append | 自动 | 发送 session.finish |
| 同传(手动提交) | qwen3.5-livetranslate-flash-realtime | session.update | input_audio_buffer.append | input_audio_buffer.commit | response.done 后发送 session.finish |
| 流式识别任务 | fun-asr-realtime、paraformer-realtime-v2、qwen-audio-3.1-asr-flash-streaming | run-task(JSON) | 二进制 PCM 帧 | 自动 | 发送 finish-task,收到 task-finished |
每个型号所属的类型与可直接运行的示例,见模型目录中该型号页面的实时示例。
会话配置的平台规则#
- 首个事件必须是
session.update(流式识别任务为run-task);在此之前发送音频会被拒绝。 - 轮次由客户端控制:
turn_detection必须为null或不设置,不支持服务端语音活动检测。 - 不支持会话内工具(
tools)和附加转写(input_audio_transcription),这些附加产品未单独定价。 service_tier只能为standard或default。- 平台为每轮响应设置输出上限(多数型号 4096 token)。
- 会话中不能切换型号。
音频格式#
| 项目 | 要求 |
|---|---|
| 编码 | PCM 16 位有符号小端(pcm16 / s16le),单声道 |
| 采样率 | 默认 16 kHz;型号 ID 含 8k 的为 8 kHz;阶跃星辰的语音对话型号为 24 kHz |
| 发送节奏 | 建议每 100 毫秒发送一段(16 kHz 时为 3200 字节) |
| 合成输出 | 由 session.update 中的 response_format 与 sample_rate 决定,例如 PCM 24 kHz |
用 FFmpeg 把任意音频转成 16 kHz 单声道 PCM:
Shell
ffmpeg -i input.wav -ac 1 -ar 16000 -f s16le audio.pcm完整示例:流式语音合成#
用 qwen3-tts-flash-realtime 把一句话合成为 24 kHz PCM 音频。不需要准备音频文件。
// npm install ws;node tts.mjs
import WebSocket from 'ws';
import { writeFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
const key = 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=${encodeURIComponent(model)}`, {
headers: { Authorization: `Bearer ${key}`, 'Idempotency-Key': requestKey },
});
const send = (event) => ws.send(JSON.stringify({ event_id: `evt-${randomUUID()}`, ...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, isBinary) => {
if (isBinary) return;
const event = JSON.parse(raw.toString());
if (event.type === 'session.updated') {
send({ type: 'input_text_buffer.append', text: '你好,欢迎使用 TokenToSea 实时语音合成。' });
send({ type: 'input_text_buffer.commit' });
send({ type: 'session.finish' });
} else if (event.type?.endsWith('audio.delta') && typeof event.delta === 'string') {
audio.push(Buffer.from(event.delta, 'base64'));
} else if (event.type === 'error') {
console.error('会话错误:', event.error);
} else if (event.type === 'tokentosea.session.closed') {
console.log('会话结果:', event.result?.status, event.result?.billing_status);
}
});
ws.on('error', (error) => console.error('连接失败:', error.message));
ws.on('close', async (code) => {
writeFileSync('speech.pcm', Buffer.concat(audio));
// 无论是否收到 tokentosea.session.closed,都以结果查询为准
const result = await fetch(`https://api.tokentosea.com/v1/results/${requestKey}`, {
headers: { Authorization: `Bearer ${key}` },
}).then((response) => response.json());
console.log('关闭码', code, '最终状态', result.status, result.billing_status ?? '');
});# pip install "websockets>=14" requests;python tts.py
import asyncio
import base64
import json
import os
import uuid
import requests
from websockets.asyncio.client import connect
from websockets.exceptions import ConnectionClosed, InvalidStatus
KEY = os.environ["TOKENTOSEA_API_KEY"]
MODEL = "qwen3-tts-flash-realtime"
def event(payload):
return json.dumps({"event_id": f"evt-{uuid.uuid4()}", **payload}, ensure_ascii=False)
async def main():
request_key = str(uuid.uuid4())
audio = bytearray()
url = f"wss://api.tokentosea.com/v1/realtime?model={MODEL}"
headers = {"Authorization": f"Bearer {KEY}", "Idempotency-Key": request_key}
try:
async with connect(url, additional_headers=headers) as ws:
await ws.send(event({
"type": "session.update",
"session": {"voice": "Cherry", "mode": "commit", "language_type": "Chinese",
"response_format": "pcm", "sample_rate": 24000},
}))
async for raw in ws:
if isinstance(raw, bytes):
continue
message = json.loads(raw)
kind = message.get("type", "")
if kind == "session.updated":
await ws.send(event({"type": "input_text_buffer.append", "text": "你好,欢迎使用 TokenToSea 实时语音合成。"}))
await ws.send(event({"type": "input_text_buffer.commit"}))
await ws.send(event({"type": "session.finish"}))
elif kind.endswith("audio.delta") and isinstance(message.get("delta"), str):
audio += base64.b64decode(message["delta"])
elif kind == "error":
print("会话错误:", message.get("error"))
elif kind == "tokentosea.session.closed":
print("会话结果:", message["result"].get("status"))
except InvalidStatus as rejected:
print("握手被拒绝:HTTP", rejected.response.status_code)
except ConnectionClosed as closed:
print("连接关闭:", closed.rcvd.code if closed.rcvd else None)
with open("speech.pcm", "wb") as output:
output.write(audio)
# 无论是否收到 tokentosea.session.closed,都以结果查询为准
result = requests.get(
f"https://api.tokentosea.com/v1/results/{request_key}",
headers={"Authorization": f"Bearer {KEY}"},
timeout=30,
).json()
print("最终状态:", result.get("status"), result.get("billing_status"))
asyncio.run(main())把得到的 PCM 转成可播放的 WAV:
Shell
ffmpeg -f s16le -ar 24000 -ac 1 -i speech.pcm speech.wav示例:流式识别任务#
fun-asr-realtime 等流式识别型号使用任务协议:先发 JSON 格式的 run-task,收到 task-started 后发送二进制 PCM 帧,最后发送 finish-task。
Node.js
// 先准备音频:ffmpeg -i your-audio.wav -ac 1 -ar 16000 -f s16le audio.pcm
import WebSocket from 'ws';
import { readFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
const key = process.env.TOKENTOSEA_API_KEY;
const model = 'fun-asr-realtime';
const requestKey = randomUUID();
const taskId = randomUUID().replaceAll('-', '');
const header = (action) => ({ action, task_id: taskId, streaming: 'duplex' });
const ws = new WebSocket(`wss://api.tokentosea.com/v1/realtime?model=${model}`, {
headers: { Authorization: `Bearer ${key}`, 'Idempotency-Key': requestKey },
});
ws.on('open', () => {
ws.send(JSON.stringify({
header: header('run-task'),
payload: { model, parameters: { format: 'pcm', sample_rate: 16000 }, input: {} },
}));
});
ws.on('message', async (raw, isBinary) => {
if (isBinary) return;
const event = JSON.parse(raw.toString());
const name = event.header?.event ?? event.type;
if (name === 'task-started') {
const pcm = readFileSync('audio.pcm');
for (let offset = 0; offset < pcm.length && ws.readyState === WebSocket.OPEN; offset += 3200) {
ws.send(pcm.subarray(offset, offset + 3200)); // 100 ms
await new Promise((resolve) => setTimeout(resolve, 100));
}
ws.send(JSON.stringify({ header: header('finish-task'), payload: { input: {} } }));
} else if (name === 'result-generated') {
console.log(JSON.stringify(event.payload?.output));
} else if (['task-finished', 'task-failed', 'error'].includes(name)) {
if (name !== 'task-finished') console.error(event);
ws.close();
}
});
ws.on('error', (error) => console.error('连接失败:', error.message));
ws.on('close', async () => {
const result = await fetch(`https://api.tokentosea.com/v1/results/${requestKey}`, {
headers: { Authorization: `Bearer ${key}` },
}).then((response) => response.json());
console.log('最终状态:', result.status, result.billing_status ?? '');
});会话结束与结果#
- 会话结束时,平台会尽量发送
{"type": "tokentosea.session.closed", "result": {…}},其中result是结果信封,随后关闭连接。 - 某些结束路径上连接会先于该事件关闭。无论是否收到它,都以
GET /v1/results/{Idempotency-Key}的结果为准;状态仍为streaming或billing时稍后再查。 - 断线后不要自动重连。重连是一次新的会话,会重新预留并计费;用同一个幂等键重连会在握手时被拒绝。
限制#
| 限制 | 值 |
|---|---|
| 会话时长 | 60 秒,到时平台主动结束会话,并等待进行中的响应完成(最多约 20 秒) |
| 轮次 | 8 轮 |
| 单条客户端消息 | 1 MiB |
| 排队中的客户端输入 | 4 MiB,超出时关闭连接 |
| 音频输入总量 | 60 秒(按采样率换算的 PCM 字节数) |
| 文本输入总量 | 按字符计费的型号(如语音合成)5,000 字节;其他 32,768 字节 |
| 图片或视频帧 | 8 个 |
需要更长的识别或合成时,请把内容拆成多个会话,或使用 /v1/audio/transcriptions、/v1/audio/speech 等非实时接口。
错误与关闭码#
| 信号 | 含义 | 处理 |
|---|---|---|
{"type":"error","error":{"code":"session_limit_or_invalid_event"}} | 事件不合法,或超出会话限制,连接随即关闭 | 检查事件顺序、格式与上表限制 |
{"type":"error","error":{"code":"supplier_usage_pending_reconciliation"}} | 厂商未返回可核验的用量,本次会话转为待核对 | 查询结果,必要时联系支持 |
厂商的 error / task-failed 事件 | 原样转发,随后关闭连接 | 查看事件内容 |
关闭码 1013 | 任一方向的缓冲积压过多 | 降低发送速率 |
关闭码 1011 | 内部错误或上游连接异常 | 查询结果 |
关闭码 1012 | 服务重启 | 查询结果 |
计费#
- 连接建立时按 60 秒会话和 32,768 个输出 token 的上限预留金额;每轮开始前会按需追加预留。
- 每轮响应完成后,平台按厂商报告的累计用量分段结算;会话结束时结清,未用完的预留退回。
- 厂商未报告可核验用量时,会话进入待核对。