AI-translated from English; not yet reviewed by a fluent editor.
# Crie uma conversa de voz local no navegador com OpenAI Realtime
> Crie uma conversa local com o microfone do navegador usando OpenAI Realtime e WebRTC. O código completo do servidor e da página mantém a chave API no servidor e mostra como conferir reprodução, parar o microfone e tratar falhas.
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.
A API Realtime da OpenAI permite enviar áudio do microfone e reproduzir uma resposta falada por WebRTC. Este guia cria uma conversa local com `gpt-realtime-2.1`. A chave API fica em um servidor Node no seu computador. É necessário um projeto API OpenAI com acesso ao modelo e cobrança; uma assinatura ChatGPT não paga uso da API. O BIG CHANGE conferiu a documentação atual e o código, mas não abriu sessão paga nem ouviu resposta.
## A grande mudança
- **O que mudou:** A OpenAI documenta um caminho WebRTC no navegador para conversar com Realtime: um servidor local guarda a chave e troca a oferta de conexão do navegador por uma resposta.
- **Por que importa:** Um desenvolvedor pode verificar todo o caminho do microfone à resposta falada em uma página local antes de criar um produto maior. O dono do projeto controla a chave e paga a API.
- **O que observar:** Uma troca local bem-sucedida não comprova prontidão para produção. Uma versão compartilhada precisa autenticação e controle de gastos; uma solicitação cancelada ou expirada pode já ter chegado à OpenAI.
## Antes de começar
Instale Node.js 22.6 ou posterior e use um navegador atual com microfone e alto-falante funcionando. O microfone exige contexto seguro; `http://localhost` serve para este exemplo local. Tenha uma chave de projeto API OpenAI com acesso a `gpt-realtime-2.1` e cobrança habilitada. Verifique antes limites de taxa e gastos. A página do modelo indica que o nível Free não é compatível. Guarde a chave no ambiente do processo servidor por seu gerenciador de segredos ou configuração local habitual. Não coloque em `index.html`, não faça commit em um arquivo `.env` nem cole em transcrições. [O guia WebRTC da OpenAI](https://developers.openai.com/api/docs/guides/voice-webrtc) documenta a troca pelo servidor confiável; [a página do modelo](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) mostra acesso e preços atuais.
Esta é a rota unificada `/v1/realtime/calls` da API Realtime. O guia WebRTC também descreve GPT-Live por `/v1/live/sessions`, com outro protocolo de sessão. Mantenha juntos a rota e o conteúdo a seguir. O exemplo se limita ao seu computador; verificações locais de Host e Origin não autenticam usuários de um serviço compartilhado.
1. Crie um diretório vazio para a demonstração e instale Express nele:
```bash
npm init -y
npm install express
```
A instalação cria `package.json` e adiciona a dependência do servidor web. Salve os próximos dois arquivos nesse diretório. O código usa os componentes integrados do Node `fetch`, `FormData` e `AbortSignal.timeout`; a configuração revisada exige Node.js 22.6 ou posterior.
2. Salve o seguinte como `server.mjs`. Ele recebe a oferta SDP do navegador (descrição da conexão WebRTC proposta) e envia à OpenAI com os ajustes Realtime. Somente esse servidor lê a chave API padrão.
```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"));
```
O servidor se vincula a `127.0.0.1:3000`, aceita apenas host e origem POST da página local e devolve a resposta SDP. O prazo de 25 segundos para a solicitação ao provedor é uma escolha do exemplo. Tempo esgotado ou desconexão não prova que a OpenAI recusou a solicitação; confira o uso antes de repetir um início incerto.
3. Salve o seguinte como `index.html`. A página pede permissão do microfone após **Start voice**, envia a oferta ao servidor local e reproduz o áudio recebido. **Stop** fecha a conexão e para as faixas do microfone mesmo durante a configuração. O temporizador de 30 segundos também é uma escolha do exemplo, não garantia da 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>
```
O navegador também fecha a sessão se houver falha do par ou canal de eventos e ao sair da página. A permissão pode chegar após uma parada ou prazo; a faixa tardia é encerrada. A linha de estado mostra `response.done` com o status informado. Esse evento sozinho não demonstra que a resposta foi audível nem que uma resposta cancelada ou falha teve sucesso.
4. Inicie o servidor no diretório dos dois arquivos, com `OPENAI_API_KEY` já definido no ambiente desse processo:
```bash
node server.mjs
```
Abra `http://localhost:3000`. Você deve ver **Start voice**, **Stop**, o estado **Idle** e um reprodutor. Clique em **Start voice**, permita o microfone e faça uma pergunta curta sem informações sensíveis. Segundo o [guia de conversas Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations), a detecção de atividade de voz padrão pode transformar uma pausa após a fala em resposta. Para conferir sua execução, verifique o microfone ativo, a conexão em `connected`, um evento `session.created`, a fala audível e `response.done` indicando resposta concluída. Clique em **Stop** e confira que o indicador do microfone apaga. Você deve fazer esses testes; o BIG CHANGE não executou esta sessão paga.
## Se a verificação falhar
Se a permissão for negada ou não houver dispositivo, confira navegador e microfone e use a URL local `localhost` ou HTTPS. Se a página informar falha `/session`, confira o status HTTP do servidor, chave, acesso ao modelo, cobrança, limites e endpoint. O servidor não imprime a chave nem o corpo da resposta OpenAI. Um 502 também pode significar falha de rede ou prazo de 25 segundos; não informa se uma solicitação incerta foi aceita.
Se a sinalização funcionar e a conexão falhar, confira estado, restrições de rede e resposta SDP. Se conectar sem som, confira entrada de microfone e saída escolhida. O navegador pode bloquear reprodução automática: clique em **Play** no controle de áudio e, se necessário, pare e inicie outra sessão. Em qualquer falha, use **Stop** e confirme que o indicador apagou. Confira o uso antes de repetir inícios com resultado incerto.
## Custo, privacidade e limites
A OpenAI lista para `gpt-realtime-2.1` texto não armazenado em cache de entrada/saída a **$4/$24 por milhão de tokens** e áudio de entrada/saída a **$32/$64 por milhão de tokens**, verificado em 19 de outubro de 2026. A cobrança depende dos tokens usados, inclusive turnos posteriores com contexto anterior. Confira [preços e limites do modelo](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) e o uso do projeto antes de começar; a duração sozinha não permite estimar a conta. O [guia de conversas Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) define máximo de 60 minutos por sessão.
O áudio do microfone vai à OpenAI durante a sessão. Use fala que pode transmitir e evite áudio privado ou de terceiros. Esta página local não oferece login nem orçamento por usuário. Um serviço compartilhado precisaria acesso `/session` autenticado e limitado por taxa, e identificador de segurança gerado no servidor; Host e Origin locais não bastam. O exemplo não chama ferramentas externas nem faz ligações. Não se mediu latência, confiabilidade, qualidade de voz, economia ou implantação em produção.
## Fontes e leituras adicionais
- [Guia WebRTC da OpenAI](https://developers.openai.com/api/docs/guides/voice-webrtc): troca Realtime unificada, áudio do navegador e protocolo GPT-Live separado.
- [Guia de conversas OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations): eventos, turnos, status de resposta e limite de duração.
- [OpenAI `gpt-realtime-2.1`: página do modelo](https://developers.openai.com/api/docs/models/gpt-realtime-2.1): acesso ao modelo, preços de tokens e limites. Confira preços e acesso ao executar o exemplo.
## Sources
- [Guia WebRTC da OpenAI](https://developers.openai.com/api/docs/guides/voice-webrtc) — Interface unificada Realtime calls, chave confiável, SDP, áudio do navegador e protocolo GPT-Live separado.
- [Conversas OpenAI Realtime](https://developers.openai.com/api/docs/guides/realtime-conversations) — Eventos, VAD, status da resposta e duração de 60 minutos.
- [Modelo OpenAI gpt-realtime-2.1](https://developers.openai.com/api/docs/models/gpt-realtime-2.1) — Preços de tokens e acesso do modelo.
- [Otimização de custos de voz OpenAI](https://developers.openai.com/api/docs/guides/voice-latency-cost) — Contexto e uso por resposta.
- [Cobrança de API OpenAI e ChatGPT](https://help.openai.com/en/articles/9039756-managing-billing-for-chatgpt-and-the-api-platform) — Cobranças separadas.
Newsletter da BIG CHANGE
A visão ampla, no seu ritmo.
Matérias recentes sobre IA e robótica, mudanças que merecem atenção e ideias práticas para usar. Escolha um briefing diário, um resumo semanal ou uma perspectiva mensal.
Next scheduled send (UTC): . Your first edition arrives at the next scheduled send after you confirm.
Sua privacidade, sua escolha.
O armazenamento necessário ajuda a proteger o site e a lembrar suas escolhas. O Google Analytics opcional permanece desativado até você autorizá-lo. Você pode ler todas as matérias usando apenas o armazenamento necessário. Detalhes de privacidade