OpenAI Realtime API omogućava pregledaču slanje zvuka mikrofona i reprodukciju govornog odgovora preko WebRTC-a. Vodič pravi jedan lokalni razgovor uz gpt-realtime-2.1. API ključ ostaje u Node serveru na vašem računaru. Potreban je OpenAI API projekat sa pristupom modelu i naplatom; ChatGPT pretplata ne plaća API upotrebu. BIG CHANGE je proverio aktuelnu dokumentaciju i primer koda, ali nije otvorio plaćenu sesiju niti čuo odgovor.

Velika promena

  • Šta se promenilo: OpenAI dokumentuje WebRTC putanju za razgovor Realtime u pregledaču, uz lokalni server koji čuva ključ projekta i razmenjuje ponudu veze pregledača za odgovor.
  • Zašto je važno: Programer može proveriti ceo put od mikrofona do govornog odgovora na jednoj lokalnoj stranici pre razvoja većeg govornog proizvoda. Vlasnik projekta upravlja ključem i plaća API.
  • Na šta paziti: Uspešna lokalna razmena ne dokazuje spremnost za produkciju. Deljena verzija zahteva autentifikaciju pozivaoca i kontrolu troška; otkazan ili istekao zahtev je možda već stigao do OpenAI-a.

Pre početka

Instalirajte Node.js 22.6 ili noviji i koristite aktuelan pregledač sa ispravnim mikrofonom i zvučnikom. Mikrofon zahteva bezbedan kontekst; http://localhost odgovara ovom lokalnom primeru. Pripremite OpenAI API ključ projekta sa pristupom gpt-realtime-2.1 i uključenom naplatom. Najpre proverite ograničenja učestalosti i troška. Stranica modela navodi da Free nivo nije podržan. Čuvajte ključ u okruženju serverskog procesa kroz uobičajeni upravljač tajnama ili lokalno shell podešavanje. Ne stavljajte ga u index.html, ne commit-ujte ga u .env datoteci i ne lepite u transkript. OpenAI WebRTC vodič dokumentuje razmenu preko pouzdanog servera; stranica modela navodi aktuelni pristup i cene.

Ovo je objedinjena /v1/realtime/calls putanja Realtime API-ja. WebRTC vodič opisuje i GPT-Live preko /v1/live/sessions, sa drugačijim protokolom sesije. Koristite navedenu putanju i sadržaj zahteva zajedno. Primer je vezan za vaš računar; lokalne Host i Origin provere ne autentifikuju korisnike deljenog servisa.

  1. Napravite prazan direktorijum za primer i iz njega instalirajte Express:
Terminal
npm init -y
npm install express

Instalacija pravi package.json i dodaje zavisnost za web server. Sačuvajte naredne dve datoteke u istom direktorijumu. Kod koristi Node ugrađene fetch, FormData i AbortSignal.timeout; provereno podešavanje zahteva Node.js 22.6 ili noviji.

  1. Sačuvajte sledeće kao server.mjs. Server prima SDP ponudu pregledača (opis predložene WebRTC veze) i šalje je OpenAI-u sa Realtime podešavanjima. Standardni API ključ čita se samo na ovom serveru.
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"));

Server se vezuje za 127.0.0.1:3000, prihvata samo host i POST origin lokalne stranice i vraća SDP odgovor pregledaču. Rok od 25 sekundi za zahtev ka dobavljaču izbor je ovog primera. Istek roka ili prekid pregledača ne dokazuje da OpenAI nije prihvatio zahtev; proverite upotrebu pre ponavljanja neizvesnog pokretanja.

  1. Sačuvajte sledeće kao index.html. Stranica traži dozvolu mikrofona nakon Start voice, šalje ponudu lokalnom serveru i reprodukuje dolazni zvuk. Stop zatvara vezu i zaustavlja trake mikrofona, čak i tokom povezivanja. Tajmer od 30 sekundi je takođe izbor primera, ne API garancija.
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>

Pregledač zatvara sesiju i kod kvara druge strane ili kanala događaja i pri napuštanju stranice. Dozvola mikrofona može stići posle zaustavljanja ili isteka roka; zakašnjela traka se zaustavlja. Statusni red prikazuje response.done sa prijavljenim stanjem. Sam događaj ne dokazuje da se odgovor čuo niti da je otkazan ili neuspešan odgovor uspeo.

  1. Pokrenite server iz direktorijuma obe datoteke, sa OPENAI_API_KEY već podešenim u okruženju tog procesa:
Terminal
node server.mjs

Otvorite http://localhost:3000. Treba da vidite Start voice, Stop, stanje Idle i audio-plejer. Kliknite Start voice, dozvolite mikrofon i izgovorite kratko pitanje bez osetljivih podataka. OpenAI vodič za Realtime razgovore kaže da podrazumevano prepoznavanje govorne aktivnosti može pokrenuti odgovor nakon pauze. Za uspešno sopstveno pokretanje proverite aktivan mikrofon, vezu u stanju connected, prikazan događaj session.created, zvuk govora i da response.done prijavljuje dovršen odgovor. Kliknite Stop i proverite da se indikator mikrofona ugasio. Ove provere obavljate vi; BIG CHANGE nije pokrenuo ovu plaćenu sesiju.

Ako provera ne uspe

Ako je mikrofon odbijen ili nema ulaznog uređaja, proverite dozvole i uređaj, pa koristite lokalni localhost URL ili HTTPS. Ako stranica prijavi /session grešku, proverite serverski HTTP status, ključ, pristup modelu, naplatu, ograničenja i endpoint. Server namerno ne ispisuje ključ niti OpenAI telo odgovora. Status 502 može biti mrežna greška ili rok 25 sekundi; ne govori da li je neizvestan zahtev prihvaćen.

Ako signalizacija uspe, a veza ne, proverite status stranice, mrežna ograničenja i vraćeni SDP odgovor. Za aktivnu ali tihu vezu proverite mikrofon i izabrani izlaz. Pregledač može blokirati automatsku reprodukciju: kliknite Play na audio-kontroli, pa po potrebi zaustavite i otvorite novu sesiju. Pri svakom neuspehu koristite Stop i potvrdite ugašen indikator mikrofona. Proverite upotrebu pre ponovljenih pokretanja sa neizvesnim ishodom zahteva.

Trošak, privatnost i ograničenja

OpenAI za gpt-realtime-2.1 navodi tekstualni ulaz/izlaz bez keša po $4/$24 za milion tokena i audio-ulaz/izlaz po $32/$64 za milion tokena, provereno 19. oktobra 2026. Trošak zavisi od tokena, uključujući kasnije poteze sa prethodnim kontekstom. Pre upotrebe proverite cene i ograničenja modela i upotrebu projekta; samo trajanje nije dovoljno za procenu računa. Vodič za Realtime razgovore navodi najviše 60 minuta po sesiji.

Zvuk mikrofona tokom sesije ide OpenAI-u. Koristite govor koji smete da pošaljete i izbegavajte privatni ili tuđi zvuk. Lokalna stranica nema prijavu ni budžet po korisniku. Deljeni servis zahteva autentifikovan, ograničen /session pristup i bezbednosni identifikator izveden na serveru; lokalne Host i Origin provere nisu dovoljne. Primer ne poziva spoljne alate i ne telefonira. Nisu mereni kašnjenje, pouzdanost, kvalitet govora, uštede niti rezultat produkcionog puštanja.

Izvori i dodatna literatura