TokenToSea开发者文档
控制台

实时音频

通过 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,其他原因(密钥无效、型号不可用、多余的查询参数、重复的幂等键、余额不足等)统一返回 HTTP 403。

会话类型#

平台按型号把会话分为六类。不同类型的首个事件、轮次触发方式和结束方式不同:

类型典型型号首个事件客户端输入触发一轮输出结束
语音对话qwen3.8-omni-flash-realtime、qwen3.5-omni-plus-realtime、qwen-audio-3.1-realtime-plus、stepaudio-2.5-realtimesession.updateinput_audio_buffer.append(base64 PCM)input_audio_buffer.commit 后发送 response.create收到 response.done 后可继续下一轮,结束时关闭连接
语音识别qwen3-asr-flash-realtimesession.updateinput_audio_buffer.appendinput_audio_buffer.commit收到 conversation.item.input_audio_transcription.completed
语音合成qwen3-tts-flash-realtimesession.update(mode 为 commit 或 server_commit)input_text_buffer.appendinput_text_buffer.commit(commit 模式)发送 session.finish,收到 session.finished
同传(自动)qwen3.8-livetranslate-flash-realtimesession.updateinput_audio_buffer.append自动发送 session.finish
同传(手动提交)qwen3.5-livetranslate-flash-realtimesession.updateinput_audio_buffer.appendinput_audio_buffer.commitresponse.done 后发送 session.finish
流式识别任务fun-asr-realtime、paraformer-realtime-v2、qwen-audio-3.1-asr-flash-streamingrun-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 ?? '');
});

把得到的 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 的上限预留金额;每轮开始前会按需追加预留。
  • 每轮响应完成后,平台按厂商报告的累计用量分段结算;会话结束时结清,未用完的预留退回。
  • 厂商未报告可核验用量时,会话进入待核对。

下一步#

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