feat: fixes and formatter
This commit is contained in:
+99
-114
@@ -1,44 +1,42 @@
|
||||
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");
|
||||
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";
|
||||
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;
|
||||
let aiInstance = null
|
||||
|
||||
const getAiInstance = () => {
|
||||
if (!aiInstance) {
|
||||
console.log("⚡ [Gemini] Initialisation froide (Cold Start)");
|
||||
console.log('⚡ [Gemini] Initialisation froide (Cold Start)')
|
||||
aiInstance = genkit({
|
||||
plugins: [googleAI({ apiKey: GEMINI_API_KEY })],
|
||||
});
|
||||
})
|
||||
}
|
||||
return aiInstance;
|
||||
};
|
||||
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
|
||||
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.",
|
||||
);
|
||||
throw new Error('Vous devez spécifier un prompt pour effectuer cette action.')
|
||||
}
|
||||
|
||||
console.log(`🧠 [generateAI] Start (${TEXT_MODEL_NAME})`);
|
||||
const startedAt = Date.now();
|
||||
console.log(`🧠 [generateAI] Start (${TEXT_MODEL_NAME})`)
|
||||
const startedAt = Date.now()
|
||||
|
||||
try {
|
||||
const { output } = await ai.generate({
|
||||
@@ -49,76 +47,68 @@ exports.generateAI = async ({ system = "", prompt = "", schema }) => {
|
||||
config: {
|
||||
temperature: 0.7, // Créativité équilibrée
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return output;
|
||||
return output
|
||||
} catch (error) {
|
||||
console.error("❌ [generateAI] Error:", error.message);
|
||||
throw error;
|
||||
console.error('❌ [generateAI] Error:', error.message)
|
||||
throw error
|
||||
} finally {
|
||||
console.log(`⏱️ [generateAI] Durée: ${Date.now() - startedAt}ms`);
|
||||
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();
|
||||
exports.analyseLyrics = async ({ title = '', lyrics }) => {
|
||||
const ai = getAiInstance()
|
||||
|
||||
// --- Normalisation ---
|
||||
const normalizeLyrics = (raw) => {
|
||||
if (!raw) return "";
|
||||
if (typeof raw === "string") return 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 || ""}`;
|
||||
if (!s) return ''
|
||||
const label = s.type ? String(s.type).toUpperCase() : 'SECTION'
|
||||
return `[${label}]\n${s.lyrics || ''}`
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n\n");
|
||||
.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");
|
||||
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 || "");
|
||||
};
|
||||
return String(raw || '')
|
||||
}
|
||||
|
||||
const lyricsText = normalizeLyrics(lyrics).trim();
|
||||
if (!lyricsText) throw new Error("analyseLyrics: paroles requises.");
|
||||
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."),
|
||||
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).
|
||||
@@ -127,20 +117,20 @@ TA MISSION : Distinguer l'expression artistique (même crue/vulgaire) du contenu
|
||||
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.`;
|
||||
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"}
|
||||
Titre : ${title || 'Inconnu'}
|
||||
|
||||
PAROLES :
|
||||
"""
|
||||
${lyricsText}
|
||||
"""
|
||||
`;
|
||||
`
|
||||
|
||||
console.log(`🛡️ [analyseLyrics] Start (${TEXT_MODEL_NAME})`);
|
||||
const startedAt = Date.now();
|
||||
console.log(`🛡️ [analyseLyrics] Start (${TEXT_MODEL_NAME})`)
|
||||
const startedAt = Date.now()
|
||||
|
||||
try {
|
||||
const { output } = await ai.generate({
|
||||
@@ -151,108 +141,103 @@ ${lyricsText}
|
||||
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_HATE_SPEECH', threshold: 'BLOCK_NONE' },
|
||||
{
|
||||
category: "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||||
threshold: "BLOCK_NONE",
|
||||
category: 'HARM_CATEGORY_SEXUALLY_EXPLICIT',
|
||||
threshold: 'BLOCK_NONE',
|
||||
},
|
||||
{
|
||||
category: "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||||
threshold: "BLOCK_NONE",
|
||||
category: 'HARM_CATEGORY_DANGEROUS_CONTENT',
|
||||
threshold: 'BLOCK_NONE',
|
||||
},
|
||||
{ category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_NONE" },
|
||||
{ category: 'HARM_CATEGORY_HARASSMENT', threshold: 'BLOCK_NONE' },
|
||||
],
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
if (!output) throw new Error("Échec de l'analyse de modération.");
|
||||
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;
|
||||
output.flagged = true
|
||||
if (output.score < 0.7) output.score = 0.85
|
||||
}
|
||||
|
||||
return output;
|
||||
return output
|
||||
} finally {
|
||||
console.log(`⏱️ [analyseLyrics] Durée: ${Date.now() - startedAt}ms`);
|
||||
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();
|
||||
exports.generateImageV2 = async (prompt, size = 1024, path = '') => {
|
||||
const ai = getAiInstance()
|
||||
|
||||
if (typeof prompt !== "string" || prompt.trim().length < 1) {
|
||||
throw new Error("Prompt requis.");
|
||||
if (typeof prompt !== 'string' || prompt.trim().length < 1) {
|
||||
throw new Error('Prompt requis.')
|
||||
}
|
||||
|
||||
console.log(`🎨 [generateImageV2] Start (${IMAGE_MODEL_NAME})`);
|
||||
const startedAt = Date.now();
|
||||
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";
|
||||
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`;
|
||||
enhancedPrompt = `${enhancedPrompt} --aspect-ratio 1:1`
|
||||
|
||||
const maxAttempts = 3;
|
||||
let lastError = null;
|
||||
const maxAttempts = 3
|
||||
let lastError = null
|
||||
|
||||
try {
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
console.log(`🔄 Tentative ${attempt}/${maxAttempts}`);
|
||||
console.log(`🔄 Tentative ${attempt}/${maxAttempts}`)
|
||||
|
||||
const response = await ai.generate({
|
||||
model: googleAI.model(IMAGE_MODEL_NAME),
|
||||
prompt: enhancedPrompt,
|
||||
});
|
||||
})
|
||||
|
||||
const media = response.media;
|
||||
const media = response.media
|
||||
|
||||
if (media && media.url) {
|
||||
console.log("✅ Image générée.");
|
||||
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 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);
|
||||
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",
|
||||
contentType: media.contentType || 'image/png',
|
||||
metadata: { firebaseStorageDownloadTokens: token },
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
return `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(path)}?alt=media&token=${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.");
|
||||
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);
|
||||
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}`,
|
||||
);
|
||||
throw new Error(`Échec final après ${maxAttempts} tentatives: ${lastError?.message}`)
|
||||
} finally {
|
||||
console.log(
|
||||
`⏱️ [generateImageV2] Durée totale: ${Date.now() - startedAt}ms`,
|
||||
);
|
||||
console.log(`⏱️ [generateImageV2] Durée totale: ${Date.now() - startedAt}ms`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user