AI-translated from English; not yet reviewed by a fluent editor.
# أنشئ محادثة صوتية محلية في المتصفح باستخدام OpenAI Realtime
> أنشئ محادثة محلية بميكروفون المتصفح عبر OpenAI Realtime وWebRTC. تبقي شفرة الخادم والصفحة الكاملة مفتاح API على الخادم وتوضح فحص التشغيل وإيقاف الميكروفون ومعالجة الإخفاقات.
By BIG CHANGE Editorial
Published: 2026-10-19T19:20:34.863Z
Updated: 2026-10-19T19:20:34.863Z
Canonical: https://bigchange.ai/blog/openai-realtime-browser-voice-webrtc-guide

AI-generated conceptual illustration by BIG CHANGE; no actual OpenAI interface, browser test or heard reply is depicted.
تتيح 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](https://developers.openai.com/api/docs/guides/voice-webrtc) يوثّق التبادل عبر الخادم الموثوق؛ [صفحة النموذج](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) تعرض الوصول والأسعار الحالية.
هذا مسار `/v1/realtime/calls` الموحّد في Realtime API. ويشرح دليل WebRTC أيضاً GPT-Live عبر `/v1/live/sessions`، لكنه يستخدم بروتوكول جلسة مختلفاً. احتفظ بالمسار والحمولة أدناه معاً. المثال محصور بحاسوبك؛ فحوص Host وOrigin المحلية لا تصادق مستخدمي خدمة مشتركة.
1. أنشئ دليلاً فارغاً للمثال وثبّت Express من داخله:
```bash
npm init -y
npm install express
```
ينشئ التثبيت `package.json` ويضيف اعتماد خادم الويب. احفظ الملفين التاليين في الدليل نفسه. تستخدم الشفرة وظائف Node المدمجة `fetch` و `FormData` و `AbortSignal.timeout`؛ يتطلب الإعداد المراجَع Node.js 22.6 أو أحدث.
2. احفظ التالي باسم `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 لم تقبل الطلب؛ راجع الاستخدام قبل تكرار بدء غير مؤكد.
3. احفظ التالي باسم `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` مع حالته المعلنة. الحدث وحده لا يثبت أن الإجابة مسموعة أو أن استجابة ملغاة أو فاشلة نجحت.
4. ابدأ الخادم من دليل الملفين، مع ضبط `OPENAI_API_KEY` مسبقاً في بيئة عملية الخادم:
```bash
node server.mjs
```
افتح `http://localhost:3000`. ينبغي رؤية **Start voice** و **Stop** وحالة **Idle** ومشغّل صوت. اضغط **Start voice**، واسمح بالميكروفون وقل سؤالاً قصيراً لا يتضمن معلومات حساسة. يقول [دليل محادثات Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) إن كشف النشاط الصوتي الافتراضي قد يحول وقفة بعد الكلام إلى استجابة. لنجاح تشغيلك، تحقق من ميكروفون نشط واتصال يصل إلى `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 المستخدمة، بما فيها الأدوار اللاحقة التي تحمل سياق الحوار السابق. راجع [أسعار النموذج وحدوده](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) واستخدام مشروعك قبل البدء؛ الوقت وحده لا يكفي لتقدير الفاتورة. يحدد [دليل محادثات Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) حداً أقصى للجلسة 60 دقيقة.
يذهب صوت الميكروفون إلى OpenAI أثناء الجلسة. استخدم كلاماً يحق لك نقله وتجنب الصوت الخاص أو المملوك لغيرك في هذا الفحص. لا توفر الصفحة دخولاً أو ميزانية فردية. تحتاج الخدمة المشتركة إلى وصول `/session` موثّق ومحدود المعدل ومعرّف أمان مشتق من الخادم؛ لا تكفي فحوص Host وOrigin المحلية. لا يستدعي المثال أدوات خارجية ولا يجري مكالمات هاتفية. لم تُقَس سرعة الاستجابة أو الموثوقية أو جودة الصوت أو التوفير أو نتائج نشر إنتاجي.
## المصادر وقراءات إضافية
- [دليل OpenAI WebRTC](https://developers.openai.com/api/docs/guides/voice-webrtc): تبادل Realtime الموحّد ومسار صوت المتصفح وبروتوكول GPT-Live المنفصل.
- [دليل محادثات OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations): أحداث الجلسة والأدوار وحالة الاستجابة وحد المدة.
- [OpenAI `gpt-realtime-2.1` صفحة النموذج](https://developers.openai.com/api/docs/models/gpt-realtime-2.1): الوصول للنموذج وأسعار tokens وحدود المعدل. أعد فحص الأسعار والوصول عند تشغيل المثال.
## Sources
- [دليل OpenAI WebRTC](https://developers.openai.com/api/docs/guides/voice-webrtc) — واجهة Realtime calls الموحّدة ومفتاح موثوق وSDP وصوت المتصفح وبروتوكول GPT-Live المنفصل.
- [محادثات OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) — أحداث الجلسة وVAD وحالة الاستجابة ومدة 60 دقيقة.
- [نموذج OpenAI gpt-realtime-2.1](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) — أسعار tokens والوصول الخاصة بالنموذج.
- [تحسين تكلفة صوت OpenAI](https://developers.openai.com/api/docs/guides/voice-latency-cost) — السياق والاستخدام لكل استجابة.
- [فوترة OpenAI API وChatGPT](https://help.openai.com/en/articles/9039756-managing-billing-for-chatgpt-and-the-api-platform) — فوترة منفصلة.
نشرة BIG CHANGE البريدية
الصورة الأوسع. على وتيرتك.
قصص حديثة عن الذكاء الاصطناعي والروبوتات، وتحولات تستحق المتابعة، وأفكار عملية للتطبيق. اختر موجزًا يوميًا أو خلاصة أسبوعية أو رؤية شهرية.
Next scheduled send (UTC): . Your first edition arrives at the next scheduled send after you confirm.
خصوصيتك، خيارك.
يساعد التخزين الضروري على حماية الموقع وتذكّر خياراتك. تظل Google Analytics الاختيارية متوقفة حتى تسمح بها. يمكنك قراءة كل قصة باستخدام التخزين الضروري فقط. تفاصيل الخصوصية