Skip to main content

AI 语音通话接口

提供两种 AI 语音对话模式:

  • WebRTC 实时语音模式:基于 WebRTC 协议的实时音视频通话,低延迟双向对话,适合长时间连续语音对话场景
  • REST 模式:基于 HTTP 的单轮对话,每次上传一段音频,返回 AI 语音回复,适合非连续的对话场景

认证方式

所有接口均通过请求头中的 X-API-Key 进行认证。

参数名类型必填说明
X-API-Keystring应用密钥

注意:X-API-Key 用于验证账户信息,进入 控制台 → 项目空间,选择对应项目,左侧导航进入「API Key 管理」即可创建/查看。


WebRTC 实时语音模式

接入流程:

客户端 ──HTTP──► /v1/audio/luna/start  ──► 拿到 livekit_url + token + bot_identity


客户端 ──WebRTC── livekit-client(SDK) ────► wss://rtc.vuilabs.cn 入会


AI Bot 自动入会 + 双向音频流


客户端 ──每 2s POST──► /v1/audio/luna/heartbeat (保活 + 探测应否结束)

通话结束 ──POST──► /v1/audio/luna/stop (结算扣费)

1. 开启对话

创建实时语音房间并获取入会 Token,AI Bot 会自动加入房间。

并发支持:同一 API Key 支持多路并发通话,可同时为多个用户提供 AI 服务。每路通话具有独立的 session_id 和 RTC 房间,分别计费,互不干扰。默认不限制并发数,请根据应用需要控制并发。

  • 接口路径/v1/audio/luna/start
  • 请求方式POST

请求参数

参数名类型必填说明
timbrestring角色 ID(默认 default)(联系运营获取可用音色 ID 列表)
client_noncestring客户端生成的唯一标识(UUID),用于幂等去重(30 秒内同 nonce 返回同一会话)
languagestring语言,zh(默认)或 en
sample_rateint上传音频的采样率,默认自动协商
previous_session_idstring上次会话 ID,带上则尝试接续历史对话上下文(网络断开重连场景)

请求示例

curl -X POST "https://api.vuilabs.cn/v1/audio/luna/start" \
-H "X-API-Key: your-secret-key-here" \
-H "Content-Type: application/json" \
-d '{
"timbre": "qinyao",
"client_nonce": "550e8400-e29b-41d4-a716-446655440000",
"language": "zh"
}'

响应参数

参数名类型说明
error_codenumber错误码,0 表示成功
error_messagestring错误信息,成功时为空
succeedboolean请求是否成功
data.session_idstring会话 ID,后续 heartbeat / stop 接口必传
data.livekit_urlstring实时语音服务地址(wss://...),客户端 SDK 入会用
data.room_namestring房间名称
data.identitystring当前用户在房间中的身份标识
data.tokenstring入会 Token(JWT,2 小时有效期)
data.bot_identitystringAI Bot 在房间中的身份标识(用于客户端识别 Bot 音轨)
data.expire_atnumberToken 过期时间(Unix 秒)

成功响应示例

{
"error_code": 0,
"error_message": "",
"succeed": true,
"data": {
"session_id": "8f918356376b4fd2b2e0fe22965c1d0c",
"livekit_url": "wss://rtc.vuilabs.cn",
"room_name": "8f918356376b4fd2b2e0fe22965c1d0c",
"identity": "user_f267d71682ac49f2",
"token": "eyJhbGciOiJIUzI1NiIs...",
"bot_identity": "agent-luna-AJ_exnxZ",
"expire_at": 1778167601
}
}

2. 心跳保活

通话期间请每 2 秒发送一次心跳以保持会话,并通过响应判断是否需要结束通话(如余额耗尽或超时)。

  • 接口路径/v1/audio/luna/heartbeat
  • 请求方式POST

重要:心跳停止超过 30 秒,会话将自动结束,并按已通话时长扣费。

请求参数

参数名类型必填说明
session_idstringstart 接口返回的会话 ID

请求示例

curl -X POST "https://api.vuilabs.cn/v1/audio/luna/heartbeat" \
-H "X-API-Key: your-secret-key-here" \
-H "Content-Type: application/json" \
-d '{
"session_id": "8f918356376b4fd2b2e0fe22965c1d0c"
}'

