| import { FastifyInstance } from 'fastify'; |
| import { aiService } from '../services/ai'; |
| import { PdfOnePagerRenderer } from '../services/renderers/pdf-renderer'; |
| import { PptxDeckRenderer } from '../services/renderers/pptx-renderer'; |
| import { uploadFile } from '../services/storage'; |
| import { convertToMp3IfNeeded } from '../services/ai/ffmpeg'; |
| import { z } from 'zod'; |
|
|
|
|
| export async function aiRoutes(fastify: FastifyInstance) { |
| const pdfRenderer = new PdfOnePagerRenderer(); |
| const pptxRenderer = new PptxDeckRenderer(); |
|
|
| |
| fastify.post('/onepager', async (request) => { |
| const bodySchema = z.object({ |
| userContext: z.string(), |
| language: z.string().optional().default('FR'), |
| businessProfile: z.any().optional() |
| }); |
| const { userContext, language, businessProfile } = bodySchema.parse(request.body); |
|
|
| console.log(`Generating One-Pager (${language}) for context:`, userContext.substring(0, 50)); |
|
|
| |
| const onePagerData = await aiService.generateOnePagerData(userContext, language, businessProfile); |
|
|
| |
| if (onePagerData.mainImage && !onePagerData.mainImage.startsWith('http')) { |
| console.log(`[AI_ROUTE] Generating brand image for One-Pager: ${onePagerData.title}`); |
| try { |
| const imageUrl = await aiService.generateImage(onePagerData.mainImage); |
| if (imageUrl) { |
| onePagerData.mainImage = imageUrl; |
| } |
| } catch (imgErr) { |
| console.error(`[AI_ROUTE] Image generation failed for One-Pager:`, imgErr); |
| } |
| } |
|
|
| |
| const pdfBuffer = await pdfRenderer.render(onePagerData); |
|
|
| |
| const downloadUrl = await uploadFile(pdfBuffer, `onepager-${Date.now()}.pdf`, 'application/pdf'); |
|
|
| return { success: true, url: downloadUrl, data: onePagerData, aiSource: onePagerData.aiSource }; |
| }); |
|
|
| |
| fastify.post('/deck', async (request) => { |
| const bodySchema = z.object({ |
| userContext: z.string(), |
| language: z.string().optional().default('FR'), |
| businessProfile: z.any().optional() |
| }); |
| const { userContext, language, businessProfile } = bodySchema.parse(request.body); |
|
|
| console.log(`Generating Pitch Deck (${language}) for context:`, userContext.substring(0, 50)); |
|
|
| |
| const deckData = await aiService.generatePitchDeckData(userContext, language, businessProfile); |
|
|
| |
| for (const slide of deckData.slides) { |
| if (slide.visualType === 'IMAGE' && slide.visualData && typeof slide.visualData === 'string' && !slide.visualData.startsWith('http')) { |
| console.log(`[AI_ROUTE] Generating image for slide: ${slide.title}`); |
| try { |
| const imageUrl = await aiService.generateImage(slide.visualData); |
| if (imageUrl) { |
| slide.visualData = imageUrl; |
| } |
| } catch (imgErr) { |
| console.error(`[AI_ROUTE] Image generation failed for slide ${slide.title}:`, imgErr); |
| } |
| } |
| } |
|
|
| |
| const pptxBuffer = await pptxRenderer.render(deckData); |
|
|
| |
| const downloadUrl = await uploadFile(pptxBuffer, `deck-${Date.now()}.pptx`, 'application/vnd.openxmlformats-officedocument.presentationml.presentation'); |
|
|
| return { success: true, url: downloadUrl, data: deckData, aiSource: deckData.aiSource }; |
| }); |
|
|
| |
| fastify.post('/personalize-lesson', async (request) => { |
| const bodySchema = z.object({ |
| lessonText: z.string(), |
| userActivity: z.string(), |
| userLanguage: z.string().optional().default('FR'), |
| businessProfile: z.any().optional(), |
| previousResponses: z.array(z.object({ |
| day: z.number(), |
| response: z.string() |
| })).optional() |
| }); |
| const { lessonText, userActivity, userLanguage, businessProfile, previousResponses } = bodySchema.parse(request.body); |
|
|
| console.log(`[AI] Personalizing lesson for activity: ${userActivity} (Lang: ${userLanguage}) with ${previousResponses?.length || 0} prev responses.`); |
|
|
| const { lessonText: personalizedText, aiSource } = await aiService.generatePersonalizedLesson(lessonText, userActivity, userLanguage, businessProfile, previousResponses); |
|
|
| return { success: true, text: personalizedText, aiSource }; |
| }); |
|
|
| |
| fastify.post('/tts', async (request, reply) => { |
| const bodySchema = z.object({ text: z.string() }); |
| const { text } = bodySchema.parse(request.body); |
|
|
| console.log(`Generating TTS audio...`); |
|
|
| try { |
| const audioBuffer = await aiService.generateSpeech(text); |
| const downloadUrl = await uploadFile(audioBuffer, `lesson-audio-${Date.now()}.mp3`, 'audio/mpeg'); |
| return { success: true, url: downloadUrl }; |
| } catch (err: unknown) { |
| if ((err as any)?.name === 'QuotaExceededError') { |
| return reply.code(429).send({ error: 'quota_exceeded' }); |
| } |
| throw err; |
| } |
| }); |
|
|
| |
| |
| |
| fastify.post('/transcribe', async (request, reply) => { |
| const bodySchema = z.object({ |
| audioBase64: z.string(), |
| filename: z.string().default('message.ogg'), |
| language: z.string().optional() |
| }); |
|
|
| const { audioBase64, filename, language } = bodySchema.parse(request.body); |
| const buffer = Buffer.from(audioBase64, 'base64'); |
|
|
| console.log(`[AI] 🚀 DEPLOY V4 - Transcribing: ${filename} (size: ${buffer.length})`); |
|
|
| try { |
| const { buffer: audioToTranscribe, format } = await convertToMp3IfNeeded(buffer, filename); |
| console.log(`[AI] Calling transcribeAudio for format: ${format} (Lang: ${language || 'none'})`); |
| const { text, confidence } = await aiService.transcribeAudio(audioToTranscribe, `message.${format}`, language); |
|
|
| |
| |
| const isSuspect = text.length < 3 || /[^a-zA-Z0-9\sàâäéèêëîïôöùûüçÀÂÄÉÈÊËÎÏÔÖÙÛÜÇ,.!?'\-]/.test(text.slice(0, 10)); |
|
|
| return { success: true, text, confidence, isSuspect }; |
| } catch (err: unknown) { |
| console.error(`[AI] ❌ Transcription error:`, err); |
| if ((err as any)?.name === 'QuotaExceededError') { |
| return reply.code(429).send({ error: 'quota_exceeded', retryAfterMs: (err as any).retryAfterMs }); |
| } |
| |
| return reply.code(500).send({ error: 'transcription_failed', message: (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)), stack: process.env.NODE_ENV === 'development' ? (err as Error).stack : undefined }); |
| } |
| }); |
|
|
| |
| |
| |
| fastify.post('/store-audio', async (request) => { |
| const bodySchema = z.object({ |
| audioBase64: z.string(), |
| mimeType: z.string().default('audio/ogg'), |
| phone: z.string() |
| }); |
|
|
| const { audioBase64, mimeType, phone } = bodySchema.parse(request.body); |
|
|
| let ext = 'ogg'; |
| let folder = 'audio'; |
| if (mimeType.includes('image/')) { |
| ext = mimeType.split('/')[1] || 'jpeg'; |
| folder = 'images'; |
| } else if (mimeType.includes('mp4')) { |
| ext = 'mp4'; |
| } else if (mimeType.includes('mpeg')) { |
| ext = 'mp3'; |
| } |
| const filename = `${folder}/${phone}-${Date.now()}.${ext}`; |
| const buffer = Buffer.from(audioBase64, 'base64'); |
|
|
| try { |
| |
| const { mkdir } = require('fs/promises'); |
| await mkdir(`/tmp/${folder}`, { recursive: true }).catch(() => { }); |
|
|
| const url = await uploadFile(buffer, filename, mimeType); |
| console.log(`[AI] ✅ Media stored: ${url}`); |
| return { success: true, url }; |
| } catch (err: unknown) { |
| console.error('[AI] store-audio failed:', (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err))); |
| return { success: false, error: (err instanceof Error ? (err instanceof Error ? err.message : String(err)) : String(err)) }; |
| } |
| }); |
|
|
| |
| fastify.post('/generate-feedback', async (request, reply) => { |
| const bodySchema = z.object({ |
| answers: z.string(), |
| lessonText: z.string(), |
| exercisePrompt: z.string(), |
| userLanguage: z.string().optional().default('FR'), |
| businessProfile: z.any().nullish(), |
| exerciseCriteria: z.any().nullish(), |
| |
| userActivity: z.string().nullish(), |
| userRegion: z.string().nullish(), |
| dayNumber: z.number().nullish(), |
| previousResponses: z.array(z.object({ |
| day: z.number(), |
| response: z.string() |
| })).optional(), |
| totalDays: z.number().optional().default(1), |
| isDeepDive: z.boolean().optional().default(false), |
| iterationCount: z.number().optional().default(0), |
| imageUrl: z.string().optional(), |
| isButtonChoice: z.boolean().optional().default(false) |
| }); |
| const { |
| answers, lessonText, exercisePrompt, userLanguage, businessProfile, exerciseCriteria, |
| userActivity, userRegion, dayNumber, previousResponses, isDeepDive, iterationCount, imageUrl, |
| isButtonChoice |
| } = bodySchema.parse(request.body); |
|
|
| console.log(`[AI] Generating feedback for user... (Lang: ${userLanguage}, Button: ${isButtonChoice}, DeepDive: ${isDeepDive})`); |
|
|
| try { |
| const feedback = await aiService.generateFeedback( |
| answers, exercisePrompt, lessonText, userLanguage, businessProfile ?? undefined, exerciseCriteria ?? undefined, |
| userActivity ?? undefined, userRegion ?? undefined, dayNumber ?? undefined, previousResponses ?? undefined, |
| isDeepDive, iterationCount, imageUrl ?? undefined, isButtonChoice |
| ); |
|
|
| |
| |
| |
| |
| const formattedFeedback = `✨ *Coach XAMLÉ* :\n\n` + |
| `🌟 ${feedback.validation}\n\n` + |
| `🚀 ${feedback.enrichedVersion}\n\n` + |
| `💡 ${feedback.actionableAdvice}`; |
|
|
| return { |
| success: true, |
| text: formattedFeedback, |
| isQualified: feedback.isQualified, |
| missingElements: feedback.missingElements || [], |
| confidence: feedback.confidence, |
| notes: feedback.notes, |
| searchResults: feedback.searchResults, |
| aiSource: feedback.aiSource |
| }; |
| } catch (err: unknown) { |
| if ((err as any)?.name === 'QuotaExceededError') { |
| return reply.code(429).send({ error: 'quota_exceeded' }); |
| } |
| throw err; |
| } |
| }); |
|
|
| |
| fastify.post('/extract-profile', async (request) => { |
| const bodySchema = z.object({ |
| userInput: z.string(), |
| dayNumber: z.number(), |
| userLanguage: z.string().optional().default('FR') |
| }); |
| const { userInput, dayNumber, userLanguage } = bodySchema.parse(request.body); |
|
|
| console.log(`[AI] Extracting business profile for Day ${dayNumber}`); |
| const profileData = await aiService.extractBusinessProfile(userInput, dayNumber, userLanguage); |
| return { success: true, data: profileData, aiSource: profileData.aiSource }; |
| }); |
| } |
|
|