OpenAI Realtime API омогућава прегледачу слање звука микрофона и репродукцију говорног одговора преко 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, не commit-ујте га у .env датотеци и не лепите у транскрипт. OpenAI WebRTC водич документује размену преко поузданог сервера; страница модела наводи актуелни приступ и цене.

Ово је обједињена /v1/realtime/calls путања Realtime API-ја. WebRTC водич описује и GPT-Live преко /v1/live/sessions, са другачијим протоколом сесије. Користите наведену путању и садржај захтева заједно. Пример је везан за ваш рачунар; локалне Host и Origin провере не аутентификују кориснике дељеног сервиса.

  1. Направите празан директоријум за пример и из њега инсталирајте Express:
Terminal
npm init -y
npm install express

Инсталација прави package.json и додаје зависност за wеб сервер. Сачувајте наредне две датотеке у истом директоријуму. Код користи Node уграђене fetch, FormData и AbortSignal.timeout; проверено подешавање захтева Node.js 22.6 или новији.

  1. Сачувајте следеће као server.mjs. Сервер прима SDP понуду прегледача (опис предложене WebRTC везе) и шаље је OpenAI-у са Realtime подешавањима. Стандардни API кључ чита се само на овом серверу.
JavaScript
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, прихвата само хост и POST оригин локалне странице и враћа SDP одговор прегледачу. Рок од 25 секунди за захтев ка добављачу избор је овог примера. Истек рока или прекид прегледача не доказује да OpenAI није прихватио захтев; проверите употребу пре понављања неизвесног покретања.

  1. Сачувајте следеће као index.html. Страница тражи дозволу микрофона након Start voice, шаље понуду локалном серверу и репродукује долазни звук. Stop затвара везу и зауставља траке микрофона, чак и током повезивања. Тајмер од 30 секунди је такође избор примера, не API гаранција.
HTML
<!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 са пријављеним стањем. Сам догађај не доказује да се одговор чуо нити да је отказан или неуспешан одговор успео.

  1. Покрените сервер из директоријума обе датотеке, са OPENAI_API_KEY већ подешеним у окружењу тог процеса:
Terminal
node server.mjs

Отворите http://localhost:3000. Треба да видите Start voice, Stop, стање Idle и аудио-плејер. Кликните Start voice, дозволите микрофон и изговорите кратко питање без осетљивих података. OpenAI водич за 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 провере нису довољне. Пример не позива спољне алате и не телефонира. Нису мерени кашњење, поузданост, квалитет говора, уштеде нити резултат продукционог пуштања.

Извори и додатна литература