响应参数

参数名类型说明
error_codenumber错误码,0 表示成功
error_messagestring错误信息,成功时为空
succeedboolean请求是否成功
data.should_closeboolean是否需要结束通话(余额耗尽 / 超时 / 异常)
data.remaining_secondsnumber剩余可通话秒数(基于余额估算)
data.elapsed_secondsnumber已通话秒数

成功响应示例

{
"error_code": 0,
"error_message": "",
"succeed": true,
"data": {
"should_close": false,
"remaining_seconds": 1503,
"elapsed_seconds": 45
}
}

客户端处理建议should_close=true 时立刻调用 /stop 接口结束会话并向用户提示原因(余额不足等)。


3. 结束对话

通话结束时调用此接口以关闭房间、结束与 AI Bot 的通话,并按通话时长结算扣费。接口幂等 — 重复调用返回同一结果,不会重复扣费。

  • 接口路径/v1/audio/luna/stop
  • 请求方式POST

通话结束后请务必调用此接口。如果应用异常退出未调用,会话将在心跳超时(30 秒)后自动结算。

请求参数

参数名类型必填说明
session_idstringstart 接口返回的会话 ID
reasonstring结束原因,如 user_stop(用户主动)、tab_closed(关闭页面)、balance_exhausted(余额耗尽)。仅用于日志/统计

请求示例

curl -X POST "https://api.vuilabs.cn/v1/audio/luna/stop" \
-H "X-API-Key: your-secret-key-here" \
-H "Content-Type: application/json" \
-d '{
"session_id": "8f918356376b4fd2b2e0fe22965c1d0c",
"reason": "user_stop"
}'

响应参数

参数名类型说明
error_codenumber错误码,0 表示成功
error_messagestring错误信息,成功时为空
succeedboolean请求是否成功
data.total_secnumber本次通话总秒数
data.total_billednumber实际扣费(微元,1 元 = 1,000,000 微元)。免费池抵扣的部分计入 0

成功响应示例

{
"error_code": 0,
"error_message": "",
"succeed": true,
"data": {
"total_sec": 45,
"total_billed": 0
}
}

4. Web SDK 接入(livekit-client)

拿到 start 接口返回的 livekit_url + token 后,使用官方 livekit-client 加入房间。

安装依赖

npm install livekit-client
# 或 pnpm / yarn
pnpm add livekit-client

完整接入示例

<!DOCTYPE html>
<html>
<head>
<title>VUI Labs Luna 实时对话</title>
</head>
<body>
<button id="startBtn">开始通话</button>
<button id="stopBtn" disabled>结束通话</button>
<div id="status"></div>

<script type="module">
import { Room, RoomEvent, Track } from 'https://cdn.skypack.dev/livekit-client@2.5.0';

const API_KEY = 'your-secret-key-here';
const API_BASE = 'https://api.vuilabs.cn';

let room = null;
let sessionId = null;
let heartbeatTimer = null;

const statusEl = document.getElementById('status');
const setStatus = (s) => { statusEl.textContent = s; };

document.getElementById('startBtn').onclick = async () => {
setStatus('正在开启会话...');

// 1. 调用 start 接口拿入会 URL + token
const startResp = await fetch(`${API_BASE}/v1/audio/luna/start`, {
method: 'POST',
headers: {
'X-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
timbre: 'qinyao',
client_nonce: crypto.randomUUID(),
language: 'zh',
}),
}).then(r => r.json());

if (!startResp.succeed) {
setStatus(`失败: ${startResp.error_message}`);
return;
}
const { livekit_url, token, bot_identity } = startResp.data;
sessionId = startResp.data.session_id;

// 2. 创建 Room
room = new Room({ adaptiveStream: true, dynacast: true });

// 监听 AI Bot 的音频(根据 bot_identity 过滤)
room.on(RoomEvent.TrackSubscribed, (track, publication, participant) => {
if (track.kind !== Track.Kind.Audio) return;
if (participant.identity !== bot_identity) return;
const audioEl = track.attach();
audioEl.autoplay = true;
document.body.appendChild(audioEl);
});

