fix tickets
This commit is contained in:
+44
-38
@@ -106,50 +106,56 @@ Retourne UNIQUEMENT un JSON conforme au schéma. Pas de texte hors JSON.
|
||||
`.trim();
|
||||
|
||||
// Règles détaillées injectées dans le prompt utilisateur pour guider la décision
|
||||
const policy = `
|
||||
RUBRIQUE DE DÉCISION (résumée):
|
||||
|
||||
A. QUAND BLOQUER (blocked = true) — cas graves et clairs (pas ambigus):
|
||||
1) Sexualité impliquant des mineurs (référence explicite, érotisation, incitation).
|
||||
2) Apologie explicite et sérieuse de terrorisme/extrémisme réel, ou appel à rejoindre/soutenir.
|
||||
3) Appels explicites à la violence ou à un préjudice grave contre une personne/groupe réel.
|
||||
4) Déshumanisation évidente et sérieuse de groupes protégés couplée à menace/incitation.
|
||||
5) Instruction ou encouragement clair à commettre des crimes violents.
|
||||
6) Nazisme/fascisme réel promu (praise/advocacy) ou symboles glorifiés sans critique.
|
||||
7) Partage de contenu sexuel non consenti réel (ex: doxxing sexuel, vengeance) ou exploitation.
|
||||
|
||||
B. QUAND NE PAS BLOQUER (blocked = false, mais flagged peut être true):
|
||||
- Œuvres qui mettent en scène un personnage toxique pour le critiquer, le montrer ou le contextualiser.
|
||||
- Citations, descriptions historiques, satire, ironie, mises en garde, ou re-appropriation par les communautés visées.
|
||||
- Métaphores violentes ou langage dur SANS cible réelle ni intention d'incitation.
|
||||
- Références à des thèmes sombres dans un but narratif/éducatif/critique, sans promotion réelle.
|
||||
|
||||
C. SCORE ET SEVERITY:
|
||||
- Calibre "score" selon l'impact potentiel et la clarté du tort.
|
||||
- Indice: low≈0.1–0.3, medium≈0.3–0.6, high≈0.6–0.85, critical>0.85.
|
||||
- Utilise severity par extrait pour refléter le pire passage.
|
||||
|
||||
D. RELECTURE DE CONTEXTE:
|
||||
- Demande-toi si le passage est: (i) narration/roleplay/critique —> plutôt allow; (ii) injonction réelle —> plutôt block.
|
||||
- Si ambigu OU probablement artistique, NE PAS bloquer: flagged=true si nécessaire, blocked=false.
|
||||
const decisionPolicy = `
|
||||
<POLITIQUE>
|
||||
<SECTION id="A">
|
||||
<TITRE>QUAND BLOQUER (blocked = true) — cas graves et clairs (pas ambigus)</TITRE>
|
||||
<CAS>Sexualité impliquant des mineurs (référence explicite, érotisation, incitation).</CAS>
|
||||
<CAS>Apologie explicite et sérieuse de terrorisme ou extrémisme réel, ou appel à rejoindre/soutenir.</CAS>
|
||||
<CAS>Appels explicites à la violence ou à un préjudice grave contre une personne ou un groupe réel.</CAS>
|
||||
<CAS>Déshumanisation évidente d'un groupe protégé accompagnée d'une menace ou d'une incitation.</CAS>
|
||||
<CAS>Instruction ou encouragement clair à commettre des crimes violents.</CAS>
|
||||
<CAS>Nazisme ou fascisme réel promu sans distance critique.</CAS>
|
||||
<CAS>Partage de contenu sexuel non consenti ou exploitation sexuelle réelle.</CAS>
|
||||
</SECTION>
|
||||
<SECTION id="B">
|
||||
<TITRE>QUAND NE PAS BLOQUER (blocked = false, flagged possible)</TITRE>
|
||||
<CAS>Œuvres montrant un personnage toxique pour le critiquer, le contextualiser ou le dénoncer.</CAS>
|
||||
<CAS>Citations, descriptions historiques, satire, ironie ou réappropriation par les communautés visées.</CAS>
|
||||
<CAS>Métaphores violentes sans cible réelle ni intention d'incitation.</CAS>
|
||||
<CAS>Références à des thèmes sombres à des fins narratives, éducatives ou critiques sans promotion réelle.</CAS>
|
||||
</SECTION>
|
||||
<SECTION id="C">
|
||||
<TITRE>CALIBRAGE DU SCORE ET DES SEVERITY</TITRE>
|
||||
<CAS>Score low ≈ 0.1-0.3, medium ≈ 0.3-0.6, high ≈ 0.6-0.85, critical > 0.85.</CAS>
|
||||
<CAS>Utilise la colonne severity pour refléter le passage le plus problématique.</CAS>
|
||||
</SECTION>
|
||||
<SECTION id="D">
|
||||
<TITRE>RELECTURE DU CONTEXTE</TITRE>
|
||||
<CAS>Si le passage relève de la narration, satire, critique ou roleplay: privilégie flagged=true et blocked=false.</CAS>
|
||||
<CAS>Si le passage constitue une injonction réelle ou un appel clair au tort: blocked=true.</CAS>
|
||||
</SECTION>
|
||||
</POLITIQUE>
|
||||
`.trim();
|
||||
|
||||
const userPrompt = `
|
||||
Analyse de paroles de chanson
|
||||
Titre: ${title || "Sans titre"}
|
||||
|
||||
Paroles:
|
||||
"""
|
||||
<ANALYSE_PAROLES>
|
||||
<METADONNEES>
|
||||
<TITRE>${title || "Sans titre"}</TITRE>
|
||||
</METADONNEES>
|
||||
<PAROLES><![CDATA[
|
||||
${lyricsText}
|
||||
"""
|
||||
]]></PAROLES>
|
||||
</ANALYSE_PAROLES>
|
||||
|
||||
Consigne:
|
||||
- Applique la RUBRIQUE DE DÉCISION ci-dessous pour autoriser la création artistique tout en bloquant les cas réellement dangereux.
|
||||
- "blocked" = true UNIQUEMENT pour les cas A.1 à A.7 (non ambigus).
|
||||
- Si le contenu semble artistique (récit, personnage, satire, citation, dénonciation) et ne fait pas d'apologie/incitation réelle: "blocked" = false (tu peux mettre "flagged" = true + "score" adapté).
|
||||
- Donne des "excerpts" courts (citations EXACTES) si possible.
|
||||
<DIRECTIVES>
|
||||
<OBJECTIF>Autoriser l'expression artistique tout en bloquant les contenus réellement dangereux.</OBJECTIF>
|
||||
<REGLE>"blocked" = true UNIQUEMENT pour les cas graves listés dans la section A.</REGLE>
|
||||
<REGLE>Si le contenu relève du récit, de la critique, de la satire ou d'une mise en contexte artistique, laisse blocked=false (flagged=true si nécessaire).</REGLE>
|
||||
<REGLE>Fournis des "excerpts" courts en citant exactement les passages sensibles.</REGLE>
|
||||
</DIRECTIVES>
|
||||
|
||||
${policy}
|
||||
${decisionPolicy}
|
||||
`.trim();
|
||||
|
||||
const { output } = await genkit({
|
||||
|
||||
@@ -44,25 +44,30 @@ exports.generatePicturePrompt = (project = {}) => {
|
||||
? "Palette vive et contrastée (magenta, cyan, jaune, bleu électrique)"
|
||||
: "Palette harmonieuse et douce (bleu nuit, violet, corail, or pâle)";
|
||||
|
||||
// Prompt simplifié et optimisé pour Gemini
|
||||
const tagsLine = tags.join(" ; ") || "Non spécifié";
|
||||
const lyricsLine = lyricsSample || "Pas d'extraits fournis";
|
||||
|
||||
const prompt = `
|
||||
Crée une pochette d'album musicale abstraite et colorée au format carré 1024x1024.
|
||||
<BRIEF>
|
||||
<OBJECTIF>Créer une pochette d'album abstraite et lumineuse.</OBJECTIF>
|
||||
<TITRE>${title || "Sans titre"}</TITRE>
|
||||
<STYLE_MUSICAL>${tagsLine}</STYLE_MUSICAL>
|
||||
<EXTRAITS_PAROLES>${lyricsLine}</EXTRAITS_PAROLES>
|
||||
</BRIEF>
|
||||
|
||||
Contexte:
|
||||
- Titre: ${title || "Sans titre"}
|
||||
- Style musical: ${tags.join(" ; ")}
|
||||
<CONTEXTES_VISUELS>
|
||||
<FORMAT>Image carrée 1024x1024 pixels.</FORMAT>
|
||||
<STYLE>Abstrait, graphique, formes géométriques et dégradés.</STYLE>
|
||||
<PALETTE>${paletteHint}</PALETTE>
|
||||
<COMPOSITION>Remplis toute la surface sans laisser de bordures ou zones vides.</COMPOSITION>
|
||||
</CONTEXTES_VISUELS>
|
||||
|
||||
Directives essentielles:
|
||||
- Style: Abstrait, graphique, avec formes géométriques et dégradés de couleurs
|
||||
- Couleurs: ${paletteHint}
|
||||
- IMPORTANT: Image plein cadre sans AUCUN bord blanc ou espace vide
|
||||
- Composition: Éléments visuels répartis sur TOUTE la surface de l'image
|
||||
|
||||
Contraintes:
|
||||
- PAS de texte ou typographie
|
||||
- PAS de personnes ou visages
|
||||
- PAS de fond blanc ou bordures
|
||||
- PAS de filigrane
|
||||
<CONTRAINTES>
|
||||
<INTERDIT>Texte ou typographie.</INTERDIT>
|
||||
<INTERDIT>Personnes ou visages.</INTERDIT>
|
||||
<INTERDIT>Fond blanc ou bordures.</INTERDIT>
|
||||
<INTERDIT>Filigrane ou logo.</INTERDIT>
|
||||
</CONTRAINTES>
|
||||
`.trim();
|
||||
|
||||
return prompt;
|
||||
|
||||
+105
-1
@@ -1,4 +1,4 @@
|
||||
const { onCall } = require("firebase-functions/v2/https");
|
||||
const { onCall, HttpsError } = require("firebase-functions/v2/https");
|
||||
const { z } = require("genkit");
|
||||
const { generateAI, analyseLyrics } = require("../helpers/gemini");
|
||||
const {
|
||||
@@ -10,6 +10,64 @@ const axios = require("axios");
|
||||
const admin = require("firebase-admin");
|
||||
const { refList } = require("../index");
|
||||
|
||||
const TOXIC_KEYWORDS = [
|
||||
/\bnazis?\b/i,
|
||||
/\bnazisme\b/i,
|
||||
/\bnéo[- ]?nazis?\b/i,
|
||||
/\banti[- ]?s[eé]mites?\b/i,
|
||||
/\bantis[eé]mitisme\b/i,
|
||||
/\bracis(tes?|me)\b/i,
|
||||
/\bwhite\s+supremac(?:y|istes?|ism)\b/i,
|
||||
/\bku\s*klux\s*klan\b/i,
|
||||
];
|
||||
|
||||
const buildModerationBrief = ({
|
||||
objective,
|
||||
context,
|
||||
style,
|
||||
audience,
|
||||
emotion,
|
||||
structure,
|
||||
rhymes,
|
||||
}) => {
|
||||
const sections = [
|
||||
`<OBJECTIF>${objective || ""}</OBJECTIF>`,
|
||||
`<CONTEXTE>${context || ""}</CONTEXTE>`,
|
||||
`<EMOTION>${emotion || ""}</EMOTION>`,
|
||||
`<STYLE>${style || ""}</STYLE>`,
|
||||
`<AUDIENCE>${audience || ""}</AUDIENCE>`,
|
||||
`<RIMES>${Array.isArray(rhymes) ? rhymes.join(", ") : rhymes || ""}</RIMES>`,
|
||||
];
|
||||
|
||||
if (Array.isArray(structure) && structure.length) {
|
||||
sections.push(`<STRUCTURE>${structure.join(" | ")}</STRUCTURE>`);
|
||||
}
|
||||
|
||||
return `<BRIEF_UTILISATEUR>\n${sections.join("\n")}\n</BRIEF_UTILISATEUR>`;
|
||||
};
|
||||
|
||||
const moderationIndicatesBlock = (moderation = {}, fallbackText = "") => {
|
||||
if (!moderation || typeof moderation !== "object") return false;
|
||||
|
||||
if (moderation.blocked === true) return true;
|
||||
|
||||
const excerpts = Array.isArray(moderation.excerpts)
|
||||
? moderation.excerpts
|
||||
: [];
|
||||
const reasons = Array.isArray(moderation.reasons)
|
||||
? moderation.reasons
|
||||
: [];
|
||||
|
||||
const keywordDetected = (value) =>
|
||||
typeof value === "string" && TOXIC_KEYWORDS.some((rx) => rx.test(value));
|
||||
|
||||
if (excerpts.some((item) => keywordDetected(item?.quote))) return true;
|
||||
if (reasons.some((item) => keywordDetected(item))) return true;
|
||||
if (keywordDetected(fallbackText)) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
||||
try {
|
||||
const {
|
||||
@@ -22,6 +80,52 @@ exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
||||
rhymes = "",
|
||||
} = data;
|
||||
|
||||
const moderationPayload = {
|
||||
objective,
|
||||
context,
|
||||
style,
|
||||
audience,
|
||||
emotion,
|
||||
structure,
|
||||
rhymes,
|
||||
};
|
||||
|
||||
const moderationInput = buildModerationBrief(moderationPayload);
|
||||
|
||||
try {
|
||||
const moderation = await analyseLyrics({
|
||||
title: "Brief utilisateur",
|
||||
lyrics: moderationInput,
|
||||
});
|
||||
|
||||
if (moderationIndicatesBlock(moderation, moderationInput)) {
|
||||
console.warn("generateLyrics blocked by moderation", {
|
||||
reasons: moderation?.reasons,
|
||||
excerpts: moderation?.excerpts,
|
||||
});
|
||||
|
||||
const summary = Array.isArray(moderation?.reasons)
|
||||
? moderation.reasons.slice(0, 3).join(", ")
|
||||
: "contenu sensible";
|
||||
|
||||
throw new HttpsError(
|
||||
"invalid-argument",
|
||||
`Certaines informations saisies contiennent des termes interdits (${summary}). Merci de reformuler pour poursuivre la création des paroles.`,
|
||||
{ moderation },
|
||||
);
|
||||
}
|
||||
} catch (moderationError) {
|
||||
if (moderationError instanceof HttpsError) {
|
||||
throw moderationError;
|
||||
}
|
||||
|
||||
console.error("generateLyrics moderation failure", moderationError);
|
||||
throw new HttpsError(
|
||||
"internal",
|
||||
"Impossible de vérifier le brief pour le moment. Merci de réessayer dans quelques instants.",
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Function generate lyrics start with data", data);
|
||||
|
||||
const system = `
|
||||
|
||||
Reference in New Issue
Block a user