Fun-ASR-Realtime
Fun-ASR 实时语音识别,支持热词。
模型 ID(请求中的 model 字段)
fun-asr-realtime快速开始#
- 创建 API Key
在控制台 我的 Key 创建通用 API Key(明文只显示一次),并确认账户余额充足。
- 设置环境变量
export TOKENTOSEA_API_KEY="sk-relay_…",示例从环境变量读取密钥。 - 发送请求
以下为 实时会话 · WebSocket 示例;其他接口见全部调用示例。
- 需要准备单声道 PCM16 音频文件 audio.pcm(可用示例中的 ffmpeg 命令转换)
- 单次会话最多 60 秒。断线后先查询原会话状态,不会自动重连并重新生成。
// npm install ws
// export TOKENTOSEA_API_KEY="你的主站通用 API Key"
// ffmpeg -i your-audio.wav -ac 1 -ar 16000 -f s16le audio.pcm
// node realtime.mjs
import WebSocket from 'ws';
import { readFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
const key = process.env.TOKENTOSEA_API_KEY;
const requestKey = randomUUID();
const taskId = randomUUID().replaceAll('-', '');
const kind = "inference";
const init = {
"header": {
"action": "run-task",
"task_id": taskId,
"streaming": "duplex"
},
"payload": {
"model": "fun-asr-realtime",
"parameters": {
"format": "pcm",
"sample_rate": 16000
},
"input": {}
}
};
const ws = new WebSocket("wss://api.tokentosea.com/v1/realtime?model=fun-asr-realtime", {
headers: { Authorization: 'Bearer ' + key, 'Idempotency-Key': requestKey }
});
let finishing = false;
const send = value => {
if (ws.readyState !== WebSocket.OPEN || value.type === 'session.finish' && finishing) return;
if (value.type === 'session.finish') finishing = true;
ws.send(JSON.stringify({...value,...(value.type ? {event_id:'example-'+randomUUID()} : {})}));
};
let started = false;
async function produce() {
if (started) return; started = true;
if (kind === 'tts-session') {
send({type:'input_text_buffer.append', text:""});
send({type:'input_text_buffer.commit'});
send({type:'session.finish'}); return;
}
const audio = readFileSync('audio.pcm'), step = 3200;
for (let i = 0; i < audio.length && ws.readyState === WebSocket.OPEN; i += step) {
const part = audio.subarray(i, i + step);
if (kind === 'inference') ws.send(part);
else send({type:'input_audio_buffer.append', audio:part.toString('base64')});
await new Promise(resolve => setTimeout(resolve,100));
}
if (kind === 'inference') send({header:{action:'finish-task',task_id:taskId,streaming:'duplex'},payload:{input:{}}});
else send({type:kind === 'translate-auto' ? 'session.finish' : 'input_audio_buffer.commit'});
}
ws.on('open',()=>send(init));
ws.on('message',raw=>{
const event = JSON.parse(raw.toString()), type = event.type ?? event.header?.event;
console.log(event);
if (type === 'session.updated' || type === 'task-started') void produce();
if (type === 'input_audio_buffer.committed' && kind === 'session') send({type:'response.create'});
if (type === 'response.done') {
if (['tts-session','translate-commit'].includes(kind)) send({type:'session.finish'});
else if (kind !== 'translate-auto') ws.close();
}
if (['session.finished','task-finished','error','task-failed'].includes(type) || type === 'conversation.item.input_audio_transcription.completed' && kind === 'asr-session') ws.close();
});
const timer = setTimeout(()=>ws.close(),60000);
ws.on('close',async()=>{
clearTimeout(timer);
const result = await fetch('https://api.tokentosea.com/v1/results/'+requestKey,{headers:{Authorization:'Bearer '+key}}).then(r=>r.json());
console.log('Result / billing status:',result);
// 如仍在结算或处理中,继续 GET 同一地址;不要自动重新连接生成。
});
ws.on('error',error=>console.error(error.message));# pip install "websockets>=13" requests
# export TOKENTOSEA_API_KEY="你的主站通用 API Key"
# ffmpeg -i your-audio.wav -ac 1 -ar 16000 -f s16le audio.pcm
# python realtime.py
import asyncio
import json
import os
import uuid
import requests
from websockets.asyncio.client import connect
from websockets.exceptions import ConnectionClosed
API_KEY = os.environ["TOKENTOSEA_API_KEY"] # 控制台「我的 Key」创建的通用 API Key
REQUEST_KEY = str(uuid.uuid4()) # 握手 Idempotency-Key:会话结束或断线后用它查询结果
TASK_ID = uuid.uuid4().hex
URL = "wss://api.tokentosea.com/v1/realtime?model=fun-asr-realtime"
AUDIO_FILE = "audio.pcm" # 单声道 16000 Hz PCM16(s16le)
CHUNK = 3200 # 每 100 ms 的音频字节数
SESSION = {
"header": {"action": "run-task", "task_id": TASK_ID, "streaming": "duplex"},
"payload": {
"model": "fun-asr-realtime",
"parameters": {"format": "pcm", "sample_rate": 16000},
"input": {},
},
}
async def run_session():
finishing = False
headers = {"Authorization": f"Bearer {API_KEY}", "Idempotency-Key": REQUEST_KEY}
async with connect(URL, additional_headers=headers) as ws:
async def send(event):
nonlocal finishing
if event.get("type") == "session.finish":
if finishing:
return
finishing = True
if "type" in event:
event = {**event, "event_id": f"example-{uuid.uuid4()}"}
try:
await ws.send(json.dumps(event, ensure_ascii=False))
except ConnectionClosed:
pass
async def produce():
with open(AUDIO_FILE, "rb") as source:
audio = source.read()
for start in range(0, len(audio), CHUNK):
try:
await ws.send(audio[start:start + CHUNK])
except ConnectionClosed:
return
await asyncio.sleep(0.1)
await send({"header": {"action": "finish-task", "task_id": TASK_ID, "streaming": "duplex"}, "payload": {"input": {}}})
producer = None
await send(SESSION)
async for message in ws:
if isinstance(message, bytes):
print(f"<{len(message)} 字节二进制帧>")
continue
event = json.loads(message)
print(event)
name = event.get("type") or (event.get("header") or {}).get("event")
if name in ("session.updated", "task-started") and producer is None:
producer = asyncio.create_task(produce())
if name in ("session.finished", "task-finished", "error", "task-failed"):
break
if producer:
producer.cancel()
try:
asyncio.run(asyncio.wait_for(run_session(), timeout=60)) # 单次会话最多 60 秒
except (asyncio.TimeoutError, ConnectionClosed) as error:
print("会话已结束:", type(error).__name__)
# 查询原会话的结果与结算状态;如仍在结算或处理中,继续 GET 同一地址,不要自动重连重新生成。
result = requests.get(
f"https://api.tokentosea.com/v1/results/{REQUEST_KEY}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
).json()
print("Result / billing status:", result)# 实时会话使用 WebSocket,cURL 无法收发会话事件;完整会话见 Python / Node.js 示例。
# 握手地址:wss://api.tokentosea.com/v1/realtime?model=fun-asr-realtime
# 握手请求头:Authorization: Bearer <主站通用 API Key>;Idempotency-Key: <16–128 位幂等键>(可选)
# 首个客户端事件:run-task;音频为单声道 16000 Hz PCM16。单次会话最多 60 秒。
# 会话结束或断线后,用握手时的同一个 Idempotency-Key 查询结果与结算状态;不要自动重连重新生成。
export TOKENTOSEA_API_KEY="你的主站通用 API Key"
REQUEST_KEY="握手时发送的 Idempotency-Key"
curl "https://api.tokentosea.com/v1/results/$REQUEST_KEY" \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY"能力与接口#
实时音频
下表只列出已在该型号上完成验收的接口、响应方式与输入类型;其他组合会返回 422(如 endpoint_not_verified、response_mode_not_verified、input_modality_not_verified)。
| 接口 | JSON | SSE 流式 | WebSocket | 已验证输入 |
|---|---|---|---|---|
实时会话/v1/realtime | 不支持 | 不支持 | 支持 | 音频 |
价格#
官方价格以人民币(CNY)公布,核对日期 2026-10-19。TokenToSea 按官方价格计费、不加价;调用从账户余额(美元)扣除,人民币价格按请求受理时冻结的汇率换算为美元,金额保留 6 位小数并向上取整。
| 计费项 | 单价 | 计量单位 |
|---|---|---|
| 输入时长 | ¥0.00033 | 每秒 |
每次请求先按可能的最高用量预留金额,完成后按供应商返回的实际用量结算并退回差额。计费方式详见计费与结算。
官方价格来源
限制#
本页暂未收录此型号的上下文窗口与最大输出长度,请以厂商文档为准;以下为平台规则。
响应方式:WebSocket。
- 同一账户最多同时进行 8 个已预留或执行中的调用(平台规则)。
- JSON 请求体不超过 48 MB;multipart 上传仅 1 个
file,不超过 25 MB。 - 实时会话:单次最长 60 秒、最多 8 轮,排队中的输入音频不超过 4 MiB;断线后请查询结果,不要自动重连重新生成。
全部调用示例#
每个示例都包含 cURL、Python 与 Node.js 三种写法,发送完全相同的请求体,并用同一个 Idempotency-Key 查询结果。
实时会话 · WebSocket#
WS /v1/realtime?model=fun-asr-realtime 接口参考
- 需要准备单声道 PCM16 音频文件 audio.pcm(可用示例中的 ffmpeg 命令转换)
- 单次会话最多 60 秒。断线后先查询原会话状态,不会自动重连并重新生成。
// npm install ws
// export TOKENTOSEA_API_KEY="你的主站通用 API Key"
// ffmpeg -i your-audio.wav -ac 1 -ar 16000 -f s16le audio.pcm
// node realtime.mjs
import WebSocket from 'ws';
import { readFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
const key = process.env.TOKENTOSEA_API_KEY;
const requestKey = randomUUID();
const taskId = randomUUID().replaceAll('-', '');
const kind = "inference";
const init = {
"header": {
"action": "run-task",
"task_id": taskId,
"streaming": "duplex"
},
"payload": {
"model": "fun-asr-realtime",
"parameters": {
"format": "pcm",
"sample_rate": 16000
},
"input": {}
}
};
const ws = new WebSocket("wss://api.tokentosea.com/v1/realtime?model=fun-asr-realtime", {
headers: { Authorization: 'Bearer ' + key, 'Idempotency-Key': requestKey }
});
let finishing = false;
const send = value => {
if (ws.readyState !== WebSocket.OPEN || value.type === 'session.finish' && finishing) return;
if (value.type === 'session.finish') finishing = true;
ws.send(JSON.stringify({...value,...(value.type ? {event_id:'example-'+randomUUID()} : {})}));
};
let started = false;
async function produce() {
if (started) return; started = true;
if (kind === 'tts-session') {
send({type:'input_text_buffer.append', text:""});
send({type:'input_text_buffer.commit'});
send({type:'session.finish'}); return;
}
const audio = readFileSync('audio.pcm'), step = 3200;
for (let i = 0; i < audio.length && ws.readyState === WebSocket.OPEN; i += step) {
const part = audio.subarray(i, i + step);
if (kind === 'inference') ws.send(part);
else send({type:'input_audio_buffer.append', audio:part.toString('base64')});
await new Promise(resolve => setTimeout(resolve,100));
}
if (kind === 'inference') send({header:{action:'finish-task',task_id:taskId,streaming:'duplex'},payload:{input:{}}});
else send({type:kind === 'translate-auto' ? 'session.finish' : 'input_audio_buffer.commit'});
}
ws.on('open',()=>send(init));
ws.on('message',raw=>{
const event = JSON.parse(raw.toString()), type = event.type ?? event.header?.event;
console.log(event);
if (type === 'session.updated' || type === 'task-started') void produce();
if (type === 'input_audio_buffer.committed' && kind === 'session') send({type:'response.create'});
if (type === 'response.done') {
if (['tts-session','translate-commit'].includes(kind)) send({type:'session.finish'});
else if (kind !== 'translate-auto') ws.close();
}
if (['session.finished','task-finished','error','task-failed'].includes(type) || type === 'conversation.item.input_audio_transcription.completed' && kind === 'asr-session') ws.close();
});
const timer = setTimeout(()=>ws.close(),60000);
ws.on('close',async()=>{
clearTimeout(timer);
const result = await fetch('https://api.tokentosea.com/v1/results/'+requestKey,{headers:{Authorization:'Bearer '+key}}).then(r=>r.json());
console.log('Result / billing status:',result);
// 如仍在结算或处理中,继续 GET 同一地址;不要自动重新连接生成。
});
ws.on('error',error=>console.error(error.message));# pip install "websockets>=13" requests
# export TOKENTOSEA_API_KEY="你的主站通用 API Key"
# ffmpeg -i your-audio.wav -ac 1 -ar 16000 -f s16le audio.pcm
# python realtime.py
import asyncio
import json
import os
import uuid
import requests
from websockets.asyncio.client import connect
from websockets.exceptions import ConnectionClosed
API_KEY = os.environ["TOKENTOSEA_API_KEY"] # 控制台「我的 Key」创建的通用 API Key
REQUEST_KEY = str(uuid.uuid4()) # 握手 Idempotency-Key:会话结束或断线后用它查询结果
TASK_ID = uuid.uuid4().hex
URL = "wss://api.tokentosea.com/v1/realtime?model=fun-asr-realtime"
AUDIO_FILE = "audio.pcm" # 单声道 16000 Hz PCM16(s16le)
CHUNK = 3200 # 每 100 ms 的音频字节数
SESSION = {
"header": {"action": "run-task", "task_id": TASK_ID, "streaming": "duplex"},
"payload": {
"model": "fun-asr-realtime",
"parameters": {"format": "pcm", "sample_rate": 16000},
"input": {},
},
}
async def run_session():
finishing = False
headers = {"Authorization": f"Bearer {API_KEY}", "Idempotency-Key": REQUEST_KEY}
async with connect(URL, additional_headers=headers) as ws:
async def send(event):
nonlocal finishing
if event.get("type") == "session.finish":
if finishing:
return
finishing = True
if "type" in event:
event = {**event, "event_id": f"example-{uuid.uuid4()}"}
try:
await ws.send(json.dumps(event, ensure_ascii=False))
except ConnectionClosed:
pass
async def produce():
with open(AUDIO_FILE, "rb") as source:
audio = source.read()
for start in range(0, len(audio), CHUNK):
try:
await ws.send(audio[start:start + CHUNK])
except ConnectionClosed:
return
await asyncio.sleep(0.1)
await send({"header": {"action": "finish-task", "task_id": TASK_ID, "streaming": "duplex"}, "payload": {"input": {}}})
producer = None
await send(SESSION)
async for message in ws:
if isinstance(message, bytes):
print(f"<{len(message)} 字节二进制帧>")
continue
event = json.loads(message)
print(event)
name = event.get("type") or (event.get("header") or {}).get("event")
if name in ("session.updated", "task-started") and producer is None:
producer = asyncio.create_task(produce())
if name in ("session.finished", "task-finished", "error", "task-failed"):
break
if producer:
producer.cancel()
try:
asyncio.run(asyncio.wait_for(run_session(), timeout=60)) # 单次会话最多 60 秒
except (asyncio.TimeoutError, ConnectionClosed) as error:
print("会话已结束:", type(error).__name__)
# 查询原会话的结果与结算状态;如仍在结算或处理中,继续 GET 同一地址,不要自动重连重新生成。
result = requests.get(
f"https://api.tokentosea.com/v1/results/{REQUEST_KEY}",
headers={"Authorization": f"Bearer {API_KEY}"},
timeout=30,
).json()
print("Result / billing status:", result)# 实时会话使用 WebSocket,cURL 无法收发会话事件;完整会话见 Python / Node.js 示例。
# 握手地址:wss://api.tokentosea.com/v1/realtime?model=fun-asr-realtime
# 握手请求头:Authorization: Bearer <主站通用 API Key>;Idempotency-Key: <16–128 位幂等键>(可选)
# 首个客户端事件:run-task;音频为单声道 16000 Hz PCM16。单次会话最多 60 秒。
# 会话结束或断线后,用握手时的同一个 Idempotency-Key 查询结果与结算状态;不要自动重连重新生成。
export TOKENTOSEA_API_KEY="你的主站通用 API Key"
REQUEST_KEY="握手时发送的 Idempotency-Key"
curl "https://api.tokentosea.com/v1/results/$REQUEST_KEY" \
-H "Authorization: Bearer $TOKENTOSEA_API_KEY"相关文档#
在控制台测试#
登录后可在 模型测试 页面直接调用 fun-asr-realtime,查看返回结果、token 用量与实际扣费。网页调用同样从账户余额扣费。