AI-translated from English; not yet reviewed by a fluent editor.

# Créer une conversation vocale locale dans le navigateur avec OpenAI Realtime

> Créez une conversation locale au microphone avec OpenAI Realtime via WebRTC. Le serveur et la page complets gardent la clé API côté serveur et montrent comment vérifier la lecture, arrêter le micro et traiter les erreurs.

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

![Conceptual charcoal illustration of a desktop microphone beside a blank browser on a monitor and a small speaker.](https://bigchange.ai/api/media/file/realtime-browser-voice-hero-v1.png)
AI-generated conceptual illustration by BIG CHANGE; no actual OpenAI interface, browser test or heard reply is depicted.

L’API Realtime d’OpenAI permet au navigateur d’envoyer le son du microphone et de lire une réponse vocale via WebRTC. Ce guide crée une conversation locale avec `gpt-realtime-2.1`. La clé API reste dans un serveur Node sur votre ordinateur. Il faut un projet API OpenAI avec accès au modèle et facturation ; un abonnement ChatGPT ne paie pas l’usage de l’API. BIG CHANGE a vérifié la documentation actuelle et le code, sans ouvrir de session payante ni entendre de réponse.

## Le grand changement

- **Ce qui change :** OpenAI documente une connexion WebRTC de navigateur pour une conversation Realtime, avec un serveur local qui échange l’offre de connexion du navigateur contre une réponse tout en conservant la clé du projet.
- **Pourquoi c’est important :** Un développeur peut vérifier le parcours complet du microphone à la réponse vocale dans une page locale avant de concevoir un produit plus vaste. Le propriétaire du projet contrôle la clé et paie l’usage de l’API.
- **À surveiller :** Un échange local réussi ne prouve pas que le système est prêt pour la production. Une version partagée nécessite l’authentification des appelants et des contrôles de dépenses ; une requête annulée ou expirée peut déjà être parvenue à OpenAI.

## Avant de commencer

Installez Node.js 22.6 ou ultérieur et utilisez un navigateur actuel avec microphone et haut-parleur fonctionnels. L’accès au microphone exige un contexte sécurisé ; `http://localhost` convient à cet exemple local. Préparez une clé de projet API OpenAI avec accès à `gpt-realtime-2.1` et facturation activée. Vérifiez d’abord les limites de débit et de dépenses du projet. La page du modèle indique que le niveau Free n’est pas pris en charge. Gardez la clé dans l’environnement du processus serveur via votre gestionnaire de secrets ou votre configuration shell locale habituelle. Ne la placez pas dans `index.html`, ne la commitez pas dans un fichier `.env` et ne la collez pas dans une transcription. [Le guide WebRTC d’OpenAI](https://developers.openai.com/api/docs/guides/voice-webrtc) documente l’échange avec le serveur de confiance ; [la page du modèle](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) indique les accès et prix actuels.

Il s’agit de la route unifiée `/v1/realtime/calls` de l’API Realtime. Le guide WebRTC décrit aussi GPT-Live via `/v1/live/sessions`, avec un protocole de session différent. Conservez ensemble la route et la charge utile ci-dessous. L’exemple est limité à votre ordinateur ; ses contrôles locaux Host et Origin n’authentifient pas les utilisateurs d’un service partagé.

1. Créez un répertoire vide pour la démonstration. Depuis ce répertoire, installez Express :

```bash
npm init -y
npm install express
```

L’installation crée `package.json` et ajoute la dépendance du serveur web. Enregistrez les deux fichiers suivants dans ce même répertoire. Le code utilise les fonctions intégrées de Node `fetch`, `FormData` et `AbortSignal.timeout` ; la configuration vérifiée exige Node.js 22.6 ou ultérieur.

2. Enregistrez ce qui suit sous `server.mjs`. Le serveur reçoit l’offre SDP du navigateur (une description de la connexion WebRTC proposée) et l’envoie à OpenAI avec les paramètres de session Realtime. La clé API standard est lue uniquement sur ce serveur.

```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"));
```

Le serveur écoute sur `127.0.0.1:3000`, n’accepte que l’hôte et l’origine POST de la page locale et renvoie la réponse SDP au navigateur. Le délai amont de 25 secondes est un choix de cet exemple. Un dépassement ou une déconnexion ne prouve pas qu’OpenAI n’a pas accepté la requête : inspectez l’usage avant de réessayer un démarrage incertain.

3. Enregistrez ce qui suit sous `index.html`. La page demande l’autorisation du microphone après **Start voice**, envoie l’offre au serveur local et lit le son reçu. **Stop** ferme la connexion et arrête les pistes du microphone, même pendant l’établissement. Le délai initial de 30 secondes est aussi un choix d’exemple, pas une garantie de l’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>
```

Le navigateur ferme également la session en cas de défaillance du pair ou du canal d’événements et lorsque vous quittez la page. L’autorisation du micro peut arriver après un arrêt ou un délai dépassé ; la piste tardive est alors arrêtée. La ligne d’état affiche `response.done` avec son statut déclaré. Cet événement seul ne montre pas que la réponse était audible ni qu’une réponse annulée ou échouée a réussi.

4. Démarrez le serveur depuis le répertoire contenant les deux fichiers, avec `OPENAI_API_KEY` déjà défini dans l’environnement du processus serveur :

```bash
node server.mjs
```

Ouvrez `http://localhost:3000`. Vous devriez voir **Start voice**, **Stop**, l’état **Idle** et un lecteur audio. Cliquez sur **Start voice**, autorisez le microphone et posez une brève question sans information sensible. Le [guide des conversations Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) indique que la détection d’activité vocale par défaut peut déclencher une réponse après une pause. Pour réussir votre essai, vérifiez que le navigateur signale un micro actif, que la connexion atteint `connected`, qu’un événement `session.created` apparaît, que vous entendez la voix et que `response.done` signale une réponse terminée. Cliquez sur **Stop** et vérifiez que le témoin du microphone s’éteint. Ces vérifications vous incombent ; BIG CHANGE n’a pas effectué cette session payante dans le navigateur.

## Si la vérification échoue

Si l’autorisation du micro est refusée ou qu’aucun appareil n’apparaît, vérifiez les permissions du navigateur et l’appareil, puis utilisez l’URL locale `localhost` ou HTTPS. Si la page signale un échec `/session`, regardez le statut HTTP côté serveur et vérifiez présence de clé, accès du projet au modèle, facturation, limites de débit et point de terminaison. Le serveur n’imprime volontairement ni la clé ni le corps de réponse d’OpenAI. Un code 502 peut aussi indiquer un problème réseau ou le délai de 25 secondes ; il ne prouve pas qu’une requête amont incertaine a été acceptée ou refusée.

Si la signalisation réussit mais que la connexion échoue, vérifiez son état, les restrictions réseau et la réponse SDP. Si elle est active mais silencieuse, vérifiez le microphone et le périphérique de sortie. Le navigateur peut bloquer la lecture automatique : cliquez sur **Play** dans le lecteur, puis arrêtez et ouvrez une nouvelle session si nécessaire. En cas d’échec, utilisez **Stop** et confirmez l’extinction du témoin du micro. Vérifiez l’usage du projet avant des démarrages répétés si le résultat d’une requête est incertain.

## Coût, confidentialité et limites

OpenAI indique pour `gpt-realtime-2.1` des tarifs texte non mis en cache en entrée/sortie de **$4/$24 par million de tokens** et audio en entrée/sortie de **$32/$64 par million de tokens**, vérifiés le 19 octobre 2026. Le coût dépend des tokens consommés, y compris les tours ultérieurs reprenant du contexte précédent. Vérifiez [les prix et limites du modèle](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) et l’usage de votre projet avant utilisation ; la durée seule ne permet pas d’estimer la facture. Le [guide des conversations Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) fixe une session maximale de 60 minutes.

Le son de votre microphone est transmis à OpenAI pendant la session. Utilisez des paroles que vous êtes autorisé à transmettre et évitez le son privé ou tiers pour cet essai. Cette page locale n’a ni connexion utilisateur ni budget individuel. Un service partagé nécessiterait un accès `/session` authentifié et limité en débit ainsi qu’un identifiant de sécurité calculé côté serveur ; les contrôles Host et Origin locaux ne suffisent pas. L’exemple n’appelle pas d’outils externes et ne passe pas d’appels téléphoniques. Aucun résultat de latence, fiabilité, qualité vocale, économie ou déploiement en production n’a été mesuré.

## Sources et lectures complémentaires

- [Guide WebRTC d’OpenAI](https://developers.openai.com/api/docs/guides/voice-webrtc): échange serveur Realtime unifié, audio du navigateur et protocole GPT-Live distinct.
- [Guide des conversations OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations): événements de session, tours de parole, statut des réponses et durée maximale.
- [OpenAI `gpt-realtime-2.1` — page du modèle](https://developers.openai.com/api/docs/models/gpt-realtime-2.1): accès au modèle, prix des tokens et limites de débit. Revérifiez prix et accès au moment de l’essai.

## Sources

- [Guide WebRTC d’OpenAI](https://developers.openai.com/api/docs/guides/voice-webrtc) — Interface unifiée Realtime calls, clé de confiance, SDP, audio du navigateur et protocole GPT-Live distinct.
- [Conversations OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) — Événements, VAD, statut de réponse et durée de 60 minutes.
- [Modèle OpenAI gpt-realtime-2.1](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) — Prix des tokens et accès propres au modèle.
- [Optimisation des coûts vocaux OpenAI](https://developers.openai.com/api/docs/guides/voice-latency-cost) — Contexte et usage par réponse.
- [Facturation API OpenAI et ChatGPT](https://help.openai.com/en/articles/9039756-managing-billing-for-chatgpt-and-the-api-platform) — Dispositifs de facturation distincts.