room.on(RoomEvent.Disconnected, () => {
setStatus('已断开');
cleanup();
});

// 3. 入会 + 开麦
await room.connect(livekit_url, token);
await room.localParticipant.setMicrophoneEnabled(true);
setStatus('通话中...');
document.getElementById('startBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;

// 4. 心跳(每 2s)
heartbeatTimer = setInterval(async () => {
const hb = await fetch(`${API_BASE}/v1/audio/luna/heartbeat`, {
method: 'POST',
headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId }),
}).then(r => r.json());

if (hb.data?.should_close) {
setStatus('通话需要结束(余额不足或超时)');
await endCall('balance_exhausted');
}
}, 2000);
};

document.getElementById('stopBtn').onclick = () => endCall('user_stop');

// 关闭页面时尝试 stop(避免会话泄漏)
// 用 fetch + keepalive 而非 sendBeacon — keepalive 允许跨 unload 发请求且支持 X-API-Key header
// (sendBeacon 不能加自定义 header,会丢 API Key 鉴权)
window.addEventListener('pagehide', () => {
if (!sessionId) return;
fetch(`${API_BASE}/v1/audio/luna/stop`, {
method: 'POST',
headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, reason: 'tab_closed' }),
keepalive: true,
}).catch(() => { /* 页面卸载时请求失败,会话将在心跳超时后自动结束并结算 */ });
});

async function endCall(reason) {
if (!sessionId) return;
const sid = sessionId;
cleanup();
setStatus('正在结束...');
const resp = await fetch(`${API_BASE}/v1/audio/luna/stop`, {
method: 'POST',
headers: { 'X-API-Key': API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sid, reason }),
}).then(r => r.json());
setStatus(`已结束,通话 ${resp.data?.total_sec ?? 0}s,扣费 ${resp.data?.total_billed ?? 0} 微元`);
}

function cleanup() {
if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; }
if (room) { room.disconnect(); room = null; }
sessionId = null;
document.getElementById('startBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
}
</script>
</body>
</html>

关键注意事项

  1. 浏览器自动播放限制:首次连接前必须有用户点击事件,否则 audio.play() 会被拦截。本示例的"开始通话"按钮已满足此要求。
  2. 心跳间隔:推荐 2 秒一次,心跳停止超过 30 秒会自动结束会话。
  3. stop 必调:页面关闭/刷新前用 fetch + keepalive: true 发送 stop 请求(允许跨页面卸载且支持自定义 header),不要用 navigator.sendBeacon — sendBeacon 不能加自定义 header,会丢 X-API-Key 鉴权。未调用 stop 时,会话将在心跳超时(30 秒)后自动结算。为及时结束通话并结算,请主动调用。
  4. AI 音轨识别:start 接口返回的 bot_identity 是 AI Bot 在房间中的标识,客户端通过它过滤出 Bot 的音频流(避免播放回声等其他流)。
  5. 媒体面端口:实时语音媒体面优先走 UDP 50000-60000,严格 NAT 网络下 SDK 自动 fallback 到 TCP 7881,无需额外配置。

REST 模式

REST 模式提供流式对话:

  • 流式对话:WebSocket 双向通信,边录边传,首字响应 < 3 秒(推荐实时对话使用)

推荐:实时语音通话场景优先选 WebRTC 实时语音模式;REST 流式对话适合不需要持续双工的场景(如一问一答、长输入推理)。

5. 创建对话会话

创建一个 REST 对话会话,返回 session_id。每次对话请求需携带 session_id 以维持上下文连续性。

  • 接口路径/api/v1/audio/sessions
  • 请求方式POST

请求参数

参数名类型必填说明
voice_idstring角色 ID(联系运营获取可用音色 ID 列表)

请求示例

curl -X POST "https://api.vuilabs.cn/api/v1/audio/sessions" \
-H "X-API-Key: your-secret-key-here" \
-H "Content-Type: application/json" \
-d '{
"voice_id": "qinyao"
}'

响应参数

