CognxSafeTrack commited on
Commit
866b42d
·
1 Parent(s): 29ead2c

docs: Add detailed audio pipeline investigation report

Browse files
Files changed (1) hide show
  1. docs/audio_pipeline_investigation.md +140 -0
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é.