CognxSafeTrack commited on
Commit ·
866b42d
1
Parent(s): 29ead2c
docs: Add detailed audio pipeline investigation report
Browse files
docs/audio_pipeline_investigation.md
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Rapport d'Audit & Investigation Pipeline Audio (WhatsApp STT/TTS)
|
| 2 |
+
|
| 3 |
+
## 📌 Contexte de l'Architecture
|
| 4 |
+
|
| 5 |
+
Le système (EdTech) repose sur une architecture distribuée gérant les webhooks WhatsApp :
|
| 6 |
+
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).
|
| 7 |
+
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`).
|
| 8 |
+
3. **Fournisseurs Tiers** :
|
| 9 |
+
* **OpenAI** : `gpt-4o-mini` (Génération/Feedback), `whisper-1` (STT Inbound), `tts-1` (TTS Outbound).
|
| 10 |
+
* **Cloudflare R2** : Stockage tampon des objets audios (Bucket S3 API).
|
| 11 |
+
* **WhatsApp Graph API** : Pour les webhooks et l'envoi (`sendAudioMessage`, `sendTextMessage`).
|
| 12 |
+
|
| 13 |
+
---
|
| 14 |
+
|
| 15 |
+
## 🛑 Problèmes Initiaux Identifiés & Causes Réelles (Avant Patch)
|
| 16 |
+
|
| 17 |
+
### 1. Rejet Silentieux de l'Audio Entrant (STT/Whisper)
|
| 18 |
+
* **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.
|
| 19 |
+
* **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.
|
| 20 |
+
* **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.
|
| 21 |
+
|
| 22 |
+
### 2. Plantage des Fetch (AI_API_BASE_URL sans Schéma)
|
| 23 |
+
* **Symptôme** : Logs crachant `Failed to parse URL ... input: 'whatsapp-worker-production-0bc0.up.railway.app/v1/ai/tts'`
|
| 24 |
+
* **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).
|
| 25 |
+
|
| 26 |
+
### 3. Audio Sortant (TTS) Invisible
|
| 27 |
+
* **Symptôme** : Les logs affichent `[PEDAGOGY] Generating TTS Audio...` et `[TTS] Audio generated`, mais WhatsApp n'envoie aucun message vocal natif `[🎙️]`.
|
| 28 |
+
* **Cause** :
|
| 29 |
+
* 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.
|
| 30 |
+
|
| 31 |
+
---
|
| 32 |
+
|
| 33 |
+
## 🛠️ Fichiers Impactés & Solutions Implémentées
|
| 34 |
+
|
| 35 |
+
Afin qu'un développeur expert puisse tracer ou étendre la solution, voici les modifications fondamentales apportées.
|
| 36 |
+
|
| 37 |
+
### A) Rigidité de l'URL Applicative (`apps/whatsapp-worker/src/config.ts`)
|
| 38 |
+
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.
|
| 39 |
+
|
| 40 |
+
```typescript
|
| 41 |
+
// Extrait de apps/whatsapp-worker/src/config.ts
|
| 42 |
+
export function requireHttpUrl(url: string | undefined, keyName: string): string {
|
| 43 |
+
if (!url) throw new Error(`[CONFIG] Missing environment variable: ${keyName}`);
|
| 44 |
+
let normalized = url.trim();
|
| 45 |
+
// Auto-prefix with https://
|
| 46 |
+
if (!normalized.startsWith('http')) {
|
| 47 |
+
normalized = `https://${normalized}`;
|
| 48 |
+
console.warn(`[CONFIG] Warning: Auto-prefixed ${keyName} with https://`);
|
| 49 |
+
}
|
| 50 |
+
// Formellement interdire HTTP en production
|
| 51 |
+
if (normalized.startsWith('http://') && !normalized.includes('localhost')) {
|
| 52 |
+
throw new Error(`[CONFIG] ❌ CRITICAL: ${keyName} MUST use https:// (got ${normalized})`);
|
| 53 |
+
}
|
| 54 |
+
return normalized.replace(/\/$/, ""); // Supression des trailing slashes
|
| 55 |
+
}
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
### B) FFMPEG Middleware pour l'Audio Entrant (`apps/api/src/services/ai/ffmpeg.ts` & `nixpacks.toml`)
|
| 59 |
+
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.
|
| 60 |
+
|
| 61 |
+
```toml
|
| 62 |
+
# Extrait de nixpacks.toml (Déclaration dépendance serveur)
|
| 63 |
+
providers = ["node"]
|
| 64 |
+
|
| 65 |
+
[phases.setup]
|
| 66 |
+
nixPkgs = ["...", "ffmpeg"]
|
| 67 |
+
```
|
| 68 |
+
|
| 69 |
+
```typescript
|
| 70 |
+
// Extrait de apps/api/src/services/ai/ffmpeg.ts
|
| 71 |
+
export async function convertToMp3IfNeeded(inputBuffer: Buffer, filename: string) {
|
| 72 |
+
if (!filename.toLowerCase().endsWith('.ogg') && !filename.toLowerCase().endsWith('.opus')) {
|
| 73 |
+
return { buffer: inputBuffer, format: filename.split('.').pop()! };
|
| 74 |
+
}
|
| 75 |
+
// ...
|
| 76 |
+
// Exécution de sous-processus FFMPEG robuste pour transformer l'OGG en MP3 propre
|
| 77 |
+
await execAsync(`ffmpeg -y -i "${inputPath}" -vn -ar 44100 -ac 1 -b:a 64k "${outputPath}"`);
|
| 78 |
+
const mp3Buffer = await readFile(outputPath);
|
| 79 |
+
// ...
|
| 80 |
+
}
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
### C) Workflow "Inbound Audio" Tracé (`apps/whatsapp-worker/src/index.ts`)
|
| 84 |
+
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.
|
| 85 |
+
|
| 86 |
+
```typescript
|
| 87 |
+
// Extrait de index.ts (Worker BullMQ - job 'download-media')
|
| 88 |
+
const { buffer } = await downloadMedia(mediaId, accessToken);
|
| 89 |
+
console.log(`[MEDIA] downloaded file size=${buffer.length} contentType=${mimeType}`);
|
| 90 |
+
|
| 91 |
+
// Envoi asynchrone pour la Transcription + FFMPEG
|
| 92 |
+
console.log(`[STT] transcribe start calling ${AI_API_BASE_URL.replace(/\/$/, "")}/v1/ai/transcribe`);
|
| 93 |
+
const transcribeRes = await fetch(`${AI_API_BASE_URL.replace(/\/$/, "")}/v1/ai/transcribe`, {
|
| 94 |
+
method: 'POST',
|
| 95 |
+
body: JSON.stringify({ audioBase64: buffer.toString('base64'), filename: `msg.ogg` })
|
| 96 |
+
// -> Cet appel intercepte l'audio avec ffmpeg.ts avant de l'envoyer à Whisper
|
| 97 |
+
});
|
| 98 |
+
|
| 99 |
+
// Réponse UX
|
| 100 |
+
console.log(`[STT] transcribe result="${transcribedText.substring(0, 80)}"`);
|
| 101 |
+
const confirmationText = `J'ai compris :\n"${transcribedText}"`;
|
| 102 |
+
await sendTextMessage(phone, confirmationText);
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
### D) Workflow "Outbound Audio" (`apps/whatsapp-worker/src/pedagogy.ts`)
|
| 106 |
+
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.
|
| 107 |
+
|
| 108 |
+
```typescript
|
| 109 |
+
// Extrait de pedagogy.ts (Génération & Envoi de la leçon/Exercice)
|
| 110 |
+
const AI_API_BASE_URL = requireHttpUrl(process.env.AI_API_BASE_URL, 'AI_API_BASE_URL');
|
| 111 |
+
const ttsRes = await fetch(`${AI_API_BASE_URL.replace(/\/$/, "")}/v1/ai/tts`, { ... });
|
| 112 |
+
|
| 113 |
+
if (finalAudioUrl) {
|
| 114 |
+
try {
|
| 115 |
+
console.log(`[TTS] audioUrl=${finalAudioUrl} sending to WhatsApp...`);
|
| 116 |
+
// WhatsApp API exigeant un lien HTTP direct pour le Content-Type: audio
|
| 117 |
+
await sendAudioMessage(user.phone, finalAudioUrl);
|
| 118 |
+
console.log(`[WhatsApp] ✅ Audio message sent to ${user.phone}`);
|
| 119 |
+
} catch (err) {
|
| 120 |
+
console.error(`[PEDAGOGY] Failed to send native audio, falling back to text. Error:`, err);
|
| 121 |
+
await sendTextMessage(user.phone, lessonText); // Ne jamais bloquer l'étudiant
|
| 122 |
+
}
|
| 123 |
+
}
|
| 124 |
+
```
|
| 125 |
+
|
| 126 |
+
---
|
| 127 |
+
|
| 128 |
+
## 🔎 Guide de Dépannage pour Développeur (Si l'audio bloque encore)
|
| 129 |
+
|
| 130 |
+
Si l'audio ne fonctionne toujours pas après avoir vérifié que le code ci-dessus est déployé sur `main` :
|
| 131 |
+
|
| 132 |
+
1. **Vérifier le `[CONFIG] AI_API_BASE_URL effective: ...` au démarrage du Worker.**
|
| 133 |
+
* S'il y a un warning HTTP, corrigez l'environnement sur Railway.
|
| 134 |
+
2. **L'audio STT plante silencieusement sans logs ?**
|
| 135 |
+
* 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.
|
| 136 |
+
3. **L'audio TTS ne part pas côté WhatsApp ?**
|
| 137 |
+
* Vérifiez le cloudflare R2 Bucket. Cliquez sur l'URL `[TTS] audioUrl=https://pub-xxx.r2.dev/fichier.mp3` générée.
|
| 138 |
+
* 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).
|
| 139 |
+
4. **L'erreur 429 Quota ?**
|
| 140 |
+
* 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é.
|