CognxSafeTrack commited on
Commit
082d3db
·
1 Parent(s): 83c2a9a

fix(qa): resolve missing image anomalies and secure pedagogy worker TTS audio flow

Browse files
apps/api/src/scripts/fix-qa-anomalies.ts ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ const prisma = new PrismaClient();
4
+
5
+ async function fixAnomalies() {
6
+ console.log("🛠️ Démarrage de la correction des anomalies QA...");
7
+
8
+ // 1. Fix Pitch Deck Badges on Day 12
9
+ const day12s = await prisma.trackDay.findMany({
10
+ where: { dayNumber: 12 }
11
+ });
12
+
13
+ let day12Fixed = 0;
14
+ for (const day of day12s) {
15
+ let badges: string[] = [];
16
+ if (day.badges) {
17
+ try {
18
+ badges = typeof day.badges === 'string' ? JSON.parse(day.badges as string) : day.badges as string[];
19
+ } catch (e) {
20
+ // Ignore parse errors if somehow invalid
21
+ }
22
+ }
23
+
24
+ if (!badges.includes("PITCH_DECK") && !badges.includes("PITCH_AI") && !badges.includes("DOCUMENT_GENERATION")) {
25
+ badges.push("PITCH_AI");
26
+
27
+ await prisma.trackDay.update({
28
+ where: { id: day.id },
29
+ data: { badges }
30
+ });
31
+ console.log(`✅ Fixed badges for ${day.trackId} Day 12: added PITCH_AI`);
32
+ day12Fixed++;
33
+ }
34
+ }
35
+
36
+ // 2. Fix empty activityLabel in BusinessProfiles to avoid Pitch Card crashes
37
+ const profiles = await prisma.businessProfile.findMany({
38
+ where: {
39
+ OR: [
40
+ { activityLabel: null },
41
+ { activityLabel: "" }
42
+ ]
43
+ },
44
+ include: { user: true }
45
+ });
46
+
47
+ let profileFixed = 0;
48
+ for (const profile of profiles) {
49
+ let label = profile.activityLabel;
50
+ if (!label || label.trim() === "") {
51
+ label = profile.user?.activity || "Projet Entrepreneurial";
52
+ await prisma.businessProfile.update({
53
+ where: { id: profile.id },
54
+ data: { activityLabel: label }
55
+ });
56
+ console.log(`✅ Fixed activityLabel for user ${profile.userId} -> ${label}`);
57
+ profileFixed++;
58
+ }
59
+ }
60
+
61
+ console.log(`\n🎉 Bilan des corrections :`);
62
+ console.log(`- ${day12Fixed} Jours 12 mis à jour avec la métadonnée PITCH_AI`);
63
+ console.log(`- ${profileFixed} Profils complétés avec une activityLabel par défaut`);
64
+
65
+ await prisma.$disconnect();
66
+ }
67
+
68
+ fixAnomalies().catch(e => {
69
+ console.error(e);
70
+ process.exit(1);
71
+ });
apps/api/src/scripts/inject-missing-images.ts ADDED
@@ -0,0 +1,48 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { PrismaClient } from '@prisma/client';
2
+
3
+ const prisma = new PrismaClient();
4
+
5
+ async function injectMissingImages() {
6
+ console.log("🖼️ Démarrage de l'injection des visuels manquants...");
7
+
8
+ const tracks = await prisma.track.findMany({
9
+ include: { days: true }
10
+ });
11
+
12
+ let imagesInjected = 0;
13
+
14
+ for (const track of tracks) {
15
+ // Find sector generic name
16
+ const sectorStr = track.title.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "_");
17
+ let fallbackUrl = `https://r2.xamle.sn/branding/${sectorStr}_generic.png`;
18
+
19
+ // If it's the core tracks, we might just use a general fallback if sector isn't in title
20
+ if (track.id.startsWith("T")) {
21
+ fallbackUrl = 'https://r2.xamle.sn/branding/general_generic.png';
22
+ if (track.title.toLowerCase().includes("restauration")) fallbackUrl = 'https://r2.xamle.sn/branding/restauration_generic.png';
23
+ if (track.title.toLowerCase().includes("couture")) fallbackUrl = 'https://r2.xamle.sn/branding/couture_generic.png';
24
+ } else {
25
+ fallbackUrl = `https://r2.xamle.sn/branding/${sectorStr}_generic.png`;
26
+ }
27
+
28
+ for (const day of track.days) {
29
+ // Specifically fixing days 5, 8, 10 and any other day missing visuals
30
+ if (!day.imageUrl && !day.videoUrl) {
31
+ await prisma.trackDay.update({
32
+ where: { id: day.id },
33
+ data: { imageUrl: fallbackUrl }
34
+ });
35
+ console.log(`✅ Injected generic image for Track ${track.id} Day ${day.dayNumber}`);
36
+ imagesInjected++;
37
+ }
38
+ }
39
+ }
40
+
41
+ console.log(`\n🎉 Injection terminée ! ${imagesInjected} images de secours (Couture, Restauration...) ajoutées en BDD.`);
42
+ await prisma.$disconnect();
43
+ }
44
+
45
+ injectMissingImages().catch(e => {
46
+ console.error(e);
47
+ process.exit(1);
48
+ });
apps/whatsapp-worker/src/index.ts CHANGED
@@ -141,11 +141,12 @@ const worker = new Worker('whatsapp-queue', async (job: Job) => {
141
  });
