diff --git a/functions/helpers/gemini.js b/functions/helpers/gemini.js
index 4e596a9..4dcf6a1 100644
--- a/functions/helpers/gemini.js
+++ b/functions/helpers/gemini.js
@@ -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 = `
+
+
+ QUAND BLOQUER (blocked = true) — cas graves et clairs (pas ambigus)
+ Sexualité impliquant des mineurs (référence explicite, érotisation, incitation).
+ Apologie explicite et sérieuse de terrorisme ou extrémisme réel, ou appel à rejoindre/soutenir.
+ Appels explicites à la violence ou à un préjudice grave contre une personne ou un groupe réel.
+ Déshumanisation évidente d'un groupe protégé accompagnée d'une menace ou d'une incitation.
+ Instruction ou encouragement clair à commettre des crimes violents.
+ Nazisme ou fascisme réel promu sans distance critique.
+ Partage de contenu sexuel non consenti ou exploitation sexuelle réelle.
+
+
+ QUAND NE PAS BLOQUER (blocked = false, flagged possible)
+ Œuvres montrant un personnage toxique pour le critiquer, le contextualiser ou le dénoncer.
+ Citations, descriptions historiques, satire, ironie ou réappropriation par les communautés visées.
+ Métaphores violentes sans cible réelle ni intention d'incitation.
+ Références à des thèmes sombres à des fins narratives, éducatives ou critiques sans promotion réelle.
+
+
+ CALIBRAGE DU SCORE ET DES SEVERITY
+ Score low ≈ 0.1-0.3, medium ≈ 0.3-0.6, high ≈ 0.6-0.85, critical > 0.85.
+ Utilise la colonne severity pour refléter le passage le plus problématique.
+
+
+ RELECTURE DU CONTEXTE
+ Si le passage relève de la narration, satire, critique ou roleplay: privilégie flagged=true et blocked=false.
+ Si le passage constitue une injonction réelle ou un appel clair au tort: blocked=true.
+
+
`.trim();
const userPrompt = `
-Analyse de paroles de chanson
-Titre: ${title || "Sans titre"}
-
-Paroles:
-"""
+
+
+ ${title || "Sans titre"}
+
+
+
-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.
+
+ Autoriser l'expression artistique tout en bloquant les contenus réellement dangereux.
+ "blocked" = true UNIQUEMENT pour les cas graves listés dans la section A.
+ 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).
+ Fournis des "excerpts" courts en citant exactement les passages sensibles.
+
-${policy}
+${decisionPolicy}
`.trim();
const { output } = await genkit({
diff --git a/functions/helpers/prompts.js b/functions/helpers/prompts.js
index 013e451..120acfe 100644
--- a/functions/helpers/prompts.js
+++ b/functions/helpers/prompts.js
@@ -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.
+
+ Créer une pochette d'album abstraite et lumineuse.
+ ${title || "Sans titre"}
+ ${tagsLine}
+ ${lyricsLine}
+
-Contexte:
-- Titre: ${title || "Sans titre"}
-- Style musical: ${tags.join(" ; ")}
+
+ Image carrée 1024x1024 pixels.
+
+ ${paletteHint}
+ Remplis toute la surface sans laisser de bordures ou zones vides.
+
-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
+
+ Texte ou typographie.
+ Personnes ou visages.
+ Fond blanc ou bordures.
+ Filigrane ou logo.
+
`.trim();
return prompt;
diff --git a/functions/src/lyrics.js b/functions/src/lyrics.js
index a268958..aedb356 100644
--- a/functions/src/lyrics.js
+++ b/functions/src/lyrics.js
@@ -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 = [
+ `${objective || ""}`,
+ `${context || ""}`,
+ `${emotion || ""}`,
+ ``,
+ `${audience || ""}`,
+ `${Array.isArray(rhymes) ? rhymes.join(", ") : rhymes || ""}`,
+ ];
+
+ if (Array.isArray(structure) && structure.length) {
+ sections.push(`${structure.join(" | ")}`);
+ }
+
+ return `\n${sections.join("\n")}\n`;
+};
+
+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 = `
diff --git a/src/assets/UI/writingBgWeb.png b/src/assets/UI/writingBgWeb.png
new file mode 100644
index 0000000..170a638
Binary files /dev/null and b/src/assets/UI/writingBgWeb.png differ
diff --git a/src/assets/index.js b/src/assets/index.js
index bebb459..0f554ce 100644
--- a/src/assets/index.js
+++ b/src/assets/index.js
@@ -211,6 +211,7 @@ export const tutorial = {
export const background = {
writingBG,
+ writingBgWeb: require("./UI/writingBgWeb.png"),
studioBG,
studioBG2,
productionBG,
diff --git a/src/components/BorderGradientButton.js b/src/components/BorderGradientButton.js
index cf30682..291de65 100644
--- a/src/components/BorderGradientButton.js
+++ b/src/components/BorderGradientButton.js
@@ -14,12 +14,17 @@ const BorderGradientButton = ({
containerStyle = {},
tint = "dark",
disabled = false,
+ maxWidth = null,
}) => {
return (
toggleSelect(item)}
gradientProps={
useEmotionColors && !sel ? { locations: [0.24, 1] } : undefined
}
- showBorder={!sel} // mobile: bordure d’origine
+ showBorder={shouldShowBorder}
disableBlur={sel && isWeb} // web: pas de blur si gradient
blurViewStyle={
sel && isWeb
@@ -323,7 +329,7 @@ const ListSelection = ({
{withWebBlueGradientIfSelected(baseContent, sel)}
- {showHoverOutline && (
+ {(showHoverOutline || showSelectionOutline) && (
{
headerType="NONE"
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 64 : 100}
- backgroundImg={background.homeBG}
+ backgroundImg={background.writingBG}
>
{
headerType="NONE"
behavior={Platform.OS === "ios" ? "padding" : "height"}
keyboardVerticalOffset={Platform.OS === "ios" ? 64 : 100}
- backgroundImg={background.homeBG}
+ backgroundImg={background.writingBgWeb}
>
{
const [called, setCalled] = useState(false);
const [result, setResult] = useState(null);
const [saved, setSaved] = useState(false);
+ const [error, setError] = useState(null);
+ const hasShownErrorAlert = useRef(false);
const { setIsLoading } = useMinuit();
const { updateProjectData } = useUser();
@@ -28,6 +29,8 @@ const CreatingLyrics = ({ active, config, selections }) => {
setResult(null);
setProgress(0);
setSaved(false);
+ setError(null);
+ hasShownErrorAlert.current = false;
}
}, [active]);
@@ -52,6 +55,9 @@ const CreatingLyrics = ({ active, config, selections }) => {
try {
setCalled(true);
await setIsLoading(true);
+ console.log("🚀 [CreatingLyrics] Lancement de la génération", {
+ platform: Platform.OS,
+ });
const callable = firebase
.functions()
.httpsCallable("lyrics-generateLyrics");
@@ -66,8 +72,22 @@ const CreatingLyrics = ({ active, config, selections }) => {
});
setResult(data);
setProgress(100);
+ console.log("✨ [CreatingLyrics] Génération réussie");
} catch (e) {
- console.log(e);
+ console.error("🔥 [CreatingLyrics] Erreur de génération", e);
+ const rawMessage = typeof e?.message === "string" ? e.message : "";
+ const cleanedMessage = rawMessage.replace(
+ /^functions error: \w+-\w+:\s*/i,
+ "",
+ );
+ setError({
+ message:
+ cleanedMessage?.length > 0
+ ? cleanedMessage
+ : "Une erreur est survenue lors de la génération des paroles.",
+ code: e?.code,
+ details: e?.details,
+ });
} finally {
await setIsLoading(false);
}
@@ -85,6 +105,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
if (saved) return;
setSaved(true);
await setIsLoading(true);
+ console.log("💾 [CreatingLyrics] Sauvegarde automatique des paroles");
await updateProjectData({
title: result?.title || "",
lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [],
@@ -95,13 +116,13 @@ const CreatingLyrics = ({ active, config, selections }) => {
});
navigate(Routes.Lyrics);
} catch (e) {
- console.log("Auto save generated lyrics error", e?.message);
+ console.error("⚠️ [CreatingLyrics] Erreur lors de la sauvegarde", e);
} finally {
await setIsLoading(false);
}
};
- if (active && result && progress >= 100 && !saved) {
+ if (active && result && progress >= 100 && !saved && !error) {
autoSaveAndGo();
}
}, [
@@ -109,12 +130,39 @@ const CreatingLyrics = ({ active, config, selections }) => {
result,
progress,
saved,
+ error,
setIsLoading,
updateProjectData,
config,
selections,
]);
+ useEffect(() => {
+ if (error && !hasShownErrorAlert.current) {
+ hasShownErrorAlert.current = true;
+ const redirect = () => {
+ console.log("↩️ [CreatingLyrics] Retour à WritingLyrics après erreur");
+ navigate(Routes.WritingLyrics);
+ };
+
+ Alert.alert(
+ "Brief à reformuler",
+ error.message,
+ [
+ {
+ text: "OK",
+ onPress: redirect,
+ },
+ ],
+ { cancelable: false },
+ );
+
+ if (Platform.OS === "web") {
+ redirect();
+ }
+ }
+ }, [error]);
+
return (
{
textAlign: "center",
}}
>
- Ton texte est{"\n"}en cours de création
+ {error
+ ? "Impossible de générer les paroles"
+ : "Ton texte est\nen cours de création"}
@@ -183,7 +233,7 @@ const CreatingLyrics = ({ active, config, selections }) => {
{progress}%
- {!saved && (
+ {!saved && result && !error && (
{
if (saved) return;
setSaved(true);
await setIsLoading(true);
+ console.log(
+ "📄 [CreatingLyrics] Consultation manuelle du texte",
+ );
await updateProjectData({
title: result?.title || "",
lyrics: Array.isArray(result?.lyrics)
@@ -206,7 +259,10 @@ const CreatingLyrics = ({ active, config, selections }) => {
});
navigate(Routes.Lyrics);
} catch (e) {
- console.log("Save generated lyrics error", e?.message);
+ console.error(
+ "⚠️ [CreatingLyrics] Erreur lors de l'ouverture manuelle",
+ e,
+ );
} finally {
await setIsLoading(false);
}
diff --git a/src/screens/Writing/Lyrics.js b/src/screens/Writing/Lyrics.js
index ba57b28..4d65490 100644
--- a/src/screens/Writing/Lyrics.js
+++ b/src/screens/Writing/Lyrics.js
@@ -12,6 +12,8 @@ import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
import CustomInput from "./components/CustomInput";
+import { background } from "../../assets";
+import { isWeb } from "../../hooks/useLayoutType";
const Lyrics = ({ navigation }) => {
const scrollRef = useRef(null);
@@ -239,7 +241,10 @@ const Lyrics = ({ navigation }) => {
]);
return (
-
+
navigate(Routes.Home)}
progress={95}
diff --git a/src/screens/Writing/WritingLyrics.js b/src/screens/Writing/WritingLyrics.js
index dfa563b..32e3717 100644
--- a/src/screens/Writing/WritingLyrics.js
+++ b/src/screens/Writing/WritingLyrics.js
@@ -6,11 +6,13 @@ import BorderGradientButton from "../../components/BorderGradientButton";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton";
import MusicLandHeader from "../../components/MusicLandHeader";
-import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { goBack, navigate } from "../../navigation/NavigationService";
import { useUserData } from "../../providers/UserDataProvider";
import { gutters } from "../../styles";
+import { background } from "../../assets";
+import { isWeb } from "../../hooks/useLayoutType";
+import Page from "../../layouts/Page";
const WritingLyrics = () => {
const { createNewProject, selectedProject, updateProjectData } =
@@ -42,7 +44,10 @@ const WritingLyrics = () => {
}
return (
-
+
{
}}
>
createAndNavigate({ hasLyrics: true })}
+ containerStyle={{ alignSelf: "center", width: "100%" }}
/>
createAndNavigate({ hasLyrics: false })}
/>