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-bit 取樣對齊');
}
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
本 API 使用 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 模型、工具、權限和目前站點支援範圍 |