Realtime API OpenAI позволяет браузеру передавать звук микрофона и воспроизводить голосовой ответ по WebRTC. Это руководство создает один локальный разговор с gpt-realtime-2.1. Ключ API остается на Node-сервере вашего компьютера. Нужен проект OpenAI API с доступом к модели и оплатой; подписка ChatGPT не оплачивает API. BIG CHANGE проверил текущую документацию и пример кода, но не открывал платную сессию и не слышал ответа.
Главное изменение
- Что изменилось: OpenAI документирует браузерный WebRTC для разговора Realtime: локальный сервер хранит ключ проекта и обменивает предложение соединения браузера на ответ.
- Почему это важно: Разработчик может проверить весь путь от микрофона до голосового ответа на локальной странице до создания большого продукта. Владелец проекта контролирует ключ и платит за API.
- На что обратить внимание: Работающий локальный обмен не доказывает готовность к эксплуатации. Общей версии нужны аутентификация вызывающего и контроль расходов; отмененный или просроченный запрос уже мог дойти до OpenAI.
Перед началом
Установите Node.js 22.6 или новее и используйте современный браузер с рабочими микрофоном и динамиком. Доступ к микрофону требует безопасного контекста; http://localhost подходит для локального примера. Подготовьте ключ проекта OpenAI API с доступом к gpt-realtime-2.1 и включенной оплатой. Сначала проверьте ограничения частоты и расходов. Страница модели указывает, что уровень Free не поддерживается. Храните ключ в окружении серверного процесса через обычный менеджер секретов или локальную настройку shell. Не помещайте его в index.html, не коммитьте в файле .env и не вставляйте в расшифровку разговора. Руководство OpenAI по WebRTC описывает обмен через доверенный сервер; страница модели содержит текущие цены и доступ.
Здесь используется унифицированный маршрут /v1/realtime/calls Realtime API. Руководство WebRTC также описывает GPT-Live через /v1/live/sessions, но с другим протоколом сессии. Используйте маршрут и данные ниже вместе. Пример привязан к вашему компьютеру; локальные проверки Host и Origin не аутентифицируют пользователей общего сервиса.
- Создайте пустой каталог для примера и установите в нем Express:
npm init -y
npm install expressУстановка создает package.json и добавляет зависимость веб-сервера. Сохраните следующие два файла в одном каталоге. Код использует встроенные в Node fetch, FormData и AbortSignal.timeout; проверенная конфигурация требует Node.js 22.6 или новее.
- Сохраните следующее как
server.mjs. Сервер получает SDP-предложение браузера (описание предлагаемого WebRTC-соединения) и отправляет его OpenAI с настройками Realtime. Обычный ключ API читается только здесь.
import express from "express";
const app = express();
// This demo accepts only the local page, including protection against DNS rebinding.
app.use((req, res, next) => {
const hosts = ["localhost:3000", "127.0.0.1:3000"];
if (!hosts.includes(req.get("host"))) return res.status(403).send("Local host required");
if (req.method === "POST" && req.get("origin") !== `http://${req.get("host")}`) {
return res.status(403).send("Same-origin local page required");
}
next();
});
app.use(express.text({ type: "application/sdp", limit: "64kb" }));
app.get("/", (_req, res) => res.sendFile("index.html", { root: process.cwd() }));
app.post("/session", async (req, res) => {
if (!process.env.OPENAI_API_KEY) return res.status(500).send("Server API key missing");
if (typeof req.body !== "string" || !req.body.startsWith("v=")) {
return res.status(400).send("Expected an SDP offer");
}
const form = new FormData();
form.set("sdp", req.body);
form.set("session", JSON.stringify({
type: "realtime",
model: "gpt-realtime-2.1",
output_modalities: ["audio"],
audio: { output: { voice: "marin" } },
instructions: "Be a concise, helpful voice assistant. Do not claim access to external tools."
}));
try {
const upstream = await fetch("https://api.openai.com/v1/realtime/calls", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` },
body: form,
signal: AbortSignal.timeout(25000)
});
const answer = await upstream.text();
if (!upstream.ok) {
console.error("Realtime session rejected", upstream.status);
return res.status(502).send(`OpenAI session request failed (${upstream.status})`);
}
res.type("application/sdp").send(answer);
} catch (error) {
console.error("Realtime session network failure", error.name);
res.status(502).send("Could not reach the Realtime API");
}
});
app.listen(3000, "127.0.0.1", () => console.log("Open http://localhost:3000"));Сервер привязывается к 127.0.0.1:3000, принимает только host и POST origin локальной страницы и возвращает SDP-ответ. Ограничение внешнего запроса в 25 секунд выбрано для примера. Тайм-аут или отключение браузера не доказывает, что OpenAI не принял запрос; перед повторением неопределенного запуска проверьте использование.
- Сохраните следующее как
index.html. Страница запрашивает микрофон после Start voice, отправляет предложение локальному серверу и воспроизводит поступающий звук. Stop закрывает соединение и останавливает микрофонные дорожки даже во время настройки. Таймер в 30 секунд также выбран для примера, а не гарантирован API.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Local Realtime voice check</title>
<button id="start">Start voice</button>
<button id="stop" disabled>Stop</button>
<p id="status" role="status">Idle</p>
<audio id="reply" autoplay controls></audio>
<script type="module">
const start = document.querySelector("#start");
const stop = document.querySelector("#stop");
const status = document.querySelector("#status");
const reply = document.querySelector("#reply");
let current;
function closeSession(session = current, message = "Stopped") {
if (!session) return;
session.closed = true;
clearTimeout(session.timer);
session.abort.abort();
session.mic?.getTracks().forEach(track => track.stop());
if (session.pc) session.pc.onconnectionstatechange = null;
session.dc?.close();
session.pc?.close();
if (current === session) {
current = null;
reply.srcObject = null;
start.disabled = false;
stop.disabled = true;
status.textContent = message;
}
}
start.onclick = async () => {
const session = { abort: new AbortController(), closed: false };
current = session;
start.disabled = true;
stop.disabled = false;
status.textContent = "Requesting microphone";
session.timer = setTimeout(() => closeSession(session, "Setup timed out; microphone stopped"), 30000);
try {
const mic = await navigator.mediaDevices.getUserMedia({ audio: true });
if (session.closed) { mic.getTracks().forEach(track => track.stop()); return; }
session.mic = mic;
const pc = session.pc = new RTCPeerConnection();
pc.ontrack = event => { if (!session.closed) reply.srcObject = event.streams[0]; };
pc.onconnectionstatechange = () => {
if (session.closed) return;
status.textContent = `Connection: ${pc.connectionState}`;
if (pc.connectionState === "connected") clearTimeout(session.timer);
if (["failed", "closed", "disconnected"].includes(pc.connectionState)) {
closeSession(session, "Connection ended; microphone stopped. Start a new session.");
}
};
pc.addTrack(mic.getAudioTracks()[0], mic);
const dc = session.dc = pc.createDataChannel("oai-events");
dc.onmessage = message => {
if (session.closed) return;
try {
const event = JSON.parse(message.data);
if (event.type === "session.created") status.textContent = "Session ready: speak when connected";
if (event.type === "response.done") {
status.textContent = `Response status: ${event.response?.status || "unknown"}; verify audible playback`;
}
if (event.type === "error") closeSession(session, `Realtime error: ${event.error?.message || "unknown"}; stopped`);
} catch { closeSession(session, "Invalid event; stopped"); }
};
dc.onclose = () => { if (!session.closed) closeSession(session, "Event channel closed; stopped"); };
dc.onerror = () => closeSession(session, "Event channel failed; stopped");
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
if (session.closed) return;
// The official WebRTC example submits the generated offer directly.
const response = await fetch("/session", {
method: "POST",
headers: { "Content-Type": "application/sdp" },
body: offer.sdp,
signal: session.abort.signal
});
if (!response.ok) throw new Error(await response.text());
const answer = await response.text();
if (session.closed) return;
await pc.setRemoteDescription({ type: "answer", sdp: answer });
if (!session.closed && pc.connectionState !== "connected") status.textContent = "Signaling complete; waiting for connection";
} catch (error) {
if (!session.closed) closeSession(session, `Start failed: ${error.message}`);
}
};
stop.onclick = () => closeSession();
window.addEventListener("pagehide", () => closeSession());
</script>
</html>Браузер закрывает сессию и при сбое соединения с другой стороной, канала событий или уходе со страницы. Разрешение микрофона может прийти после остановки или тайм-аута; поздняя дорожка тоже останавливается. Строка состояния показывает response.done с сообщенным статусом. Одно событие не доказывает слышимость ответа или успех отмененного либо неудачного ответа.
- Запустите сервер из каталога обоих файлов, заранее задав
OPENAI_API_KEYв окружении этого процесса:
node server.mjsОткройте http://localhost:3000. Должны отображаться Start voice, Stop, состояние Idle и аудиоплеер. Нажмите Start voice, разрешите микрофон и задайте короткий безобидный вопрос. Согласно руководству по разговорам Realtime, стандартное определение голосовой активности может превращать паузу после речи в ответ. Для проверки своего запуска убедитесь в активном микрофоне, состоянии соединения connected, появлении события session.created, слышимой речи и сообщении о завершенном ответе в response.done. Нажмите Stop и убедитесь, что индикатор микрофона погас. Эти проверки выполняете вы; BIG CHANGE такую платную сессию не запускал.
Если проверка не прошла
Если микрофон запрещен или устройства нет, проверьте права браузера и устройство и используйте локальный localhost URL либо HTTPS. При ошибке /session проверьте серверный HTTP-статус, наличие ключа, доступ к модели, оплату, ограничения и endpoint. Сервер намеренно не печатает ключ и тело ответа OpenAI. 502 также означает сетевой сбой или срок в 25 секунд и не сообщает, принят ли неопределенный внешний запрос.
Если сигнализация удалась, а соединение нет, проверьте состояние страницы, сетевые ограничения и SDP-ответ. При активном соединении без звука проверьте микрофон и выбранный выход. Автовоспроизведение может блокироваться: нажмите Play в плеере, затем при необходимости остановите и откройте новую сессию. При любом сбое используйте Stop и проверьте выключение индикатора. Перед повторными запусками с неясным результатом запроса проверьте использование проекта.
Стоимость, приватность и ограничения
OpenAI указывает для gpt-realtime-2.1 некешируемого текстового ввода/вывода $4/$24 за миллион токенов и аудиоввода/вывода $32/$64 за миллион токенов, проверено 19 октября 2026 года. Оплата зависит от токенов, включая поздние реплики с прежним контекстом. Перед использованием проверьте цены и ограничения модели и свой проект; по длительности одной сессии нельзя оценить счет. Руководство по разговорам Realtime устанавливает максимум 60 минут.
Во время сессии звук микрофона идет OpenAI. Используйте речь, которую вправе передавать, избегайте частного и чужого аудио. У страницы нет входа или бюджета на пользователя. Общему сервису нужны аутентифицированный доступ /session с ограничением частоты и серверный идентификатор безопасности; локальных Host и Origin недостаточно. Пример не вызывает внешние инструменты и не звонит по телефону. Задержка, надежность, качество речи, экономия и результаты промышленного развертывания не измерялись.
Источники и дополнительные материалы
- Руководство OpenAI по WebRTC: унифицированный обмен Realtime, звук браузера и отдельный протокол GPT-Live.
- Руководство OpenAI по разговорам Realtime: события, реплики, статус ответа и ограничение длительности.
- OpenAI
gpt-realtime-2.1: страница модели: доступ, цены токенов и ограничения частоты. Перепроверьте цены и доступ при запуске.



