const { googleAI } = require("@genkit-ai/googleai"); const { genkit, z } = require("genkit"); const { GEMINI_API_KEY } = require("../config/keys"); 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-pro-preview"; const IMAGE_MODEL_NAME = "gemini-3-pro-image-preview"; // --- 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 })], }); } 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`, ); } };