参数名类型说明
error_codenumber错误码,0 表示成功
error_messagestring错误信息,成功时为空
succeedboolean请求是否成功
data.session_idstring会话 ID,后续对话请求中使用
data.voice_idstring角色 ID
data.created_atnumber会话创建时间(Unix 秒)

成功响应示例

{
"error_code": 0,
"error_message": "",
"succeed": true,
"data": {
"session_id": "sess_xxxxxxxx",
"voice_id": "qinyao",
"created_at": 1743696000
}
}

6. 删除对话会话

删除指定会话。接口幂等,会话不存在时同样返回成功。

  • 接口路径/api/v1/audio/sessions/:session_id
  • 请求方式DELETE

请求示例

curl -X DELETE "https://api.vuilabs.cn/api/v1/audio/sessions/sess_xxxxxxxx" \
-H "X-API-Key: your-secret-key-here"

响应参数

参数名类型说明
error_codenumber错误码,0 表示成功
error_messagestring错误信息,成功时为空
succeedboolean请求是否成功
data.deletedboolean是否已删除(幂等,会话不存在时也返回 true

成功响应示例

{
"error_code": 0,
"error_message": "",
"succeed": true,
"data": {
"deleted": true
}
}

7. 流式对话

通过 WebSocket 建立双向流式通信,边录音边发送音频,并实时接收识别结果和语音回复,首字响应 < 3 秒。适合实时语音通话、AI 助手对话等场景。

  • 接口路径/api/v1/audio/conversation/stream
  • 通信协议:WebSocket
  • 鉴权方式:URL 参数 api_key

连接示例

// 建立 WebSocket 连接(api_key 通过 URL 参数传递)
const ws = new WebSocket(
"wss://api.vuilabs.cn/api/v1/audio/conversation/stream?api_key=your-secret-key-here"
);

通信流程

1. 发送元信息(JSON 文本消息)

连接建立后,首先发送会话元信息:

ws.send(JSON.stringify({
meta: {
session_id: "sess_xxxxxxxx", // 可选,不传则自动创建
voice_id: "qinyao", // 必填,角色 ID
audio_format: "pcm16", // 音频格式:pcm16(16kHz, 16bit, 单声道)
language: "zh" // 语言,默认 zh
}
}));
字段类型必填说明
session_idstring会话 ID,不传则自动创建
voice_idstring角色 ID(联系运营获取可用音色 ID 列表)
audio_formatstring音频格式,固定为 pcm16(16kHz, 16bit, 单声道)
languagestring语言,zh(默认)或 en

2. 发送音频流(二进制消息)

客户端边录边传,实时发送音频片段:

// 录音并发送音频片段(每 100ms 发送一次)
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
const mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0) {
ws.send(e.data); // 发送二进制音频片段
}
};
mediaRecorder.start(100); // 每 100ms 触发一次
});

输入音频格式:PCM 16bit, 16kHz, 单声道(通过 WebSocket BinaryMessage 发送原始 PCM 数据)

3. 发送结束标记(可选)

用户停止录音时发送结束标记:

ws.send(JSON.stringify({ is_final: true }));

推送事件

通过 WebSocket 接收以下事件:

session_created — 会话创建成功

字段类型说明
typestring"session_created"
session_idstring会话 ID(若未传则自动创建)
{"type":"session_created","session_id":"sess_auto_12345"}

asr_partial — 实时语音识别中间结果

边说话边推送识别中间文本,实时反馈用户输入。

字段类型说明
typestring"asr_partial"
asr_textstring当前识别到的文本(可能不完整)
{"type":"asr_partial","asr_text":"你好"}

asr_final — 语音识别最终结果

用户说完一句话后推送最终识别文本。

字段类型说明
typestring"asr_final"
asr_textstring最终识别文本
{"type":"asr_final","asr_text":"你好,我是"}

llm_first_token — LLM 首字生成

大模型开始生成回复时推送首字,标志着 AI 开始回答。

字段类型说明
typestring"llm_first_token"
first_tokenstring首个生成的字符
{"type":"llm_first_token","first_token":"我"}

audio — 语音回复音频片段(二进制消息)

AI 回复的音频片段以二进制消息推送,客户端可直接播放。

ws.onmessage = (event) => {
if (event.data instanceof ArrayBuffer) {
// 二进制音频消息,直接播放
playAudioChunk(event.data);
}
};

