# Como transcrever áudios e fotos do WhatsApp em um agente de IA

> Como um agente de IA no WhatsApp lê áudios, fotos e arquivos: baixe o link de 24 horas, converta e transcreva com gpt-transcribe, descreva imagens, em TypeScript.

Source: https://whasp.ai/pt/blog/transcrever-audio-whatsapp-agente-ia · 2026-10-20

## O essencial

- Todo arquivo que uma pessoa envia chega ao seu webhook como um link assinado que funciona por 24 horas, depois das quais a whasp apaga o arquivo, então copie o que for guardar. Um arquivo acima de 30 MB chega sem link.
- Para ler uma mensagem de voz, baixe o áudio, converta para MP3 com ffmpeg, transcreva com o gpt-transcribe da OpenAI ou com um modelo Whisper hospedado por você e trate a transcrição como o texto da pessoa.
- Para fotos, descreva a imagem uma única vez com um modelo de visão e guarde a descrição no histórico, em vez de reenviar a imagem a cada turno.
- As respostas podem levar um arquivo por URL HTTPS pública de até 64 MB, mas não é possível enviar mensagens de voz: um arquivo de áudio que você envia chega como arquivo.
- A whasp espera 10 segundos por um 2xx do seu webhook, então confirme primeiro e transcreva em segundo plano.

Um agente de IA no WhatsApp trata mensagens de voz, fotos e arquivos lendo o objeto `media` de cada evento `message.received`: baixe o link assinado dentro de 24 horas, transcreva o áudio com um modelo de speech-to-text como o gpt-transcribe da OpenAI, envie as imagens a um modelo de visão e guarde o resultado como texto. As respostas voltam como texto ou como arquivo por URL. Mensagens de voz não podem ser enviadas.

## O que a whasp entrega quando alguém envia mídia?

A whasp entrega a mídia recebida como um link assinado dentro do evento `message.received`. O objeto `media` tem quatro campos: `url`, `mime`, `filename` e `size`. O link vale por 24 horas e a whasp apaga o arquivo depois das 24 horas, então copie-o se precisar dele. Imagens, áudio e MP4 abrem no navegador, e os outros arquivos são baixados.

Dois limites importam. Por enquanto, um arquivo acima de 30 MB chega sem link, com `media` igual a null. E os servidores da whasp não guardam mensagens: o conteúdo chega ao seu webhook uma única vez, nesse evento, então uma transcrição que você não guarda está perdida.

O evento também pode trazer `location` (latitude, longitude, nome, endereço, ao vivo), `contacts` (vCards como strings), `reply_to` (a mensagem citada, com o texto) e `sender_name`. Os campos documentados não incluem nenhuma flag que marque uma mensagem de voz, então decida pelo `mime`: tudo que começa com `audio/` vai para a transcrição.

## Como transcrever um áudio do WhatsApp?

