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 |
-
|
|
|
|
| 145 |
try {
|
| 146 |
const { generatePitchCard } = await import('./visuals');
|
| 147 |
const { uploadFile } = await import('./storage');
|
| 148 |
-
const cardBuffer = await generatePitchCard(
|
| 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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;
|