تتيح 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 أو تلصقه في سجل محادثة. دليل WebRTC من OpenAI يوثّق التبادل عبر الخادم الموثوق؛ صفحة النموذج تعرض الوصول والأسعار الحالية.

هذا مسار /v1/realtime/calls الموحّد في Realtime API. ويشرح دليل WebRTC أيضاً GPT-Live عبر /v1/live/sessions، لكنه يستخدم بروتوكول جلسة مختلفاً. احتفظ بالمسار والحمولة أدناه معاً. المثال محصور بحاسوبك؛ فحوص Host وOrigin المحلية لا تصادق مستخدمي خدمة مشتركة.

  1. أنشئ دليلاً فارغاً للمثال وثبّت Express من داخله:
Terminal
npm init -y
npm install express

ينشئ التثبيت package.json ويضيف اعتماد خادم الويب. احفظ الملفين التاليين في الدليل نفسه. تستخدم الشفرة وظائف 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، ويقبل فقط host وPOST origin للصفحة المحلية، ويعيد إجابة 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، واسمح بالميكروفون وقل سؤالاً قصيراً لا يتضمن معلومات حساسة. يقول دليل محادثات Realtime إن كشف النشاط الصوتي الافتراضي قد يحول وقفة بعد الكلام إلى استجابة. لنجاح تشغيلك، تحقق من ميكروفون نشط واتصال يصل إلى connected وظهور حدث session.created وسماع الكلام وأن response.done يبلّغ استجابة مكتملة. اضغط Stop وتحقق من انطفاء مؤشر الميكروفون. هذه فحوص تنفذها أنت؛ لم تشغّل BIG CHANGE هذه الجلسة المدفوعة.

إذا فشل الفحص

إذا رُفض الإذن أو لم يظهر جهاز إدخال، افحص أذونات المتصفح والجهاز ثم استخدم عنوان localhost المحلي أو HTTPS. إذا أبلغت الصفحة عن فشل /session، راجع حالة HTTP في الخادم ووجود المفتاح ووصول النموذج والفوترة وحدود المعدل ونقطة النهاية. لا يطبع الخادم المفتاح أو متن استجابة OpenAI عمداً. قد تعني 502 أيضاً فشل شبكة أو مهلة 25 ثانية؛ لا تحدد هل قُبل طلب خارجي غير مؤكد.

إذا نجحت الإشارة وفشل الاتصال، افحص حالة الصفحة وقيود الشبكة وإجابة SDP. إذا اتصل دون صوت، افحص إدخال الميكروفون والمخرج المختار. قد يُحظر التشغيل التلقائي: اضغط Play في مشغل الصوت، ثم أوقف وابدأ جلسة جديدة عند الحاجة. عند أي فشل استخدم Stop وتحقق من انطفاء مؤشر الميكروفون. افحص استخدام المشروع قبل تكرار بدايات غير مؤكدة.

التكلفة والخصوصية والحدود

تحدد OpenAI لـ gpt-realtime-2.1 إدخال/إخراج نص غير مخزّن مؤقتاً بسعر $4/$24 لكل مليون tokens وإدخال/إخراج صوت بسعر $32/$64 لكل مليون tokens، فُحص في 19 أكتوبر 2026. تعتمد الرسوم على tokens المستخدمة، بما فيها الأدوار اللاحقة التي تحمل سياق الحوار السابق. راجع أسعار النموذج وحدوده واستخدام مشروعك قبل البدء؛ الوقت وحده لا يكفي لتقدير الفاتورة. يحدد دليل محادثات Realtime حداً أقصى للجلسة 60 دقيقة.

يذهب صوت الميكروفون إلى OpenAI أثناء الجلسة. استخدم كلاماً يحق لك نقله وتجنب الصوت الخاص أو المملوك لغيرك في هذا الفحص. لا توفر الصفحة دخولاً أو ميزانية فردية. تحتاج الخدمة المشتركة إلى وصول /session موثّق ومحدود المعدل ومعرّف أمان مشتق من الخادم؛ لا تكفي فحوص Host وOrigin المحلية. لا يستدعي المثال أدوات خارجية ولا يجري مكالمات هاتفية. لم تُقَس سرعة الاستجابة أو الموثوقية أو جودة الصوت أو التوفير أو نتائج نشر إنتاجي.

المصادر وقراءات إضافية