Para transcrever uma mensagem de voz, baixe o arquivo, converta-o para um MP3 pequeno e envie a um modelo de speech-to-text. A conversão é uma precaução com um motivo. A API empresarial do próprio WhatsApp [descreve uma gravação de voz](https://developers.facebook.com/docs/whatsapp/cloud-api/webhooks/reference/messages/audio) como `audio/ogg; codecs=opus`. O [guia de speech-to-text](https://developers.openai.com/api/docs/guides/speech-to-text) da OpenAI lista mp3, mp4, mpeg, mpga, m4a, wav e webm como formatos de entrada e não cita Ogg, enquanto os comentários de tipos no pacote npm `openai` listam também ogg e flac. Os dois discordam, e uma chamada ao ffmpeg resolve.

Mono, 16 kHz e 48 kbps bastam para fala e dão cerca de 0,36 MB por minuto, então o limite de arquivo de 25 MB da OpenAI equivale a mais de uma hora de áudio. Esta é a primeira metade do `media.ts`:

```ts
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import OpenAI, { toFile } from "openai";

const run = promisify(execFile);
const openai = new OpenAI(); // lê OPENAI_API_KEY

export type Media = { url: string; mime: string; filename: string | null; size: number };

// O link assinado da whasp dispensa o header Authorization: um GET simples, dentro de 24 horas.
async function download(media: Media): Promise<Buffer> {
  const res = await fetch(media.url);
  if (!res.ok) throw new Error(`media download failed: ${res.status}`);
  return Buffer.from(await res.arrayBuffer());
}

// As mensagens de voz são Ogg/Opus. Recodifique para um MP3 mono pequeno, que qualquer modelo aceita.
async function toMp3(audio: Buffer): Promise<Buffer> {
  const dir = await mkdtemp(join(tmpdir(), "voice-"));
  try {
    await writeFile(join(dir, "in"), audio);
    await run("ffmpeg", ["-v", "error", "-i", join(dir, "in"), "-vn", "-ac", "1", "-ar", "16000", "-b:a", "48k", join(dir, "out.mp3")]);
    return await readFile(join(dir, "out.mp3"));
  } finally {
    await rm(dir, { recursive: true, force: true });
  }
}

export async function transcribe(media: Media): Promise<string> {
  const mp3 = await toMp3(await download(media));
  const file = await toFile(mp3, "voice.mp3", { type: "audio/mpeg" });
  const result = await openai.audio.transcriptions.create({ file, model: "gpt-transcribe" });
  return result.text.trim();
}
```

O código grava arquivos temporários para que o ffmpeg consiga buscar posições em qualquer contêiner que o arquivo use. Se o seu host não tem ffmpeg, um build estático do pacote npm `ffmpeg-static` funciona, ou você pode usar um modelo hospedado por você que decodifica Ogg sozinho.

A OpenAI lista o `gpt-transcribe` como o modelo recomendado para fala gravada, a $0,0045 por minuto na sua [página de preços](https://developers.openai.com/api/docs/pricing). Uma mensagem de voz de 30 segundos custa cerca de um quinto de centavo, e 1.000 delas custam cerca de $2,25. Se você sabe quais idiomas os seus usuários falam, o guia documenta um array `languages` para esse modelo, como `["en", "es"]`. Ele também diz que a precisão varia por idioma.

### Dá para transcrever no seu próprio servidor?

Dá, e duas opções de código aberto cobrem a maioria dos casos.

| Opção | Onde roda | Entrada de áudio | Licença |
| --- | --- | --- | --- |
| OpenAI `gpt-transcribe` | API da OpenAI | Converta para MP3 antes; arquivos de até 25 MB | Paga, por minuto |
| [faster-whisper](https://github.com/SYSTRAN/faster-whisper) | Seu servidor, Python | Decodifica com o PyAV, que embute as bibliotecas do FFmpeg | MIT |
| [whisper.cpp](https://github.com/ggml-org/whisper.cpp) | Seu servidor, C e C++ | WAV de 16 bits por padrão, então converta com o ffmpeg; inclui um servidor HTTP com uma API parecida com a da OpenAI | MIT |

Escolha a API hospedada quando quiser o mínimo de código. Hospede por conta própria quando o áudio não puder sair da sua infraestrutura, quando o volume fizer o preço por minuto pesar ou quando a API lidar mal com algum dos seus idiomas.

## Como o agente lê uma foto?

Para ler uma foto, baixe os bytes e envie-os a um modelo de visão como uma data URL em base64, junto com uma pergunta. Os dois principais provedores aceitam o mesmo tipo de entrada. O [guia de visão](https://developers.openai.com/api/docs/guides/images-vision) da OpenAI lista PNG, JPEG, WEBP e GIF não animado. A [documentação de visão](https://platform.claude.com/docs/en/build-with-claude/vision) da Anthropic lista JPEG, PNG, GIF e WebP, com 10 MB por imagem na API do Claude. A segunda metade do `media.ts` usa o `gpt-6-luna` da OpenAI, que a página do modelo lista como aceitando entrada de texto e de imagem:

```ts
// media.ts, continuação. Fotos e PDFs: uma chamada que devolve texto que você pode guardar.
export async function look(media: Media, ask: string): Promise<string> {
  const data = (await download(media)).toString("base64");
  const part =
    media.mime === "application/pdf"
      ? { type: "input_file" as const, filename: media.filename ?? "file.pdf", file_data: `data:application/pdf;base64,${data}` }
      : { type: "input_image" as const, image_url: `data:${media.mime};base64,${data}`, detail: "auto" as const };
  const response = await openai.responses.create({
    model: "gpt-6-luna",
    store: false, // não guarde o arquivo do usuário no provedor
    input: [{ role: "user", content: [{ type: "input_text", text: ask }, part] }],
  });
  return response.output_text;
}

type Inbound = {
  text: string | null;
  media: Media | null;
  location?: { latitude: number; longitude: number; name?: string; address?: string };
  contacts?: string[];
  reply_to?: { id: string | null; text: string };
};

const IMAGES = ["image/jpeg", "image/png", "image/webp", "image/gif"];

// Tudo o que uma pessoa pode enviar, como o texto que o seu agente lê.
export async function toText(m: Inbound): Promise<string> {
  const lines: string[] = [];
  if (m.reply_to) lines.push(`(respondendo a: "${m.reply_to.text}")`);
  if (m.text) lines.push(m.text);
  const f = m.media;
  if (f?.mime.startsWith("audio/")) {
    lines.push(`[mensagem de voz, transcrita: ${await transcribe(f)}]`);
  } else if (f && IMAGES.includes(f.mime)) {
    lines.push(`[foto: ${await look(f, "Descreva esta foto para um assistente de chat em menos de 80 palavras. Copie exatamente qualquer texto visível.")}]`);
  } else if (f?.mime === "application/pdf") {
    lines.push(`[PDF ${f.filename}: ${await look(f, "Resuma este documento para um assistente de chat em menos de 150 palavras.")}]`);
  } else if (f) {
    lines.push(`[arquivo ${f.filename ?? "sem nome"}, ${f.mime}, ${f.size} bytes: não é possível ler]`);
  }
  if (m.location) lines.push(`[localização: ${m.location.name ?? ""} ${m.location.address ?? ""} (${m.location.latitude}, ${m.location.longitude})]`);
  if (m.contacts?.length) lines.push(`[compartilhou ${m.contacts.length} cartão(ões) de contato]`);
  return lines.join("\n") || "[mensagem vazia: um arquivo acima de 30 MB chega sem link]";
}
```

`toText` devolve texto simples para qualquer mensagem, que é o que o seu histórico e o seu prompt querem. Uma mensagem pode trazer `text` e `media` ao mesmo tempo, então ele lê os dois. Quando nada é legível, devolve um marcador, e o modelo pode responder com "esse arquivo não chegou, você pode mandar um menor?" Os `[colchetes]` correspondem à linha do [modelo de system prompt](https://whasp.ai/pt/blog/prompt-agente-ia-whatsapp-formatacao) que diz ao agente o que eles significam.

A função `look` descreve uma vez e guarda texto, não pixels. O arquivo some da whasp depois de 24 horas, e a documentação da Anthropic observa que, quando as imagens vão codificadas em base64 em um chat de vários turnos, cada requisição reenvia os bytes da imagem. Guarde a descrição na sua [memória de conversa](https://whasp.ai/pt/blog/memoria-conversa-agente-ia-whatsapp) e peça detalhe no prompt da descrição: copiar exatamente o texto visível é o que permite ao agente, na semana que vem, responder a uma pergunta sobre um recibo. Se perguntas de acompanhamento sobre a imagem importarem, copie também o arquivo para o seu próprio armazenamento.

Para usar o Claude no mesmo trabalho, envie os bytes como um bloco `image`, com a imagem antes do texto, o que a documentação da Anthropic diz funcionar melhor. O `claude-haiku-5-5` é o modelo que a Anthropic indica para trabalho de alto volume e sensível à latência:

```ts
import Anthropic from "@anthropic-ai/sdk";

const claude = new Anthropic(); // lê ANTHROPIC_API_KEY

export async function lookWithClaude(base64: string, mediaType: "image/jpeg" | "image/png" | "image/gif" | "image/webp"): Promise<string> {
  const message = await claude.messages.create({
    model: "claude-haiku-5-5",
    max_tokens: 300,
    messages: [{
      role: "user",
      content: [
        { type: "image", source: { type: "base64", media_type: mediaType, data: base64 } },
        { type: "text", text: "Descreva esta foto para um assistente de chat em menos de 80 palavras. Copie exatamente qualquer texto visível." },
      ],
    }],
  });
  return message.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("");
}
```

## E documentos, localizações e contatos?

Um PDF vai para o modelo do mesmo jeito que uma foto. O [guia de PDF](https://developers.openai.com/api/docs/guides/pdf-files) da OpenAI o aceita como um `input_file` com `file_data` em base64 e um `filename`, até 50 MB por arquivo, e entrega ao modelo tanto o texto extraído quanto as imagens das páginas, que é o ramo `application/pdf` acima. Para arquivos Word ou Excel, extraia o texto com uma biblioteca de sua confiança ou peça à pessoa que envie um PDF.

Uma localização chega como coordenadas com nome e endereço opcionais, então descreva-a em uma frase e deixe o modelo decidir o que fazer com ela. Um contato chega como texto vCard: faça o parse das linhas `FN` e `TEL` se o seu agente precisar de nomes e números. Uma resposta a uma mensagem anterior traz o texto dessa mensagem em `reply_to`, e é por isso que o `toText` o inclui: sem ele, um simples "sim" não tem sentido.

Trate tudo o que você extrair como não confiável. Uma transcrição, uma descrição ou o texto dentro de um PDF são palavras da outra pessoa, e alguém pode dizer ou escrever "ignore suas instruções" em qualquer um deles. Mantenha essas strings como dados no prompt e nunca como instruções.

## Como enviar um arquivo de volta?

Para enviar um arquivo, passe uma URL HTTPS pública em `media` no `POST /v1/messages`. A URL pode apontar para um arquivo de até 64 MB. A whasp o baixa uma vez no envio e apaga a cópia dela quando o envio termina. Quando você envia `text` e `media` juntos, o celular manda o arquivo primeiro e o texto como uma segunda mensagem do WhatsApp, sem legenda por enquanto, e ainda conta como uma única mensagem da API.

```ts
await fetch("https://api.whasp.ai/v1/messages", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WHASP_API_KEY}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "evt_2bX9kq7TfLw3:receipt",
  },
  body: JSON.stringify({
    to: "+15555550111",
    media: { url: "https://files.example.com/receipt.pdf?sig=abc123", filename: "receipt.pdf" },
    text: "Aqui está o seu recibo.",
  }),
});
```

O arquivo precisa continuar acessível enquanto a mensagem espera na fila e é enviada. Mantenha o link válido até chegar `message.sent` ou `message.failed` para aquela mensagem.

Mensagens de voz são a única coisa que a API não consegue enviar. Mensagens de voz enviadas não são suportadas, e um arquivo de áudio que você envia chega como arquivo, não como mensagem de voz. A regra prática é responder a uma mensagem de voz com texto. Se ainda assim você quer respostas faladas, gere o áudio com um modelo de text-to-speech e envie-o como arquivo, sabendo o que a pessoa vai ver.

## Como ligar tudo ao webhook?

Confirme o evento primeiro e processe em segundo plano, porque a whasp espera 10 segundos por um 2xx e depois tenta de novo com backoff, e a transcrição mais a chamada ao modelo podem levar mais que isso. Depois da verificação de assinatura de [tratamento de webhooks do WhatsApp](https://whasp.ai/pt/blog/webhook-whatsapp-assinatura-tentativas-duplicatas), o lado receptor fica assim:

```ts
if (event.type === "message.received") {
  // Responda 2xx agora. A transcrição e a chamada ao modelo rodam depois da resposta.
  void handleInbound(event.id, event.data.message).catch(console.error);
}
return new Response(null, { status: 204 });
```

`handleInbound` chama `toText`, guarda o resultado, consulta o seu modelo e envia a resposta, como em [criar um agente com a API da OpenAI](https://whasp.ai/pt/blog/integracao-openai-whatsapp-agente-gpt). Ignore um id de evento que você já tratou, ou uma nova tentativa vai pagar pela mesma transcrição duas vezes. Disparar e esquecer (fire-and-forget) funciona em um servidor que fica sempre ligado. Em um host serverless, coloque o evento em uma fila.

## O que guardar, e por quanto tempo?

Guarde a transcrição ou a descrição e apague o áudio e a imagem quando terminar de usá-los. A whasp remove a cópia dela depois de 24 horas, e o resto é decisão sua. Uma gravação de voz é dado pessoal, então defina por escrito quanto tempo a guarda e apague quando a pessoa pedir. [Descadastro (opt-out) e consentimento para agentes de IA](https://whasp.ai/pt/blog/opt-out-consentimento-whatsapp-agente-ia) cobre as regras. Passe `store: false` na chamada ao modelo, como o `look` faz, se você não quer que a OpenAI guarde a resposta.

## O que fazer agora

Adicione o `toText` ao handler que você já tem, mande para si mesmo uma mensagem de voz e uma foto e leia o que o agente enxerga. Depois planeje os casos de uso que dependem disso: [12 agentes de IA que funcionam melhor no WhatsApp](https://whasp.ai/pt/blog/casos-uso-agente-ia-whatsapp) lista vários em que voz e fotos são a entrada principal. A [documentação da whasp](https://whasp.ai/docs) descreve cada campo do objeto `media`.

## Perguntas

### Um agente de IA no WhatsApp pode enviar mensagens de voz?

Na whasp, não. Mensagens de voz enviadas não são suportadas, e um arquivo de áudio que você envia chega como arquivo, não como mensagem de voz. Responda às mensagens de voz com texto.

### Por quanto tempo posso baixar um arquivo que alguém enviou ao meu agente?

Por 24 horas. O link de mídia no evento message.received é assinado e vale por 24 horas, e a whasp apaga o arquivo depois disso, então copie-o se precisar dele.

### O que acontece quando alguém envia um arquivo acima de 30 MB?

A mensagem chega do mesmo jeito, mas o campo media vem null porque não há link. Seu agente pode perceber isso e pedir um arquivo menor.

### Qual modelo deve transcrever as mensagens de voz do WhatsApp?

O guia de speech-to-text da OpenAI recomenda o gpt-transcribe para fala gravada. Se o áudio precisa ficar nos seus próprios servidores, faster-whisper e whisper.cpp são opções com licença MIT.

### Preciso do ffmpeg para transcrever mensagens de voz do WhatsApp?

É o caminho seguro. A API empresarial do próprio WhatsApp descreve gravações de voz como Ogg com o codec Opus, e o guia da OpenAI lista mp3, mp4, mpeg, mpga, m4a, wav e webm, então converter para MP3 elimina a dúvida.
