const { googleAI } = require('@genkit-ai/googleai') const { genkit, z } = require('genkit') const { GEMINI_API_KEY } = require('../config/secrets') const admin = require('firebase-admin') const { Buffer } = require('buffer') const { setTimeout } = require('timers/promises') // --- CONFIGURATION --- // Plus intelligente que le Flash original, ultra rapide, et stable sur l'API. const TEXT_MODEL_NAME = 'gemini-3.5-flash' const IMAGE_MODEL_NAME = 'gemini-3-pro-image' // --- SINGLETON PATTERN (WARM START) --- // On stocke l'instance en dehors de la fonction pour la réutiliser // entre les invocations si le conteneur est "chaud". let aiInstance = null const getAiInstance = () => { if (!aiInstance) { console.log('⚡ [Gemini] Initialisation froide (Cold Start)') aiInstance = genkit({ plugins: [googleAI({ apiKey: GEMINI_API_KEY.value() })], }) } return aiInstance } /** * Génération de texte générique */ exports.generateAI = async ({ system = '', prompt = '', schema }) => { const ai = getAiInstance() // Récupère l'instance singleton if (prompt?.length < 1) { throw new Error('Vous devez spécifier un prompt pour effectuer cette action.') } console.log(`🧠 [generateAI] Start (${TEXT_MODEL_NAME})`) const startedAt = Date.now() try { const { output } = await ai.generate({ model: googleAI.model(TEXT_MODEL_NAME), system, prompt, output: { schema }, config: { temperature: 0.7, // Créativité équilibrée }, }) return output } catch (error) { console.error('❌ [generateAI] Error:', error.message) throw error } finally { console.log(`⏱️ [generateAI] Durée: ${Date.now() - startedAt}ms`) } } /** * Analyse la toxicité des paroles. * Utilise gemini-1.5-flash-002 avec des réglages permissifs pour l'analyse. */ exports.analyseLyrics = async ({ title = '', lyrics }) => { const ai = getAiInstance() // --- Normalisation --- const normalizeLyrics = (raw) => { if (!raw) return '' if (typeof raw === 'string') return raw if (Array.isArray(raw)) { return raw .map((s) => { if (!s) return '' const label = s.type ? String(s.type).toUpperCase() : 'SECTION' return `[${label}]\n${s.lyrics || ''}` }) .filter(Boolean) .join('\n\n') } if (typeof raw === 'object') { const parts = [] if (raw.couplet) parts.push(`[COUPLET]\n${raw.couplet}`) if (raw.refrain) parts.push(`[REFRAIN]\n${raw.refrain}`) return parts.join('\n\n') } return String(raw || '') } const lyricsText = normalizeLyrics(lyrics).trim() if (!lyricsText) throw new Error('analyseLyrics: paroles requises.') // --- Schéma --- const moderationSchema = z.object({ title: z.string().describe('Titre analysé'), flagged: z.boolean().describe('Vrai si le contenu nécessite un avertissement.'), blocked: z.boolean().describe('Vrai UNIQUEMENT si violation grave (Haine, Violence réelle).'), score: z.number().min(0).max(1).describe('Score de risque (0=Sûr, 1=Dangereux).'), reasons: z.array(z.string()).describe('Liste concise des raisons.'), excerpts: z .array( z.object({ quote: z.string(), category: z.string(), severity: z.string(), }) ) .max(10), success: z.boolean(), }) // --- Prompt --- const system = `Tu es un Expert en Modération de Contenu Musical (Trust & Safety). TA MISSION : Distinguer l'expression artistique (même crue/vulgaire) du contenu réellement dangereux. 1. "FLAGGED" (Avertissement) : Vulgarités, thèmes matures, drogue, sexe consensuel. 2. "BLOCKED" (Interdit) : Discours de haine, harcèlement ciblé, pédopornographie, incitation explicite violence/suicide. Analyse le CONTEXTE. Une insulte dans un clash de rap est différente d'un appel au meurtre.` const userPrompt = ` ANALYSE CETTE CHANSON : Titre : ${title || 'Inconnu'} PAROLES : """ ${lyricsText} """ ` console.log(`🛡️ [analyseLyrics] Start (${TEXT_MODEL_NAME})`) const startedAt = Date.now() try { const { output } = await ai.generate({ model: googleAI.model(TEXT_MODEL_NAME), system, prompt: userPrompt, output: { schema: moderationSchema }, config: { // Paramètres de sécurité permissifs pour laisser l'IA voir et juger le contenu safetySettings: [ { category: 'HARM_CATEGORY_HATE_SPEECH', threshold: 'BLOCK_NONE' }, { category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT', threshold: 'BLOCK_NONE', }, { category: 'HARM_CATEGORY_DANGEROUS_CONTENT', threshold: 'BLOCK_NONE', }, { category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_NONE' }, ], }, }) if (!output) throw new Error("Échec de l'analyse de modération.") // Correction de cohérence if (output.blocked) { output.flagged = true if (output.score < 0.7) output.score = 0.85 } return output } finally { console.log(`⏱️ [analyseLyrics] Durée: ${Date.now() - startedAt}ms`) } } /** * Génération d'image via Imagen 3 */ exports.generateImageV2 = async (prompt, size = 1024, path = '') => { const ai = getAiInstance() if (typeof prompt !== 'string' || prompt.trim().length < 1) { throw new Error('Prompt requis.') } console.log(`🎨 [generateImageV2] Start (${IMAGE_MODEL_NAME})`) const startedAt = Date.now() // Optimisation du prompt pour Imagen let enhancedPrompt = prompt.trim() if (!enhancedPrompt.toLowerCase().includes('high quality')) { enhancedPrompt += ', high quality, detailed, 4k' } // Aspect ratio 1:1 pour les pochettes enhancedPrompt = `${enhancedPrompt} --aspect-ratio 1:1` const maxAttempts = 3 let lastError = null try { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { console.log(`🔄 Tentative ${attempt}/${maxAttempts}`) const response = await ai.generate({ model: googleAI.model(IMAGE_MODEL_NAME), prompt: enhancedPrompt, }) const media = response.media if (media && media.url) { console.log('✅ Image générée.') // --- Sauvegarde dans Firebase Storage --- const dataUrl = String(media.url) const commaIdx = dataUrl.indexOf(',') const b64 = commaIdx !== -1 ? dataUrl.substring(commaIdx + 1) : dataUrl const buffer = Buffer.from(b64, 'base64') const bucket = admin.storage().bucket() const token = require('crypto').randomUUID() const file = bucket.file(path) await file.save(buffer, { resumable: false, metadata: { contentType: media.contentType || 'image/png', metadata: { firebaseStorageDownloadTokens: token }, }, }) return `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(path)}?alt=media&token=${token}` } throw new Error('Pas de média dans la réponse IA.') } catch (err) { console.warn(`⚠️ Erreur tentative ${attempt}:`, err.message) lastError = err if (attempt < maxAttempts) await setTimeout(2000 * attempt) } } throw new Error(`Échec final après ${maxAttempts} tentatives: ${lastError?.message}`) } finally { console.log(`⏱️ [generateImageV2] Durée totale: ${Date.now() - startedAt}ms`) } }