Voice Agent 前端接入接口文档
本文档面向前端工程师,描述与后端实时语音代理的 WebSocket 协议、二进制音频格式、事件契约与接入示例。文档基于后端的桥接实现,目标是帮助前端同学快速对接并稳定播放 / 采集音频与显示转写与事件。
1 概要
- 连接方式:WebSocket(推荐使用 WSS)
- 默认路径示例:
wss://{host}/api/v1/voice/agent(服务器可按需要更改) - 消息格式:控制类消息使用 JSON 文本;音频数据使用裸二进制(Int16 LE)发送/接收
- 单连接对应一条后台会话(bridge);前端负责音频采集、重采样、发送与播放
2 认证与连接(建议)
后端应要求鉴权。常见做法(任选其一并与后端约定):
- 在连接 URL 上加入短期 token:
wss://host/api/v1/voice/agent?token=xxxxx - 或使用
Sec-WebSocket-Protocol/ 子协议传 token - 或先通过 HTTP 登录拿到 cookie / bearer token,再建立同源 WSS
前端示例(带 query token):
const url = `wss://api.example.com/api/v1/voice/agent?token=${encodeURIComponent(token)}`;
const ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
3 消息类型总览
请求(前端 → 后端)和响应(后端 → 前端)各自的常用 type 列表与含义如下。
3.1 前端 → 后端(请求)
setup:初始化会话(可带 system_prompt / user_prompt){ "type": "setup", "system_prompt": "你是个简短回答的助手", "user_prompt": "上下文..." }text:发送文本(即时作为用户输入){ "type": "text", "text": "今天天气怎样?" }audio_end:告知服务端当前音频流已结束(可选,提示 turn 结尾){ "type": "audio_end" }close:客户端主动关闭会话(也可直接关闭 WebSocket){ "type": "close" }二进制流(裸二进制 ArrayBuffer):16 kHz、16-bit PCM、小端序(Int16),每次发送一段音频数据包(不封 JSON)。
3.2 后端 → 前端(响应 / 事件)
setup_received/setup_sent_to_model/setup_complete:setup 流程相关gemini_connected:后端与模型已连接,通常携带sessionIdtranscript_in:用户语音转写片段(text 字段)transcript_out:模型输出转写片段(text 字段)text:模型输出文本(text 字段),可多次增量发送turn_complete:一次对话回合完成go_away:服务端提示将断开(可能含 timeLeft)usage:usage/token 统计(promptTokenCount / responseTokenCount / totalTokenCount)inline_data:当某些 inline 数据不是标准 PCM 时(以 base64 文本)function_call:模型发起函数/工具调用(name/参数)error:错误消息(message / where)
后端也会直接发送二进制音频数据包(ArrayBuffer),表示模型合成的 PCM 音频流(通常 24 kHz、16-bit PCM)。
4 JSON 消息字段定义(Schema 说明)
通用响应体(简化):
{
"type": "transcript_in", // 事件类型
"text": "部分文本", // 事件文本(视类型而定)
"sessionId": "xxx", // 仅部分事件包含
"timeLeft": "PT30S", // go_away
"promptTokenCount": 12, // usage
"responseTokenCount": 34,
"totalTokenCount": 46,
"message": "错误说明", // error
"where": "位置或模块" // error
}
前端发送 setup:
{
"type": "setup",
"system_prompt": "string (optional)",
"user_prompt": "string (optional)"
}
发送 text:
{ "type": "text", "text": "..." }
音频二进制格式说明(非常重要):
- 前端 → 后端:Int16 PCM,小端(little-endian),采样率:16000 Hz,单声道
- 后端 → 前端(模型合成):Int16 PCM,小端,采样率常见为 24000 Hz(以实际返回的 mime 为准,客户端应按约定或 mime 处理)
5 WebSocket 生命周期与事件处理(JS 样例)
下面给出前端主流程示例(主要片段),便于直接复制到项目中使用。
// 创建连接
const ws = new WebSocket(url);
ws.binaryType = "arraybuffer";
ws.onopen = () => {
// 连接建立后发送 setup(可为空)
ws.send(JSON.stringify({
type: "setup",
system_prompt: systemPrompt,
user_prompt: userPrompt
}));
};
// 接收消息
ws.onmessage = async (evt) => {
if (typeof evt.data === "string") {
const obj = JSON.parse(evt.data);
handleTextEvent(obj);
} else if (evt.data instanceof ArrayBuffer) {
// 二进制:模型合成的 PCM(例如 24k Int16)
playReceivedAudio(evt.data);
}
};
function handleTextEvent(obj) {
switch (obj.type) {
case "transcript_in": showTranscriptIn(obj.text); break;
case "transcript_out": showTranscriptOut(obj.text); break;
case "text": appendModelText(obj.text); break;
case "turn_complete": handleTurnComplete(); break;
case "setup_complete": onSetupComplete(); break;
case "usage": showUsage(obj); break;
case "go_away": notifyGoAway(obj.timeLeft); break;
case "error": showError(obj.where, obj.message); break;
default: console.debug("未知事件", obj);
}
}
6 发送音频(前端采集与发送要点)
- 采样:建议在前端将麦克风采样重采样到 16000 Hz(若设备采样率不同),并转换为 Int16(范围 -32768..32767)。示例代码中使用
AudioWorklet或ScriptProcessor。 - 打包:每次发送建议 20–80 ms 的音频(例如 40 ms → 640 samples @16k),以获得低延迟与良好网络利用率。
- 发送示例(Int16Array 转 ArrayBuffer):
// i16 是 Int16Array
ws.send(i16.buffer);
- 发送
audio_end用于显式告知后端本次说话已结束(可触发模型端 turn 结算);若不发送,也可依赖服务端 VAD。
7 播放模型返回音频(接收端重采样与播放要点)
- 接收到后端二进制(Int16Array),先转为 Float32([-1,1]),再重采样到本地
AudioContext.sampleRate,然后用AudioBuffer播放。 - 保持一个简单播放队列(
nextPlayTime)来保证连续性;若队列落后过多可丢弃旧段并追赶实时。 - 示例播放函数(简化):
function playReceivedAudio(arrayBuffer) {
const i16 = new Int16Array(arrayBuffer);
const f32 = new Float32Array(i16.length);
for (let i = 0; i < i16.length; i++) f32[i] = i16[i] / 32768;
const resampled = resampleLinear(f32, 24000, audioCtx.sampleRate);
const buf = audioCtx.createBuffer(1, resampled.length, audioCtx.sampleRate);
buf.copyToChannel(resampled, 0);
const src = audioCtx.createBufferSource();
src.buffer = buf;
src.connect(audioCtx.destination);
src.start(nextPlayTime);
nextPlayTime += buf.duration;
}
- 注意:实际输出采样率应以后端返回的 mime 或协议约定为准(示例常见为 24000 Hz)。
8 状态与错误处理建议
- 在
onclose中判断code与reason,如果是短期网络问题可尝试重连(带指数退避),但要注意后端会话可能不可恢复(需重新setup)。 - 在接收到
go_away时,提示用户并准备重连或保存本地状态。 - 对
error事件友好提示用户并把错误发回 Sentry / 日志以便排查。 - 对后端
usage事件进行统计并可在界面显示消耗提醒。
9 性能与带宽优化
- 前端打包音频帧大小需权衡延迟与包开销(20–80 ms 常用)。
- 如果网络不稳定,可暂时降低语音发送频率或合并多帧。
- 可在服务端启用 VAD/活动检测,避免空白静音数据上传(示例后端已启用 AutomaticActivityDetection)。
10 调试方法与常见问题排查清单
无法建立 WebSocket:
- 检查 WSS/WS 地址、端口、防火墙与证书(WSS 需要有效证书)。
后端收不到二进制:
- 确认
ws.binaryType = "arraybuffer";确认发送方发送的是i16.buffer而非i16的副本错误。
- 确认
转写为空/不连贯:
- 检查前端是否正确重采样到 16k;检查是否正确发送小端 Int16。
合成音频异常(噪音/不可播放):
- 确认后端发送的是 Int16 PCM 小端,并与约定采样率一致;前端按正确采样率解码并重采样。
出现
go_away或 1011/1012 类型关闭:- 查看后端日志(可能是模型端配额或超时),后端应将
go_away的 timeLeft 告知前端。
- 查看后端日志(可能是模型端配额或超时),后端应将
11 示例完整接入流程(序列)
[浏览器] --(WS open)--> [后端]
[浏览器] --JSON setup--> [后端] --(send prompts to model)--> [模型]
[浏览器] --Int16 PCM--> [后端] --(realtime input)--> [模型]
[模型] --audio/text/transcript--> [后端] --JSON/text or binary--> [浏览器]
[浏览器] --audio_end or text--> [后端] --(client content to model)--> [模型]
[模型] --turnComplete/usage--> [后端] --> [浏览器]
12 最佳实践小结(给前端同学)
- 优先使用
AudioWorklet做实时采集并重采样到 16k;若不可用,使用ScriptProcessor回退。 - 发送二进制时确保使用 Int16 小端序的
ArrayBuffer。 - 在播放层实现小队列(
nextPlayTime)以保证连续播放且在落后时丢弃旧帧。 - 在建立连接前完成鉴权(token/cookie),并保护 token。
- 实现健壮的错误 / 断连重连策略并保存必要的会话状态(例如最后一条 transcript)。
- 与后端约定好 model 返回的输出采样率(若不确定,在二进制消息外协商或在 JSON 事件里包含 mime / sampleRate 字段)。
13 附:常用代码片段集合(发送 setup、发送文本、发送 Int16)
发送 setup:
ws.send(JSON.stringify({
type: "setup",
system_prompt: "你是一个简洁的语音助手",
user_prompt: "context info"
}));
发送文本:
ws.send(JSON.stringify({ type: "text", text: "帮我做个总结" }));
发送 Int16 PCM(示例:i16 是 Int16Array):
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(i16.buffer);
}
发送 audio_end:
ws.send(JSON.stringify({ type: "audio_end" }));