音频格式:MP3 格式(原始二进制数据,非 Base64 编码,可直接使用 Audio 元素播放)

done — 对话完成

包含完整的统计信息,特别是首字响应、首音频响应时间和音频时长。

字段类型说明
typestring"done"
reply_textstringAI 回复完整文本
first_token_msnumberLLM 首字响应时间(毫秒)
first_audio_msnumber首音频响应时间(毫秒)
asr_msnumber语音识别总耗时(毫秒)
llm_msnumber大模型推理耗时(毫秒)
tts_msnumber语音合成总耗时(毫秒)
total_msnumber本轮对话总耗时(毫秒)
audio_duration_secnumberAI 回复音频时长(秒),用于计费
{
"type":"done",
"reply_text":"你好,我是AI助手沁瑶,有什么可以帮助你的吗?",
"first_token_ms":1200,
"first_audio_ms":2500,
"asr_ms":800,
"llm_ms":1500,
"tts_ms":1000,
"total_ms":3300,
"audio_duration_sec":5
}

error — 错误事件

字段类型说明
typestring"error"
codestring错误码,如 "grpc_error"
messagestring错误描述
{"type":"error","code":"grpc_error","message":"创建连接失败"}

完整示例(JavaScript)

<!DOCTYPE html>
<html>
<head>
<title>全双工语音对话示例</title>
</head>
<body>
<button id="startBtn">开始对话</button>
<button id="stopBtn">停止对话</button>
<div id="status"></div>
<div id="asrText"></div>
<div id="replyText"></div>

<script>
let ws;
let mediaRecorder;

document.getElementById('startBtn').onclick = async () => {
ws = new WebSocket('wss://api.vuilabs.cn/api/v1/audio/conversation/stream?api_key=your-secret-key-here');

ws.onopen = async () => {
document.getElementById('status').textContent = 'WebSocket 已连接';
ws.send(JSON.stringify({
meta: { voice_id: 'qinyao', audio_format: 'pcm16', language: 'zh' }
}));

const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
mediaRecorder = new MediaRecorder(stream);
mediaRecorder.ondataavailable = (e) => {
if (e.data.size > 0 && ws.readyState === WebSocket.OPEN) {
ws.send(e.data);
}
};
mediaRecorder.start(100);
};

ws.onmessage = (event) => {
if (typeof event.data === 'string') {
const msg = JSON.parse(event.data);
if (msg.type === 'asr_partial') {
document.getElementById('asrText').textContent = `实时识别: ${msg.asr_text}`;
} else if (msg.type === 'asr_final') {
document.getElementById('asrText').textContent = `最终识别: ${msg.asr_text}`;
} else if (msg.type === 'done') {
document.getElementById('replyText').textContent = `AI回复: ${msg.reply_text}`;
document.getElementById('status').textContent =
`完成 - 首字响应: ${msg.first_token_ms}ms, 首音频: ${msg.first_audio_ms}ms`;
stopConversation();
} else if (msg.type === 'error') {
document.getElementById('status').textContent = `错误: ${msg.code} - ${msg.message}`;
}
} else if (event.data instanceof ArrayBuffer) {
playAudio(event.data);
}
};

ws.onclose = () => {
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
};
};

document.getElementById('stopBtn').onclick = stopConversation;

function stopConversation() {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ is_final: true }));
ws.close();
}
if (mediaRecorder && mediaRecorder.state !== 'inactive') {
mediaRecorder.stop();
}
}

function playAudio(audioData) {
const blob = new Blob([audioData], { type: 'audio/mpeg' });
const audioUrl = URL.createObjectURL(blob);
const audio = new Audio(audioUrl);
audio.play();
}
</script>
</body>
</html>

计费说明

具体计费规则、价格和免费额度请参见 产品计费


错误码

错误码说明
0成功
20001未授权,请检查 X-API-Key
20003参数错误
40011余额不足,无法开始通话
40012通话进行中,请先结束当前通话(仅 Web JWT 单路场景)
40013并发数已达上限(默认 API Key 不限制;如需启用上限请联系运营)
50000服务异常