# Rapport d'Audit & Investigation Pipeline Audio (WhatsApp STT/TTS) ## 📌 Contexte de l'Architecture Le système (EdTech) repose sur une architecture distribuée gérant les webhooks WhatsApp : 1. **HuggingFace Space (`safetrack-edtech`)** : Agit comme une passerelle publique pour recevoir les événements de l'API Meta WhatsApp Cloud. Il filtre et enfile les requêtes dans une file de messages (BullMQ / Redis). 2. **Railway / Vercel (`whatsapp-worker-production...`)** : Héberge le véritable moteur applicatif. Il contient un serveur API Fastify (`apps/api`) et un Worker (`apps/whatsapp-worker`) qui consomme les queues BullMQ pour le routage des messages, la sauvegarde DB, et les appels aux services IA (via `apps/api/src/services/ai`). 3. **Fournisseurs Tiers** : * **OpenAI** : `gpt-4o-mini` (Génération/Feedback), `whisper-1` (STT Inbound), `tts-1` (TTS Outbound). * **Cloudflare R2** : Stockage tampon des objets audios (Bucket S3 API). * **WhatsApp Graph API** : Pour les webhooks et l'envoi (`sendAudioMessage`, `sendTextMessage`). --- ## 🛑 Problèmes Initiaux Identifiés & Causes Réelles (Avant Patch) ### 1. Rejet Silentieux de l'Audio Entrant (STT/Whisper) * **Symptôme** : L'utilisateur envoie une note vocale. Le bot répond le fallback : *"Je n'ai pas pu lire l'audio. Envoie ta réponse en texte !"* ou affiche le log `Job completed!` sans retranscrire. * **Cause structurelle (Format)** : WhatsApp compresse ses notes vocales natives en conteneurs `OGG` encodés en `OPUS` (`audio/ogg; codecs=opus`). L'API Whisper d'OpenAI est extrêmement capricieuse avec les headers de fichiers `OGG` provenant des serveurs Meta. Sans un transcodage propre, la requête part mais OpenAI rejette le blob silencieusement ou le traite de travers. * **Cause Config** : Si l'API Key OpenAI manquait *du côté Railway*, ou si la connexion au S3 R2 échouait `(Missing R2 Credentials)`, le worker de téléchargement attrapait l'erreur et interrompait le flux d'analyse AI de la réponse. ### 2. Plantage des Fetch (AI_API_BASE_URL sans Schéma) * **Symptôme** : Logs crachant `Failed to parse URL ... input: 'whatsapp-worker-production-0bc0.up.railway.app/v1/ai/tts'` * **Cause** : La variable d'environnement `API_URL` (ou `AI_API_BASE_URL`) fournie dans le dashboard d'hébergement omettait le préfixe HTTPS. La fonction `fetch` de Node.js lève une exception TypeError immédiate lorsqu'on tente une requête HTTP sans schéma valide (`http://` ou `https://`). Ceci crashait *tous* les workers qui tentaient d'appeler localement le module AI (TTS, Personalize, Transcribe). ### 3. Audio Sortant (TTS) Invisible * **Symptôme** : Les logs affichent `[PEDAGOGY] Generating TTS Audio...` et `[TTS] Audio generated`, mais WhatsApp n'envoie aucun message vocal natif `[🎙️]`. * **Cause** : * L'appel à `sendAudioMessage(phone, url)` exige une URL directe publique (Cloudflare R2) téléchargeable par les serveurs proxy de Meta, avec un `Content-Type` valide (`audio/mpeg`). Si R2 configurait un accès privé, ou si l'API TTS d'OpenAI plantait en arrière-plan à cause de l'erreur d'URL évoquée au point 2, la variable `audioUrl` restait vide. Le Worker traitait un string vide, donc `sendAudioMessage` ne partait pas. --- ## 🛠️ Fichiers Impactés & Solutions Implémentées Afin qu'un développeur expert puisse tracer ou étendre la solution, voici les modifications fondamentales apportées. ### A) Rigidité de l'URL Applicative (`apps/whatsapp-worker/src/config.ts`) On interdit purement et simplement le lancement de l'application si l'URL IA est mal formée, et on auto-corrige les erreurs de saisie courantes. ```typescript // Extrait de apps/whatsapp-worker/src/config.ts export function requireHttpUrl(url: string | undefined, keyName: string): string { if (!url) throw new Error(`[CONFIG] Missing environment variable: ${keyName}`); let normalized = url.trim(); // Auto-prefix with https:// if (!normalized.startsWith('http')) { normalized = `https://${normalized}`; console.warn(`[CONFIG] Warning: Auto-prefixed ${keyName} with https://`); } // Formellement interdire HTTP en production if (normalized.startsWith('http://') && !normalized.includes('localhost')) { throw new Error(`[CONFIG] ❌ CRITICAL: ${keyName} MUST use https:// (got ${normalized})`); } return normalized.replace(/\/$/, ""); // Supression des trailing slashes } ``` ### B) FFMPEG Middleware pour l'Audio Entrant (`apps/api/src/services/ai/ffmpeg.ts` & `nixpacks.toml`) Pour contrer le rejet OPUS/OGG de WhatsApp par Whisper, un adaptateur qui intercepte le buffer, écrit un fichier temp `/tmp`, compile avec FFmpeg (`1 channel`, `64k`), et renvoie le buffer MP3. ```toml # Extrait de nixpacks.toml (Déclaration dépendance serveur) providers = ["node"] [phases.setup] nixPkgs = ["...", "ffmpeg"] ``` ```typescript // Extrait de apps/api/src/services/ai/ffmpeg.ts export async function convertToMp3IfNeeded(inputBuffer: Buffer, filename: string) { if (!filename.toLowerCase().endsWith('.ogg') && !filename.toLowerCase().endsWith('.opus')) { return { buffer: inputBuffer, format: filename.split('.').pop()! }; } // ... // Exécution de sous-processus FFMPEG robuste pour transformer l'OGG en MP3 propre await execAsync(`ffmpeg -y -i "${inputPath}" -vn -ar 44100 -ac 1 -b:a 64k "${outputPath}"`); const mp3Buffer = await readFile(outputPath); // ... } ``` ### C) Workflow "Inbound Audio" Tracé (`apps/whatsapp-worker/src/index.ts`) Le Worker logge méticuleusement la réception du Payload Meta, la conversion, et le ping OpenAI STT pour ne plus *jamais* silencier une panne. ```typescript // Extrait de index.ts (Worker BullMQ - job 'download-media') const { buffer } = await downloadMedia(mediaId, accessToken); console.log(`[MEDIA] downloaded file size=${buffer.length} contentType=${mimeType}`); // Envoi asynchrone pour la Transcription + FFMPEG console.log(`[STT] transcribe start calling ${AI_API_BASE_URL.replace(/\/$/, "")}/v1/ai/transcribe`); const transcribeRes = await fetch(`${AI_API_BASE_URL.replace(/\/$/, "")}/v1/ai/transcribe`, { method: 'POST', body: JSON.stringify({ audioBase64: buffer.toString('base64'), filename: `msg.ogg` }) // -> Cet appel intercepte l'audio avec ffmpeg.ts avant de l'envoyer à Whisper }); // Réponse UX console.log(`[STT] transcribe result="${transcribedText.substring(0, 80)}"`); const confirmationText = `J'ai compris :\n"${transcribedText}"`; await sendTextMessage(phone, confirmationText); ``` ### D) Workflow "Outbound Audio" (`apps/whatsapp-worker/src/pedagogy.ts`) On requiert l'audio (TTS), on log l'URL R2 générée, et on *protège* l'envoi. Si Meta rejette l'envoi de l'audio `type: 'audio'`, un bloc `catch` permet un Fallback élégant en envoyant quand même le cours en texte. ```typescript // Extrait de pedagogy.ts (Génération & Envoi de la leçon/Exercice) const AI_API_BASE_URL = requireHttpUrl(process.env.AI_API_BASE_URL, 'AI_API_BASE_URL'); const ttsRes = await fetch(`${AI_API_BASE_URL.replace(/\/$/, "")}/v1/ai/tts`, { ... }); if (finalAudioUrl) { try { console.log(`[TTS] audioUrl=${finalAudioUrl} sending to WhatsApp...`); // WhatsApp API exigeant un lien HTTP direct pour le Content-Type: audio await sendAudioMessage(user.phone, finalAudioUrl); console.log(`[WhatsApp] ✅ Audio message sent to ${user.phone}`); } catch (err) { console.error(`[PEDAGOGY] Failed to send native audio, falling back to text. Error:`, err); await sendTextMessage(user.phone, lessonText); // Ne jamais bloquer l'étudiant } } ``` --- ## 🔎 Guide de Dépannage pour Développeur (Si l'audio bloque encore) Si l'audio ne fonctionne toujours pas après avoir vérifié que le code ci-dessus est déployé sur `main` : 1. **Vérifier le `[CONFIG] AI_API_BASE_URL effective: ...` au démarrage du Worker.** * S'il y a un warning HTTP, corrigez l'environnement sur Railway. 2. **L'audio STT plante silencieusement sans logs ?** * L'erreur vient de `nixpacks.toml` : le serveur (Railway/Vercel) n'a pas pris en compte l'installation asynchrone de `ffmpeg`. Regardez dans les logs de **Build / Deploy** si `ffmpeg` est bien `apt-get install`. S'il manque, le wrapper `ffmpeg.ts` échouera et enverra le `.ogg` corrompu à Whisper. 3. **L'audio TTS ne part pas côté WhatsApp ?** * Vérifiez le cloudflare R2 Bucket. Cliquez sur l'URL `[TTS] audioUrl=https://pub-xxx.r2.dev/fichier.mp3` générée. * Si le navigateur dit "Access Denied" (403), c'est que le bucket `R2_PUBLIC_URL` n'est pas configuré en accès `Public Routing` chez Cloudflare. WhatsApp ne peut pas télécharger et jouer de fichiers protégés derrière une permission Auth (S3). 4. **L'erreur 429 Quota ?** * Regardez les logs : `[WORKER] 429 Error during generate-feedback`. C'est l'API OpenAI qui est à sec (Limites de taux Free Tier ou facturation bloquée). Heureusement, le fallback affichera le mode texte grâce au bloc try/catch résilient ajouté.