GPT-Live 实时语音
通过 GPT-Live WebSocket 进行全双工语音交互并处理会话事件。
GPT-Live 通过持久 WebSocket 提供全双工语音交互,可在同一会话中发送输入音频、接收语音与转写结果、更新会话配置并保持多轮上下文。当前模型记录为 gpt-live-1,实际可用范围以当前账户和模型目录为准。
wss://api.agtcloud.cn/v1/live/sessions认证与连接
服务端应用在 WebSocket 握手 Header 中使用 Bearer API Key。模型不放在查询参数中,而是在连接后的首个 session.start 事件中配置。
| 项目 | 值 |
|---|---|
| 连接地址 | wss://api.agtcloud.cn/v1/live/sessions |
| Authorization | Bearer your-api-key |
| 模型位置 | session.start.session.model |
| 输入音频 | Base64 编码的裸 PCM 音频块 |
长期 API Key 只应保存在可信服务端。本文只覆盖该 WebSocket 路由,不包含 WebRTC、SIP、sideband、会话 fork 或录音下载。
会话流程
- 使用 Bearer API Key 建立 WebSocket 连接。
- 发送
session.start,配置模型、指令、音频和委托方式。 - 收到
session.started后开始发送音频或应用命令。 - 持续处理输入转写、输出转写、输出音频和用量事件。
- 停止音频生产后发送
session.close。 - 继续接收事件,以
session.closed中的最终用量完成收尾。
说话轮次结束不等于会话结束。多轮对话应保持同一连接,只有业务准备结束整个会话时才发送 session.close。
启动参数
| 字段 | 类型 | 说明 |
|---|---|---|
type | string | 首帧固定为 session.start |
event_id | string | 可选的客户端事件标识,用于关联错误 |
session.model | string | 必填,GPT-Live 模型 ID |
session.instructions | string | 可选的语音助手指令 |
session.audio.format | object | 音频格式,例如 audio/pcm、24 kHz |
session.audio.output.voice | string | 输出音色,例如 marin;可选值取决于模型 |
session.delegation | object | 委托方式,使用 client 或 responses |
连接启动后可以使用 session.update 更新指令等允许修改的会话配置,并等待 session.updated 确认。不要在同一连接上再次发送 session.start,也不要通过更新事件切换顶层语音模型。
音频准备与调用示例
示例采用 PCM signed 16-bit little-endian、24 kHz、单声道音频,不包含 WAV 文件头。每 100 ms 为 4800 字节,发送时将每个音频块编码为 Base64。
# 准备输入音频
ffmpeg -i input.wav -f s16le -acodec pcm_s16le -ac 1 -ar 24000 audio.pcm
# 播放保存的输出音频
ffplay -f s16le -ar 24000 -ac 1 reply.pcmNode.js 示例需要 ws;Python 示例只检查会话启动与关闭,不发送音频。
npm install ws
python -m pip install websocket-clientimport fs from 'node:fs';
import WebSocket from 'ws';
const key = process.env.API_KEY;
const file = process.argv[2];
if (!key || !file) throw new Error('需要 API_KEY 和 PCM 文件路径');
const pcm = fs.readFileSync(file);
if (!pcm.length || pcm.length % 2) {
throw new Error('PCM 必须非空且按 16 位样本对齐');
}
const ws = new WebSocket('wss://api.agtcloud.cn/v1/live/sessions', {
headers: { Authorization: 'Bearer ' + key },
handshakeTimeout: 10000,
});
const audioOut = fs.createWriteStream('reply.pcm');
let started = false;
let closing = false;
let finalized = false;
let sender;
let drain;
let offset = 0;
let inputFinishedAt = 0;
let lastOutputAt = 0;
function send(event) {
ws.send(JSON.stringify(event));
}
function closeSession(reason) {
if (closing) return;
closing = true;
clearInterval(sender);
console.error('请求结束:', reason);
if (started && ws.readyState === WebSocket.OPEN) {
send({ type: 'session.close' });
drain = setTimeout(() => ws.terminate(), 15000);
} else {
ws.terminate();
}
}
const limit = setTimeout(() => closeSession('60 秒示例上限'), 60000);
ws.on('open', () => send({
type: 'session.start',
event_id: 'start_001',
session: {
model: 'gpt-live-1',
instructions: '请用简短中文回答,等待用户先说话。',
audio: {
format: { type: 'audio/pcm', rate: 24000 },
output: { voice: 'marin' },
},
delegation: { type: 'client' },
},
}));
ws.on('message', (raw) => {
let event;
try {
event = JSON.parse(raw.toString());
} catch {
closeSession('无效 JSON');
return;
}
if (event.type !== 'session.output_audio.delta') {
console.log(raw.toString());
}
if (event.type === 'session.started' && !started) {
started = true;
sender = setInterval(() => {
if (closing || ws.readyState !== WebSocket.OPEN) return;
if (offset >= pcm.length) {
inputFinishedAt ||= Date.now();
if (
lastOutputAt &&
Date.now() - Math.max(lastOutputAt, inputFinishedAt) >= 3000
) {
closeSession('输出结束');
return;
}
send({
type: 'session.input_audio.append',
audio: Buffer.alloc(4800).toString('base64'),
});
return;
}
const chunk = pcm.subarray(offset, offset + 4800);
offset += chunk.length;
send({
type: 'session.input_audio.append',
audio: chunk.toString('base64'),
});
}, 100);
}
if (event.type === 'session.output_audio.delta') {
lastOutputAt = Date.now();
audioOut.write(Buffer.from(event.delta, 'base64'));
}
if (event.type === 'session.output_transcript.delta') {
lastOutputAt = Date.now();
}
if (event.type === 'session.closed') {
finalized = true;
ws.close(1000);
}
if (event.type === 'error') closeSession('服务错误');
});
ws.on('error', (error) => {
console.error(error.message);
closeSession('连接错误');
});
ws.on('close', (code, reason) => {
clearTimeout(limit);
clearTimeout(drain);
clearInterval(sender);
audioOut.end();
console.error({ code, reason: reason.toString(), finalized });
if (!finalized || code !== 1000) process.exitCode = 1;
});
process.on('SIGINT', () => closeSession('用户停止'));事件 Reference
本接口使用 session.input_audio.append 发送 Base64 音频,不使用 Muse 的二进制音频帧和 endStream,也不使用 OpenAI Realtime 的 input_audio_buffer.* 事件。
session.start
启动 GPT-Live 会话并提交模型、指令、音频和委托配置。
| 字段 | 类型 | 必填 | 说明 | 示例值 |
|---|---|---|---|---|
type | 字符串 | 是 | 固定为 session.start。 | session.start |
event_id | 字符串 | 否 | 客户端事件标识。 | start_001 |
session.model | 字符串 | 是 | GPT-Live 模型 ID。 | gpt-live-1 |
session.instructions | 字符串 | 否 | 语音助手的对话指令。 | 请用简短中文回答。 |
session.audio.format.type | 字符串 | 否 | 音频格式类型。 | audio/pcm |
session.audio.format.rate | 整数 | 否 | 音频采样率。 | 24000 |
session.audio.output.voice | 字符串 | 否 | 输出音色。 | marin |
session.delegation.type | 字符串 | 是 | 委托方式。 | client |
{
"type": "session.start",
"event_id": "start_001",
"session": {
"model": "gpt-live-1",
"instructions": "请用简短中文回答。",
"audio": {
"format": {
"type": "audio/pcm",
"rate": 24000
},
"output": {
"voice": "marin"
}
},
"delegation": {
"type": "client"
}
}
}用量与计费
session.usage.updated 返回的是当前会话的累计值,不表示单独一轮的新增用量。客户端不应把多个阶段值相加;正常关闭时,以 session.closed.usage.seconds 为最终会话时长。
费用可能包含语音会话时长、后台模型用量和工具调用。具体价格及当前模型可用范围以模型广场为准。
委托方式
client:应用自行处理需要委托的任务,适合先验证基础语音会话。responses:由服务端执行配置的 Responses 模型和工具。目标模型、工具和权限必须在当前站点可用。
使用 responses 时,后台任务的模型和工具费用可能独立产生。不要把示例中的 MODEL_ID 视为固定可用模型。
错误处理
| 现象 | 检查与处理 |
|---|---|
| 握手返回 401 | 检查 Bearer API Key、模型权限和当前站点地址 |
| 路径返回 404 | 确认最终连接路径为 /v1/live/sessions,没有重复拼接 /v1 |
| 首帧错误或启动超时 | 连接后先发送 session.start,收到 session.started 后再发送音频 |
| 无声音或声音失真 | 检查 PCM 位深、字节序、采样率、声道和 WAV 文件头 |
未收到 session.closed | 保存最后一个累计用量、错误事件和关闭原因,不自动重放整段音频 |
| 后台委托被拒绝 | 检查 Responses 模型、工具、权限和当前站点支持范围 |