Laboratório 01 — Artigo arquitetural de Text to Speech com OpenAI, Next.js 15 e TypeScript 7
Um guia incremental e específico sobre TTS baseado em requisição, fronteiras de segurança, streaming, testes, Codex e as decisões que separam uma demonstração bonita de uma implementação responsável.
Autora: Glaucia Lemos
Projeto: OpenAI Voice Labs
Código deste lab: labs/lab-01-text-to-speech
Última validação técnica: 19 de julho de 2026
Idioma: Português · English
- Trilha: Módulo 01 de 02
- Tempo estimado: 2–3 horas
- Pré-requisito: nenhum módulo anterior; este tutorial contém toda a preparação necessária
- Evidência de conclusão: aplicação executada e
npm run check:lab01aprovado na raiz
Workshop passo a passo · English article · Índice do workshop · Módulo 02 — Realtime →
Antes de começar
Este não é um artigo “copie quatro arquivos e diga que está pronto para produção”. A chamada mais curta para uma API de voz cabe em poucas linhas; o trabalho de engenharia está ao redor dela:
- onde a credencial fica;
- quais dados o cliente pode escolher;
- quais limites existem antes de a requisição gerar custo;
- como o sistema falha sem expor informações internas;
- quando streaming melhora de fato a experiência;
- o que é registrado e o que jamais deve aparecer nos logs;
- como outra pessoa — ou o Codex — consegue evoluir o código sem reaprender todas as decisões.
Este é o primeiro laboratório de uma série sobre diferentes formas de criar experiências de voz com o SDK da OpenAI. Aqui o recorte é propositalmente estreito: transformar texto em áudio com uma requisição delimitada. O Laboratório 02 trata de outro problema — um agente conversacional speech-to-speech ao vivo com Realtime e WebRTC.
O objetivo é construir uma aplicação pequena com um processo que poderia existir em um time real. Ao final teremos:
- texto para voz com
gpt-4o-mini-tts; - API key somente no servidor;
- validação, erros, rate limit, request IDs e logs sem conteúdo sensível;
- interface responsiva, acessível e honesta sobre voz gerada por IA;
- testes, CI, build e deploy na Vercel;
- um fluxo reprodutível para trabalhar com Codex.
O código completo está no repositório, mas minha recomendação é seguir as fatias na ordem. O valor didático está justamente em ver o sistema ganhar capacidade e proteção ao mesmo tempo.
Como este tutorial se sustenta sozinho
Você não precisa ler outro documento para concluir o laboratório. O Módulo 00 continua existindo como uma referência compartilhada para quem fará a série inteira, mas tudo que é indispensável aparece novamente aqui: diferença entre ChatGPT e API, cobrança, projeto, API key, instalação, execução, código, testes, deploy e diagnóstico.
Cada etapa responde a cinco perguntas:
- O que estamos construindo? O comportamento observável daquela fatia.
- Por que existe? A decisão de arquitetura ou produto por trás do código.
- Onde editar? O caminho exato do arquivo e do terminal.
- Como validar? Um checkpoint com comando e resultado esperado.
- O que pode dar errado? Falhas comuns antes de avançar.
Se você optar por reconstruir o projeto, não precisa adivinhar arquivos intermediários. Os blocos essenciais aparecem no momento em que passam a fazer sentido; o código final permanece disponível para comparação no link do início do artigo.
Mapa mental do laboratório
| Conceito | Neste projeto significa |
|---|---|
| Text to speech (TTS) | converter uma entrada textual delimitada em áudio sintetizado |
| Speech API | endpoint request-based usado para gerar o áudio |
| Route Handler | backend do Next.js que valida a entrada e chama a OpenAI |
| Streaming | encaminhar bytes de áudio sem montar o arquivo inteiro no servidor |
| Contrato | conjunto pequeno e tipado de valores que o cliente pode enviar |
| Guardas | verificações de origem, acesso, tamanho e quota antes da chamada faturável |
| Harness | regras, testes e comandos que tornam a implementação verificável por pessoas e agentes |
Sumário
- Escolha como acompanhar e prepare o computador
- Escolha a arquitetura antes da API
- Defina o contrato e as fatias verticais
- Crie uma base reproduzível
- Estabeleça a fronteira servidor–OpenAI
- Implemente texto para voz com streaming
- Consuma o stream no navegador sem mentir sobre a UX
- Projete erros, observabilidade e privacidade
- Proteja custo e uso público
- Teste as regras, não a OpenAI
- Use Codex como parte de um harness
- Faça deploy na Vercel
- O que ainda falta para o seu contexto de produção
0. Escolha como acompanhar e prepare o computador
Este workshop oferece três caminhos. Escolha um antes de digitar comandos:
- Caminho A — executar e estudar: você clona o repositório, instala exatamente o lockfile e abre o projeto pronto. É o caminho recomendado para uma primeira leitura.
- Caminho B — construir pelo starter (recomendado): você recebe configuração, dependências, página compilável e primeiro teste, mas implementa as capacidades de voz na ordem dos capítulos.
- Caminho C — reconstruir absolutamente do zero: você cria cada pasta, instala as dependências e implementa também o scaffolding. Use quando configuração de projeto fizer parte do objetivo da aula.
Os três chegam à mesma arquitetura. Não misture comandos sem observar o bloco “terminal” de cada etapa. O guia de acompanhamento explica como comparar checkpoints sem apagar sua tentativa.
0.1 Confira os pré-requisitos
Abra um terminal e execute:
node --version
npm --version
git --version
Resultado esperado:
- Node.js
v20ou superior; - npm instalado junto com o Node;
- Git disponível;
- uma conta na plataforma OpenAI com um projeto e faturamento apropriado ao teste;
- um editor como Visual Studio Code.
Se node não for reconhecido, instale uma versão LTS pelo site do Node.js e feche e reabra o terminal. Não continue enquanto os três comandos falharem.
A API key é um segredo. Este guia nunca pede para colá-la no código, no tutorial, no GitHub ou em
.env.example.
0.1.1 Entenda conta, cobrança e projeto antes de criar a chave
Uma assinatura ChatGPT Free, Plus, Pro, Business ou Enterprise não inclui automaticamente créditos da API. ChatGPT e OpenAI API Platform são produtos com cobrança separada. Este laboratório faz chamadas pela API e, portanto, depende de faturamento ou créditos configurados na Platform.
Use esta sequência:
- Entre em platform.openai.com com a conta que será responsável pelo laboratório.
- Abra o seletor de projeto. Use um projeto de estudo existente ou crie um chamado, por exemplo,
openai-voice-labs. - Abra as configurações de cobrança da API e confirme que há um meio de pagamento ou saldo disponível.
- No projeto, configure alertas de orçamento e confira quais modelos estão permitidos.
Um orçamento de projeto é um mecanismo de visibilidade e alerta; não trate a notificação como a única barreira contra gasto. O próprio app continuará precisando de autenticação, validação, rate limit e limites de sessão.
Se você participa de mais de uma organização, confirme o nome da organização e do projeto antes de gerar a chave. Uso, permissões e orçamento são atribuídos a esse escopo.
0.1.2 Crie uma chave específica e guarde-a uma única vez
Dentro do projeto escolhido, abra API Keys e selecione Create new secret key. Dê um nome identificável, como voice-labs-local, e use as permissões mínimas compatíveis com o exercício.
A chave completa é exibida somente na criação. Copie-a para um gerenciador de segredos ou diretamente para o arquivo local descrito na próxima etapa. Não envie a chave por chat, e-mail ou print; não a compartilhe com colegas; não a reutilize em aplicações públicas.
Se uma chave aparecer em um commit, log, gravação ou mensagem, considere-a comprometida: revogue-a na Platform, crie outra e atualize o ambiente. Apagar apenas o arquivo ou o commit não revoga a credencial.
0.1.3 Saiba o que o navegador nunca deve receber
OPENAI_API_KEY é uma credencial de servidor. Neste projeto:
- ela fica em
.env.localdurante o desenvolvimento; - entra como variável protegida no provedor de deploy;
- nunca recebe prefixo
NEXT_PUBLIC_; - nunca aparece em componente React, resposta de health check ou log;
- nunca é enviada ao navegador.
O browser chama /api/speech; essa rota chama a OpenAI. Esse limite será implementado e testado ao longo do artigo.
0.2 Caminho A: clone e execute o laboratório pronto
No diretório onde você guarda projetos:
git clone https://github.com/glaucia86/openai-voice-playground.git
cd openai-voice-playground/labs/lab-01-text-to-speech
npm ci
Por que npm ci, e não npm install? Porque o projeto já possui package-lock.json. npm ci instala o grafo registrado, falha se manifesto e lockfile divergirem e não reescreve versões silenciosamente.
Crie sua configuração local. Em macOS, Linux ou Git Bash:
cp .env.example .env.local
No PowerShell:
Copy-Item .env.example .env.local
Abra .env.local no editor e preencha somente:
OPENAI_API_KEY=cole_a_sua_chave_de_projeto_aqui
Não adicione aspas, não use NEXT_PUBLIC_ e não altere .env.example. Para o primeiro teste local, token, origem e Upstash podem ficar vazios; em produção eles são obrigatórios e a API falha com 503 se estiverem ausentes. Antes de iniciar, confirme que o Git ignora o arquivo:
git check-ignore -v .env.local
O comando deve imprimir uma regra de .gitignore. Se não imprimir nada, pare e corrija o ignore antes de continuar.
Inicie o servidor:
npm run dev
Abra http://localhost:3000. Em outro terminal, ainda dentro da pasta do Lab 01, valide o servidor:
curl http://localhost:3000/api/health
No PowerShell, você também pode usar:
Invoke-RestMethod http://localhost:3000/api/health
O JSON deve conter "ok":true, "configured":true, "speechModel":"gpt-4o-mini-tts" e nunca deve conter a chave. Se configured for false, pare o servidor com Ctrl+C, confirme o nome .env.local, salve o arquivo e execute npm run dev novamente.
0.3 Caminho B: construa a partir do starter recomendado
No diretório onde você guarda projetos:
git clone --branch workshop/lab-01-v1-starter \
https://github.com/glaucia86/openai-voice-playground.git
cd openai-voice-playground
git switch -c minha-solucao-lab-01
npm ci --prefix labs/lab-01-text-to-speech
npm run check:lab01
O primeiro gate deve passar sem API key e sem chamada paga. O starter contém uma página bilíngue, um health check com stage: "starter", dependências fixadas e um teste inicial. A rota /api/speech, os contratos e a interface funcional ainda não existem: eles serão construídos no workshop.
Criar minha-solucao-lab-01 evita commits acidentais na referência starter. Leia também o START-HERE-PT-BR.md incluído naquela branch.
Mapa de checkpoints do Lab 01
| Etapa | Referência somente de leitura | Evidência | Comparação |
|---|---|---|---|
| Starter | workshop/lab-01-v1-starter |
npm run check:lab01 |
ponto inicial |
| 01 — contrato | workshop/lab-01-v1-step-01-contract |
schema e testes do contrato passam | ver mudanças |
| 02 — servidor | workshop/lab-01-v1-step-02-server |
rota, guardas e testes server-side passam | ver mudanças |
| 03 — interface | workshop/lab-01-v1-step-03-interface |
npm run check:lab01 passa por completo |
ver mudanças |
| Solução final | main |
mesmo código do lab, acrescido da trilha publicada | abrir solução |
Para consultar um checkpoint sem substituir seus arquivos:
git fetch origin
git diff --stat HEAD..origin/workshop/lab-01-v1-step-01-contract
git show origin/workshop/lab-01-v1-step-01-contract:labs/lab-01-text-to-speech/src/lib/schemas.ts
Faça commit do seu progresso antes de investigar uma referência. Não use git reset --hard como mecanismo de acompanhamento.
0.4 Caminho C: crie a base a partir de uma pasta vazia
Os comandos abaixo usam Bash, Git Bash ou WSL. No PowerShell, New-Item -ItemType Directory -Force caminho equivale a mkdir -p caminho, e New-Item -ItemType File caminho cria um arquivo vazio.
Crie o diretório e entre nele:
mkdir -p openai-voice-labs/labs/lab-01-text-to-speech
cd openai-voice-labs/labs/lab-01-text-to-speech
npm init -y
Confira onde está antes de instalar qualquer coisa:
pwd
O caminho precisa terminar em labs/lab-01-text-to-speech. Isso evita instalar dependências na raiz errada.
Instale as dependências de runtime validadas pelo laboratório:
npm install next@15.5.20 react@19.2.7 react-dom@19.2.7 openai@^6.48.0 zod@^4.4.3 lucide-react@^1.25.0 @fontsource-variable/manrope@^5.2.8 @fontsource-variable/jetbrains-mono@^5.2.8 @upstash/ratelimit@2.0.8 @upstash/redis@1.38.0
Instale as ferramentas de desenvolvimento:
npm install --save-dev @types/node@^26.1.1 @types/react@^19.2.17 @types/react-dom@^19.2.3 oxlint@^1.74.0 vitest@^4.1.10 @vitest/coverage-v8@^4.1.10 typescript@5.8.2 typescript7@npm:typescript@7.0.2
Crie a árvore inicial:
mkdir -p scripts src/app/api/health src/app/api/speech src/components src/lib src/types tests tutorial
touch .env.example .gitignore next.config.mjs tsconfig.json vitest.config.ts scripts/typecheck.mjs
touch src/app/globals.css src/app/layout.tsx src/app/page.tsx
touch src/app/api/health/route.ts src/app/api/speech/route.ts
touch src/lib/constants.ts src/lib/openai.ts src/lib/schemas.ts
Neste ponto, a estrutura importante deve ser:
lab-01-text-to-speech/
├── .env.example
├── .gitignore
├── package.json
├── package-lock.json
├── scripts/typecheck.mjs
├── src/
│ ├── app/
│ │ ├── api/health/route.ts
│ │ ├── api/speech/route.ts
│ │ ├── globals.css
│ │ ├── layout.tsx
│ │ └── page.tsx
│ ├── components/
│ ├── lib/
│ └── types/
├── tests/
└── tutorial/
Edite package.json e substitua a seção scripts por:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "oxlint --deny-warnings src tests",
"typecheck": "node scripts/typecheck.mjs --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"test:coverage": "vitest run --coverage",
"check": "npm run lint && npm run typecheck && npm run test && npm run build"
}
}
O trecho acima mostra somente scripts: preserve name, version, dependencies e devDependencies que o npm criou.
Em .gitignore, escreva estas regras antes de criar um segredo:
.env*
!.env.example
.next/
node_modules/
coverage/
*.tsbuildinfo
Em .env.example, registre apenas nomes vazios:
OPENAI_API_KEY=
PLAYGROUND_ACCESS_TOKEN=
APP_ORIGIN=
UPSTASH_REDIS_REST_URL=
UPSTASH_REDIS_REST_TOKEN=
CLIENT_IP_HEADER=
Agora crie .env.local a partir do exemplo e use git check-ignore -v .env.local, como no Caminho A. A única cópia com valor real deve continuar local.
0.4 Crie uma página mínima antes da integração
Começar pela API e pela UI complexa ao mesmo tempo torna qualquer erro ambíguo. Primeiro prove que o framework sobe.
Em src/app/layout.tsx:
import type { Metadata } from "next";
import "./globals.css";
export const metadata: Metadata = { title: "Voice Lab 01" };
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="pt-BR">
<body>{children}</body>
</html>
);
}
Em src/app/page.tsx:
export default function HomePage() {
return <main><h1>Lab 01: Text to Speech</h1></main>;
}
Em src/app/globals.css:
* { box-sizing: border-box; }
body { margin: 0; font-family: system-ui, sans-serif; }
main { max-width: 72rem; margin: 0 auto; padding: 2rem; }
Execute:
npm run dev
Checkpoint: http://localhost:3000 deve mostrar o título sem erro no terminal. Pare com Ctrl+C. A partir daqui, os capítulos seguintes substituem a base mínima por contratos, rota, componentes, segurança e testes do projeto final.
0.5 Como ler cada etapa sem se perder
Para cada capítulo, repita o mesmo ciclo:
- confira o caminho do arquivo no título ou no parágrafo;
- crie o arquivo se ele ainda não existir;
- digite ou cole o bloco indicado;
- salve;
- execute o checkpoint daquele capítulo;
- só avance quando o resultado esperado aparecer.
Se ocorrer um erro, leia a primeira mensagem útil, não apenas a última linha. Confirme nome do arquivo, diretório atual e extensão antes de reinstalar tudo. A seção de armadilhas ao final funciona como roteiro de diagnóstico.
1. Escolha a arquitetura antes da API
“Aplicação de voz” pode significar produtos muito diferentes. Antes de escolher endpoint ou SDK, classifique a interação.
| Necessidade | Arquitetura mais adequada | Exemplo |
|---|---|---|
| Texto delimitado vira arquivo de áudio | Speech API request-based | Narração, acessibilidade, prévia de locução |
| Arquivo pronto vira texto | Transcriptions API request-based | Nota de voz, reunião gravada, legenda pós-processada |
| Microfone envia deltas enquanto a pessoa fala | Realtime transcription | Legenda ao vivo, ditado contínuo |
| Modelo escuta, raciocina e responde por voz com baixa latência | Realtime voice session | Agente conversacional |
A própria visão geral de Audio and speech separa APIs request-based de sessões Realtime: entradas delimitadas são mais simples; áudio ao vivo exige uma conexão de baixa latência e eventos parciais.
Nossa decisão
O recorte deste tutorial é uma interação delimitada: uma string de até 4.096 caracteres entra e um arquivo de áudio sai. Portanto, usaremos a Speech API. Não abriremos uma sessão Realtime somente para tornar o diagrama mais sofisticado.
Essa decisão reduz estado, custo operacional, tratamento de reconexão e superfície de erro. Ela também deixa explícito o próximo passo da série: quando o requisito é conversar por voz, com interrupções e turnos naturais, não devemos “esticar” TTS com gravações em loop; devemos adotar uma sessão Realtime. Essa é a arquitetura do Laboratório 02.
A fronteira principal
flowchart LR
UI["Browser"] -->|"JSON tipado"| API["Next.js Route Handler"]
API --> G["Contrato + guardas"]
G --> OA["OpenAI Audio API"]
OA -->|"stream de áudio"| API
API --> UI
O navegador não chama api.openai.com diretamente. Ele chama nossa aplicação. A Route Handler:
- autentica a implantação, se configurado;
- valida origem e quota;
- valida o corpo;
- escolhe modelo e parâmetros permitidos;
- chama a OpenAI com a chave do servidor;
- devolve um contrato estável para a interface.
Essa camada não existe apenas para “esconder a chave”. Ela impede que o cliente transforme qualquer string em um nome de modelo, envie texto arbitrariamente grande ou passe opções que o produto não decidiu suportar.
2. Defina o contrato e as fatias verticais
Antes de gerar arquivos, escreva uma definição curta de pronto.
Objetivo
Permitir que uma pessoa transforme texto em voz, entenda o caminho da requisição e receba feedback acessível em todos os estados relevantes.
Restrições
- Next.js 15 e App Router;
- TypeScript 7 em modo estrito;
- SDK oficial
openaino servidor; - nenhuma credencial, entrada de texto ou saída de áudio persistida;
- voz sintética identificada como gerada por IA;
- sem chamada paga em teste automatizado;
- deploy compatível com Vercel Functions.
Definição de pronto
- happy path e erros desenhados;
- lint, type-check, testes e build aprovados;
.env.localignorado pelo Git;- documentação coerente com o código;
- limitações de produção declaradas.
Fatias verticais
Em vez de “fazer todo o backend” e depois “fazer todo o frontend”, entregamos capacidade verificável de ponta a ponta.
| Fatia | Comportamento observável | Gate |
|---|---|---|
| 0. Base | Projeto sobe e /api/health informa configuração sem expor segredo |
type-check + build |
| 1. TTS mínimo | Texto validado retorna MP3 pelo servidor | teste de schema + chamada manual |
| 2. TTS utilizável | Voz, instrução, formato, velocidade, stream e download | acessibilidade + cancelamento |
| 3. Hardening | Token obrigatório em produção, origem, quota distribuída, request ID e logs sanitizados | testes de guardas |
| 4. Entrega | README, tutorial, CI e Vercel | npm run check |
O ponto importante é que cada fatia atravessa interface, contrato, servidor e validação. Se o contexto acabar ou outra pessoa assumir, há um estado funcional e explicável para continuar.
3. Crie uma base reproduzível
Versões usadas
O repositório fixa as versões principais no package.json:
{
"dependencies": {
"next": "15.5.20",
"openai": "^6.48.0",
"react": "19.2.7",
"zod": "^4.4.3"
},
"devDependencies": {
"oxlint": "^1.74.0",
"typescript": "5.8.2",
"typescript7": "npm:typescript@7.0.2",
"vitest": "^4.1.10"
}
}
Usamos Node.js 22 ou superior. O package-lock.json entra no repositório e o CI usa npm ci. “Funcionou hoje na minha máquina com latest” não é reprodutibilidade.
TypeScript 7 com Next.js 15: o detalhe que apareceu na validação
TypeScript 7 é a versão estável atual — a própria página de instalação do TypeScript informa a série 7.0. Todo o código do projeto é validado pelo compilador 7.0.2. Para gerar o bundle com Next.js 15, porém, precisamos registrar uma incompatibilidade real em vez de escondê-la.
Ferramentas auxiliares distribuídas com Next.js 15 foram publicadas antes da mudança interna do TypeScript 7. O parser TypeScript do preset ESLint falha antes de analisar nosso código; o carregador de next.config.ts usa uma API removida; e o bootstrap de build importa typescript/lib/typescript.js literalmente, mesmo com a checagem interna desabilitada. Instalar apenas o pacote 7 faz o Next tentar substituí-lo por 5.8 durante o build.
Adotamos uma toolchain dupla e explícita. O pacote canônico typescript@5.8.2 existe somente para a infraestrutura interna do Next 15; o alias npm typescript7 é a autoridade sobre o código. Um pequeno runner multiplataforma chama o binário correto sem depender de qual pacote ganhou o link node_modules/.bin/tsc:
{
"scripts": {
"lint": "oxlint --deny-warnings src tests",
"typecheck": "node scripts/typecheck.mjs --noEmit"
},
"devDependencies": {
"typescript": "5.8.2",
"typescript7": "npm:typescript@7.0.2"
}
}
Trade-off: mantemos dois compiladores instalados e deixamos de usar algumas regras específicas do preset next/core-web-vitals. Isso aumenta um pouco as dependências, mas torna a fronteira honesta e verificável: npm run typecheck imprime e executa TypeScript 7; o pacote 5.8 nunca decide os tipos da aplicação. A configuração do framework fica em next.config.mjs, carregada pelo Node. Como o bundler do Next 15 também não interpreta corretamente o paths do TS 7, o alias @ é declarado no callback webpack; o tsconfig continua sendo a fonte para o compilador e o editor. Desabilitamos o checker e o lint duplicados dentro de next build; npm run check e o CI exigem Oxlint e TS 7 antes do bundle. Rodar npm run build isoladamente não substitui esses gates. Quando o Next suportar integralmente TS 7, removemos o adaptador 5.8, o alias e o runner — compatibilidade deve ter prazo de validade.
No caminho “do zero”, scripts/typecheck.mjs ainda está vazio. Preencha-o agora:
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
const compilerPath = join(process.cwd(), "node_modules", "typescript7", "bin", "tsc");
if (!existsSync(compilerPath)) {
console.error("TypeScript 7 não foi encontrado. Execute `npm install` antes do typecheck.");
process.exit(1);
}
const result = spawnSync(process.execPath, [compilerPath, ...process.argv.slice(2)], {
stdio: "inherit",
});
if (result.error) {
console.error(`Não foi possível executar o TypeScript 7: ${result.error.message}`);
process.exit(1);
}
process.exit(result.status ?? 1);
Crie tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": false,
"skipLibCheck": true,
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [{ "name": "next" }],
"paths": { "@/*": ["./src/*"] }
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
Execute o primeiro gate:
npm run typecheck
O projeto mínimo deve passar sem emitir arquivos. Se aparecer “TypeScript 7 não foi encontrado”, confirme que o comando de instalação de dev dependencies terminou e que você está dentro do Lab 01.
Strict não é decoração
O tsconfig.json ativa:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noEmit": true,
"moduleResolution": "bundler"
}
}
exactOptionalPropertyTypes encontrou, por exemplo, a diferença entre omitir instructions e enviar instructions: undefined ao SDK. O objeto final usa spread condicional. É uma correção pequena, mas traduz uma ideia importante: contrato opcional não é necessariamente igual a campo presente com valor indefinido.
Segredo local
Copie o modelo:
cp .env.example .env.local
E preencha apenas localmente:
OPENAI_API_KEY=your_project_key
O .gitignore começa protegendo qualquer arquivo de ambiente e libera somente o exemplo:
.env*
!.env.example
Não use NEXT_PUBLIC_OPENAI_API_KEY. Variáveis com NEXT_PUBLIC_ são incorporadas ao bundle do navegador pelo Next.js. Uma Route Handler não corrige uma chave que já foi publicada pelo frontend.
4. Estabeleça a fronteira servidor–OpenAI
Um cliente centralizado e preguiçoso
src/lib/openai.ts cria o cliente somente quando uma rota realmente precisa dele:
import OpenAI from "openai";
const globalForOpenAI = globalThis as unknown as { openai?: OpenAI };
export function getOpenAIClient(): OpenAI {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new AppError(
503,
"configuration_error",
"The voice service is not configured.",
);
}
globalForOpenAI.openai ??= new OpenAI({
apiKey,
maxRetries: 2,
timeout: 45_000,
});
return globalForOpenAI.openai;
}
A checagem explícita produz um erro de produto melhor que uma exceção genérica do SDK. Reutilizar a instância também evita recriar configuração a cada chamada em um processo já aquecido.
Health check sem vazamento
GET /api/health responde se a aplicação está configurada, não qual é a chave:
{
"ok": true,
"configured": true,
"requiresAccessToken": false,
"capabilities": {
"speechModel": "gpt-4o-mini-tts",
"streamedSpeech": true
}
}
Evite “testar” configuração retornando os primeiros caracteres do segredo. Redação parcial continua sendo material sensível e costuma acabar em screenshot, log ou analytics.
Contratos com Zod
O tipo TypeScript desaparece em runtime. A requisição HTTP precisa de validação real:
export const speechRequestSchema = z
.object({
text: z.string().trim().min(1).max(4_096),
voice: z.enum(VOICE_IDS),
format: z.enum(["mp3", "wav", "opus"]).default("mp3"),
instructions: z.string().trim().max(4_096).optional().default(""),
speed: z.coerce.number().min(0.25).max(4).default(1),
})
.strict();
O limite de 4.096 caracteres vem da referência de Create speech. Repetimos esse limite no cliente por usabilidade e no servidor por segurança. O cliente ajuda; o servidor decide.
strict() rejeita campos extras. Se alguém enviar model: "qualquer-modelo" ou apiKey: "…", isso não será aceito silenciosamente.
readJsonBody não confia somente em Content-Length: ele lê o ReadableStream, soma os bytes reais e interrompe em 16 KiB. Isso cobre requisições chunked ou clientes que omitem o header. Ele também exige application/json e transforma JSON/UTF-8 inválido em erro estável, antes de qualquer chamada faturável.
5. Implemente texto para voz com streaming
A documentação de Text to speech recomenda gpt-4o-mini-tts para geração de voz atual e mostra que instructions pode controlar tom, ritmo, emoção, entonação e outros aspectos de entrega.
O núcleo de POST /api/speech é:
const payload = speechRequestSchema.parse(
await readJsonBody(request, 16 * 1024, "The speech request is too large."),
);
const openai = getOpenAIClient();
const speech = await openai.audio.speech.create(
{
model: "gpt-4o-mini-tts",
input: payload.text,
voice: payload.voice,
...(payload.instructions ? { instructions: payload.instructions } : {}),
response_format: payload.format,
speed: payload.speed,
stream_format: "audio",
},
{ signal: request.signal },
);
Note três decisões.
5.1 O cliente não escolhe o modelo livremente
O servidor fixa gpt-4o-mini-tts. Um seletor arbitrário parece flexível, mas transfere ao visitante uma decisão de capacidade, disponibilidade e custo que pertence ao produto. Quando houver uma razão para oferecer outro modelo, transforme isso em uma opção enumerada e testada.
5.2 Cancelamento atravessa a fronteira
O AbortSignal recebido pela Route Handler é enviado ao SDK. Cancelar a operação no navegador deixa de ser apenas esconder um spinner: a intenção de cancelamento chega à chamada de rede quando possível.
5.3 O servidor não transforma o áudio inteiro em Buffer
O antipadrão comum é:
// Evite no caminho de streaming:
const buffer = Buffer.from(await speech.arrayBuffer());
return new Response(buffer);
Isso espera o arquivo inteiro e mantém todos os bytes na memória da função. Em vez disso, devolvemos o corpo recebido:
if (!speech.body) {
throw new AppError(502, "empty_audio_stream", "OpenAI returned an empty audio stream.");
}
return new Response(speech.body, {
headers: {
"Content-Type": CONTENT_TYPES[payload.format],
"Cache-Control": "no-store",
"X-Request-Id": requestId,
"X-Model": "gpt-4o-mini-tts"
},
});
O ReadableStream passa pela Route Handler sem buffer integral no servidor. A Speech API usa transferência em chunks. A documentação também explica os formatos: MP3 é geral, Opus é adequado a comunicação/streaming e WAV evita custo de decodificação, mas é maior.
Divulgação obrigatória da voz sintética
A documentação de TTS afirma que a aplicação deve informar claramente que a voz ouvida é gerada por IA, não humana. Por isso a interface mostra “AI-generated voice” ao lado do resultado. Isso não deve ficar escondido nos termos de uso.
Também evitamos apresentar instructions como um campo de “clone qualquer pessoa”. O texto de ajuda orienta estilo de entrega e desestimula personificação de uma pessoa real. Em um produto, acrescente política, moderação e consentimento apropriados ao caso de uso.
6. Consuma o stream no navegador sem mentir sobre a UX
No cliente, fetch expõe response.body como ReadableStream:
const response = await fetch("/api/speech", {
method: "POST",
headers: {
"Content-Type": "application/json",
...authorizationHeaders(accessToken),
},
body: JSON.stringify({ text, instructions, voice, format, speed }),
signal: controller.signal,
});
const reader = response.body.getReader();
const chunks: ArrayBuffer[] = [];
let totalBytes = 0;
while (true) {
const { done, value } = await reader.read();
if (done) break;
const copy = new Uint8Array(value.byteLength);
copy.set(value);
chunks.push(copy.buffer);
totalBytes += value.byteLength;
setReceivedBytes(totalBytes);
}
const blob = new Blob(chunks, { type: response.headers.get("content-type")! });
const audioUrl = URL.createObjectURL(blob);
O que está sendo transmitido e o que ainda está sendo acumulado?
O servidor começa a encaminhar bytes sem esperar o arquivo completo. A interface consegue exibir progresso real de bytes recebidos. Porém, para compatibilidade entre formatos e navegadores, esta primeira versão monta um Blob completo antes de ligar a URL ao <audio>.
Portanto:
- há streaming de transporte OpenAI → servidor → navegador;
- não prometemos playback progressivo no componente atual;
- o servidor evita buffer integral;
- o navegador acumula o resultado para reprodução e download compatíveis.
Playback antes do fim exigiria uma implementação com Media Source Extensions, formato cuidadosamente escolhido e fallbacks por navegador — ou uma arquitetura Realtime. Não é impossível; apenas não é gratuito.
Limpeza de memória
Object URLs precisam ser revogadas:
if (currentObjectUrlRef.current) {
URL.revokeObjectURL(currentObjectUrlRef.current);
}
Também abortamos a requisição ao desmontar o componente. Microinteração bonita não compensa vazamento de memória em uma sessão longa.
Estados que desenhamos
- configuração do servidor em verificação;
- chave ausente;
- token da implantação necessário;
- pronto para gerar;
- stream iniciado e bytes recebidos;
- cancelado;
- erro sanitizado com request ID;
- áudio pronto, player e download.
O aria-live anuncia o progresso sem roubar foco. Animações respeitam prefers-reduced-motion.
7. Projete erros, observabilidade e privacidade
Envelope estável
O frontend recebe:
{
"error": {
"code": "invalid_request",
"message": "Text must contain at most 4096 characters.",
"requestId": "8d966daa-..."
}
}
Erros do SDK são traduzidos. Não devolvemos error.message bruto da OpenAI, stack trace, nome de projeto, detalhes de autenticação ou payload original.
Mapeamentos importantes:
| Origem | Resposta do produto |
|---|---|
| Zod | 400 invalid_request |
| JSON inválido | 400 invalid_json |
| corpo JSON grande | 413 request_too_large |
| quota da aplicação | 429 rate_limit_exceeded, com Retry-After |
| limitador distribuído indisponível | 503 rate_limiter_unavailable |
| configuração de segurança incompleta | 503 security_configuration_incomplete |
| rate limit da OpenAI | 429 upstream_rate_limit |
| credencial upstream inválida | 503 upstream_authentication_error |
| outro erro upstream | 502 upstream_error |
Request ID
Cada rota gera um UUID, devolve X-Request-Id e inclui o mesmo valor no erro. Assim, suporte consegue correlacionar relato e log sem pedir ao usuário que publique o texto ou as instruções de voz.
Logue metadados, não conteúdo
Exemplo de evento:
{
"timestamp": "2026-07-19T03:00:00.000Z",
"event": "speech.completed",
"requestId": "8d966daa-...",
"route": "speech",
"durationMs": 1840,
"status": 200,
"model": "gpt-4o-mini-tts",
"inputSize": 187,
"outputSize": 49312
}
Não entram:
- texto enviado ao TTS;
- instruções de voz;
- bytes de áudio;
- API key;
- access token.
Mesmo o tamanho pode ser sensível em alguns domínios. O schema de log deve passar pela sua análise de risco, e acesso/retenção precisam ser controlados.
Privacidade de API não substitui governança do produto
A documentação de controles de dados da API informa que dados da API não são usados para treinamento por padrão. Nos controles padrão, logs de monitoramento de abuso podem conter conteúdo e ser retidos por até 30 dias. Organizações elegíveis podem solicitar controles adicionais, mas o código não deve prometer Zero Data Retention sem confirmar configuração e compatibilidade do endpoint. A sua organização ainda precisa avaliar região, consentimento, base legal, dados de crianças, dados biométricos/voz e termos do caso de uso.
Um README não é DPIA, política de privacidade ou parecer jurídico.
8. Proteja custo e uso público
Uma chave escondida no servidor ainda pode ser consumida por uma rota pública. “A chave não vazou” e “ninguém consegue gastar minha quota” são problemas diferentes.
Camadas incluídas
- Same-origin check — bloqueia requisições cross-site explícitas de browser.
- Limite de corpo real — mede bytes do stream, inclusive sem
Content-Length. - Allowlist — modelo, voz e formato são enumerados; velocidade e texto têm limites.
- Pré-limite de autenticação — limita trinta tentativas por minuto antes de comparar o token.
- Quota da operação — dez chamadas por minuto, distribuídas via Upstash Redis em produção.
- Token obrigatório em produção —
PLAYGROUND_ACCESS_TOKENsó é opcional localmente. - Fail closed — configuração ou Redis indisponível produz
503, não custo upstream. - Timeout/retries limitados — o SDK usa timeout de 45 segundos e duas tentativas.
- No-store — respostas sensíveis não devem ser armazenadas por caches intermediários.
Dois modos deliberados de rate limit
No desenvolvimento e nos testes, uma Map mantém o laboratório reproduzível sem infraestrutura. Em produção, getSecurityConfiguration exige UPSTASH_REDIS_REST_URL e UPSTASH_REDIS_REST_TOKEN; todas as instâncias passam a compartilhar a janela no Redis. Se o Redis falhar, a rota bloqueia a chamada.
O endereço bruto não é gravado como chave: a aplicação usa SHA-256 truncado. Na Vercel, a origem do identificador é x-vercel-forwarded-for, sobrescrito pela plataforma. Fora dela, CLIENT_IP_HEADER deve nomear um header que o seu proxy confiável sobrescreve. Nunca confie em um header enviado diretamente da internet.
Redis resolve a coordenação entre instâncias, não identidade, NAT compartilhado, bot detection ou orçamento. Um produto público deve limitar também por conta autenticada, concorrência e orçamento do projeto OpenAI.
Token compartilhado também não é identidade
Em produção, PLAYGROUND_ACCESS_TOKEN é obrigatório. O health check informa apenas que o token é necessário. A pessoa digita o valor, que fica no estado React e vai no header Authorization; não usamos localStorage. Tentativas inválidas consomem a quota de autenticação antes da comparação em tempo constante sobre hashes.
Isso é suficiente para uma audiência conhecida em um workshop. Não oferece revogação individual, papel, auditoria por usuário ou MFA. Para isso, implemente autenticação real.
Headers de segurança
src/middleware.ts cria um nonce por resposta e envia uma CSP com strict-dynamic, sem unsafe-inline para scripts em produção. Isso exige renderização dinâmica — é um trade-off consciente contra HTML estático/CDN. next.config.mjs adiciona HSTS em produção, X-Content-Type-Options, política de referrer, restrição de framing, COOP/CORP e Permissions-Policy. Como este laboratório é TTS, câmera, geolocalização e microfone ficam desabilitados.
Headers reduzem superfície, mas precisam ser testados junto com analytics, fontes, CDN e ferramentas reais do produto. Copiar uma CSP pronta e depois adicionar * quando algo quebra elimina seu valor.
9. Teste as regras, não a OpenAI
Testes automatizados deste repositório não consomem a API. Eles testam nossa responsabilidade:
- defaults e limites do schema de TTS;
- rejeição de voz e campos extras;
- formato, velocidade e instruções;
- janela e headers do rate limiter;
- tentativas inválidas limitadas antes da autenticação;
- configuração fail-closed de produção;
- corpo acima do limite mesmo sem
Content-Length; - origem e token de acesso;
- parser do envelope de erro;
- formatação de bytes.
Exemplo:
it("blocks a request after the limit", async () => {
await checkRateLimit("speech:client", {
limit: 1,
windowMs: 1_000,
now: 100,
});
const blocked = await checkRateLimit("speech:client", {
limit: 1,
windowMs: 1_000,
now: 200,
});
expect(blocked).toEqual({
allowed: false,
limit: 1,
remaining: 0,
resetAt: 1_100,
});
});
Pirâmide de validação
Durante a fatia: teste focado + type-check
Antes do commit: lint + type-check + testes
Antes do deploy: coverage + build de produção
Depois do deploy: health check + smoke test manual controlado
Comandos:
npm run lint
npm run typecheck
npm test
npm run test:coverage
npm run build
O CI usa npm ci, falha em advisories high/critical, e usa uma chave fictícia somente para o build. Nenhuma rota é executada durante o build; se isso mudar, o teste precisa continuar impedindo uma chamada paga acidental. Dependabot abre atualizações semanais e CodeQL analisa JavaScript/TypeScript com Actions fixadas por SHA, reduzindo risco de supply chain sem fingir que automação substitui revisão.
O que testar em integração depois
Para um time com ambiente de staging e orçamento controlado, adicione poucos contract tests contra a API real:
- TTS curto retorna content type e bytes válidos;
- modelo sem permissão produz erro mapeado;
- latência e tamanho ficam abaixo de SLOs definidos.
Esses testes não devem rodar em todo pull request sem controle de custo e flakiness.
10. Use Codex como parte de um harness
Codex ajudou a acelerar implementação e validação, mas o processo não foi “crie um app bonito”. Um agente precisa de objetivo, contexto, restrições e definição de pronto. A documentação de Codex best practices recomenda exatamente esses quatro elementos e orienta usar AGENTS.md como instrução durável do repositório.
Prompt de uma fatia, não do universo
Exemplo para implementar TTS:
Objetivo:
Implementar POST /api/speech e um cliente mínimo que gere áudio.
Contexto:
- src/lib/constants.ts
- src/lib/schemas.ts
- documentação oficial de Create speech
Restrições:
- OPENAI_API_KEY somente no servidor
- modelo fixo gpt-4o-mini-tts
- texto máximo de 4096 caracteres
- não registrar texto ou instruções
- encaminhar o ReadableStream sem Buffer integral
- erro sanitizado com request ID
Pronto quando:
- MP3 é reproduzível no navegador
- cancelamento usa AbortSignal
- schema e erros possuem testes
- lint, type-check e testes passam
Esse prompt reduz ambiguidade sem despejar o repositório inteiro no contexto.
AGENTS.md como memória operacional
O arquivo na raiz registra:
- mapa do repositório;
- comandos oficiais;
- proibições de segurança;
- convenções de TypeScript e UI;
- diferença entre streaming de TTS e Realtime;
- definition of done.
Isso evita repetir “nunca logue texto ou instruções de voz” em toda tarefa e torna a revisão humana mais objetiva. Se uma regra mudar, atualize o arquivo; não confie na memória de uma conversa antiga.
Context Engineering aplicado
Forneça apenas o conjunto de contexto necessário à fatia:
- contrato e constantes;
- rota vizinha como padrão;
- trecho exato da documentação oficial;
- teste que demonstra a regra;
- erro atual, se houver.
Não entregue 40 screenshots, toda a documentação da OpenAI e o node_modules. Contexto demais também degrada decisão.
Harness Engineering aplicado
O harness deste projeto não é um framework mágico. É o sistema ao redor da geração:
flowchart TD
S["Spec da fatia"] --> C["Contexto selecionado"]
C --> I["Implementação"]
I --> V["lint + tipos + testes + build"]
V --> R{"Gate aprovado?"}
R -->|"não"| F["feedback específico"]
F --> I
R -->|"sim"| H["handoff + documentação"]
O agente gera uma proposta; o harness produz evidência.
Como lidar com APIs atuais
Não peça ao Codex para “lembrar quais vozes existem”. Peça para consultar a fonte atual e registrar a referência. APIs, modelos e SDKs mudam.
Para este projeto, fontes de verdade incluem:
Handoff limpo
Ao terminar uma fatia, registre:
Estado:
- POST /api/speech implementado e stream validado.
Decisões:
- modelo fixo no servidor;
- MP3 default; WAV e Opus enumerados;
- browser acumula Blob por compatibilidade.
Validação:
- npm run lint ✅
- npm run typecheck ✅
- npm test ✅
Próxima fatia:
- hardening, documentação e deploy.
Riscos abertos:
- token compartilhado ainda não representa identidade individual.
Esse resumo é mais útil que compactar toda a conversa automaticamente e torcer para a próxima janela reconstruir as decisões.
11. Faça deploy na Vercel
11.1 Importe o repositório
Na Vercel, crie um projeto a partir de:
https://github.com/glaucia86/openai-voice-playground
Antes de importar, configure Root Directory como:
labs/lab-01-text-to-speech
Mantenha main como Production Branch. O framework será detectado como Next.js a partir da pasta do laboratório.
11.2 Configure variáveis
Em Project Settings → Environment Variables:
OPENAI_API_KEY=...
PLAYGROUND_ACCESS_TOKEN=uma-frase-longa-e-aleatoria
APP_ORIGIN=https://seu-dominio.example
UPSTASH_REDIS_REST_URL=...
UPSTASH_REDIS_REST_TOKEN=...
Todas as variáveis acima são obrigatórias em produção. A Vercel faz o código usar x-vercel-forwarded-for, portanto CLIENT_IP_HEADER pode ficar ausente ali; em outro provedor, configure-o com um header sobrescrito pelo proxy confiável. A documentação de Environment variables na Vercel explica o armazenamento fora do código.
Escolha conscientemente os ambientes:
- Production;
- Preview;
- Development.
Uma chave de produção não precisa estar disponível em todo preview criado por qualquer branch. Prefira projetos/chaves separados e permissões mínimas.
11.3 Faça o deploy
O pipeline executa:
npm ci
npm audit --audit-level=high
npm run lint
npm run typecheck
npm run test:coverage
npm run build
11.4 Valide sem expor segredo
Abra:
https://seu-dominio.example/api/health
Confirme:
{
"ok": true,
"configured": true
}
Depois execute smoke tests curtos:
- TTS com uma frase sem dado pessoal;
- cancelamento durante geração;
- texto vazio e texto acima do limite são rejeitados antes do upstream;
- token incorreto produz
401; - request ID aparece em falha;
- player, download, layout e teclado funcionam em viewport móvel.
11.5 Configure operação
Antes de divulgar a URL:
- orçamento e alerta no projeto OpenAI;
- identidade real ou Deployment Protection além do token do workshop;
- quota por usuário, concorrência e orçamento além do rate limit distribuído;
- logs e alertas sem conteúdo;
- política de retenção da plataforma;
- domínio e
APP_ORIGINcorretos; - plano de rotação de chave;
- owner para incidente e abuso.
12. O que ainda falta para o seu contexto de produção
“Production-minded” descreve os padrões incorporados. “Pronto para a sua produção” depende de requisitos que nenhum template conhece.
Checklist de decisão
Produto e UX
- O usuário entende quando o texto é enviado e quando o áudio é recebido?
- A voz sintética é claramente identificada?
- O texto enviado está autorizado e livre de dados pessoais desnecessários?
- Cancelamento e retry são compreensíveis?
- Teclado, leitor de tela, contraste e reduced motion foram testados?
Segurança e abuso
- Há autenticação individual?
- A quota é por usuário/tenant e distribuída?
- Existe limite financeiro no projeto OpenAI?
- Limites de entrada e saída são adequados ao risco e ao orçamento?
- Chaves têm escopo, rotação e owner?
- CSP e proxy headers foram validados no ambiente real?
Dados e compliance
- Base legal e consentimento estão definidos?
- Retenção e deleção estão documentadas?
- Regiões e subprocessadores foram avaliados?
- Dados de voz sensíveis são permitidos?
- Logs, suporte e analytics evitam conteúdo?
Confiabilidade e custo
- Há SLO de latência e erro?
- Retry não multiplica custo indevidamente?
- Circuit breaker ou degradação foi considerado?
- Métricas diferenciam erro local e upstream?
- Existe teste de contrato controlado?
Trade-offs que ficaram explícitos
| Decisão | Benefício | Limite assumido |
|---|---|---|
| Route Handlers server-side | chave isolada e contrato controlado | servidor vira ponto de custo e latência |
| modelo fixo para TTS | previsibilidade | menos experimentação arbitrária |
| stream no servidor + Blob no browser | memória menor no servidor e player compatível | playback só após completar |
| Redis em produção; memória local | coordena instâncias sem dificultar o primeiro exercício | quota por IP não substitui identidade |
| access token compartilhado | protege workshop rapidamente | não substitui identidade |
Oxlint + tsc + config .mjs |
compatibilidade com TS 7 | gates separados; build isolado não faz type-check |
Armadilhas comuns
- Chamar a OpenAI diretamente do browser com a chave. Não faça isso.
- Validar apenas no formulário. Qualquer cliente pode chamar a rota.
- Aceitar nome de modelo arbitrário. Isso entrega decisão de custo ao visitante.
- Converter stream em Buffer no servidor. Você perde o benefício antes de o cliente receber um byte.
- Dizer “Realtime” porque existe uma animação. Realtime envolve sessão e deltas, não um spinner.
- Logar prompt para “debugar depois”. Esse atalho vira incidente de privacidade.
- Acreditar que serverless compartilha memória. O rate limiter local não é global.
- Não revogar Object URLs. A aba mantém áudio anterior na memória.
- Chamar erro bruto do provedor de mensagem amigável. Ele pode vazar detalhes e acoplar o frontend.
- Usar Codex sem definition of done. Código gerado não é evidência de conclusão.
- Congelar o artigo e atualizar apenas dependências. Documentação divergente ensina o comportamento errado.
Exercícios para evoluir o projeto
- Adicione Media Source Extensions e compare time-to-first-audio por navegador.
- Compare esta arquitetura com o agente Realtime do Laboratório 02 e documente as diferenças de estado, latência e segurança.
- Adicione um teste de integração do Upstash num ambiente isolado e valide concorrência entre instâncias.
- Adicione autenticação e budget por usuário.
- Crie um teste de contrato que valide assinatura do arquivo, duração e latência sem comparar áudio byte a byte.
- Instrumente OpenTelemetry sem registrar conteúdo.
- Faça um scorecard de acessibilidade com Playwright e axe.
- Adicione cache somente após definir uma política segura de chave, retenção e invalidação.
Conclusão
A chamada à API foi a parte fácil. O aprendizado relevante está em transformar uma capacidade poderosa em um comportamento limitado, observável e revisável.
Neste projeto:
- a OpenAI fica atrás de uma fronteira server-side;
- os contratos são menores que a capacidade total do provedor;
- streaming é usado onde reduz buffer, com a limitação do player declarada;
- segurança, custo, acessibilidade e disclosure aparecem na experiência;
- Codex participa de um loop com contexto, regras e gates;
- documentação registra tanto as escolhas quanto o que ainda não foi resolvido.
Esse é o padrão que vale carregar para outros projetos de IA: não demonstrar apenas que o modelo responde, mas mostrar que o sistema ao redor dele merece confiança.
Glossário rápido
| Termo | Explicação prática |
|---|---|
| API key | segredo que autoriza e atribui custo às chamadas do projeto OpenAI |
| Backend for Frontend | camada server-side específica para proteger e limitar o que a interface pode fazer |
| CSP | política de navegador que limita de onde scripts, mídia e conexões podem ser carregados |
| Object URL | URL temporária criada pelo navegador para reproduzir ou baixar o áudio em memória |
| Rate limit | regra que limita requisições por identidade ou sinal técnico em uma janela de tempo |
| Request ID | identificador seguro para correlacionar uma falha sem registrar o conteúdo do usuário |
| Schema | contrato executável que valida forma, tipo e limites da entrada |
| Server-only | código ou segredo que não pode entrar no bundle enviado ao navegador |
| Vertical slice | incremento pequeno que atravessa contrato, backend, interface e validação |
Referências oficiais
- OpenAI — Audio and speech
- OpenAI — Text to speech
- OpenAI — TypeScript SDK reference
- OpenAI — Create speech reference
- OpenAI — Managing projects in the API Platform
- OpenAI — API key safety
- OpenAI — ChatGPT and API billing are separate
- OpenAI — Codex best practices
- OpenAI — Enterprise privacy
- TypeScript — Installation
- Vercel — Environment variables
Workshop passo a passo · English article · Índice do workshop · Módulo 02 — Realtime →