142
 
143
  // 🌟 Visuals WOW: Generate Pitch Card for Day 1 🌟
144
- if (isFeatureEnabled('FEATURE_SHARE_CARD') && currentDay === 1 && (profileData as any).activityLabel) {
 
145
  try {
146
  const { generatePitchCard } = await import('./visuals');
147
  const { uploadFile } = await import('./storage');
148
- const cardBuffer = await generatePitchCard((profileData as any).activityLabel);
149
  const cardUrl = await uploadFile(cardBuffer, 'pitch-card.png', 'image/png');
150
 
151
  const caption = language === 'WOLOF'
 
141
  });
142
 
143
  // 🌟 Visuals WOW: Generate Pitch Card for Day 1 🌟
144
+ const finalLabel = (profileData as any).activityLabel || user.activity || "Projet Entrepreneurial";
145
+ if (isFeatureEnabled('FEATURE_SHARE_CARD') && currentDay === 1 && finalLabel) {
146
  try {
147
  const { generatePitchCard } = await import('./visuals');
148
  const { uploadFile } = await import('./storage');
149
+ const cardBuffer = await generatePitchCard(finalLabel);
150
  const cardUrl = await uploadFile(cardBuffer, 'pitch-card.png', 'image/png');
151
 
152
  const caption = language === 'WOLOF'
apps/whatsapp-worker/src/pedagogy.ts CHANGED
@@ -190,6 +190,16 @@ export async function sendLessonDay(userId: string, trackId: string, dayNumber:
190
  if ((trackDay as any).imageUrl && !imageAlreadySent) {
191
  console.log(`[PEDAGOGY] Sending daily image infographic: ${(trackDay as any).imageUrl}`);
192
  await sendImageMessage(user.phone, (trackDay as any).imageUrl);
 
 
 
 
 
 
 
 
 
 
193
  }
194
 
195
  // 🌟 1. Send Lesson (audio or text) 🌟
@@ -249,7 +259,7 @@ export async function sendLessonDay(userId: string, trackId: string, dayNumber:
249
  console.log(`[WhatsApp] ✅ Day 1 intro image sent to ${user.phone}`);
250
  }
251
 
252
- const lessonMsg = `${lessonText}\n(FR) ${textFR}`;
253
  const formattedMessages = shortenForWhatsApp(lessonMsg);
254
  for (const msg of formattedMessages) {
255
  await sendTextMessage(user.phone, msg);
@@ -258,13 +268,24 @@ export async function sendLessonDay(userId: string, trackId: string, dayNumber:
258
  console.error(`[PEDAGOGY] Failed to send native audio, falling back to text. Error:`, err);
259
  // Fallback: Send at least the text if audio fails entirely
260
  if (lessonText) {
261
- const formattedMessages = shortenForWhatsApp(lessonText);
 
 
 
 
 
 
 
262
  for (const msg of formattedMessages) {
263
  await sendTextMessage(user.phone, msg);
264
  }
265
  }
266
  }
267
  } else if (lessonText) {
 
 
 
 
268
  let textFR = '';
269
  if ((trackDay as any).buttonsJson?.content?.FR) {
270
  textFR = (trackDay as any).buttonsJson.content.FR.lessonText;
 
190
  if ((trackDay as any).imageUrl && !imageAlreadySent) {
191
  console.log(`[PEDAGOGY] Sending daily image infographic: ${(trackDay as any).imageUrl}`);
192
  await sendImageMessage(user.phone, (trackDay as any).imageUrl);
193
+ } else if (!imageAlreadySent) {
194
+ // FALLBACK: Inject missing image using the user sector
195
+ const sectorStr = user.activity ? user.activity.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "_") : "general";
196
+ const fallbackImageUrl = `https://r2.xamle.sn/branding/${sectorStr}_generic.png`;
197
+ console.log(`[PEDAGOGY] Missing imageUrl on Day ${dayNumber}! Using fallback: ${fallbackImageUrl}`);
198
+ try {
199
+ await sendImageMessage(user.phone, fallbackImageUrl);
200
+ } catch (e: any) {
201
+ console.warn(`[PEDAGOGY] Fallback image also failed: ${e.message}`);
202
+ }
203
  }
204
 
205
  // 🌟 1. Send Lesson (audio or text) 🌟
 
259
  console.log(`[WhatsApp] ✅ Day 1 intro image sent to ${user.phone}`);
260
  }
261
 
262
+ const lessonMsg = textFR ? `${lessonText}\n(FR) ${textFR}` : lessonText;
263
  const formattedMessages = shortenForWhatsApp(lessonMsg);
264
  for (const msg of formattedMessages) {
265
  await sendTextMessage(user.phone, msg);
 
268
  console.error(`[PEDAGOGY] Failed to send native audio, falling back to text. Error:`, err);
269
  // Fallback: Send at least the text if audio fails entirely
270
  if (lessonText) {
271
+ const alertMsg = isWolof ? "⚠️ Kàddu gi mënul a yónnee. Làng gi a ngi nii ci mbind:" : "⚠️ Impossible de charger l'audio de la leçon. Voici le contenu au format texte :";
272
+ await sendTextMessage(user.phone, alertMsg);
273
+ let textFR = '';
274
+ if ((trackDay as any).buttonsJson?.content?.FR) {
275
+ textFR = (trackDay as any).buttonsJson.content.FR.lessonText;
276
+ }
277
+ const lessonMsg = textFR ? `${lessonText}\n(FR) ${textFR}` : lessonText;
278
+ const formattedMessages = shortenForWhatsApp(lessonMsg);
279
  for (const msg of formattedMessages) {
280
  await sendTextMessage(user.phone, msg);
281
  }
282
  }
283
  }
284
  } else if (lessonText) {
285
+ // Fallback: Alert discreetly if no audio URL could be produced
286
+ const alertMsg = isWolof ? "⚠️ Kàddu gi mënul a yónnee. Làng gi a ngi nii ci mbind:" : "⚠️ Impossible de charger l'audio de la leçon. Voici le contenu au format texte :";
287
+ await sendTextMessage(user.phone, alertMsg);
288
+
289
  let textFR = '';
290
  if ((trackDay as any).buttonsJson?.content?.FR) {
291
  textFR = (trackDay as any).buttonsJson.content.FR.lessonText;