Transcrire un message vocal WhatsApp avec un agent IA
Comment un agent IA lit les messages vocaux, photos et fichiers WhatsApp : lien de 24 h, conversion ffmpeg, transcription gpt-transcribe, légendes d'images, TypeScript.

L’essentiel
- Chaque fichier envoyé par une personne arrive sur votre webhook sous forme de lien signé valable 24 heures, après quoi whasp supprime le fichier : copiez donc ce que vous gardez. Un fichier de plus de 30 Mo arrive sans lien.
- Pour lire un message vocal, téléchargez-le, convertissez-le en MP3 avec ffmpeg, transcrivez-le avec gpt-transcribe d'OpenAI ou un modèle Whisper auto-hébergé, et traitez la transcription comme le texte de la personne.
- Pour les photos, décrivez l'image une seule fois avec un modèle de vision et stockez la description dans votre historique plutôt que de renvoyer l'image à chaque tour.
- Les réponses peuvent contenir un fichier via une URL HTTPS publique jusqu'à 64 Mo, mais les messages vocaux ne peuvent pas être envoyés : un fichier audio que vous envoyez arrive comme un fichier.
- whasp attend 10 secondes un 2xx de votre webhook : accusez réception d'abord et transcrivez en arrière-plan.
Un agent IA WhatsApp traite les messages vocaux, les photos et les fichiers en lisant l’objet media de chaque événement message.received : téléchargez le lien signé dans les 24 heures, transcrivez l’audio avec un modèle de speech-to-text comme gpt-transcribe d’OpenAI, envoyez les images à un modèle de vision et stockez le résultat sous forme de texte. Les réponses repartent en texte ou en fichier via une URL. Les messages vocaux ne peuvent pas être envoyés.
Que livre whasp quand quelqu’un envoie un média ?
whasp livre les médias entrants sous forme de lien signé dans l’événement message.received. L’objet media a quatre champs : url, mime, filename et size. Le lien est valable 24 heures et whasp supprime le fichier au bout de 24 heures : copiez-le si vous en avez besoin. Les images, l’audio et le MP4 s’ouvrent dans le navigateur, les autres fichiers se téléchargent.
Deux limites comptent. Pour l’instant, un fichier de plus de 30 Mo arrive sans lien, avec media à null. Et les serveurs de whasp ne conservent aucun message : le contenu n’arrive qu’une fois sur votre webhook, dans cet événement ; une transcription que vous ne stockez pas est donc perdue.
L’événement peut aussi contenir location (latitude, longitude, nom, adresse, live), contacts (vCards sous forme de chaînes), reply_to (le message cité, avec son texte) et sender_name. Les champs documentés ne comprennent aucun indicateur qui marque un message vocal : décidez donc d’après mime. Tout ce qui commence par audio/ part en transcription.
Comment transcrire un message vocal WhatsApp ?
Pour transcrire un message vocal, téléchargez le fichier, convertissez-le en petit MP3 et envoyez-le à un modèle de speech-to-text. La conversion est une précaution qui a une raison. L’API Business de WhatsApp décrit un enregistrement vocal comme audio/ogg; codecs=opus. Le guide speech-to-text d’OpenAI liste mp3, mp4, mpeg, mpga, m4a, wav et webm comme formats d’entrée et ne cite pas l’Ogg, alors que les commentaires de types du paquet npm openai listent aussi ogg et flac. Les deux ne concordent pas, et un appel à ffmpeg tranche.
Mono, 16 kHz et 48 kbit/s suffisent largement pour la parole et donnent environ 0,36 Mo par minute : la limite de fichier de 25 Mo d’OpenAI représente donc plus d’une heure d’audio. Voici la première moitié de media.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(); // lit OPENAI_API_KEY
export type Media = { url: string; mime: string; filename: string | null; size: number };
// Le lien signé de whasp n'a pas besoin d'en-tête Authorization : un simple GET, dans les 24 heures.
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());
}
// Les messages vocaux sont en Ogg/Opus. On les réencode en petit MP3 mono pour que n'importe quel modèle les accepte.
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();
}
Le code écrit des fichiers temporaires pour que ffmpeg puisse se déplacer dans n’importe quel conteneur. Si votre hôte n’a pas ffmpeg, un build statique du paquet npm ffmpeg-static fait l’affaire, ou vous pouvez utiliser un modèle auto-hébergé qui décode lui-même l’Ogg.
OpenAI indique gpt-transcribe comme le modèle recommandé pour la parole enregistrée, à $0,0045 la minute sur sa page de tarifs. Un message vocal de 30 secondes coûte environ un cinquième de cent, et 1 000 messages coûtent environ $2,25. Si vous savez quelles langues parlent vos utilisateurs, le guide documente un tableau languages pour ce modèle, par exemple ["fr", "en"]. Il précise aussi que la précision varie selon la langue.
Peut-on transcrire sur son propre serveur à la place ?
Oui, et deux options open source couvrent la plupart des cas.
| Option | Où ça tourne | Entrée audio | Licence |
|---|---|---|---|
OpenAI gpt-transcribe |
API d’OpenAI | À convertir d’abord en MP3 ; fichiers jusqu’à 25 Mo | Payant, à la minute |
| faster-whisper | Votre serveur, Python | Décode avec PyAV, qui embarque les bibliothèques FFmpeg | MIT |
| whisper.cpp | Votre serveur, C et C++ | WAV 16 bits par défaut, donc à convertir avec ffmpeg ; inclut un serveur HTTP avec une API de type OpenAI | MIT |
Choisissez l’API hébergée quand vous voulez le moins de code. Auto-hébergez quand l’audio ne doit pas quitter votre infrastructure, quand le volume rend le prix à la minute important, ou quand l’API gère mal l’une de vos langues.
Comment un agent lit-il une photo ?
Pour lire une photo, téléchargez les octets et envoyez-les à un modèle de vision sous forme d’URL de données base64, avec une question. Les deux grands fournisseurs acceptent le même type d’entrée. Le guide de vision d’OpenAI liste PNG, JPEG, WEBP et GIF non animé. La documentation de vision d’Anthropic liste JPEG, PNG, GIF et WebP, avec 10 Mo par image sur l’API Claude. La seconde moitié de media.ts utilise gpt-6-luna d’OpenAI, que sa page de modèle indique comme acceptant du texte et des images en entrée :
// media.ts, suite. Photos et PDF : un seul appel qui renvoie du texte à stocker.
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, // ne pas garder le fichier de l'utilisateur chez le fournisseur
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"];
// Tout ce qu'une personne peut envoyer, sous forme de texte que lit votre agent.
export async function toText(m: Inbound): Promise<string> {
const lines: string[] = [];
if (m.reply_to) lines.push(`(en réponse à : "${m.reply_to.text}")`);
if (m.text) lines.push(m.text);
const f = m.media;
if (f?.mime.startsWith("audio/")) {
lines.push(`[message vocal, transcrit : ${await transcribe(f)}]`);
} else if (f && IMAGES.includes(f.mime)) {
lines.push(`[photo : ${await look(f, "Décris cette photo pour un assistant de chat en moins de 80 mots. Recopie exactement tout texte visible.")}]`);
} else if (f?.mime === "application/pdf") {
lines.push(`[PDF ${f.filename} : ${await look(f, "Résume ce document pour un assistant de chat en moins de 150 mots.")}]`);
} else if (f) {
lines.push(`[fichier ${f.filename ?? "sans nom"}, ${f.mime}, ${f.size} octets : illisible]`);
}
if (m.location) lines.push(`[position : ${m.location.name ?? ""} ${m.location.address ?? ""} (${m.location.latitude}, ${m.location.longitude})]`);
if (m.contacts?.length) lines.push(`[${m.contacts.length} carte(s) de contact partagée(s)]`);
return lines.join("\n") || "[message vide : un fichier de plus de 30 Mo arrive sans lien]";
}
toText renvoie du texte brut pour n’importe quel message, ce que veulent votre historique et votre prompt. Un message peut contenir à la fois text et media : la fonction lit les deux. Quand rien n’est lisible, elle renvoie un texte de remplacement, et le modèle peut répondre « ce fichier n’est pas passé, tu peux m’en envoyer un plus petit ? » Les [crochets] correspondent à la ligne du modèle de system prompt qui explique à l’agent ce qu’ils signifient.
La fonction look décrit une seule fois et garde du texte, pas des pixels. Le fichier disparaît de whasp au bout de 24 heures, et la documentation d’Anthropic souligne que, lorsque les images sont encodées en base64 dans une conversation à plusieurs tours, chaque requête renvoie à nouveau les octets de l’image. Stockez la description dans votre mémoire de conversation et demandez du détail dans le prompt de description : recopier exactement le texte visible, c’est ce qui permet à l’agent de répondre la semaine prochaine à une question sur un ticket de caisse. Si les questions de suivi sur l’image comptent, copiez aussi le fichier dans votre propre stockage.
Pour confier la même tâche à Claude, envoyez les octets dans un bloc image, avec l’image avant le texte, ce qui, selon la documentation d’Anthropic, donne les meilleurs résultats. claude-haiku-5-5 est le modèle qu’Anthropic positionne pour les usages à fort volume et sensibles à la latence :
import Anthropic from "@anthropic-ai/sdk";
const claude = new Anthropic(); // lit 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: "Décris cette photo pour un assistant de chat en moins de 80 mots. Recopie exactement tout texte visible." },
],
}],
});
return message.content.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("");
}
Et les documents, les localisations et les contacts ?
Un PDF va au modèle de la même façon qu’une photo. Le guide PDF d’OpenAI l’accepte comme input_file avec file_data en base64 et un filename, jusqu’à 50 Mo par fichier, et donne au modèle à la fois le texte extrait et les images des pages : c’est la branche application/pdf ci-dessus. Pour les fichiers Word ou Excel, extrayez le texte avec une bibliothèque de confiance, ou demandez à la personne d’envoyer un PDF.
Une localisation arrive sous forme de coordonnées avec un nom et une adresse facultatifs : décrivez-la en une phrase et laissez le modèle décider quoi en faire. Un contact arrive sous forme de texte vCard : analysez les lignes FN et TEL si votre agent a besoin de noms et de numéros. Une réponse à un message précédent porte le texte de ce message dans reply_to, et c’est pourquoi toText l’inclut : sans lui, un « oui » tout seul ne veut rien dire.
Considérez tout ce que vous extrayez comme non fiable. Une transcription, une description ou le texte d’un PDF, ce sont les mots de l’autre personne, et quelqu’un peut dire ou écrire « ignore tes instructions » dans n’importe lequel. Gardez ces chaînes comme des données dans le prompt, jamais comme des instructions.
Comment renvoyer un fichier ?
Pour envoyer un fichier, passez une URL HTTPS publique dans media sur POST /v1/messages. L’URL peut pointer vers un fichier de 64 Mo au maximum. whasp le télécharge une fois à l’envoi et supprime sa copie à la fin de l’envoi. Quand vous envoyez à la fois text et media, le téléphone envoie d’abord le fichier, puis le texte comme second message WhatsApp, sans légende pour l’instant, et cela compte quand même pour un seul message d’API.
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: "Voici votre reçu.",
}),
});
Le fichier doit rester accessible pendant que le message attend dans la file et est envoyé. Gardez le lien valide jusqu’à l’arrivée de message.sent ou de message.failed pour ce message.
Les messages vocaux sont la seule chose que l’API ne peut pas envoyer. Les messages vocaux sortants ne sont pas pris en charge, et un fichier audio que vous envoyez arrive comme un fichier, pas comme un message vocal. La règle pratique : répondre à un message vocal par du texte. Si vous tenez quand même à des réponses parlées, générez l’audio avec un modèle de synthèse vocale (text-to-speech) et envoyez-le comme un fichier, en sachant ce que la personne verra.
Comment le brancher sur le webhook ?
Accusez réception de l’événement d’abord et traitez-le en arrière-plan, car whasp attend 10 secondes un 2xx puis réessaie avec un backoff, et la transcription plus un appel au modèle peuvent durer plus longtemps. Après la vérification de signature décrite dans la gestion des webhooks WhatsApp, le côté réception ressemble à ceci :
if (event.type === "message.received") {
// On répond 2xx tout de suite. La transcription et l'appel au modèle tournent après la réponse.
void handleInbound(event.id, event.data.message).catch(console.error);
}
return new Response(null, { status: 204 });
handleInbound appelle toText, stocke le résultat, interroge votre modèle et envoie la réponse, comme dans la création d’un agent avec l’API OpenAI. Ignorez un identifiant d’événement que vous avez déjà traité, sinon un nouvel essai vous fera payer deux fois la même transcription. Le « lancer sans attendre » (fire-and-forget) fonctionne sur un serveur qui reste allumé. Sur un hébergeur serverless, placez plutôt l’événement dans une file.
Que conserver, et pendant combien de temps ?
Gardez la transcription ou la description, et supprimez l’audio et l’image dès que vous n’en avez plus besoin. whasp supprime sa copie au bout de 24 heures, le reste est votre décision. Un enregistrement vocal est une donnée personnelle : notez combien de temps vous le gardez et supprimez-le quand une personne le demande. Opt-out et consentement pour les agents IA détaille les règles. Passez store: false à l’appel du modèle, comme le fait look, si vous ne voulez pas qu’OpenAI conserve la réponse.
Que faire ensuite ?
Ajoutez toText au gestionnaire que vous avez déjà, envoyez-vous un message vocal et une photo, et lisez ce que voit l’agent. Planifiez ensuite les cas d’usage qui en dépendent : 12 cas d’usage d’agents IA sur WhatsApp en liste plusieurs où la voix et les photos sont l’entrée principale. La documentation whasp décrit chaque champ de l’objet media.
Questions
Un agent IA WhatsApp peut-il envoyer des messages vocaux ?
Pas sur whasp. Les messages vocaux sortants ne sont pas pris en charge, et un fichier audio que vous envoyez arrive comme un fichier, pas comme un message vocal. Répondez aux messages vocaux par du texte.
Pendant combien de temps puis-je télécharger un fichier envoyé à mon agent ?
Pendant 24 heures. Le lien du média dans l'événement message.received est signé et valable 24 heures, et whasp supprime le fichier ensuite : copiez-le si vous en avez besoin.
Que se passe-t-il quand quelqu'un envoie un fichier de plus de 30 Mo ?
Le message arrive quand même, mais son champ media vaut null car il n'y a pas de lien. Votre agent peut le remarquer et demander un fichier plus petit.
Quel modèle choisir pour transcrire les messages vocaux WhatsApp ?
Le guide speech-to-text d'OpenAI recommande gpt-transcribe pour la parole enregistrée. Si l'audio doit rester sur vos propres serveurs, faster-whisper et whisper.cpp sont des options sous licence MIT.
Ai-je besoin de ffmpeg pour transcrire les messages vocaux WhatsApp ?
C'est la voie sûre. L'API Business de WhatsApp décrit les enregistrements vocaux comme de l'Ogg avec le codec Opus, et le guide d'OpenAI liste mp3, mp4, mpeg, mpga, m4a, wav et webm : convertir en MP3 règle la question.


