Files
musicland/functions/helpers/gemini.js
T
Thomas Demirdjian d7b14bd873 clear last tickets
2025-11-26 15:33:22 +01:00

259 lines
7.6 KiB
JavaScript

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`,
);
}
};